diff options
| author | SiHyunLee <antoliny0919@gmail.com> | 2026-02-20 23:43:41 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-20 09:43:41 -0500 |
| commit | 283ea9e9e014adf0013c18700c36b98efa2f0aac (patch) | |
| tree | b8725eb30bae3e1f3e81d9538f74db419f07432e | |
| parent | 96984b9b0f1d88f096985a908ee67dc6f2b9a682 (diff) | |
Fixed #36127 -- Applied default empty display value to links otherwise containing only whitespace in admin.
| -rw-r--r-- | django/contrib/admin/options.py | 24 | ||||
| -rw-r--r-- | django/contrib/admin/sites.py | 3 | ||||
| -rw-r--r-- | django/contrib/admin/templates/admin/auth/user/change_password.html | 4 | ||||
| -rw-r--r-- | django/contrib/admin/templates/admin/change_form.html | 4 | ||||
| -rw-r--r-- | django/contrib/admin/templates/admin/delete_confirmation.html | 10 | ||||
| -rw-r--r-- | django/contrib/admin/templates/admin/index.html | 6 | ||||
| -rw-r--r-- | django/contrib/admin/templates/admin/object_history.html | 4 | ||||
| -rw-r--r-- | django/contrib/admin/templatetags/admin_filters.py | 12 | ||||
| -rw-r--r-- | django/contrib/admin/templatetags/admin_list.py | 2 | ||||
| -rw-r--r-- | django/contrib/admin/utils.py | 13 | ||||
| -rw-r--r-- | tests/admin_changelist/tests.py | 11 | ||||
| -rw-r--r-- | tests/admin_utils/tests.py | 13 | ||||
| -rw-r--r-- | tests/admin_views/admin.py | 2 | ||||
| -rw-r--r-- | tests/admin_views/tests.py | 184 |
14 files changed, 253 insertions, 39 deletions
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index b67b023bd3..c59cb2ab4c 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -22,6 +22,7 @@ from django.contrib.admin.templatetags.admin_urls import add_preserved_filters 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 @@ IS_POPUP_VAR = "_popup" 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 @@ class ModelAdmin(BaseModelAdmin): 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('<a href="{}">{}</a>', urlquote(obj_url), obj) + obj_repr = format_html( + '<a href="{}">{}</a>', 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 @@ class ModelAdmin(BaseModelAdmin): 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('<a href="{}">{}</a>', urlquote(request.path), obj), + "obj": format_html( + '<a href="{}">{}</a>', urlquote(request.path), obj_display + ), } if "_continue" in request.POST: msg = format_html( @@ -1728,7 +1736,7 @@ class ModelAdmin(BaseModelAdmin): _("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 @@ class ModelAdmin(BaseModelAdmin): 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 @@ class ModelAdmin(BaseModelAdmin): "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 @@ class ModelAdmin(BaseModelAdmin): 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 410bf20da0..d160ec06d5 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -5,6 +5,7 @@ from django.apps import apps 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 d3e546d28d..e8d3da82bd 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 @@ <li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li> <li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li> <li><a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a></li> -<li><a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|truncatewords:"18" }}</a></li> +<li><a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|to_object_display_value|truncatewords:"18" }}</a></li> <li aria-current="page">{% if form.user.has_usable_password %}{% translate 'Change password' %}{% else %}{% translate 'Set password' %}{% endif %}</li> </ol> {% endblock %} diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index 7116f1b8b8..2e06fab63f 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 @@ <li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li> <li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li> <li>{% if has_view_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}</li> -<li aria-current="page">{% if add %}{% blocktranslate with name=opts.verbose_name %}Add {{ name }}{% endblocktranslate %}{% else %}{{ original|truncatewords:"18" }}{% endif %}</li> +<li aria-current="page">{% if add %}{% blocktranslate with name=opts.verbose_name %}Add {{ name }}{% endblocktranslate %}{% else %}{{ original|to_object_display_value|truncatewords:"18" }}{% endif %}</li> </ol> {% endblock %} {% endif %} diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html index 1d04008cc0..07823db373 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 @@ <li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li> <li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li> <li><a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a></li> -<li><a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a></li> +<li><a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|to_object_display_value|truncatewords:"18" }}</a></li> <li aria-current="page">{% translate 'Delete' %}</li> </ol> {% endblock %} @@ -22,17 +22,17 @@ {% block content %} {% if perms_lacking %} {% block delete_forbidden %} - <p>{% 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 %}</p> + <p>{% 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 %}</p> <ul id="deleted-objects">{{ perms_lacking|unordered_list }}</ul> {% endblock %} {% elif protected %} {% block delete_protected %} - <p>{% blocktranslate with escaped_object=object %}Deleting the {{ object_name }} “{{ escaped_object }}” would require deleting the following protected related objects:{% endblocktranslate %}</p> + <p>{% blocktranslate %}Deleting the {{ object_name }} “{{ escaped_object }}” would require deleting the following protected related objects:{% endblocktranslate %}</p> <ul id="deleted-objects">{{ protected|unordered_list }}</ul> {% endblock %} {% else %} {% block delete_confirm %} - <p>{% 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 %}</p> + <p>{% blocktranslate %}Are you sure you want to delete the {{ object_name }} “{{ escaped_object }}”? All of the following related items will be deleted:{% endblocktranslate %}</p> {% include "admin/includes/object_delete_summary.html" %} <h2>{% translate "Objects" %}</h2> <ul id="deleted-objects">{{ deleted_objects|unordered_list }}</ul> diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html index 502515a8f5..6f39d375eb 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 }}<link rel="stylesheet" href="{% static "admin/css/dashboard.css" %}">{% endblock %} @@ -32,9 +32,9 @@ <li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}"> <span class="visually-hidden">{% if entry.is_addition %}{% translate 'Added:' %}{% elif entry.is_change %}{% translate 'Changed:' %}{% elif entry.is_deletion %}{% translate 'Deleted:' %}{% endif %}</span> {% if entry.is_deletion or not entry.get_admin_url %} - {{ entry.object_repr }} + {{ entry.object_repr|to_object_display_value }} {% else %} - <a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a> + <a href="{{ entry.get_admin_url }}">{{ entry.object_repr|to_object_display_value }}</a> {% endif %} <br> {% 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 130232666f..108483a53d 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 %} <ol class="breadcrumbs"> <li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li> <li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li> <li><a href="{% url opts|admin_urlname:'changelist' %}">{{ module_name }}</a></li> -<li><a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a></li> +<li><a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|to_object_display_value|truncatewords:"18" }}</a></li> <li aria-current="page">{% translate 'History' %}</li> </ol> {% endblock %} diff --git a/django/contrib/admin/templatetags/admin_filters.py b/django/contrib/admin/templatetags/admin_filters.py new file mode 100644 index 0000000000..d0b17970a6 --- /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 3aa937c787..a6adafabbe 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -226,8 +226,6 @@ def items_for_result(cl, result, form): 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 e21a6102b5..e604a27750 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admin/utils.py @@ -18,6 +18,7 @@ from django.utils import formats, timezone 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 get_deleted_objects(objs, request, admin_site): return no_edit_link # Display a link to the admin page. + obj_display = display_for_value(str(obj), EMPTY_VALUE_STRING) return format_html( - '{}: <a href="{}">{}</a>', capfirst(opts.verbose_name), admin_url, obj + '{}: <a href="{}">{}</a>', + 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/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index b067bc9660..f051849449 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -1193,17 +1193,6 @@ class ChangeListTests(TestCase): "http://blues_history.com</a>" % 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, - '<a href="/admin/admin_changelist/grandchild/%s/change/">-</a>' % 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 81c6f495f8..eced9d206e 100644 --- a/tests/admin_utils/tests.py +++ b/tests/admin_utils/tests.py @@ -313,6 +313,19 @@ class UtilsTests(SimpleTestCase): 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(" <em>soy chicken</em> "), " <em>soy chicken</em> "), + ] + 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 8e8f7a32cc..6f7cd79e50 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 fa9d9a2dc6..ea657fbf9f 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -2862,7 +2862,9 @@ class AdminViewPermissionsTest(TestCase): self.assertContains(response, "<h1>Select article to view</h1>") self.assertEqual(response.context["title"], "Select article to view") response = self.client.get(article_change_url) - self.assertContains(response, "<title>View article | Django site admin</title>") + self.assertContains( + response, "<title>- | View article | Django site admin</title>" + ) self.assertContains(response, "<h1>View article</h1>") self.assertContains(response, "<label>Extra form field:</label>") self.assertContains( @@ -2891,7 +2893,7 @@ class AdminViewPermissionsTest(TestCase): self.assertEqual(response.context["title"], "Change article") self.assertContains( response, - "<title>Change article | Django site admin</title>", + "<title>- | Change article | Django site admin</title>", ) self.assertContains(response, "<h1>Change article</h1>") post = self.client.post(article_change_url, change_dict) @@ -3016,7 +3018,9 @@ class AdminViewPermissionsTest(TestCase): self.client.force_login(self.viewuser) response = self.client.get(change_url) self.assertEqual(response.context["title"], "View article") - self.assertContains(response, "<title>View article | Django site admin</title>") + self.assertContains( + response, "<title>- | View article | Django site admin</title>" + ) self.assertContains(response, "<h1>View article</h1>") self.assertContains( response, @@ -3608,6 +3612,180 @@ class AdminViewPermissionsTest(TestCase): ) +@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, + '<li><a href="/test_admin/admin/admin_views/coverletter/">' + 'Cover letters</a></li><li aria-current="page">-</li>', + ), + ( + reverse("admin:admin_views_coverletter_delete", args=(self.obj.pk,)), + f'<li><a href="{self.change_link}">-</a></li><li aria-current="page">' + "Delete</li>", + ), + ( + reverse("admin:admin_views_coverletter_history", args=(self.obj.pk,)), + f'<li><a href="{self.change_link}">-</a></li><li aria-current="page">' + "History</li>", + ), + ( + reverse("admin:auth_user_password_change", args=(self.user.pk,)), + f'<li><a href="{user_change_link}">-</a></li><li aria-current="page">' + "Change password</li>", + ), + ] + 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'<a href="{self.change_link}">-</a>') + + 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, + '<ul id="deleted-objects">' + f'<li>Cover letter: <a href="{self.change_link}">-</a></li></ul>', + 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, + '<li class="addlink"><span class="visually-hidden">Added:</span>' + f'<a href="{self.change_link}">-</a><br><span class="mini quiet">' + "Cover letter</span></li>", + html=True, + ) + self.assertContains( + response, + '<li class="deletelink">' + '<span class="visually-hidden">Deleted:</span>-' + '<br><span class="mini quiet">Cover letter</span></li>', + 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 “<a href="{change_link}">-</a>” ' + "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 “<a href="{change_link}">-</a>” ' + "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, "<h2>-</h2>") + response = self.client.get( + reverse("admin:admin_views_coverletter_history", args=(self.obj.pk,)) + ) + self.assertContains(response, "<h1>Change history: -</h1>") + + @override_settings( ROOT_URLCONF="admin_views.urls", TEMPLATES=[ |
