mirror of
https://github.com/jazzband/django-auditlog.git
synced 2026-05-21 22:01:52 +00:00
Add new settings attr AUDITLOG_REGISTRY to allow custom auditlogs
This commit is contained in:
parent
a0ae594425
commit
7a52038259
11 changed files with 187 additions and 12 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
16
auditlog_tests/test_registry.py
Normal file
16
auditlog_tests/test_registry.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
------
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue