diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index c5968a79ed..100e481229 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1927,6 +1927,7 @@ class ModelAdmin(BaseModelAdmin):
def history_view(self, request, object_id, extra_context=None):
"The 'history' admin view for this model."
from django.contrib.admin.models import LogEntry
+ from django.contrib.admin.views.main import PAGE_VAR
# First check if the user can see this history.
model = self.model
@@ -1945,11 +1946,19 @@ class ModelAdmin(BaseModelAdmin):
content_type=get_content_type_for_model(model)
).select_related().order_by('action_time')
+ paginator = self.get_paginator(request, action_list, 100)
+ page_number = request.GET.get(PAGE_VAR, 1)
+ page_obj = paginator.get_page(page_number)
+ page_range = paginator.get_elided_page_range(page_obj.number)
+
context = {
**self.admin_site.each_context(request),
'title': _('Change history: %s') % obj,
'subtitle': None,
- 'action_list': action_list,
+ 'action_list': page_obj,
+ 'page_range': page_range,
+ 'page_var': PAGE_VAR,
+ 'pagination_required': paginator.count > 100,
'module_name': str(capfirst(opts.verbose_name_plural)),
'object': obj,
'opts': opts,
diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css
index d2e5bb6cfa..c44a265a96 100644
--- a/django/contrib/admin/static/admin/css/base.css
+++ b/django/contrib/admin/static/admin/css/base.css
@@ -774,14 +774,21 @@ a.deletelink:focus, a.deletelink:hover {
/* OBJECT HISTORY */
-table#change-history {
+#change-history table {
width: 100%;
}
-table#change-history tbody th {
+#change-history table tbody th {
width: 16em;
}
+#change-history .paginator {
+ color: var(--body-quiet-color);
+ border-bottom: 1px solid var(--hairline-color);
+ background: var(--body-bg);
+ overflow: hidden;
+}
+
/* PAGE STRUCTURE */
#container {
diff --git a/django/contrib/admin/templates/admin/object_history.html b/django/contrib/admin/templates/admin/object_history.html
index 3015f36c4a..91dad45026 100644
--- a/django/contrib/admin/templates/admin/object_history.html
+++ b/django/contrib/admin/templates/admin/object_history.html
@@ -13,10 +13,10 @@
{% block content %}
-
+
{% if action_list %}
-
+
| {% translate 'Date/time' %} |
@@ -34,6 +34,20 @@
{% endfor %}
+
+ {% if pagination_required %}
+ {% for i in page_range %}
+ {% if i == action_list.paginator.ELLIPSIS %}
+ {{ action_list.paginator.ELLIPSIS }}
+ {% elif i == action_list.number %}
+ {{ i }}
+ {% else %}
+ {{ i }}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+ {{ action_list.paginator.count }} {% if action_list.paginator.count == 1 %}{% translate "entry" %}{% else %}{% translate "entries" %}{% endif %}
+
{% else %}
{% translate 'This object doesn’t have a change history. It probably wasn’t added via this admin site.' %}
{% endif %}
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index ac8f44b765..7b97ee5638 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -2016,6 +2016,10 @@ Other methods
Django view for the page that shows the modification history for a given
model instance.
+ .. versionchanged:: 4.1
+
+ Pagination was added.
+
Unlike the hook-type ``ModelAdmin`` methods detailed in the previous section,
these five methods are in reality designed to be invoked as Django views from
the admin application URL dispatching handler to render the pages that deal
diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index 5f184d2349..ee21c63a38 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -58,6 +58,9 @@ Minor features
subclasses can now control the query string value separator when filtering
for multiple values using the ``__in`` lookup.
+* The admin :meth:`history view `
+ is now paginated.
+
:mod:`django.contrib.admindocs`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/admin_views/test_history_view.py b/tests/admin_views/test_history_view.py
index 277990cc92..c8d678476f 100644
--- a/tests/admin_views/test_history_view.py
+++ b/tests/admin_views/test_history_view.py
@@ -1,5 +1,8 @@
-from django.contrib.admin.models import LogEntry
+from django.contrib.admin.models import CHANGE, LogEntry
+from django.contrib.admin.tests import AdminSeleniumTestCase
from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.core.paginator import Paginator
from django.test import TestCase, override_settings
from django.urls import reverse
@@ -43,3 +46,52 @@ class AdminHistoryViewTests(TestCase):
'nolabel_form_field and not_a_form_field. '
'Changed City verbose_name for city “%s”.' % city
)
+
+
+@override_settings(ROOT_URLCONF='admin_views.urls')
+class SeleniumTests(AdminSeleniumTestCase):
+ available_apps = ['admin_views'] + AdminSeleniumTestCase.available_apps
+
+ def setUp(self):
+ self.superuser = User.objects.create_superuser(
+ username='super', password='secret', email='super@example.com',
+ )
+ content_type_pk = ContentType.objects.get_for_model(User).pk
+ for i in range(1, 1101):
+ LogEntry.objects.log_action(
+ self.superuser.pk,
+ content_type_pk,
+ self.superuser.pk,
+ repr(self.superuser),
+ CHANGE,
+ change_message=f'Changed something {i}',
+ )
+ self.admin_login(
+ username='super', password='secret', login_url=reverse('admin:index'),
+ )
+
+ def test_pagination(self):
+ from selenium.webdriver.common.by import By
+
+ user_history_url = reverse('admin:auth_user_history', args=(self.superuser.pk,))
+ self.selenium.get(self.live_server_url + user_history_url)
+
+ paginator = self.selenium.find_element(By.CSS_SELECTOR, '.paginator')
+ self.assertTrue(paginator.is_displayed())
+ self.assertIn('%s entries' % LogEntry.objects.count(), paginator.text)
+ self.assertIn(str(Paginator.ELLIPSIS), paginator.text)
+ # The current page.
+ current_page_link = self.selenium.find_element(By.CSS_SELECTOR, 'span.this-page')
+ self.assertEqual(current_page_link.text, '1')
+ # The last page.
+ last_page_link = self.selenium.find_element(By.CSS_SELECTOR, '.end')
+ self.assertTrue(last_page_link.text, '20')
+ # Select the second page.
+ pages = paginator.find_elements(By.TAG_NAME, 'a')
+ second_page_link = pages[0]
+ self.assertEqual(second_page_link.text, '2')
+ second_page_link.click()
+ self.assertIn('?p=2', self.selenium.current_url)
+ rows = self.selenium.find_elements(By.CSS_SELECTOR, '#change-history tbody tr')
+ self.assertIn('Changed something 101', rows[0].text)
+ self.assertIn('Changed something 200', rows[-1].text)