Skip to content

Commit f430afa

Browse files
Add CustomLogEntry model support and update tests:
- Added support for CustomLogEntry data model to extend django-auditlog capabilities - Updated existing test cases to align with new model structure and data handling logic - Added new test cases to validate CustomLogEntry behavior, model registration, and signal handling - Ensured backward compatibility with existing LogEntry model where applicable
1 parent bd03eb6 commit f430afa

27 files changed

+399
-66
lines changed

auditlog/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
from __future__ import annotations
2+
13
from importlib.metadata import version
24

5+
from django.apps import apps as django_apps
6+
from django.conf import settings
7+
from django.core.exceptions import ImproperlyConfigured
8+
39
__version__ = version("django-auditlog")
10+
11+
12+
def get_logentry_model():
13+
try:
14+
return django_apps.get_model(
15+
settings.AUDITLOG_LOGENTRY_MODEL, require_ready=False
16+
)
17+
except ValueError:
18+
raise ImproperlyConfigured(
19+
"AUDITLOG_ENTRY_MODEL must be of the form 'app_label.model_name'"
20+
)
21+
except LookupError:
22+
raise ImproperlyConfigured(
23+
"AUDITLOG_LOGENTRY_MODEL refers to model '%s' that has not been installed"
24+
% settings.AUDITLOG_LOGENTRY_MODEL
25+
)

auditlog/admin.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from django.contrib.auth import get_user_model
55
from django.utils.translation import gettext_lazy as _
66

7+
from auditlog import get_logentry_model
78
from auditlog.filters import CIDFilter, ResourceTypeFilter
89
from auditlog.mixins import LogEntryAdminMixin
9-
from auditlog.models import LogEntry
10+
11+
LogEntry = get_logentry_model()
1012

1113

1214
@admin.register(LogEntry)

auditlog/conf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,7 @@
6262
)
6363

6464
settings.AUDITLOG_MASK_CALLABLE = getattr(settings, "AUDITLOG_MASK_CALLABLE", None)
65+
66+
settings.AUDITLOG_LOGENTRY_MODEL = getattr(
67+
settings, "AUDITLOG_LOGENTRY_MODEL", "auditlog.LogEntry"
68+
)

auditlog/context.py

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,41 @@
66
from django.contrib.auth import get_user_model
77
from django.db.models.signals import pre_save
88

9-
from auditlog.models import LogEntry
9+
from auditlog import get_logentry_model
1010

1111
auditlog_value = ContextVar("auditlog_value")
1212
auditlog_disabled = ContextVar("auditlog_disabled", default=False)
1313

1414

1515
@contextlib.contextmanager
1616
def set_actor(actor, remote_addr=None, remote_port=None):
17-
"""Connect a signal receiver with current user attached."""
18-
# Initialize thread local storage
1917
context_data = {
20-
"signal_duid": ("set_actor", time.time()),
18+
"actor": actor,
2119
"remote_addr": remote_addr,
2220
"remote_port": remote_port,
2321
}
22+
return call_context_manager(context_data)
23+
24+
25+
@contextlib.contextmanager
26+
def set_extra_data(context_data):
27+
return call_context_manager(context_data)
28+
29+
30+
def call_context_manager(context_data):
31+
"""Connect a signal receiver with current user attached."""
32+
LogEntry = get_logentry_model()
33+
# Initialize thread local storage
34+
context_data["signal_duid"] = ("set_actor", time.time())
2435
auditlog_value.set(context_data)
2536

2637
# Connect signal for automatic logging
27-
set_actor = partial(
28-
_set_actor,
29-
user=actor,
38+
set_extra_data = partial(
39+
_set_extra_data,
3040
signal_duid=context_data["signal_duid"],
3141
)
3242
pre_save.connect(
33-
set_actor,
43+
set_extra_data,
3444
sender=LogEntry,
3545
dispatch_uid=context_data["signal_duid"],
3646
weak=False,
@@ -47,29 +57,42 @@ def set_actor(actor, remote_addr=None, remote_port=None):
4757
pre_save.disconnect(sender=LogEntry, dispatch_uid=auditlog["signal_duid"])
4858

4959

50-
def _set_actor(user, sender, instance, signal_duid, **kwargs):
60+
def _set_actor(auditlog, instance, sender):
61+
LogEntry = get_logentry_model()
62+
auth_user_model = get_user_model()
63+
if "actor" in auditlog:
64+
actor = auditlog.get("actor")
65+
if (
66+
sender == LogEntry
67+
and isinstance(actor, auth_user_model)
68+
and instance.actor is None
69+
):
70+
instance.actor = actor
71+
instance.actor_email = getattr(actor, "email", None)
72+
73+
74+
def _set_extra_data(sender, instance, signal_duid, **kwargs):
5175
"""Signal receiver with extra 'user' and 'signal_duid' kwargs.
5276
5377
This function becomes a valid signal receiver when it is curried with the actor and a dispatch id.
5478
"""
79+
LogEntry = get_logentry_model()
5580
try:
5681
auditlog = auditlog_value.get()
5782
except LookupError:
5883
pass
5984
else:
6085
if signal_duid != auditlog["signal_duid"]:
6186
return
62-
auth_user_model = get_user_model()
63-
if (
64-
sender == LogEntry
65-
and isinstance(user, auth_user_model)
66-
and instance.actor is None
67-
):
68-
instance.actor = user
69-
instance.actor_email = getattr(user, "email", None)
7087

71-
instance.remote_addr = auditlog["remote_addr"]
72-
instance.remote_port = auditlog["remote_port"]
88+
_set_actor(auditlog, instance, sender)
89+
90+
for key in auditlog:
91+
if key != "actor" and hasattr(LogEntry, key):
92+
if callable(auditlog[key]):
93+
setattr(instance, key, auditlog[key]())
94+
else:
95+
setattr(instance, key, auditlog[key])
7396

7497

7598
@contextlib.contextmanager

auditlog/diff.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from django.utils.encoding import smart_str
1010
from django.utils.module_loading import import_string
1111

12+
from auditlog import get_logentry_model
13+
1214

1315
def track_field(field):
1416
"""
@@ -21,7 +23,6 @@ def track_field(field):
2123
:return: Whether the given field should be tracked.
2224
:rtype: bool
2325
"""
24-
from auditlog.models import LogEntry
2526

2627
# Do not track many to many relations
2728
if field.many_to_many:
@@ -30,7 +31,7 @@ def track_field(field):
3031
# Do not track relations to LogEntry
3132
if (
3233
getattr(field, "remote_field", None) is not None
33-
and field.remote_field.model == LogEntry
34+
and field.remote_field.model == get_logentry_model()
3435
):
3536
return False
3637

auditlog/management/commands/auditlogflush.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from django.core.management.base import BaseCommand
44
from django.db import connection
55

6-
from auditlog.models import LogEntry
6+
from auditlog import get_logentry_model
7+
8+
LogEntry = get_logentry_model()
79

810

911
class Command(BaseCommand):

auditlog/management/commands/auditlogmigratejson.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
from django.core.management import CommandError, CommandParser
55
from django.core.management.base import BaseCommand
66

7-
from auditlog.models import LogEntry
7+
from auditlog import get_logentry_model
8+
9+
LogEntry = get_logentry_model()
810

911

1012
class Command(BaseCommand):
@@ -125,8 +127,8 @@ def migrate_using_sql(self, database):
125127
def postgres():
126128
with connection.cursor() as cursor:
127129
cursor.execute(
128-
"""
129-
UPDATE auditlog_logentry
130+
f"""
131+
UPDATE {LogEntry._meta.db_table}
130132
SET changes="changes_text"::jsonb
131133
WHERE changes_text IS NOT NULL
132134
AND changes_text <> ''

auditlog/middleware.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.contrib.auth import get_user_model
55

66
from auditlog.cid import set_cid
7-
from auditlog.context import set_actor
7+
from auditlog.context import set_extra_data
88

99

1010
class AuditlogMiddleware:
@@ -56,12 +56,18 @@ def _get_actor(request):
5656
return user
5757
return None
5858

59+
def _get_extra_data(self, request):
60+
context_data = {}
61+
context_data["remote_addr"] = self._get_remote_addr(request)
62+
context_data["remote_port"] = self._get_remote_port(request)
63+
64+
context_data["actor"] = self._get_actor(request)
65+
66+
return context_data
67+
5968
def __call__(self, request):
60-
remote_addr = self._get_remote_addr(request)
61-
remote_port = self._get_remote_port(request)
62-
user = self._get_actor(request)
6369

6470
set_cid(request)
6571

66-
with set_actor(actor=user, remote_addr=remote_addr, remote_port=remote_port):
72+
with set_extra_data(context_data=self._get_extra_data(request)):
6773
return self.get_response(request)

auditlog/mixins.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
from django.utils.timezone import is_aware, localtime
1515
from django.utils.translation import gettext_lazy as _
1616

17-
from auditlog.models import LogEntry
17+
from auditlog import get_logentry_model
1818
from auditlog.render import get_field_verbose_name, render_logentry_changes_html
1919
from auditlog.signals import accessed
2020

21+
LogEntry = get_logentry_model()
22+
2123
MAX = 75
2224

2325

auditlog/models.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from django.utils.encoding import smart_str
2424
from django.utils.translation import gettext_lazy as _
2525

26+
from auditlog import get_logentry_model
2627
from auditlog.diff import get_mask_function
2728

2829
DEFAULT_OBJECT_REPR = "<error forming object repr>"
@@ -303,7 +304,7 @@ def _mask_serialized_fields(
303304
return data
304305

305306

306-
class LogEntry(models.Model):
307+
class AbstractLogEntry(models.Model):
307308
"""
308309
Represents an entry in the audit log. The content type is saved along with the textual and numeric
309310
(if available) primary key, as well as the textual representation of the object when it was saved.
@@ -392,6 +393,7 @@ class Action:
392393
objects = LogEntryManager()
393394

394395
class Meta:
396+
abstract = True
395397
get_latest_by = "timestamp"
396398
ordering = ["-timestamp"]
397399
verbose_name = _("log entry")
@@ -559,6 +561,11 @@ def _get_changes_display_for_fk_field(
559561
return f"Deleted '{field.related_model.__name__}' ({value})"
560562

561563

564+
class LogEntry(AbstractLogEntry):
565+
class Meta(AbstractLogEntry.Meta):
566+
swappable = "AUDITLOG_LOGENTRY_MODEL"
567+
568+
562569
class AuditlogHistoryField(GenericRelation):
563570
"""
564571
A subclass of py:class:`django.contrib.contenttypes.fields.GenericRelation` that sets some default
@@ -579,7 +586,7 @@ class AuditlogHistoryField(GenericRelation):
579586
"""
580587

581588
def __init__(self, pk_indexable=True, delete_related=False, **kwargs):
582-
kwargs["to"] = LogEntry
589+
kwargs["to"] = get_logentry_model()
583590

584591
if pk_indexable:
585592
kwargs["object_id_field"] = "object_id"

0 commit comments

Comments
 (0)