File indexing completed on 2026-04-27 07:41:45
0001 """Entry versioning — snapshots on save.
0002
0003 Pre-save signal on Entry. When content or substantive data changes,
0004 insert an EntryVersion row capturing the PRE-save state (so "version
0005 history" means "what this entry looked like at this time").
0006
0007 Thread-local changed_by: views set it via `set_changed_by(who)` before
0008 saving; defaults to 'unknown'. Skips pure operational-key updates (e.g.
0009 bumping timestamp_modified) via _is_substantive_change().
0010
0011 Matches tjai's signals.py in spirit; simplified.
0012 """
0013 from __future__ import annotations
0014
0015 import threading
0016 import time
0017
0018 from django.db.models.signals import pre_save
0019 from django.dispatch import receiver
0020
0021 from .models import Entry, EntryVersion
0022
0023
0024 _local = threading.local()
0025
0026
0027 def set_changed_by(who: str) -> None:
0028 _local.changed_by = who
0029
0030
0031 def get_changed_by() -> str:
0032 return getattr(_local, 'changed_by', 'unknown')
0033
0034
0035
0036
0037 _OPERATIONAL_KEYS = {'last_seen', 'last_run', 'retry_count', 'cooldown_until'}
0038
0039
0040 def _data_substantive(old: dict | None, new: dict | None) -> bool:
0041 """True if anything outside _OPERATIONAL_KEYS changed between old/new."""
0042 old = dict(old or {})
0043 new = dict(new or {})
0044 for k in _OPERATIONAL_KEYS:
0045 old.pop(k, None)
0046 new.pop(k, None)
0047 return old != new
0048
0049
0050 @receiver(pre_save, sender=Entry)
0051 def snapshot_on_change(sender, instance: Entry, **kwargs):
0052 if not instance.pk:
0053 return
0054 try:
0055 prev = Entry.objects.get(pk=instance.pk)
0056 except Entry.DoesNotExist:
0057 return
0058
0059 content_changed = prev.content != instance.content
0060 title_changed = prev.title != instance.title
0061 data_changed = _data_substantive(prev.data, instance.data)
0062 if not (content_changed or title_changed or data_changed):
0063 return
0064
0065 last = (EntryVersion.objects
0066 .filter(entry=prev)
0067 .order_by('-version_num')
0068 .first())
0069 next_num = (last.version_num + 1) if last else 1
0070
0071 EntryVersion.objects.create(
0072 entry=prev,
0073 version_num=next_num,
0074 title=prev.title,
0075 content=prev.content,
0076 data=prev.data,
0077 changed_by=get_changed_by(),
0078 timestamp=time.time(),
0079 )