Skip to content

Commit b8c1a0c

Browse files
authored
Merge pull request #1128 from raunaq-sailo/feat/adding-history-diff
Add history diff column to admin change history table
2 parents 4d39103 + 733f4e0 commit b8c1a0c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1921
-456
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ repos:
44
rev: 1.7.8
55
hooks:
66
- id: bandit
7-
args:
8-
- "-x *test*.py"
7+
exclude: /.*tests/
98

109
- repo: https://github.com/psf/black-pre-commit-mirror
1110
rev: 24.3.0

CHANGES.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ Unreleased
55
----------
66

77
- Support custom History ``Manager`` and ``QuerySet`` classes (gh-1280)
8+
- Renamed the (previously internal) admin template
9+
``simple_history/_object_history_list.html`` to
10+
``simple_history/object_history_list.html``, and added the field
11+
``SimpleHistoryAdmin.object_history_list_template`` for overriding it (gh-1128)
12+
- Deprecated the undocumented template tag ``simple_history_admin_list.display_list()``;
13+
it will be removed in version 3.8 (gh-1128)
14+
- Added ``SimpleHistoryAdmin.get_history_queryset()`` for overriding which ``QuerySet``
15+
is used to list the historical records (gh-1128)
16+
- Added ``SimpleHistoryAdmin.get_history_list_display()`` which returns
17+
``history_list_display`` by default, and made the latter into an actual field (gh-1128)
18+
- ``ModelDelta`` and ``ModelChange`` (in ``simple_history.models``) are now immutable
19+
dataclasses; their signatures remain unchanged (gh-1128)
20+
- ``ModelDelta``'s ``changes`` and ``changed_fields`` are now sorted alphabetically by
21+
field name. Also, if ``ModelChange`` is for an M2M field, its ``old`` and ``new``
22+
lists are sorted by the related object. This should help prevent flaky tests. (gh-1128)
23+
- ``diff_against()`` has a new keyword argument, ``foreign_keys_are_objs``;
24+
see usage in the docs under "History Diffing" (gh-1128)
25+
- Added a "Changes" column to ``SimpleHistoryAdmin``'s object history table, listing
26+
the changes between each historical record of the object; see the docs under
27+
"Customizing the History Admin Templates" for overriding its template context (gh-1128)
828

929
3.5.0 (2024-02-19)
1030
------------------

docs/admin.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,43 @@ admin class
6969
7070
.. image:: screens/5_history_list_display.png
7171

72+
73+
Customizing the History Admin Templates
74+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
75+
76+
If you'd like to customize the HTML of ``SimpleHistoryAdmin``'s object history pages,
77+
you can override the following attributes with the names of your own templates:
78+
79+
- ``object_history_template``: The main object history page, which includes (inserts)
80+
``object_history_list_template``.
81+
- ``object_history_list_template``: The table listing an object's historical records and
82+
the changes made between them.
83+
- ``object_history_form_template``: The form pre-filled with the details of an object's
84+
historical record, which also allows you to revert the object to a previous version.
85+
86+
If you'd like to only customize certain parts of the mentioned templates, look for
87+
``block`` template tags in the source code that you can override - like the
88+
``history_delta_changes`` block in ``simple_history/object_history_list.html``,
89+
which lists the changes made between each historical record.
90+
91+
Customizing Context
92+
^^^^^^^^^^^^^^^^^^^
93+
94+
You can also customize the template context by overriding the following methods:
95+
96+
- ``render_history_view()``: Called by both ``history_view()`` and
97+
``history_form_view()`` before the templates are rendered. Customize the context by
98+
changing the ``context`` parameter.
99+
- ``history_view()``: Returns a rendered ``object_history_template``.
100+
Inject context by calling the super method with the ``extra_context`` argument.
101+
- ``get_historical_record_context_helper()``: Returns an instance of
102+
``simple_history.template_utils.HistoricalRecordContextHelper`` that's used to format
103+
some template context for each historical record displayed through ``history_view()``.
104+
Customize the context by extending the mentioned class and overriding its methods.
105+
- ``history_form_view()``: Returns a rendered ``object_history_form_template``.
106+
Inject context by calling the super method with the ``extra_context`` argument.
107+
108+
72109
Disabling the option to revert an object
73110
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
74111

docs/historical_model.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,6 @@ You will see the many to many changes when diffing between two historical record
554554
informal = Category.objects.create(name="informal questions")
555555
official = Category.objects.create(name="official questions")
556556
p = Poll.objects.create(question="what's up?")
557-
p.save()
558557
p.categories.add(informal, official)
559558
p.categories.remove(informal)
560559

docs/history_diffing.rst

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,110 @@
11
History Diffing
22
===============
33

4-
When you have two instances of the same ``HistoricalRecord`` (such as the ``HistoricalPoll`` example above),
5-
you can perform diffs to see what changed. This will result in a ``ModelDelta`` containing the following properties:
4+
When you have two instances of the same historical model
5+
(such as the ``HistoricalPoll`` example above),
6+
you can perform a diff using the ``diff_against()`` method to see what changed.
7+
This will return a ``ModelDelta`` object with the following attributes:
68

7-
1. A list with each field changed between the two historical records
8-
2. A list with the names of all fields that incurred changes from one record to the other
9-
3. the old and new records.
9+
- ``old_record`` and ``new_record``: The old and new history records
10+
- ``changed_fields``: A list of the names of all fields that were changed between
11+
``old_record`` and ``new_record``, in alphabetical order
12+
- ``changes``: A list of ``ModelChange`` objects - one for each field in
13+
``changed_fields``, in the same order.
14+
These objects have the following attributes:
1015

11-
This may be useful when you want to construct timelines and need to get only the model modifications.
16+
- ``field``: The name of the changed field
17+
(this name is equal to the corresponding field in ``changed_fields``)
18+
- ``old`` and ``new``: The old and new values of the changed field
19+
20+
- For many-to-many fields, these values will be lists of dicts from the through
21+
model field names to the primary keys of the through model's related objects.
22+
The lists are sorted by the value of the many-to-many related object.
23+
24+
This may be useful when you want to construct timelines and need to get only
25+
the model modifications.
1226

1327
.. code-block:: python
1428
15-
p = Poll.objects.create(question="what's up?")
16-
p.question = "what's up, man?"
17-
p.save()
29+
poll = Poll.objects.create(question="what's up?")
30+
poll.question = "what's up, man?"
31+
poll.save()
1832
19-
new_record, old_record = p.history.all()
33+
new_record, old_record = poll.history.all()
2034
delta = new_record.diff_against(old_record)
2135
for change in delta.changes:
22-
print("{} changed from {} to {}".format(change.field, change.old, change.new))
36+
print(f"'{change.field}' changed from '{change.old}' to '{change.new}'")
37+
38+
# Output:
39+
# 'question' changed from 'what's up?' to 'what's up, man?'
40+
41+
``diff_against()`` also accepts the following additional arguments:
42+
43+
- ``excluded_fields`` and ``included_fields``: These can be used to either explicitly
44+
exclude or include fields from being diffed, respectively.
45+
- ``foreign_keys_are_objs``:
46+
47+
- If ``False`` (default): The diff will only contain the raw primary keys of any
48+
``ForeignKey`` fields.
49+
- If ``True``: The diff will contain the actual related model objects instead of just
50+
the primary keys.
51+
Deleted related objects (both foreign key objects and many-to-many objects)
52+
will be instances of ``DeletedObject``, which only contain a ``model`` field with a
53+
reference to the deleted object's model, as well as a ``pk`` field with the value of
54+
the deleted object's primary key.
55+
56+
Note that this will add extra database queries for each related field that's been
57+
changed - as long as the related objects have not been prefetched
58+
(using e.g. ``select_related()``).
59+
60+
A couple examples showing the difference:
61+
62+
.. code-block:: python
63+
64+
# --- Effect on foreign key fields ---
65+
66+
whats_up = Poll.objects.create(pk=15, name="what's up?")
67+
still_around = Poll.objects.create(pk=31, name="still around?")
68+
69+
choice = Choice.objects.create(poll=whats_up)
70+
choice.poll = still_around
71+
choice.save()
72+
73+
new, old = choice.history.all()
74+
75+
default_delta = new.diff_against(old)
76+
# Printing the changes of `default_delta` will output:
77+
# 'poll' changed from '15' to '31'
78+
79+
delta_with_objs = new.diff_against(old, foreign_keys_are_objs=True)
80+
# Printing the changes of `delta_with_objs` will output:
81+
# 'poll' changed from 'what's up?' to 'still around?'
82+
83+
# Deleting all the polls:
84+
Poll.objects.all().delete()
85+
delta_with_objs = new.diff_against(old, foreign_keys_are_objs=True)
86+
# Printing the changes of `delta_with_objs` will now output:
87+
# 'poll' changed from 'Deleted poll (pk=15)' to 'Deleted poll (pk=31)'
88+
89+
90+
# --- Effect on many-to-many fields ---
91+
92+
informal = Category.objects.create(pk=63, name="informal questions")
93+
whats_up.categories.add(informal)
94+
95+
new = whats_up.history.latest()
96+
old = new.prev_record
97+
98+
default_delta = new.diff_against(old)
99+
# Printing the changes of `default_delta` will output:
100+
# 'categories' changed from [] to [{'poll': 15, 'category': 63}]
101+
102+
delta_with_objs = new.diff_against(old, foreign_keys_are_objs=True)
103+
# Printing the changes of `delta_with_objs` will output:
104+
# 'categories' changed from [] to [{'poll': <Poll: what's up?>, 'category': <Category: informal questions>}]
23105
24-
``diff_against`` also accepts 2 arguments ``excluded_fields`` and ``included_fields`` to either explicitly include or exclude fields from being diffed.
106+
# Deleting all the categories:
107+
Category.objects.all().delete()
108+
delta_with_objs = new.diff_against(old, foreign_keys_are_objs=True)
109+
# Printing the changes of `delta_with_objs` will now output:
110+
# 'categories' changed from [] to [{'poll': <Poll: what's up?>, 'category': DeletedObject(model=<class 'models.Category'>, pk=63)}]

docs/screens/10_revert_disabled.png

-70.8 KB
Loading

docs/screens/1_poll_history.png

-79.6 KB
Loading

docs/screens/2_revert.png

-91.3 KB
Loading

docs/screens/3_poll_reverted.png

-71.9 KB
Loading
-86.2 KB
Loading

0 commit comments

Comments
 (0)