python 50 lines · 8 steps

Tracking field changes in a Django model

An Order model snapshots audited fields on load so save() can log exactly what changed.

Explained by highlit
1from django.db import models
2 
3 
4class Order(models.Model):
5 STATUS_CHOICES = [
6 ("pending", "Pending"),
7 ("paid", "Paid"),
8 ("shipped", "Shipped"),
9 ("cancelled", "Cancelled"),
10 ]
11 
12 reference = models.CharField(max_length=32, unique=True)
13 status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="pending")
14 total = models.DecimalField(max_digits=10, decimal_places=2)
15 
16 AUDITED_FIELDS = ("status", "total")
17 
18 def __init__(self, *args, **kwargs):
19 super().__init__(*args, **kwargs)
20 self._snapshot = self._current_state()
21 
22 def _current_state(self):
23 return {field: getattr(self, field) for field in self.AUDITED_FIELDS}
24 
25 def changed_fields(self):
26 current = self._current_state()
27 return {
28 field: (self._snapshot[field], current[field])
29 for field in self.AUDITED_FIELDS
30 if self._snapshot[field] != current[field]
31 }
32 
33 def save(self, *args, **kwargs):
34 is_new = self._state.adding
35 changes = self.changed_fields()
36 
37 super().save(*args, **kwargs)
38 
39 if not is_new and changes:
40 AuditLog.objects.bulk_create([
41 AuditLog(
42 content_object=self,
43 field=field,
44 old_value=str(old),
45 new_value=str(new),
46 )
47 for field, (old, new) in changes.items()
48 ])
49 
50 self._snapshot = self._current_state()
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Snapshotting values at load time lets you diff against the current state later without extra queries.
  2. 2Overriding save() around super().save() is the natural hook for before/after side effects.
  3. 3Comparing the snapshot to current state only after a successful save keeps your audit log honest.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Tracking field changes in a Django model — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code