History Diffing
When you have two instances of the same historical model
(such as the HistoricalPoll
example above),
you can perform a diff using the diff_against()
method to see what changed.
This will return a ModelDelta
object with the following attributes:
old_record
andnew_record
: The old and new history recordschanged_fields
: A list of the names of all fields that were changed betweenold_record
andnew_record
, in alphabetical orderchanges
: A list ofModelChange
objects - one for each field inchanged_fields
, in the same order. These objects have the following attributes:field
: The name of the changed field (this name is equal to the corresponding field inchanged_fields
)old
andnew
: The old and new values of the changed fieldFor many-to-many fields, these values will be lists of dicts from the through model field names to the primary keys of the through model’s related objects. The lists are sorted by the value of the many-to-many related object.
This may be useful when you want to construct timelines and need to get only the model modifications.
poll = Poll.objects.create(question="what's up?")
poll.question = "what's up, man?"
poll.save()
new_record, old_record = poll.history.all()
delta = new_record.diff_against(old_record)
for change in delta.changes:
print(f"'{change.field}' changed from '{change.old}' to '{change.new}'")
# Output:
# 'question' changed from 'what's up?' to 'what's up, man?'
diff_against()
also accepts the following additional arguments:
excluded_fields
andincluded_fields
: These can be used to either explicitly exclude or include fields from being diffed, respectively.foreign_keys_are_objs
:If
False
(default): The diff will only contain the raw primary keys of anyForeignKey
fields.If
True
: The diff will contain the actual related model objects instead of just the primary keys. Deleted related objects (both foreign key objects and many-to-many objects) will be instances ofDeletedObject
, which only contain amodel
field with a reference to the deleted object’s model, as well as apk
field with the value of the deleted object’s primary key.Note that this will add extra database queries for each related field that’s been changed - as long as the related objects have not been prefetched (using e.g.
select_related()
).
A couple examples showing the difference:
# --- Effect on foreign key fields --- whats_up = Poll.objects.create(pk=15, name="what's up?") still_around = Poll.objects.create(pk=31, name="still around?") choice = Choice.objects.create(poll=whats_up) choice.poll = still_around choice.save() new, old = choice.history.all() default_delta = new.diff_against(old) # Printing the changes of `default_delta` will output: # 'poll' changed from '15' to '31' delta_with_objs = new.diff_against(old, foreign_keys_are_objs=True) # Printing the changes of `delta_with_objs` will output: # 'poll' changed from 'what's up?' to 'still around?' # Deleting all the polls: Poll.objects.all().delete() delta_with_objs = new.diff_against(old, foreign_keys_are_objs=True) # Printing the changes of `delta_with_objs` will now output: # 'poll' changed from 'Deleted poll (pk=15)' to 'Deleted poll (pk=31)' # --- Effect on many-to-many fields --- informal = Category.objects.create(pk=63, name="informal questions") whats_up.categories.add(informal) new = whats_up.history.latest() old = new.prev_record default_delta = new.diff_against(old) # Printing the changes of `default_delta` will output: # 'categories' changed from [] to [{'poll': 15, 'category': 63}] delta_with_objs = new.diff_against(old, foreign_keys_are_objs=True) # Printing the changes of `delta_with_objs` will output: # 'categories' changed from [] to [{'poll': <Poll: what's up?>, 'category': <Category: informal questions>}] # Deleting all the categories: Category.objects.all().delete() delta_with_objs = new.diff_against(old, foreign_keys_are_objs=True) # Printing the changes of `delta_with_objs` will now output: # 'categories' changed from [] to [{'poll': <Poll: what's up?>, 'category': DeletedObject(model=<class 'models.Category'>, pk=63)}]