Skip to content

Commit 7ea93c5

Browse files
committed
Implement history_order_by_date and get_version_ordering() hook
1 parent abe3535 commit 7ea93c5

3 files changed

Lines changed: 297 additions & 6 deletions

File tree

docs/admin.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ A subclass of ``django.contrib.ModelAdmin`` providing rollback and recovery.
9090
If ``True``, revisions will be displayed with the most recent revision first.
9191

9292

93+
``history_order_by_date = False``
94+
95+
If ``True``, revisions will be ordered by ``date_created`` instead of the numeric version ID.
96+
97+
9398
.. _VersionAdmin_register:
9499

95100
``reversion_register(model, **options)``
@@ -107,3 +112,21 @@ A subclass of ``django.contrib.ModelAdmin`` providing rollback and recovery.
107112

108113
``options``
109114
Registration options, see :ref:`reversion.register() <register>`.
115+
116+
.. _VersionAdmin_get_version_ordering:
117+
118+
``get_version_ordering(model, **options)``
119+
120+
Hook for custom or dynamic ordering of the version queryset, with similar semantics to the built-in ``get_ordering`` method in Django's ``ModelAdmin``. Implementations are expected to return a tuple specifying the field names (relative to the ``Version`` model) for ordering.
121+
122+
The values of ``history_latest_first`` and ``history_order_by_date`` are ignored when overriding this method unless explicitly used in the override logic.
123+
124+
.. code:: python
125+
126+
def get_version_ordering(self, request):
127+
if request.user.is_superuser():
128+
return ("-revision__date_created", "pk")
129+
return ("-pk",)
130+
131+
``request``
132+
The current request.

reversion/admin.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,16 @@ class VersionAdmin(admin.ModelAdmin):
4040

4141
history_latest_first = False
4242

43+
history_order_by_date = False
44+
4345
def reversion_register(self, model, **kwargs):
4446
"""Registers the model with reversion."""
4547
register(model, **kwargs)
4648

49+
def get_version_ordering(self, request):
50+
"""Hook for specifying custom field ordering for the version queryset."""
51+
pass
52+
4753
@contextmanager
4854
def create_revision(self, request):
4955
with create_revision():
@@ -60,11 +66,20 @@ def _reversion_get_template_list(self, template_name):
6066
"reversion/%s" % template_name,
6167
)
6268

63-
def _reversion_order_version_queryset(self, queryset):
69+
def _reversion_order_version_queryset(self, request, queryset):
6470
"""Applies the correct ordering to the given version queryset."""
65-
if not self.history_latest_first:
66-
queryset = queryset.order_by("pk")
67-
return queryset
71+
ordering = self.get_version_ordering(request) or ()
72+
# Default ordering logic should remain here to prevent accidental side effects
73+
# from calling super().get_version_ordering() in subclass implementations
74+
if not ordering:
75+
order_fields = ["pk"]
76+
if self.history_order_by_date:
77+
order_fields.insert(0, "revision__date_created")
78+
# history_latest_first causes order to be reversed on all fields
79+
if self.history_latest_first:
80+
order_fields = [f"-{field}" for field in order_fields]
81+
ordering = tuple(order_fields)
82+
return queryset.order_by(*ordering)
6883

6984
# Messages.
7085

@@ -258,6 +273,7 @@ def recoverlist_view(self, request, extra_context=None):
258273
model = self.model
259274
opts = model._meta
260275
deleted = self._reversion_order_version_queryset(
276+
request,
261277
Version.objects.get_deleted(self.model).select_related("revision")
262278
)
263279
# Set the app name.
@@ -298,10 +314,10 @@ def history_view(self, request, object_id, extra_context=None):
298314
),
299315
}
300316
for version
301-
in self._reversion_order_version_queryset(Version.objects.get_for_object_reference(
317+
in self._reversion_order_version_queryset(request, Version.objects.get_for_object_reference(
302318
self.model,
303319
unquote(object_id), # Underscores in primary key get quoted to "_5F"
304-
).select_related("revision__user"))
320+
).select_related("revision", "revision__user"))
305321
]
306322
# Compile the context.
307323
context = {"action_list": action_list}

tests/test_app/tests/test_admin.py

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import re
2+
from datetime import datetime, timedelta
3+
24
from django.contrib import admin
35
from django.contrib.contenttypes.admin import GenericTabularInline
46
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
57
from django.shortcuts import resolve_url
8+
69
import reversion
710
from reversion.admin import VersionAdmin
811
from reversion.models import Version
@@ -248,6 +251,255 @@ def testHistorylistView(self):
248251
Version.objects.get_for_model(TestModelParent).get().pk,
249252
))
250253

254+
def testHistorylistViewOrderDefault(self):
255+
# Create an object and multiple revisions.
256+
with reversion.create_revision():
257+
obj = TestModelParent.objects.create(name="v1", parent_name="p1")
258+
with reversion.create_revision():
259+
obj.name = "v2"
260+
obj.save()
261+
with reversion.create_revision():
262+
obj.name = "v3"
263+
obj.save()
264+
265+
# Fetch history page.
266+
response = self.client.get(resolve_url("admin:test_app_testmodelparent_history", obj.pk))
267+
content = response.content.decode()
268+
269+
# Compute expected order: default VersionAdmin orders by pk ascending (oldest first).
270+
version_ids = list(Version.objects.get_for_object(obj).values_list("pk", flat=True))
271+
expected_order = sorted(version_ids)
272+
273+
# Build the URLs as rendered in the history list and assert their order.
274+
urls_in_order = [
275+
resolve_url(
276+
"admin:test_app_testmodelparent_revision",
277+
obj.pk,
278+
vid,
279+
)
280+
for vid in expected_order
281+
]
282+
283+
# Ensure each subsequent URL appears later in the content than the previous one.
284+
last_index = -1
285+
for url in urls_in_order:
286+
index = content.find(url)
287+
self.assertNotEqual(index, -1, f"Expected to find {url} in history page")
288+
self.assertGreater(index, last_index, "History list is not ordered by ascending version pk (oldest first)")
289+
last_index = index
290+
291+
292+
class AdminHistoryViewLatestFirstTest(LoginMixin, TestBase):
293+
294+
class TestModelParentAdminLatestFirst(VersionAdmin):
295+
history_latest_first = True
296+
297+
def setUp(self):
298+
super().setUp()
299+
# Register a custom admin with history_latest_first enabled
300+
admin.site.register(TestModelParent, self.TestModelParentAdminLatestFirst)
301+
self.reloadUrls()
302+
303+
def tearDown(self):
304+
super().tearDown()
305+
admin.site.unregister(TestModelParent)
306+
self.reloadUrls()
307+
308+
def testHistorylistViewOrderLatestFirst(self):
309+
# Create an object and multiple revisions.
310+
with reversion.create_revision():
311+
obj = TestModelParent.objects.create(name="v1", parent_name="p1")
312+
with reversion.create_revision():
313+
obj.name = "v2"
314+
obj.save()
315+
with reversion.create_revision():
316+
obj.name = "v3"
317+
obj.save()
318+
319+
# Fetch history page.
320+
response = self.client.get(resolve_url("admin:test_app_testmodelparent_history", obj.pk))
321+
content = response.content.decode()
322+
323+
# Expected order: with history_latest_first=True, versions are ordered by pk descending (newest first).
324+
version_ids = list(Version.objects.get_for_object(obj).values_list("pk", flat=True))
325+
expected_order = sorted(version_ids, reverse=True)
326+
327+
urls_in_order = [
328+
resolve_url("admin:test_app_testmodelparent_revision", obj.pk, vid)
329+
for vid in expected_order
330+
]
331+
332+
last_index = -1
333+
for url in urls_in_order:
334+
index = content.find(url)
335+
self.assertNotEqual(index, -1, f"Expected to find {url} in history page")
336+
self.assertGreater(index, last_index, "History list is not ordered by descending version pk (newest first)")
337+
last_index = index
338+
339+
340+
class AdminHistoryViewOrderByDateTest(LoginMixin, TestBase):
341+
342+
class TestModelParentAdminOrderByDate(VersionAdmin):
343+
history_order_by_date = True
344+
345+
def setUp(self):
346+
super().setUp()
347+
# Register a custom admin with history_order_by_date enabled
348+
admin.site.register(TestModelParent, self.TestModelParentAdminOrderByDate)
349+
self.reloadUrls()
350+
351+
def tearDown(self):
352+
super().tearDown()
353+
admin.site.unregister(TestModelParent)
354+
self.reloadUrls()
355+
356+
def testHistorylistViewOrderByDate(self):
357+
# Create an object and multiple revisions with increasing timestamps.
358+
with reversion.create_revision():
359+
obj = TestModelParent.objects.create(name="v1", parent_name="p1")
360+
# Use an out-of-sequence date to verify correct ordering
361+
reversion.set_date_created(datetime.now() + timedelta(days=1))
362+
with reversion.create_revision():
363+
obj.name = "v2"
364+
obj.save()
365+
with reversion.create_revision():
366+
obj.name = "v3"
367+
obj.save()
368+
369+
# Fetch history page.
370+
response = self.client.get(resolve_url("admin:test_app_testmodelparent_history", obj.pk))
371+
content = response.content.decode()
372+
373+
# Expected order: ordered by revision creation date ascending (oldest date first).
374+
versions = (
375+
Version.objects.get_for_object(obj)
376+
.select_related("revision")
377+
.order_by("revision__date_created", "pk")
378+
)
379+
expected_order = list(versions.values_list("pk", flat=True))
380+
381+
urls_in_order = [
382+
resolve_url("admin:test_app_testmodelparent_revision", obj.pk, vid)
383+
for vid in expected_order
384+
]
385+
386+
last_index = -1
387+
for url in urls_in_order:
388+
index = content.find(url)
389+
self.assertNotEqual(index, -1, f"Expected to find {url} in history page")
390+
self.assertGreater(index, last_index, "History list is not ordered by revision date (oldest first)")
391+
last_index = index
392+
393+
394+
class AdminHistoryViewLatestFirstOrderByDateTest(LoginMixin, TestBase):
395+
396+
class TestModelParentAdminLatestFirstOrderByDate(VersionAdmin):
397+
history_latest_first = True
398+
history_order_by_date = True
399+
400+
def setUp(self):
401+
super().setUp()
402+
# Register a custom admin with both flags enabled
403+
admin.site.register(TestModelParent, self.TestModelParentAdminLatestFirstOrderByDate)
404+
self.reloadUrls()
405+
406+
def tearDown(self):
407+
super().tearDown()
408+
admin.site.unregister(TestModelParent)
409+
self.reloadUrls()
410+
411+
def testHistorylistViewOrderLatestFirstByDate(self):
412+
# Create an object and multiple revisions with increasing timestamps.
413+
with reversion.create_revision():
414+
obj = TestModelParent.objects.create(name="v1", parent_name="p1")
415+
# Use an out-of-sequence date to verify correct ordering
416+
reversion.set_date_created(datetime.now() + timedelta(days=1))
417+
with reversion.create_revision():
418+
obj.name = "v2"
419+
obj.save()
420+
with reversion.create_revision():
421+
obj.name = "v3"
422+
obj.save()
423+
424+
# Fetch history page.
425+
response = self.client.get(resolve_url("admin:test_app_testmodelparent_history", obj.pk))
426+
content = response.content.decode()
427+
428+
# Expected order: ordered by revision creation date descending (newest date first).
429+
versions = (
430+
Version.objects.get_for_object(obj)
431+
.select_related("revision")
432+
.order_by("-revision__date_created", "-pk")
433+
)
434+
expected_order = list(versions.values_list("pk", flat=True))
435+
436+
urls_in_order = [
437+
resolve_url("admin:test_app_testmodelparent_revision", obj.pk, vid)
438+
for vid in expected_order
439+
]
440+
441+
last_index = -1
442+
for url in urls_in_order:
443+
index = content.find(url)
444+
self.assertNotEqual(index, -1, f"Expected to find {url} in history page")
445+
self.assertGreater(index, last_index, "History list is not ordered by revision date (newest first)")
446+
last_index = index
447+
448+
449+
class AdminHistoryViewCustomOrderingTest(LoginMixin, TestBase):
450+
class TestModelParentAdminCustomOrdering(VersionAdmin):
451+
def get_version_ordering(self, request):
452+
return ("revision__comment",)
453+
454+
def setUp(self):
455+
super().setUp()
456+
# Register a custom admin with history_order_by_date enabled
457+
admin.site.register(TestModelParent, self.TestModelParentAdminCustomOrdering)
458+
self.reloadUrls()
459+
460+
def tearDown(self):
461+
super().tearDown()
462+
admin.site.unregister(TestModelParent)
463+
self.reloadUrls()
464+
465+
def testHistorylistViewCustomOrdering(self):
466+
# Create an object and multiple revisions with increasing timestamps.
467+
with reversion.create_revision():
468+
obj = TestModelParent.objects.create(name="v1", parent_name="p1")
469+
reversion.set_comment("B")
470+
with reversion.create_revision():
471+
obj.name = "v2"
472+
obj.save()
473+
reversion.set_comment("A")
474+
with reversion.create_revision():
475+
obj.name = "v3"
476+
obj.save()
477+
reversion.set_comment("C")
478+
479+
# Fetch history page.
480+
response = self.client.get(resolve_url("admin:test_app_testmodelparent_history", obj.pk))
481+
content = response.content.decode()
482+
483+
# Expected order: ordered by revision comment ascending.
484+
versions = (
485+
Version.objects.get_for_object(obj)
486+
.select_related("revision")
487+
.order_by("revision__comment")
488+
)
489+
expected_order = list(versions.values_list("pk", flat=True))
490+
491+
urls_in_order = [
492+
resolve_url("admin:test_app_testmodelparent_revision", obj.pk, vid)
493+
for vid in expected_order
494+
]
495+
496+
last_index = -1
497+
for url in urls_in_order:
498+
index = content.find(url)
499+
self.assertNotEqual(index, -1, f"Expected to find {url} in history page")
500+
self.assertGreater(index, last_index, "History list is not ordered by comment ascending")
501+
last_index = index
502+
251503

252504
class AdminQuotingTest(LoginMixin, AdminMixin, TestBase):
253505

0 commit comments

Comments
 (0)