diff --git a/CHANGES.rst b/CHANGES.rst index 0279c0dca..e008716f2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -31,6 +31,7 @@ Unreleased - Dropped support for Django 4.0, which reached end-of-life on 2023-04-01 (gh-1202) - Added support for Django 4.2 (gh-1202) - Made ``bulk_update_with_history()`` return the number of model rows updated (gh-1206) +- Added pagination to ``SimpleHistoryAdmin`` (gh-1220) - Fixed ``HistoryRequestMiddleware`` not cleaning up after itself (i.e. deleting ``HistoricalRecords.context.request``) under some circumstances (gh-1188) - Made ``HistoryRequestMiddleware`` async-capable (gh-1209) diff --git a/docs/admin.rst b/docs/admin.rst index cbec184a1..c98710112 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -36,6 +36,12 @@ An example of admin integration for the ``Poll`` and ``Choice`` models: Changing a history-tracked model from the admin interface will automatically record the user who made the change (see :doc:`/user_tracking`). +Changing the number of historical records shown in the admin history list view +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the history list view of ``SimpleHistoryAdmin`` shows the last 50 records. +You can change this by adding a `history__list_per_page` attribute to the admin class. + Displaying custom columns in the admin history list view ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -49,7 +55,7 @@ By default, the history log displays one line per change containing You can add other columns (for example the object's status to see how it evolved) by adding a ``history_list_display`` array of fields to the -admin class +admin class. .. code-block:: python @@ -62,6 +68,7 @@ admin class list_display = ["id", "name", "status"] history_list_display = ["status"] search_fields = ['name', 'user__username'] + history__list_per_page = 100 admin.site.register(Poll, PollHistoryAdmin) admin.site.register(Choice, SimpleHistoryAdmin) diff --git a/simple_history/admin.py b/simple_history/admin.py index 34e3033fe..fc25029ac 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -6,6 +6,7 @@ from django.contrib.admin.utils import unquote from django.contrib.auth import get_permission_codename, get_user_model from django.core.exceptions import PermissionDenied +from django.core.paginator import Paginator from django.shortcuts import get_object_or_404, render from django.urls import re_path, reverse from django.utils.encoding import force_str @@ -21,6 +22,7 @@ class SimpleHistoryAdmin(admin.ModelAdmin): object_history_template = "simple_history/object_history.html" object_history_form_template = "simple_history/object_history_form.html" + history__list_per_page = 50 def get_urls(self): """Returns the additional urls used by the Reversion admin.""" @@ -60,6 +62,9 @@ def history_view(self, request, object_id, extra_context=None): except action_list.model.DoesNotExist: raise http.Http404 + paginator = Paginator(action_list, self.history__list_per_page) + action_list_page = paginator.get_page(request.GET.get("page")) + if not self.has_view_history_or_change_history_permission(request, obj): raise PermissionDenied @@ -67,7 +72,7 @@ def history_view(self, request, object_id, extra_context=None): for history_list_entry in history_list_display: value_for_entry = getattr(self, history_list_entry, None) if value_for_entry and callable(value_for_entry): - for list_entry in action_list: + for list_entry in action_list_page.object_list: setattr(list_entry, history_list_entry, value_for_entry(list_entry)) content_type = self.content_type_model_cls.objects.get_for_model( @@ -80,7 +85,7 @@ def history_view(self, request, object_id, extra_context=None): ) context = { "title": self.history_view_title(request, obj), - "action_list": action_list, + "action_list": action_list_page, "module_name": capfirst(force_str(opts.verbose_name_plural)), "object": obj, "root_path": getattr(self.admin_site, "root_path", None), diff --git a/simple_history/tests/tests/test_admin.py b/simple_history/tests/tests/test_admin.py index 2b441030b..020f2fa21 100644 --- a/simple_history/tests/tests/test_admin.py +++ b/simple_history/tests/tests/test_admin.py @@ -388,6 +388,44 @@ def test_response_change(self): self.assertEqual(response["Location"], "/awesome/url/") + def test_history_view_pagination(self): + """ + Ensure the history_view handles pagination correctly. + """ + # Create a Poll object and make more than 50 changes to ensure pagination + poll = Poll.objects.create(question="what?", pub_date=today) + for i in range(60): + poll.question = f"change_{i}" + poll.save() + + # Verify that there are 60+1 (initial creation) historical records + self.assertEqual(poll.history.count(), 61) + + admin_site = AdminSite() + admin = SimpleHistoryAdmin(Poll, admin_site) + + self.login(superuser=True) + + # Simulate a request to the second page + request = RequestFactory().get("/", {"page": "2"}) + request.user = self.user + + # Patch the render function + with patch("simple_history.admin.render") as mock_render: + admin.history_view(request, str(poll.id)) + + # Ensure the render function was called + self.assertTrue(mock_render.called) + + # Extract context passed to render function + action_list_count = mock_render.call_args[0][2][ + "action_list" + ].object_list.count() + + # Check if only 10 (61 - 50 from the first page) + # objects are present in the context + self.assertEqual(action_list_count, 11) + def test_response_change_change_history_setting_off(self): """ Test the response_change method that it works with a _change_history