Skip to content

Commit

Permalink
Add new settings attr AUDITLOG_REGISTRY to allow custom auditlogs
Browse files Browse the repository at this point in the history
  • Loading branch information
hoangquochung1110 committed Apr 2, 2024
1 parent a0ae594 commit 7a52038
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
## 3.0.0-beta.4 (2024-01-02)

#### Improvements
- feat: Add `AUDITLOG_REGISTRY` settings attribute to have multiple auditlogs with custom configurations ([#623](https://github.com/jazzband/django-auditlog/pull/623))
- feat: Excluding ip address when `AUDITLOG_DISABLE_REMOTE_ADDR` is set to True ([#620](https://github.com/jazzband/django-auditlog/pull/620))
- feat: If any receiver returns False, no logging will be made. This can be useful if logging should be conditionally enabled / disabled ([#590](https://github.com/jazzband/django-auditlog/pull/590))
- Django: Confirm Django 5.0 support ([#598](https://github.com/jazzband/django-auditlog/pull/598))
Expand Down
5 changes: 5 additions & 0 deletions auditlog/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,8 @@
settings.AUDITLOG_DISABLE_REMOTE_ADDR = getattr(
settings, "AUDITLOG_DISABLE_REMOTE_ADDR", False
)

# List of paths to auditlogs
settings.AUDITLOG_REGISTRY = getattr(
settings, "AUDITLOG_REGISTRY", ["auditlog.registry.auditlog"]
)
8 changes: 4 additions & 4 deletions auditlog/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def model_instance_diff(
field values as value.
:rtype: dict
"""
from auditlog.registry import auditlog
from auditlog.registry import AuditlogRegistry

if not (old is None or isinstance(old, Model)):
raise TypeError("The supplied old instance is not a valid model instance.")
Expand All @@ -135,13 +135,13 @@ def model_instance_diff(

if old is not None and new is not None:
fields = set(old._meta.fields + new._meta.fields)
model_fields = auditlog.get_model_fields(new._meta.model)
model_fields = AuditlogRegistry.get_model_fields(new._meta.model)
elif old is not None:
fields = set(get_fields_in_model(old))
model_fields = auditlog.get_model_fields(old._meta.model)
model_fields = AuditlogRegistry.get_model_fields(old._meta.model)
elif new is not None:
fields = set(get_fields_in_model(new))
model_fields = auditlog.get_model_fields(new._meta.model)
model_fields = AuditlogRegistry.get_model_fields(new._meta.model)
else:
fields = set()
model_fields = None
Expand Down
4 changes: 2 additions & 2 deletions auditlog/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from django.utils.translation import gettext_lazy as _

from auditlog.models import LogEntry
from auditlog.registry import auditlog
from auditlog.registry import AuditlogRegistry
from auditlog.signals import accessed

MAX = 75
Expand Down Expand Up @@ -142,7 +142,7 @@ def field_verbose_name(self, obj, field_name: str):
if model is None:
return field_name
try:
model_fields = auditlog.get_model_fields(model._meta.model)
model_fields = AuditlogRegistry.get_model_fields(model._meta.model)
mapping_field_name = model_fields["mapping_fields"].get(field_name)
if mapping_field_name:
return mapping_field_name
Expand Down
10 changes: 7 additions & 3 deletions auditlog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,9 @@ def _get_pk_value(self, instance):
return pk

def _get_serialized_data_or_none(self, instance):
from auditlog.registry import auditlog
from auditlog.registry import AuditlogRegistry

auditlog = AuditlogRegistry.get_auditlog(model=instance.__class__)

opts = auditlog.get_serialize_options(instance.__class__)
if not opts["serialize_data"]:
Expand Down Expand Up @@ -436,12 +438,14 @@ def changes_display_dict(self):
"""
:return: The changes recorded in this log entry intended for display to users as a dictionary object.
"""
from auditlog.registry import auditlog
from auditlog.registry import AuditlogRegistry

# Get the model and model_fields, but gracefully handle the case where the model no longer exists
model = self.content_type.model_class()
auditlog = AuditlogRegistry.get_auditlog(model=model)

model_fields = None
if auditlog.contains(model._meta.model):
if auditlog:
model_fields = auditlog.get_model_fields(model._meta.model)

changes_display_dict = {}
Expand Down
55 changes: 55 additions & 0 deletions auditlog/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
)

from django.apps import apps
from django.core.exceptions import ImproperlyConfigured
from django.db.models import ManyToManyField, Model
from django.db.models.base import ModelBase
from django.db.models.signals import (
Expand All @@ -22,6 +23,7 @@
post_save,
pre_save,
)
from django.utils.module_loading import import_string

from auditlog.conf import settings
from auditlog.signals import accessed
Expand Down Expand Up @@ -363,3 +365,56 @@ def register_from_settings(self):


auditlog = AuditlogModelRegistry()


def get_default_auditlogs():
return get_auditlogs(settings.AUDITLOG_REGISTRY)


def get_auditlogs(auditlog_config):
auditlogs = []

for auditlog in auditlog_config:
try:
audit_logger = import_string(auditlog)
except ImportError:
msg = (

Check warning on line 381 in auditlog/registry.py

View check run for this annotation

Codecov / codecov/patch

auditlog/registry.py#L380-L381

Added lines #L380 - L381 were not covered by tests
"The module in NAME could not be imported: %s. Check your "
"AUDITLOG_REGISTRY setting."
)
raise ImproperlyConfigured(msg % auditlog["NAME"])

Check warning on line 385 in auditlog/registry.py

View check run for this annotation

Codecov / codecov/patch

auditlog/registry.py#L385

Added line #L385 was not covered by tests
else:
auditlogs.append(audit_logger)
return auditlogs


class AuditlogRegistry:
"""
A registry that keeps track of AuditlogModelRegistry instances.
"""

_registry = {}

@classmethod
def get_auditlog(cls, model: ModelBase):
"""
Retrieve the auditlog object associated with a given model.
"""
auditlogs = get_default_auditlogs()
for auditlog in auditlogs:
if auditlog.contains(model):
return auditlog

@classmethod
def get_model_fields(cls, model: ModelBase):
"""
Wrapper of AuditlogModelRegistry.get_model_fields().
"""
auditlog = cls.get_auditlog(model)
if auditlog:
model_fields = auditlog.get_model_fields(model)
return model_fields
return {}


auditlog_registry = AuditlogRegistry()
7 changes: 4 additions & 3 deletions auditlog_tests/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib import admin

from auditlog.registry import auditlog
from auditlog.registry import get_default_auditlogs

for model in auditlog.get_models():
admin.site.register(model)
for auditlog in get_default_auditlogs():
for model in auditlog.get_models():
admin.site.register(model)
9 changes: 9 additions & 0 deletions auditlog_tests/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,12 @@

class AuditlogTestConfig(AppConfig):
name = "auditlog_tests"

def ready(self) -> None:
from auditlog_tests.test_registry import (
create_only_auditlog,
update_only_auditlog,
)

create_only_auditlog.register_from_settings()
update_only_auditlog.register_from_settings()
16 changes: 16 additions & 0 deletions auditlog_tests/test_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from auditlog.registry import AuditlogModelRegistry

create_only_auditlog = AuditlogModelRegistry(
create=True,
update=False,
delete=False,
access=False,
m2m=False,
)
update_only_auditlog = AuditlogModelRegistry(
create=False,
update=True,
delete=False,
access=False,
m2m=False,
)
73 changes: 73 additions & 0 deletions auditlog_tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
SimpleNonManagedModel,
UUIDPrimaryKeyModel,
)
from auditlog_tests.test_registry import create_only_auditlog, update_only_auditlog


class SimpleModelTest(TestCase):
Expand Down Expand Up @@ -2643,3 +2644,75 @@ def test_get_changes_for_missing_model(self):
history = self.obj.history.latest()
self.assertEqual(history.changes_dict["text"][1], self.obj.text)
self.assertEqual(history.changes_display_dict["text"][1], self.obj.text)


class CustomAuditlogTest(TestCase):
def setUp(self):
super().setUp()
if auditlog.contains(SimpleModel):
auditlog.unregister(SimpleModel)

def tearDown(self):
for model in create_only_auditlog.get_models():
create_only_auditlog.unregister(model)
for model in update_only_auditlog.get_models():
update_only_auditlog.unregister(model)
auditlog.register(SimpleModel)

def make_object(self):
return SimpleModel.objects.create(text="I am not difficult.")

def update_object(self, obj):
obj.text = "Changed"
obj.save()
return obj

def test_create_only_auditlog(self):
with override_settings(
AUDITLOG_REGISTRY=[
"auditlog_tests.test_registry.create_only_auditlog",
]
):
create_only_auditlog.register(SimpleModel, include_fields=["text"])

# Get the object to work with
obj = self.make_object()

# Check for log entries
self.assertEqual(obj.history.count(), 1, msg="There is one log entry")

history = obj.history.get()
self.check_create_log_entry(obj, history)

updated_obj = self.update_object(obj)
self.check_no_update_entry(updated_obj)

def test_update_only_auditlog(self):
with override_settings(
AUDITLOG_REGISTRY=[
"auditlog_tests.test_registry.update_only_auditlog",
]
):
update_only_auditlog.register(SimpleModel, include_fields=["text"])

# Get the object to work with
obj = self.make_object()
self.assertEqual(obj.history.all().count(), 0)
updated_obj = self.update_object(obj)
self.assertEqual(updated_obj.history.all().count(), 1)
history = updated_obj.history.get()
self.assertEqual(
history.action,
LogEntry.Action.UPDATE,
)

def check_create_log_entry(self, obj, history):
self.assertEqual(
history.action, LogEntry.Action.CREATE, msg="Action is 'CREATE'"
)
self.assertEqual(history.object_repr, str(obj), msg="Representation is equal")

def check_no_update_entry(self, obj):
self.assertEqual(
obj.history.all().filter(action=LogEntry.Action.UPDATE).count(), 0
)
11 changes: 11 additions & 0 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,17 @@ If the value is `None`, the default getter will be used.

.. versionadded:: 3.0.0

**AUDITLOG_REGISTRY**
A list or tuple of AuditlogModelRegistry instances.
This settings allow to configure multiple auditlogs with conditionally disabled logging per action
or to have custom signal receivers.

If not specified, this setting defaults to:
.. code-block:: python
AUDITLOG_REGISTRY = ["auditlog.registry.auditlog"]
.. versionadded:: 3.0.0

Actors
------

Expand Down

0 comments on commit 7a52038

Please sign in to comment.