Add new settings attr AUDITLOG_REGISTRY to allow custom auditlogs

This commit is contained in:
hoangquochung1110 2024-03-31 15:54:52 +07:00
parent a0ae594425
commit 7a52038259
11 changed files with 187 additions and 12 deletions

View file

@ -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))

View file

@ -45,3 +45,8 @@ settings.AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT = getattr(
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"]
)

View file

@ -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.")
@ -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

View file

@ -11,7 +11,7 @@ from django.utils.timezone import is_aware, localtime
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
@ -142,7 +142,7 @@ class LogEntryAdminMixin:
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

View file

@ -222,7 +222,9 @@ class LogEntryManager(models.Manager):
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"]:
@ -436,12 +438,14 @@ class LogEntry(models.Model):
"""
: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 = {}

View file

@ -13,6 +13,7 @@ from typing import (
)
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 (
@ -22,6 +23,7 @@ from django.db.models.signals import (
post_save,
pre_save,
)
from django.utils.module_loading import import_string
from auditlog.conf import settings
from auditlog.signals import accessed
@ -363,3 +365,56 @@ class AuditlogModelRegistry:
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 = (
"The module in NAME could not be imported: %s. Check your "
"AUDITLOG_REGISTRY setting."
)
raise ImproperlyConfigured(msg % auditlog["NAME"])
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()

View file

@ -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)

View file

@ -3,3 +3,12 @@ from django.apps import AppConfig
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()

View file

@ -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,
)

View file

@ -59,6 +59,7 @@ from auditlog_tests.models import (
SimpleNonManagedModel,
UUIDPrimaryKeyModel,
)
from auditlog_tests.test_registry import create_only_auditlog, update_only_auditlog
class SimpleModelTest(TestCase):
@ -2643,3 +2644,75 @@ class MissingModelTest(TestCase):
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
)

View file

@ -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
------