From a107c677efc88bafb81e364029818a0ce2fa2b0b Mon Sep 17 00:00:00 2001 From: Joshua Laymon Date: Sun, 31 Aug 2025 13:07:58 +0000 Subject: [PATCH] Update web/core/models_audit.py --- web/core/models_audit.py | 59 ++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/web/core/models_audit.py b/web/core/models_audit.py index 43b3ba1..8b97164 100644 --- a/web/core/models_audit.py +++ b/web/core/models_audit.py @@ -3,7 +3,6 @@ from django.utils import timezone from django.dispatch import receiver from django.db.models.signals import pre_save, post_save, post_delete from django.forms.models import model_to_dict -from django.apps import apps class AuditLog(models.Model): ACTION_CREATE = "create" @@ -35,13 +34,13 @@ class AuditLog(models.Model): def __str__(self): return f"{self.timestamp:%Y-%m-%d %H:%M:%S} {self.action} #{self.entry_id} by {self.username or '-'}" -# ---- helpers for diffs ---- -Entry = apps.get_model("core", "Entry") # avoids circular import +# ---- helpers for diffs (lazy: derive from sender) ---- -def _tracked_field_names(): +def _tracked_field_names(sender): + """Return concrete, editable, non-PK field names for the given model.""" names = [] - for f in Entry._meta.get_fields(): + for f in sender._meta.get_fields(): if getattr(f, "many_to_many", False): continue if not getattr(f, "concrete", False): @@ -53,50 +52,61 @@ def _tracked_field_names(): names.append(f.name) return names -_TRACKED = _tracked_field_names() - -def _serialize_entry(obj): - d = model_to_dict(obj, fields=_TRACKED) +def _serialize_instance(sender, obj): + """Serialize only tracked fields; stringify values for safety.""" + fields = _tracked_field_names(sender) + d = model_to_dict(obj, fields=fields) for k, v in d.items(): d[k] = "" if v is None else str(v) return d -# ---- get current username from middleware ---- -from .middleware import get_current_username # you’ll add this in step 2 +def _is_core_entry(sender): + """True iff the signal sender is core.Entry (no import-time lookups).""" + return getattr(sender, "_meta", None) and sender._meta.label_lower == "core.entry" -# ---- signals ---- -@receiver(pre_save, sender=Entry) +# ---- current user from middleware ---- +from .middleware import get_current_username + + +# ---- signals (note: no sender=... at decorator time) ---- + +@receiver(pre_save) def _audit_entry_pre_save(sender, instance, **kwargs): + if not _is_core_entry(sender): + return # For updates, stash old snapshot on the instance if instance.pk: try: - existing = Entry.objects.get(pk=instance.pk) - instance.__audit_old__ = _serialize_entry(existing) - except Entry.DoesNotExist: + existing = sender.objects.get(pk=instance.pk) + instance.__audit_old__ = _serialize_instance(sender, existing) + except sender.DoesNotExist: instance.__audit_old__ = None else: instance.__audit_old__ = None -@receiver(post_save, sender=Entry) + +@receiver(post_save) def _audit_entry_save(sender, instance, created, **kwargs): - from .models_audit import AuditLog + if not _is_core_entry(sender): + return + from .models_audit import AuditLog # local import to avoid circulars if created: AuditLog.objects.create( entry_id=instance.id, action=AuditLog.ACTION_CREATE, username=get_current_username(), - changes={"__created__": _serialize_entry(instance)}, + changes={"__created__": _serialize_instance(sender, instance)}, ) else: old = getattr(instance, "__audit_old__", None) - new = _serialize_entry(instance) + new = _serialize_instance(sender, instance) if old is None: changes = {"__created__": new} action = AuditLog.ACTION_CREATE else: diffs = {} - for field in _TRACKED: + for field in _tracked_field_names(sender): ov = old.get(field, "") nv = new.get(field, "") if ov != nv: @@ -110,12 +120,15 @@ def _audit_entry_save(sender, instance, created, **kwargs): changes=changes, ) -@receiver(post_delete, sender=Entry) + +@receiver(post_delete) def _audit_entry_delete(sender, instance, **kwargs): + if not _is_core_entry(sender): + return from .models_audit import AuditLog AuditLog.objects.create( entry_id=instance.id, action=AuditLog.ACTION_DELETE, username=get_current_username(), - changes={"__deleted__": _serialize_entry(instance)}, + changes={"__deleted__": _serialize_instance(sender, instance)}, ) \ No newline at end of file