Audit logging & the snapshot pattern¶
Two related ideas that govern how Midsummer mutates and records changes to historical data.
The snapshot pattern¶
At the moment a paid registration is created, certain derived data is
snapshotted into Registration.data:
The tier’s included merchandise is copied into the submission JSON, with the attendee’s option selections (e.g. shirt size).
The registration level and its price are fixed.
This is intentional. A registration is a record of what someone bought at a
point in time. If an organizer later edits a RegistrationLevel (changes its
price, adds/removes an item), existing registrations are not retroactively
changed — because that would silently rewrite history.
The consequences:
A forgotten tier item, or a later-removed item, has no automatic fix path.
The Merchandise Adjustment admin tool is the audited correction path: it mutates
Registration.dataundertransaction.atomic()+select_for_update(), then writes aRegistrationMerchAdjustmentLogrow.The same “admin tools mutate the snapshot” principle applies to tier upgrades (
RegistrationTierUpgradeAuditLog) and registrant edits/forwards (RegistrationEditLog).
Append-only audit logs¶
Every data-mutating admin surface has a dedicated append-only audit model:
Audit model |
Records |
|---|---|
|
badge number assignments/changes |
|
merch add/remove with item snapshot |
|
tier upgrades |
|
edits to registrant data/form/account + forwards |
|
staff-module mutations |
Each captures who (user), what (action + a small details JSONField),
when, and a resilient reference to the target (e.g. a registration_uuid
string snapshot that survives the registration itself being deleted).
The logging helper convention¶
Every audit surface wraps its write in a silent try/except helper:
def log_registration_edit(event, user, registration, action, details, notes=""):
try:
RegistrationEditLog.objects.create(...)
except Exception:
logger.exception("failed to write audit log") # never raise
Important
A logging failure must never break a successful operation. When you add a
new mutation + audit pair, follow this pattern: run the data mutation and the
log write inside transaction.atomic() so they commit together, but if the log
write itself is the thing that fails, swallow it rather than rolling back the
user’s successful change.
Atomicity & scoped saves¶
Mutating views follow a consistent safety recipe:
with transaction.atomic():re-fetch the row via
.select_for_update()(prevents lost-update races on the JSON field),mutate
data,save(update_fields=['data'])(scoped — don’t clobber unrelated columns),write the audit row.
This mirrors the established pattern in register/views.py (e.g. the tier-
upgrade webhook path). Reuse it rather than inventing a new one.
Where to look¶
Audit models:
register/models.py,staff/models.pyMutation + audit recipe:
register/views.py(merch_adjustment_add/remove,update_registrant,forward_registration,log_registration_edit)Reference option rendering (canonical):
register__merch_summary.py