1
+ from typing import Any , Sequence
2
+
1
3
from django import http
2
4
from django .apps import apps as django_apps
3
5
from django .conf import settings
6
8
from django .contrib .admin .utils import unquote
7
9
from django .contrib .auth import get_permission_codename , get_user_model
8
10
from django .core .exceptions import PermissionDenied
11
+ from django .db .models import QuerySet
9
12
from django .shortcuts import get_object_or_404 , render
10
13
from django .urls import re_path , reverse
11
14
from django .utils .encoding import force_str
12
15
from django .utils .html import mark_safe
13
16
from django .utils .text import capfirst
14
17
from django .utils .translation import gettext as _
15
18
19
+ from .manager import HistoricalQuerySet , HistoryManager
20
+ from .models import HistoricalChanges
21
+ from .template_utils import HistoricalRecordContextHelper
16
22
from .utils import get_history_manager_for_model , get_history_model_for_model
17
23
18
24
SIMPLE_HISTORY_EDIT = getattr (settings , "SIMPLE_HISTORY_EDIT" , False )
19
25
20
26
21
27
class SimpleHistoryAdmin (admin .ModelAdmin ):
28
+ history_list_display = []
29
+
22
30
object_history_template = "simple_history/object_history.html"
31
+ object_history_list_template = "simple_history/object_history_list.html"
23
32
object_history_form_template = "simple_history/object_history_form.html"
24
33
25
34
def get_urls (self ):
@@ -46,41 +55,43 @@ def history_view(self, request, object_id, extra_context=None):
46
55
pk_name = opts .pk .attname
47
56
history = getattr (model , model ._meta .simple_history_manager_attribute )
48
57
object_id = unquote (object_id )
49
- action_list = history .filter (** {pk_name : object_id })
50
- if not isinstance (history .model .history_user , property ):
51
- # Only select_related when history_user is a ForeignKey (not a property)
52
- action_list = action_list .select_related ("history_user" )
53
- history_list_display = getattr (self , "history_list_display" , [])
58
+ historical_records = self .get_history_queryset (
59
+ request , history , pk_name , object_id
60
+ )
61
+ history_list_display = self .get_history_list_display (request )
54
62
# If no history was found, see whether this object even exists.
55
63
try :
56
64
obj = self .get_queryset (request ).get (** {pk_name : object_id })
57
65
except model .DoesNotExist :
58
66
try :
59
- obj = action_list .latest ("history_date" ).instance
60
- except action_list .model .DoesNotExist :
67
+ obj = historical_records .latest ("history_date" ).instance
68
+ except historical_records .model .DoesNotExist :
61
69
raise http .Http404
62
70
63
71
if not self .has_view_history_or_change_history_permission (request , obj ):
64
72
raise PermissionDenied
65
73
66
- # Set attribute on each action_list entry from admin methods
74
+ # Set attribute on each historical record from admin methods
67
75
for history_list_entry in history_list_display :
68
76
value_for_entry = getattr (self , history_list_entry , None )
69
77
if value_for_entry and callable (value_for_entry ):
70
- for list_entry in action_list :
71
- setattr (list_entry , history_list_entry , value_for_entry (list_entry ))
78
+ for record in historical_records :
79
+ setattr (record , history_list_entry , value_for_entry (record ))
80
+
81
+ self .set_history_delta_changes (request , historical_records )
72
82
73
83
content_type = self .content_type_model_cls .objects .get_for_model (
74
84
get_user_model ()
75
85
)
76
-
77
86
admin_user_view = "admin:{}_{}_change" .format (
78
87
content_type .app_label ,
79
88
content_type .model ,
80
89
)
90
+
81
91
context = {
82
92
"title" : self .history_view_title (request , obj ),
83
- "action_list" : action_list ,
93
+ "object_history_list_template" : self .object_history_list_template ,
94
+ "historical_records" : historical_records ,
84
95
"module_name" : capfirst (force_str (opts .verbose_name_plural )),
85
96
"object" : obj ,
86
97
"root_path" : getattr (self .admin_site , "root_path" , None ),
@@ -97,6 +108,73 @@ def history_view(self, request, object_id, extra_context=None):
97
108
request , self .object_history_template , context , ** extra_kwargs
98
109
)
99
110
111
+ def get_history_queryset (
112
+ self , request , history_manager : HistoryManager , pk_name : str , object_id : Any
113
+ ) -> QuerySet :
114
+ """
115
+ Return a ``QuerySet`` of all historical records that should be listed in the
116
+ ``object_history_list_template`` template.
117
+ This is used by ``history_view()``.
118
+
119
+ :param request:
120
+ :param history_manager:
121
+ :param pk_name: The name of the original model's primary key field.
122
+ :param object_id: The primary key of the object whose history is listed.
123
+ """
124
+ qs : HistoricalQuerySet = history_manager .filter (** {pk_name : object_id })
125
+ if not isinstance (history_manager .model .history_user , property ):
126
+ # Only select_related when history_user is a ForeignKey (not a property)
127
+ qs = qs .select_related ("history_user" )
128
+ # Prefetch related objects to reduce the number of DB queries when diffing
129
+ qs = qs ._select_related_history_tracked_objs ()
130
+ return qs
131
+
132
+ def get_history_list_display (self , request ) -> Sequence [str ]:
133
+ """
134
+ Return a sequence containing the names of additional fields to be displayed on
135
+ the object history page. These can either be fields or properties on the model
136
+ or the history model, or methods on the admin class.
137
+ """
138
+ return self .history_list_display
139
+
140
+ def get_historical_record_context_helper (
141
+ self , request , historical_record : HistoricalChanges
142
+ ) -> HistoricalRecordContextHelper :
143
+ """
144
+ Return an instance of ``HistoricalRecordContextHelper`` for formatting
145
+ the template context for ``historical_record``.
146
+ """
147
+ return HistoricalRecordContextHelper (self .model , historical_record )
148
+
149
+ def set_history_delta_changes (
150
+ self ,
151
+ request ,
152
+ historical_records : Sequence [HistoricalChanges ],
153
+ foreign_keys_are_objs = True ,
154
+ ):
155
+ """
156
+ Add a ``history_delta_changes`` attribute to all historical records
157
+ except the first (oldest) one.
158
+
159
+ :param request:
160
+ :param historical_records:
161
+ :param foreign_keys_are_objs: Passed to ``diff_against()`` when calculating
162
+ the deltas; see its docstring for details.
163
+ """
164
+ previous = None
165
+ for current in historical_records :
166
+ if previous is None :
167
+ previous = current
168
+ continue
169
+ # Related objects should have been prefetched in `get_history_queryset()`
170
+ delta = previous .diff_against (
171
+ current , foreign_keys_are_objs = foreign_keys_are_objs
172
+ )
173
+ helper = self .get_historical_record_context_helper (request , previous )
174
+ previous .history_delta_changes = helper .context_for_delta_changes (delta )
175
+
176
+ previous = current
177
+
100
178
def history_view_title (self , request , obj ):
101
179
if self .revert_disabled (request , obj ) and not SIMPLE_HISTORY_EDIT :
102
180
return _ ("View history: %s" ) % force_str (obj )
0 commit comments