diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index b67b023bd313..c59cb2ab4cf6 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -22,6 +22,7 @@
from django.contrib.admin.utils import (
NestedObjects,
construct_change_message,
+ display_for_value,
flatten_fieldsets,
get_deleted_objects,
lookup_spawns_duplicates,
@@ -74,6 +75,7 @@
SOURCE_MODEL_VAR = "_source_model"
TO_FIELD_VAR = "_to_field"
IS_FACETS_VAR = "_facets"
+EMPTY_VALUE_STRING = "-"
class ShowFacets(enum.Enum):
@@ -1394,10 +1396,13 @@ def response_add(self, request, obj, post_url_continue=None):
current_app=self.admin_site.name,
)
# Add a link to the object's change form if the user can edit the obj.
+ obj_display = display_for_value(str(obj), EMPTY_VALUE_STRING)
if self.has_change_permission(request, obj):
- obj_repr = format_html('{} ', urlquote(obj_url), obj)
+ obj_repr = format_html(
+ '{} ', urlquote(obj_url), obj_display
+ )
else:
- obj_repr = str(obj)
+ obj_repr = obj_display
msg_dict = {
"name": opts.verbose_name,
"obj": obj_repr,
@@ -1547,9 +1552,12 @@ def response_change(self, request, obj):
preserved_filters = self.get_preserved_filters(request)
preserved_qsl = self._get_preserved_qsl(request, preserved_filters)
+ obj_display = display_for_value(str(obj), EMPTY_VALUE_STRING)
msg_dict = {
"name": opts.verbose_name,
- "obj": format_html('{} ', urlquote(request.path), obj),
+ "obj": format_html(
+ '{} ', urlquote(request.path), obj_display
+ ),
}
if "_continue" in request.POST:
msg = format_html(
@@ -1728,7 +1736,7 @@ def response_delete(self, request, obj_display, obj_id):
_("The %(name)s “%(obj)s” was deleted successfully.")
% {
"name": self.opts.verbose_name,
- "obj": obj_display,
+ "obj": display_for_value(str(obj_display), EMPTY_VALUE_STRING),
},
messages.SUCCESS,
)
@@ -1951,7 +1959,9 @@ def _changeform_view(self, request, object_id, form_url, extra_context):
context = {
**self.admin_site.each_context(request),
"title": title % self.opts.verbose_name,
- "subtitle": str(obj) if obj else None,
+ "subtitle": (
+ display_for_value(str(obj), EMPTY_VALUE_STRING) if obj else None
+ ),
"adminform": admin_form,
"object_id": object_id,
"original": obj,
@@ -2252,6 +2262,7 @@ def _delete_view(self, request, object_id, extra_context):
"subtitle": None,
"object_name": object_name,
"object": obj,
+ "escaped_object": display_for_value(str(obj), EMPTY_VALUE_STRING),
"deleted_objects": deleted_objects,
"model_count": dict(model_count).items(),
"perms_lacking": perms_needed,
@@ -2300,7 +2311,8 @@ def history_view(self, request, object_id, extra_context=None):
context = {
**self.admin_site.each_context(request),
- "title": _("Change history: %s") % obj,
+ "title": _("Change history: %s")
+ % display_for_value(str(obj), EMPTY_VALUE_STRING),
"subtitle": None,
"action_list": page_obj,
"page_range": page_range,
diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
index 410bf20da0ff..d160ec06d51d 100644
--- a/django/contrib/admin/sites.py
+++ b/django/contrib/admin/sites.py
@@ -5,6 +5,7 @@
from django.conf import settings
from django.contrib.admin import ModelAdmin, actions
from django.contrib.admin.exceptions import AlreadyRegistered, NotRegistered
+from django.contrib.admin.options import EMPTY_VALUE_STRING
from django.contrib.admin.views.autocomplete import AutocompleteJsonView
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_not_required
@@ -50,7 +51,7 @@ class AdminSite:
enable_nav_sidebar = True
- empty_value_display = "-"
+ empty_value_display = EMPTY_VALUE_STRING
login_form = None
index_template = None
diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html
index d3e546d28d89..e8d3da82bda2 100644
--- a/django/contrib/admin/templates/admin/auth/user/change_password.html
+++ b/django/contrib/admin/templates/admin/auth/user/change_password.html
@@ -1,6 +1,6 @@
{% extends "admin/base_site.html" %}
{% load i18n static %}
-{% load admin_urls %}
+{% load admin_urls admin_filters %}
{% block title %}{% if form.errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
{% block extrastyle %}
@@ -15,7 +15,7 @@
{% translate 'Home' %}
{{ opts.app_config.verbose_name }}
{{ opts.verbose_name_plural|capfirst }}
-{{ original|truncatewords:"18" }}
+{{ original|to_object_display_value|truncatewords:"18" }}
{% if form.user.has_usable_password %}{% translate 'Change password' %}{% else %}{% translate 'Set password' %}{% endif %}
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index 7116f1b8b8e4..2e06fab63f7e 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n admin_urls static admin_modify %}
+{% load i18n admin_urls static admin_modify admin_filters %}
{% block title %}{% if errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
{% block extrahead %}{{ block.super }}
@@ -19,7 +19,7 @@
{% translate 'Home' %}
{{ opts.app_config.verbose_name }}
{% if has_view_permission %}{{ opts.verbose_name_plural|capfirst }} {% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
-{% if add %}{% blocktranslate with name=opts.verbose_name %}Add {{ name }}{% endblocktranslate %}{% else %}{{ original|truncatewords:"18" }}{% endif %}
+{% if add %}{% blocktranslate with name=opts.verbose_name %}Add {{ name }}{% endblocktranslate %}{% else %}{{ original|to_object_display_value|truncatewords:"18" }}{% endif %}
{% endblock %}
{% endif %}
diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html
index 1d04008cc081..07823db373f0 100644
--- a/django/contrib/admin/templates/admin/delete_confirmation.html
+++ b/django/contrib/admin/templates/admin/delete_confirmation.html
@@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n admin_urls static %}
+{% load i18n admin_urls static admin_filters %}
{% block extrahead %}
{{ block.super }}
@@ -14,7 +14,7 @@
{% translate 'Home' %}
{{ opts.app_config.verbose_name }}
{{ opts.verbose_name_plural|capfirst }}
-{{ object|truncatewords:"18" }}
+{{ object|to_object_display_value|truncatewords:"18" }}
{% translate 'Delete' %}
{% endblock %}
@@ -22,17 +22,17 @@
{% block content %}
{% if perms_lacking %}
{% block delete_forbidden %}
- {% blocktranslate with escaped_object=object %}Deleting the {{ object_name }} “{{ escaped_object }}” would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktranslate %}
+ {% blocktranslate %}Deleting the {{ object_name }} “{{ escaped_object }}” would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktranslate %}
{{ perms_lacking|unordered_list }}
{% endblock %}
{% elif protected %}
{% block delete_protected %}
- {% blocktranslate with escaped_object=object %}Deleting the {{ object_name }} “{{ escaped_object }}” would require deleting the following protected related objects:{% endblocktranslate %}
+ {% blocktranslate %}Deleting the {{ object_name }} “{{ escaped_object }}” would require deleting the following protected related objects:{% endblocktranslate %}
{{ protected|unordered_list }}
{% endblock %}
{% else %}
{% block delete_confirm %}
- {% blocktranslate with escaped_object=object %}Are you sure you want to delete the {{ object_name }} “{{ escaped_object }}”? All of the following related items will be deleted:{% endblocktranslate %}
+ {% blocktranslate %}Are you sure you want to delete the {{ object_name }} “{{ escaped_object }}”? All of the following related items will be deleted:{% endblocktranslate %}
{% include "admin/includes/object_delete_summary.html" %}
{% translate "Objects" %}
{{ deleted_objects|unordered_list }}
diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html
index 502515a8f532..6f39d375ebab 100644
--- a/django/contrib/admin/templates/admin/index.html
+++ b/django/contrib/admin/templates/admin/index.html
@@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n static %}
+{% load i18n static admin_filters %}
{% block extrastyle %}{{ block.super }} {% endblock %}
@@ -32,9 +32,9 @@ {% translate 'My actions' %}
{% if entry.is_addition %}{% translate 'Added:' %}{% elif entry.is_change %}{% translate 'Changed:' %}{% elif entry.is_deletion %}{% translate 'Deleted:' %}{% endif %}
{% if entry.is_deletion or not entry.get_admin_url %}
- {{ entry.object_repr }}
+ {{ entry.object_repr|to_object_display_value }}
{% else %}
- {{ entry.object_repr }}
+ {{ entry.object_repr|to_object_display_value }}
{% endif %}
{% if entry.content_type %}
diff --git a/django/contrib/admin/templates/admin/object_history.html b/django/contrib/admin/templates/admin/object_history.html
index 130232666f23..108483a53d08 100644
--- a/django/contrib/admin/templates/admin/object_history.html
+++ b/django/contrib/admin/templates/admin/object_history.html
@@ -1,12 +1,12 @@
{% extends "admin/base_site.html" %}
-{% load i18n admin_urls %}
+{% load i18n admin_urls admin_filters %}
{% block breadcrumbs %}
{% translate 'Home' %}
{{ opts.app_config.verbose_name }}
{{ module_name }}
-{{ object|truncatewords:"18" }}
+{{ object|to_object_display_value|truncatewords:"18" }}
{% translate 'History' %}
{% endblock %}
diff --git a/django/contrib/admin/templatetags/admin_filters.py b/django/contrib/admin/templatetags/admin_filters.py
new file mode 100644
index 000000000000..d0b17970a696
--- /dev/null
+++ b/django/contrib/admin/templatetags/admin_filters.py
@@ -0,0 +1,12 @@
+from django import template
+from django.contrib.admin.options import EMPTY_VALUE_STRING
+from django.contrib.admin.utils import display_for_value
+from django.template.defaultfilters import stringfilter
+
+register = template.Library()
+
+
+@register.filter
+@stringfilter
+def to_object_display_value(value):
+ return display_for_value(str(value), EMPTY_VALUE_STRING)
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index 3aa937c787a6..a6adafabbefb 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -226,8 +226,6 @@ def link_in_col(is_first, field_name, cl):
empty_value_display = getattr(
attr, "empty_value_display", empty_value_display
)
- if isinstance(value, str) and value.strip() == "":
- value = ""
if f is None or f.auto_created:
if field_name == "action_checkbox":
row_classes = ["action-checkbox"]
diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py
index e21a6102b590..e604a2775030 100644
--- a/django/contrib/admin/utils.py
+++ b/django/contrib/admin/utils.py
@@ -18,6 +18,7 @@
from django.utils.hashable import make_hashable
from django.utils.html import format_html
from django.utils.regex_helper import _lazy_re_compile
+from django.utils.safestring import SafeString
from django.utils.text import capfirst
from django.utils.translation import ngettext
from django.utils.translation import override as translation_override
@@ -131,6 +132,8 @@ def get_deleted_objects(objs, request, admin_site):
Return a nested list of strings suitable for display in the
template with the ``unordered_list`` filter.
"""
+ from django.contrib.admin.options import EMPTY_VALUE_STRING
+
try:
obj = objs[0]
except IndexError:
@@ -164,8 +167,12 @@ def format_callback(obj):
return no_edit_link
# Display a link to the admin page.
+ obj_display = display_for_value(str(obj), EMPTY_VALUE_STRING)
return format_html(
- '{}: {} ', capfirst(opts.verbose_name), admin_url, obj
+ '{}: {} ',
+ capfirst(opts.verbose_name),
+ admin_url,
+ obj_display,
)
else:
# Don't display link to edit, because it either has no
@@ -468,7 +475,9 @@ def display_for_value(value, empty_value_display, boolean=False):
if boolean:
return _boolean_icon(value)
- elif value in EMPTY_VALUES:
+ if isinstance(value, str) and not isinstance(value, SafeString):
+ value = value.strip()
+ if value in EMPTY_VALUES:
return empty_value_display
elif isinstance(value, bool):
return str(value)
diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py
index ea484e336e6a..39d857be5934 100644
--- a/django/db/backends/oracle/features.py
+++ b/django/db/backends/oracle/features.py
@@ -85,6 +85,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
"annotations.tests.NonAggregateAnnotationTestCase.test_custom_functions",
"annotations.tests.NonAggregateAnnotationTestCase."
"test_custom_functions_can_ref_other_functions",
+ # A bug in Django with respect to unioning ordered querysets (#36938).
+ "queries.test_qs_combinators.QuerySetSetOperationTests."
+ "test_count_union_with_select_related_in_values",
}
insert_test_table_with_defaults = (
"INSERT INTO {} VALUES (DEFAULT, DEFAULT, DEFAULT)"
diff --git a/docs/releases/6.0.3.txt b/docs/releases/6.0.3.txt
index ddd853cd49a0..1dff197d066b 100644
--- a/docs/releases/6.0.3.txt
+++ b/docs/releases/6.0.3.txt
@@ -11,3 +11,7 @@ Bugfixes
* Fixed :exc:`NameError` when inspecting functions making use of deferred
annotations in Python 3.14 (:ticket:`36903`).
+
+* Fixed :exc:`AttributeError` when subclassing builtin lookups and neglecting
+ to :ref:`override` ``as_sql()`` to accept any sequence
+ (:ticket:`36934`).
diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt
index bfad64e48580..1697a095c9bf 100644
--- a/docs/releases/6.0.txt
+++ b/docs/releases/6.0.txt
@@ -454,6 +454,8 @@ If you haven't dealt with this warning by now, add
settings, or ``default_auto_field = 'django.db.models.AutoField'`` to an app's
``AppConfig``, as needed.
+.. _tuple-for-params:
+
Custom ORM expressions should return params as a tuple
------------------------------------------------------
diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py
index b067bc96609f..f0518494490d 100644
--- a/tests/admin_changelist/tests.py
+++ b/tests/admin_changelist/tests.py
@@ -1193,17 +1193,6 @@ def test_link_field_display_links(self):
"http://blues_history.com" % g.pk,
)
- def test_blank_str_display_links(self):
- self.client.force_login(self.superuser)
- gc = GrandChild.objects.create(name=" ")
- response = self.client.get(
- reverse("admin:admin_changelist_grandchild_changelist")
- )
- self.assertContains(
- response,
- '- ' % gc.pk,
- )
-
def test_clear_all_filters_link(self):
self.client.force_login(self.superuser)
url = reverse("admin:auth_user_changelist")
diff --git a/tests/admin_utils/tests.py b/tests/admin_utils/tests.py
index 81c6f495f8ef..eced9d206e99 100644
--- a/tests/admin_utils/tests.py
+++ b/tests/admin_utils/tests.py
@@ -313,6 +313,19 @@ def test_list_display_for_value_empty(self):
display_value = display_for_value(value, self.empty_value)
self.assertEqual(display_value, self.empty_value)
+ def test_list_display_for_value_consecutive_whitespace(self):
+ cases = [
+ (" ", "-empty-"),
+ (" cheeze", "cheeze"),
+ ("pizza ", "pizza"),
+ (" chicken ", "chicken"),
+ (mark_safe(" soy chicken "), " soy chicken "),
+ ]
+ for value, expect_display_value in cases:
+ with self.subTest(value=value):
+ display_value = display_for_value(value, self.empty_value)
+ self.assertEqual(display_value, expect_display_value)
+
def test_label_for_field(self):
"""
Tests for label_for_field
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index 8e8f7a32cc00..6f7cd79e5048 100644
--- a/tests/admin_views/admin.py
+++ b/tests/admin_views/admin.py
@@ -709,6 +709,8 @@ class CoverLetterAdmin(admin.ModelAdmin):
For testing fix for ticket #14529.
"""
+ formfield_overrides = {models.CharField: {"strip": False}}
+
def get_queryset(self, request):
return super().get_queryset(request).defer("date_written")
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index fa9d9a2dc6f5..ea657fbf9fff 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -2862,7 +2862,9 @@ def test_change_view(self):
self.assertContains(response, "Select article to view ")
self.assertEqual(response.context["title"], "Select article to view")
response = self.client.get(article_change_url)
- self.assertContains(response, "View article | Django site admin ")
+ self.assertContains(
+ response, "- | View article | Django site admin "
+ )
self.assertContains(response, "View article ")
self.assertContains(response, "Extra form field: ")
self.assertContains(
@@ -2891,7 +2893,7 @@ def test_change_view(self):
self.assertEqual(response.context["title"], "Change article")
self.assertContains(
response,
- "Change article | Django site admin ",
+ "- | Change article | Django site admin ",
)
self.assertContains(response, "Change article ")
post = self.client.post(article_change_url, change_dict)
@@ -3016,7 +3018,9 @@ def test_change_view_without_object_change_permission(self):
self.client.force_login(self.viewuser)
response = self.client.get(change_url)
self.assertEqual(response.context["title"], "View article")
- self.assertContains(response, "View article | Django site admin ")
+ self.assertContains(
+ response, "- | View article | Django site admin "
+ )
self.assertContains(response, "View article ")
self.assertContains(
response,
@@ -3608,6 +3612,180 @@ def test_post_save_message_no_forbidden_links_visible(self):
)
+@override_settings(ROOT_URLCONF="admin_views.urls")
+class AdminConsecutiveWhiteSpaceObjectDisplayTest(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.user = User.objects.create_superuser(
+ username=" ", password="secret", email="super@example.com"
+ )
+ cls.obj = CoverLetter.objects.create(author=" ")
+ cls.change_link = reverse(
+ "admin:admin_views_coverletter_change", args=(cls.obj.pk,)
+ )
+
+ def setUp(self):
+ self.client.force_login(self.user)
+
+ def test_display_consecutive_whitespace_object_in_breadcrumbs(self):
+ user_change_link = reverse("admin:auth_user_change", args=(self.user.pk,))
+ cases = [
+ (
+ self.change_link,
+ ''
+ 'Cover letters - ',
+ ),
+ (
+ reverse("admin:admin_views_coverletter_delete", args=(self.obj.pk,)),
+ f'- '
+ "Delete ",
+ ),
+ (
+ reverse("admin:admin_views_coverletter_history", args=(self.obj.pk,)),
+ f'- '
+ "History ",
+ ),
+ (
+ reverse("admin:auth_user_password_change", args=(self.user.pk,)),
+ f'- '
+ "Change password ",
+ ),
+ ]
+ for url, expected_breadcrumbs in cases:
+ with self.subTest(url=url, expected_breadcrumbs=expected_breadcrumbs):
+ response = self.client.get(url)
+ self.assertContains(response, expected_breadcrumbs, html=True)
+
+ def test_display_consecutive_whitespace_object_in_delete_confirmation_page(self):
+ response = self.client.get(
+ reverse("admin:admin_views_coverletter_delete", args=(self.obj.pk,))
+ )
+ self.assertContains(
+ response,
+ "Are you sure you want to delete the cover letter “-”?",
+ )
+
+ # delete protected case
+ q = Question.objects.create(question=" ")
+ Answer.objects.create(question=q, answer="Because.")
+ response = self.client.get(
+ reverse("admin:admin_views_question_delete", args=(q.pk,))
+ )
+ self.assertContains(
+ response,
+ "Deleting the question “-” would require deleting the following protected "
+ "related objects",
+ )
+
+ # delete forbidden case
+ no_perms_user = User.objects.create_user(
+ username="no-perm", password="secret", is_staff=True
+ )
+ no_perms_user.user_permissions.add(
+ get_perm(Question, get_permission_codename("view", Question._meta))
+ )
+ no_perms_user.user_permissions.add(
+ get_perm(Question, get_permission_codename("delete", Question._meta))
+ )
+ self.client.force_login(no_perms_user)
+ response = self.client.get(
+ reverse("admin:admin_views_question_delete", args=(q.pk,))
+ )
+ self.assertContains(
+ response,
+ "Deleting the question “-” would result in deleting related objects, "
+ "but your account doesn't have permission to delete "
+ "the following types of objects",
+ )
+
+ def test_display_consecutive_whitespace_object_in_changelist(self):
+ response = self.client.get(reverse("admin:admin_views_coverletter_changelist"))
+ self.assertContains(response, f'- ')
+
+ def test_display_consecutive_whitespace_object_in_deleted_object(self):
+ response = self.client.get(
+ reverse("admin:admin_views_coverletter_delete", args=(self.obj.pk,))
+ )
+ self.assertContains(
+ response,
+ '',
+ html=True,
+ )
+
+ def test_display_consecutive_whitespace_object_in_recent_action(self):
+ for action in [ADDITION, DELETION]:
+ LogEntry.objects.log_actions(
+ user_id=self.user.pk,
+ queryset=[self.obj],
+ action_flag=action,
+ change_message=[],
+ single_object=True,
+ )
+
+ response = self.client.get(reverse("admin:index"))
+ self.assertContains(
+ response,
+ 'Added: '
+ f'- '
+ "Cover letter ",
+ html=True,
+ )
+ self.assertContains(
+ response,
+ ''
+ 'Deleted: -'
+ 'Cover letter ',
+ html=True,
+ )
+
+ def test_display_consecutive_whitespace_object_in_messages(self):
+ buttons = ["_save", "_continue", "_addanother"]
+ for button in buttons:
+ body = {"author": self.obj.author, button: "1"}
+ with self.subTest(obj=self.obj, button=button):
+ response = self.client.post(
+ reverse("admin:admin_views_coverletter_add"), body, follow=True
+ )
+ latest_cl = CoverLetter.objects.latest("id")
+ change_link = reverse(
+ "admin:admin_views_coverletter_change", args=(latest_cl.pk,)
+ )
+ self.assertContains(
+ response,
+ f'The cover letter “- ” '
+ "was added successfully.",
+ )
+ response = self.client.post(
+ reverse(
+ "admin:admin_views_coverletter_change", args=(latest_cl.pk,)
+ ),
+ {**body, "author": " "},
+ follow=True,
+ )
+ self.assertContains(
+ response,
+ f'The cover letter “- ” '
+ "was changed successfully.",
+ )
+
+ new_obj = CoverLetter.objects.create(author=self.obj.author)
+ response = self.client.post(
+ reverse("admin:admin_views_coverletter_delete", args=(new_obj.pk,)),
+ {"post": "yes"},
+ follow=True,
+ )
+ self.assertContains(response, "The cover letter “-” was deleted successfully.")
+
+ def test_display_consecutive_whitespace_object_in_sub_title(self):
+ response = self.client.get(self.change_link)
+ self.assertContains(response, "- ")
+ response = self.client.get(
+ reverse("admin:admin_views_coverletter_history", args=(self.obj.pk,))
+ )
+ self.assertContains(response, "Change history: - ")
+
+
@override_settings(
ROOT_URLCONF="admin_views.urls",
TEMPLATES=[