mirror of
https://github.com/jazzband/django-auditlog.git
synced 2026-03-16 22:20:26 +00:00
Add register model using Django settings (#368)
Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
This commit is contained in:
parent
8b47267a43
commit
32694b1324
6 changed files with 326 additions and 3 deletions
|
|
@ -1,5 +1,9 @@
|
|||
# Changes
|
||||
|
||||
#### Improvements
|
||||
|
||||
- feat: Add register model from settings ([#368](https://github.com/jazzband/django-auditlog/pull/368))
|
||||
|
||||
#### Fixes
|
||||
|
||||
- Fix inconsistent changes with JSONField ([#355](https://github.com/jazzband/django-auditlog/pull/355))
|
||||
|
|
|
|||
|
|
@ -5,3 +5,8 @@ class AuditlogConfig(AppConfig):
|
|||
name = "auditlog"
|
||||
verbose_name = "Audit log"
|
||||
default_auto_field = "django.db.models.AutoField"
|
||||
|
||||
def ready(self):
|
||||
from auditlog.registry import auditlog
|
||||
|
||||
auditlog.register_from_settings()
|
||||
|
|
|
|||
17
auditlog/conf.py
Normal file
17
auditlog/conf.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from django.conf import settings
|
||||
|
||||
# Register all models when set to True
|
||||
settings.AUDITLOG_INCLUDE_ALL_MODELS = getattr(
|
||||
settings, "AUDITLOG_INCLUDE_ALL_MODELS", False
|
||||
)
|
||||
|
||||
# Exclude models in registration process
|
||||
# It will be considered when `AUDITLOG_INCLUDE_ALL_MODELS` is True
|
||||
settings.AUDITLOG_EXCLUDE_TRACKING_MODELS = getattr(
|
||||
settings, "AUDITLOG_EXCLUDE_TRACKING_MODELS", ()
|
||||
)
|
||||
|
||||
# Register models and define their logging behaviour
|
||||
settings.AUDITLOG_INCLUDE_TRACKING_MODELS = getattr(
|
||||
settings, "AUDITLOG_INCLUDE_TRACKING_MODELS", ()
|
||||
)
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
from typing import Callable, Dict, List, Optional, Tuple
|
||||
import copy
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
|
||||
|
||||
from django.apps import apps
|
||||
from django.db.models import Model
|
||||
from django.db.models.base import ModelBase
|
||||
from django.db.models.signals import ModelSignal, post_delete, post_save, pre_save
|
||||
|
||||
from auditlog.conf import settings
|
||||
|
||||
DispatchUID = Tuple[int, str, int]
|
||||
|
||||
|
||||
|
|
@ -12,6 +16,8 @@ class AuditlogModelRegistry:
|
|||
A registry that keeps track of the models that use Auditlog to track changes.
|
||||
"""
|
||||
|
||||
DEFAULT_EXCLUDE_MODELS = ("auditlog.LogEntry", "admin.LogEntry")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
create: bool = True,
|
||||
|
|
@ -147,5 +153,92 @@ class AuditlogModelRegistry:
|
|||
"""
|
||||
return self.__hash__(), model.__qualname__, signal.__hash__()
|
||||
|
||||
def _get_model_classes(self, app_model: str) -> List[ModelBase]:
|
||||
try:
|
||||
try:
|
||||
app_label, model_name = app_model.split(".")
|
||||
return [apps.get_model(app_label, model_name)]
|
||||
except ValueError:
|
||||
return apps.get_app_config(app_model).get_models()
|
||||
except LookupError:
|
||||
return []
|
||||
|
||||
def _get_exclude_models(
|
||||
self, exclude_tracking_models: Iterable[str]
|
||||
) -> List[ModelBase]:
|
||||
exclude_models = [
|
||||
model
|
||||
for app_model in exclude_tracking_models + self.DEFAULT_EXCLUDE_MODELS
|
||||
for model in self._get_model_classes(app_model)
|
||||
]
|
||||
return exclude_models
|
||||
|
||||
def _register_models(self, models: Iterable[Union[str, Dict[str, Any]]]) -> None:
|
||||
models = copy.deepcopy(models)
|
||||
for model in models:
|
||||
if isinstance(model, str):
|
||||
for model_class in self._get_model_classes(model):
|
||||
self.unregister(model_class)
|
||||
self.register(model_class)
|
||||
elif isinstance(model, dict):
|
||||
model["model"] = self._get_model_classes(model["model"])[0]
|
||||
self.unregister(model["model"])
|
||||
self.register(**model)
|
||||
|
||||
def register_from_settings(self):
|
||||
"""
|
||||
Register models from settings variables
|
||||
"""
|
||||
if not isinstance(settings.AUDITLOG_INCLUDE_ALL_MODELS, bool):
|
||||
raise TypeError("Setting 'AUDITLOG_INCLUDE_ALL_MODELS' must be a boolean")
|
||||
|
||||
if not isinstance(settings.AUDITLOG_EXCLUDE_TRACKING_MODELS, (list, tuple)):
|
||||
raise TypeError(
|
||||
"Setting 'AUDITLOG_EXCLUDE_TRACKING_MODELS' must be a list or tuple"
|
||||
)
|
||||
|
||||
if (
|
||||
not settings.AUDITLOG_INCLUDE_ALL_MODELS
|
||||
and settings.AUDITLOG_EXCLUDE_TRACKING_MODELS
|
||||
):
|
||||
raise ValueError(
|
||||
"In order to use setting 'AUDITLOG_EXCLUDE_TRACKING_MODELS', "
|
||||
"setting 'AUDITLOG_INCLUDE_ALL_MODELS' must set to 'True'"
|
||||
)
|
||||
|
||||
if not isinstance(settings.AUDITLOG_INCLUDE_TRACKING_MODELS, (list, tuple)):
|
||||
raise TypeError(
|
||||
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' must be a list or tuple"
|
||||
)
|
||||
|
||||
for item in settings.AUDITLOG_INCLUDE_TRACKING_MODELS:
|
||||
if not isinstance(item, (str, dict)):
|
||||
raise TypeError(
|
||||
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' items must be str or dict"
|
||||
)
|
||||
|
||||
if isinstance(item, dict):
|
||||
if "model" not in item:
|
||||
raise ValueError(
|
||||
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' dict items must contain 'model' key"
|
||||
)
|
||||
if "." not in item["model"]:
|
||||
raise ValueError(
|
||||
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' model must be in the format <app_name>.<model_name>"
|
||||
)
|
||||
|
||||
if settings.AUDITLOG_INCLUDE_ALL_MODELS:
|
||||
exclude_models = self._get_exclude_models(
|
||||
settings.AUDITLOG_EXCLUDE_TRACKING_MODELS
|
||||
)
|
||||
models = apps.get_models()
|
||||
|
||||
for model in models:
|
||||
if model in exclude_models:
|
||||
continue
|
||||
self.register(model)
|
||||
|
||||
self._register_models(settings.AUDITLOG_INCLUDE_TRACKING_MODELS)
|
||||
|
||||
|
||||
auditlog = AuditlogModelRegistry()
|
||||
|
|
|
|||
|
|
@ -3,19 +3,20 @@ import json
|
|||
|
||||
import django
|
||||
from dateutil.tz import gettz
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models.signals import pre_save
|
||||
from django.http import HttpResponse
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.test import RequestFactory, TestCase, override_settings
|
||||
from django.utils import dateformat, formats, timezone
|
||||
|
||||
from auditlog.diff import model_instance_diff
|
||||
from auditlog.middleware import AuditlogMiddleware
|
||||
from auditlog.models import LogEntry
|
||||
from auditlog.registry import auditlog
|
||||
from auditlog.registry import AuditlogModelRegistry, auditlog
|
||||
from auditlog_tests.models import (
|
||||
AdditionalDataIncludedModel,
|
||||
AltPrimaryKeyModel,
|
||||
|
|
@ -792,6 +793,155 @@ class UnregisterTest(TestCase):
|
|||
self.assertEqual(LogEntry.objects.count(), 0, msg="There are no log entries")
|
||||
|
||||
|
||||
class RegisterModelSettingsTest(TestCase):
|
||||
def setUp(self):
|
||||
self.test_auditlog = AuditlogModelRegistry()
|
||||
|
||||
def tearDown(self):
|
||||
for model in self.test_auditlog.get_models():
|
||||
self.test_auditlog.unregister(model)
|
||||
|
||||
def test_get_model_classes(self):
|
||||
self.assertEqual(
|
||||
len(list(self.test_auditlog._get_model_classes("auditlog"))),
|
||||
len(list(apps.get_app_config("auditlog").get_models())),
|
||||
)
|
||||
self.assertEqual([], self.test_auditlog._get_model_classes("fake_model"))
|
||||
|
||||
def test_get_exclude_models(self):
|
||||
# By default it returns DEFAULT_EXCLUDE_MODELS
|
||||
self.assertEqual(len(self.test_auditlog._get_exclude_models(())), 2)
|
||||
|
||||
# Exclude just one model
|
||||
self.assertTrue(
|
||||
SimpleExcludeModel
|
||||
in self.test_auditlog._get_exclude_models(
|
||||
("auditlog_tests.SimpleExcludeModel",)
|
||||
)
|
||||
)
|
||||
|
||||
# Exclude all model of an app
|
||||
self.assertTrue(
|
||||
SimpleExcludeModel
|
||||
in self.test_auditlog._get_exclude_models(("auditlog_tests",))
|
||||
)
|
||||
|
||||
def test_register_models_no_models(self):
|
||||
self.test_auditlog._register_models(())
|
||||
|
||||
self.assertEqual(self.test_auditlog._registry, {})
|
||||
|
||||
def test_register_models_register_single_model(self):
|
||||
self.test_auditlog._register_models(("auditlog_tests.SimpleExcludeModel",))
|
||||
|
||||
self.assertTrue(self.test_auditlog.contains(SimpleExcludeModel))
|
||||
self.assertEqual(len(self.test_auditlog._registry), 1)
|
||||
|
||||
def test_register_models_register_app(self):
|
||||
self.test_auditlog._register_models(("auditlog_tests",))
|
||||
|
||||
self.assertTrue(self.test_auditlog.contains(SimpleExcludeModel))
|
||||
self.assertTrue(self.test_auditlog.contains(ChoicesFieldModel))
|
||||
self.assertEqual(len(self.test_auditlog.get_models()), 17)
|
||||
|
||||
def test_register_models_register_model_with_attrs(self):
|
||||
self.test_auditlog._register_models(
|
||||
(
|
||||
{
|
||||
"model": "auditlog_tests.SimpleExcludeModel",
|
||||
"include_fields": ["label"],
|
||||
"exclude_fields": [
|
||||
"text",
|
||||
],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(self.test_auditlog.contains(SimpleExcludeModel))
|
||||
fields = self.test_auditlog.get_model_fields(SimpleExcludeModel)
|
||||
self.assertEqual(fields["include_fields"], ["label"])
|
||||
self.assertEqual(fields["exclude_fields"], ["text"])
|
||||
|
||||
def test_register_from_settings_invalid_settings(self):
|
||||
with override_settings(AUDITLOG_INCLUDE_ALL_MODELS="str"):
|
||||
with self.assertRaisesMessage(
|
||||
TypeError, "Setting 'AUDITLOG_INCLUDE_ALL_MODELS' must be a boolean"
|
||||
):
|
||||
self.test_auditlog.register_from_settings()
|
||||
|
||||
with override_settings(AUDITLOG_EXCLUDE_TRACKING_MODELS="str"):
|
||||
with self.assertRaisesMessage(
|
||||
TypeError,
|
||||
"Setting 'AUDITLOG_EXCLUDE_TRACKING_MODELS' must be a list or tuple",
|
||||
):
|
||||
self.test_auditlog.register_from_settings()
|
||||
|
||||
with override_settings(AUDITLOG_EXCLUDE_TRACKING_MODELS=("app1.model1",)):
|
||||
with self.assertRaisesMessage(
|
||||
ValueError,
|
||||
"In order to use setting 'AUDITLOG_EXCLUDE_TRACKING_MODELS', "
|
||||
"setting 'AUDITLOG_INCLUDE_ALL_MODELS' must set to 'True'",
|
||||
):
|
||||
self.test_auditlog.register_from_settings()
|
||||
|
||||
with override_settings(AUDITLOG_INCLUDE_TRACKING_MODELS="str"):
|
||||
with self.assertRaisesMessage(
|
||||
TypeError,
|
||||
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' must be a list or tuple",
|
||||
):
|
||||
self.test_auditlog.register_from_settings()
|
||||
|
||||
with override_settings(AUDITLOG_INCLUDE_TRACKING_MODELS=(1, 2)):
|
||||
with self.assertRaisesMessage(
|
||||
TypeError,
|
||||
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' items must be str or dict",
|
||||
):
|
||||
self.test_auditlog.register_from_settings()
|
||||
|
||||
with override_settings(AUDITLOG_INCLUDE_TRACKING_MODELS=({"test": "test"},)):
|
||||
with self.assertRaisesMessage(
|
||||
ValueError,
|
||||
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' dict items must contain 'model' key",
|
||||
):
|
||||
self.test_auditlog.register_from_settings()
|
||||
|
||||
with override_settings(AUDITLOG_INCLUDE_TRACKING_MODELS=({"model": "test"},)):
|
||||
with self.assertRaisesMessage(
|
||||
ValueError,
|
||||
"Setting 'AUDITLOG_INCLUDE_TRACKING_MODELS' model must be in the format <app_name>.<model_name>",
|
||||
):
|
||||
self.test_auditlog.register_from_settings()
|
||||
|
||||
@override_settings(
|
||||
AUDITLOG_INCLUDE_ALL_MODELS=True,
|
||||
AUDITLOG_EXCLUDE_TRACKING_MODELS=("auditlog_tests.SimpleExcludeModel",),
|
||||
)
|
||||
def test_register_from_settings_register_all_models_with_exclude_models(self):
|
||||
self.test_auditlog.register_from_settings()
|
||||
|
||||
self.assertFalse(self.test_auditlog.contains(SimpleExcludeModel))
|
||||
self.assertTrue(self.test_auditlog.contains(ChoicesFieldModel))
|
||||
|
||||
@override_settings(
|
||||
AUDITLOG_INCLUDE_TRACKING_MODELS=(
|
||||
{
|
||||
"model": "auditlog_tests.SimpleExcludeModel",
|
||||
"include_fields": ["label"],
|
||||
"exclude_fields": [
|
||||
"text",
|
||||
],
|
||||
},
|
||||
)
|
||||
)
|
||||
def test_register_from_settings_register_models(self):
|
||||
self.test_auditlog.register_from_settings()
|
||||
|
||||
self.assertTrue(self.test_auditlog.contains(SimpleExcludeModel))
|
||||
fields = self.test_auditlog.get_model_fields(SimpleExcludeModel)
|
||||
self.assertEqual(fields["include_fields"], ["label"])
|
||||
self.assertEqual(fields["exclude_fields"], ["text"])
|
||||
|
||||
|
||||
class ChoicesFieldModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.obj = ChoicesFieldModel.objects.create(
|
||||
|
|
|
|||
|
|
@ -91,6 +91,60 @@ For example, to mask the field ``address``, use::
|
|||
|
||||
Masking fields
|
||||
|
||||
|
||||
Settings
|
||||
--------
|
||||
|
||||
**AUDITLOG_INCLUDE_ALL_MODELS**
|
||||
|
||||
You can use this setting to register all your models:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
AUDITLOG_INCLUDE_ALL_MODELS=True
|
||||
|
||||
.. versionadded:: 2.1.0
|
||||
|
||||
**AUDITLOG_EXCLUDE_TRACKING_MODELS**
|
||||
|
||||
You can use this setting to exclude models in registration process.
|
||||
It will be considered when ``AUDITLOG_INCLUDE_ALL_MODELS`` is `True`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
AUDITLOG_EXCLUDE_TRACKING_MODELS = (
|
||||
"<app_name>",
|
||||
"<app_name>.<model>"
|
||||
)
|
||||
|
||||
.. versionadded:: 2.1.0
|
||||
|
||||
**AUDITLOG_INCLUDE_TRACKING_MODELS**
|
||||
|
||||
You can use this setting to configure your models registration and other behaviours.
|
||||
It must be a list or tuple. Each item in this setting can be a:
|
||||
|
||||
* ``str``: To register a model.
|
||||
* ``dict``: To register a model and define its logging behaviour. e.g. include_fields, exclude_fields.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
AUDITLOG_INCLUDE_TRACKING_MODELS = (
|
||||
"<appname>.<model1>",
|
||||
{
|
||||
"model": "<appname>.<model1>",
|
||||
"include_fields": ["field1", "field2"],
|
||||
"exclude_fields": ["field3", "field4"],
|
||||
"mapping_fields": {
|
||||
"field1": "FIELD",
|
||||
},
|
||||
"mask_fields": ["field5", "field6"],
|
||||
},
|
||||
"<appname>.<model3>",
|
||||
)
|
||||
|
||||
.. versionadded:: 2.1.0
|
||||
|
||||
Actors
|
||||
------
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue