Skip to content

Commit

Permalink
Add register model using Django settings (#368)
Browse files Browse the repository at this point in the history
Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
  • Loading branch information
2ykwang and hramezani authored May 23, 2022
1 parent 8b47267 commit 32694b1
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
5 changes: 5 additions & 0 deletions auditlog/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 17 additions & 0 deletions auditlog/conf.py
Original file line number Diff line number Diff line change
@@ -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", ()
)
95 changes: 94 additions & 1 deletion auditlog/registry.py
Original file line number Diff line number Diff line change
@@ -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]


Expand All @@ -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,
Expand Down Expand Up @@ -147,5 +153,92 @@ def _dispatch_uid(self, signal, model) -> DispatchUID:
"""
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()
154 changes: 152 additions & 2 deletions auditlog_tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@

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,
Expand Down Expand Up @@ -792,6 +793,155 @@ def test_unregister_delete(self):
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(
Expand Down
54 changes: 54 additions & 0 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------

Expand Down

0 comments on commit 32694b1

Please sign in to comment.