Skip to content

Commit 283ea9e

Browse files
authored
Fixed #36127 -- Applied default empty display value to links otherwise containing only whitespace in admin.
1 parent 96984b9 commit 283ea9e

14 files changed

Lines changed: 253 additions & 39 deletions

File tree

django/contrib/admin/options.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from django.contrib.admin.utils import (
2323
NestedObjects,
2424
construct_change_message,
25+
display_for_value,
2526
flatten_fieldsets,
2627
get_deleted_objects,
2728
lookup_spawns_duplicates,
@@ -74,6 +75,7 @@
7475
SOURCE_MODEL_VAR = "_source_model"
7576
TO_FIELD_VAR = "_to_field"
7677
IS_FACETS_VAR = "_facets"
78+
EMPTY_VALUE_STRING = "-"
7779

7880

7981
class ShowFacets(enum.Enum):
@@ -1394,10 +1396,13 @@ def response_add(self, request, obj, post_url_continue=None):
13941396
current_app=self.admin_site.name,
13951397
)
13961398
# Add a link to the object's change form if the user can edit the obj.
1399+
obj_display = display_for_value(str(obj), EMPTY_VALUE_STRING)
13971400
if self.has_change_permission(request, obj):
1398-
obj_repr = format_html('<a href="{}">{}</a>', urlquote(obj_url), obj)
1401+
obj_repr = format_html(
1402+
'<a href="{}">{}</a>', urlquote(obj_url), obj_display
1403+
)
13991404
else:
1400-
obj_repr = str(obj)
1405+
obj_repr = obj_display
14011406
msg_dict = {
14021407
"name": opts.verbose_name,
14031408
"obj": obj_repr,
@@ -1547,9 +1552,12 @@ def response_change(self, request, obj):
15471552
preserved_filters = self.get_preserved_filters(request)
15481553
preserved_qsl = self._get_preserved_qsl(request, preserved_filters)
15491554

1555+
obj_display = display_for_value(str(obj), EMPTY_VALUE_STRING)
15501556
msg_dict = {
15511557
"name": opts.verbose_name,
1552-
"obj": format_html('<a href="{}">{}</a>', urlquote(request.path), obj),
1558+
"obj": format_html(
1559+
'<a href="{}">{}</a>', urlquote(request.path), obj_display
1560+
),
15531561
}
15541562
if "_continue" in request.POST:
15551563
msg = format_html(
@@ -1728,7 +1736,7 @@ def response_delete(self, request, obj_display, obj_id):
17281736
_("The %(name)s “%(obj)s” was deleted successfully.")
17291737
% {
17301738
"name": self.opts.verbose_name,
1731-
"obj": obj_display,
1739+
"obj": display_for_value(str(obj_display), EMPTY_VALUE_STRING),
17321740
},
17331741
messages.SUCCESS,
17341742
)
@@ -1951,7 +1959,9 @@ def _changeform_view(self, request, object_id, form_url, extra_context):
19511959
context = {
19521960
**self.admin_site.each_context(request),
19531961
"title": title % self.opts.verbose_name,
1954-
"subtitle": str(obj) if obj else None,
1962+
"subtitle": (
1963+
display_for_value(str(obj), EMPTY_VALUE_STRING) if obj else None
1964+
),
19551965
"adminform": admin_form,
19561966
"object_id": object_id,
19571967
"original": obj,
@@ -2252,6 +2262,7 @@ def _delete_view(self, request, object_id, extra_context):
22522262
"subtitle": None,
22532263
"object_name": object_name,
22542264
"object": obj,
2265+
"escaped_object": display_for_value(str(obj), EMPTY_VALUE_STRING),
22552266
"deleted_objects": deleted_objects,
22562267
"model_count": dict(model_count).items(),
22572268
"perms_lacking": perms_needed,
@@ -2300,7 +2311,8 @@ def history_view(self, request, object_id, extra_context=None):
23002311

23012312
context = {
23022313
**self.admin_site.each_context(request),
2303-
"title": _("Change history: %s") % obj,
2314+
"title": _("Change history: %s")
2315+
% display_for_value(str(obj), EMPTY_VALUE_STRING),
23042316
"subtitle": None,
23052317
"action_list": page_obj,
23062318
"page_range": page_range,

django/contrib/admin/sites.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.conf import settings
66
from django.contrib.admin import ModelAdmin, actions
77
from django.contrib.admin.exceptions import AlreadyRegistered, NotRegistered
8+
from django.contrib.admin.options import EMPTY_VALUE_STRING
89
from django.contrib.admin.views.autocomplete import AutocompleteJsonView
910
from django.contrib.auth import REDIRECT_FIELD_NAME
1011
from django.contrib.auth.decorators import login_not_required
@@ -50,7 +51,7 @@ class AdminSite:
5051

5152
enable_nav_sidebar = True
5253

53-
empty_value_display = "-"
54+
empty_value_display = EMPTY_VALUE_STRING
5455

5556
login_form = None
5657
index_template = None

django/contrib/admin/templates/admin/auth/user/change_password.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{% extends "admin/base_site.html" %}
22
{% load i18n static %}
3-
{% load admin_urls %}
3+
{% load admin_urls admin_filters %}
44

55
{% block title %}{% if form.errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
66
{% block extrastyle %}
@@ -15,7 +15,7 @@
1515
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
1616
<li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li>
1717
<li><a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a></li>
18-
<li><a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|truncatewords:"18" }}</a></li>
18+
<li><a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|to_object_display_value|truncatewords:"18" }}</a></li>
1919
<li aria-current="page">{% if form.user.has_usable_password %}{% translate 'Change password' %}{% else %}{% translate 'Set password' %}{% endif %}</li>
2020
</ol>
2121
{% endblock %}

django/contrib/admin/templates/admin/change_form.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends "admin/base_site.html" %}
2-
{% load i18n admin_urls static admin_modify %}
2+
{% load i18n admin_urls static admin_modify admin_filters %}
33

44
{% block title %}{% if errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
55
{% block extrahead %}{{ block.super }}
@@ -19,7 +19,7 @@
1919
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
2020
<li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li>
2121
<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>
22-
<li aria-current="page">{% if add %}{% blocktranslate with name=opts.verbose_name %}Add {{ name }}{% endblocktranslate %}{% else %}{{ original|truncatewords:"18" }}{% endif %}</li>
22+
<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>
2323
</ol>
2424
{% endblock %}
2525
{% endif %}

django/contrib/admin/templates/admin/delete_confirmation.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends "admin/base_site.html" %}
2-
{% load i18n admin_urls static %}
2+
{% load i18n admin_urls static admin_filters %}
33

44
{% block extrahead %}
55
{{ block.super }}
@@ -14,25 +14,25 @@
1414
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
1515
<li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li>
1616
<li><a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a></li>
17-
<li><a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a></li>
17+
<li><a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|to_object_display_value|truncatewords:"18" }}</a></li>
1818
<li aria-current="page">{% translate 'Delete' %}</li>
1919
</ol>
2020
{% endblock %}
2121

2222
{% block content %}
2323
{% if perms_lacking %}
2424
{% block delete_forbidden %}
25-
<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>
25+
<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>
2626
<ul id="deleted-objects">{{ perms_lacking|unordered_list }}</ul>
2727
{% endblock %}
2828
{% elif protected %}
2929
{% block delete_protected %}
30-
<p>{% blocktranslate with escaped_object=object %}Deleting the {{ object_name }} “{{ escaped_object }}” would require deleting the following protected related objects:{% endblocktranslate %}</p>
30+
<p>{% blocktranslate %}Deleting the {{ object_name }} “{{ escaped_object }}” would require deleting the following protected related objects:{% endblocktranslate %}</p>
3131
<ul id="deleted-objects">{{ protected|unordered_list }}</ul>
3232
{% endblock %}
3333
{% else %}
3434
{% block delete_confirm %}
35-
<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>
35+
<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>
3636
{% include "admin/includes/object_delete_summary.html" %}
3737
<h2>{% translate "Objects" %}</h2>
3838
<ul id="deleted-objects">{{ deleted_objects|unordered_list }}</ul>

django/contrib/admin/templates/admin/index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends "admin/base_site.html" %}
2-
{% load i18n static %}
2+
{% load i18n static admin_filters %}
33

44
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/dashboard.css" %}">{% endblock %}
55

@@ -32,9 +32,9 @@ <h3>{% translate 'My actions' %}</h3>
3232
<li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
3333
<span class="visually-hidden">{% if entry.is_addition %}{% translate 'Added:' %}{% elif entry.is_change %}{% translate 'Changed:' %}{% elif entry.is_deletion %}{% translate 'Deleted:' %}{% endif %}</span>
3434
{% if entry.is_deletion or not entry.get_admin_url %}
35-
{{ entry.object_repr }}
35+
{{ entry.object_repr|to_object_display_value }}
3636
{% else %}
37-
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
37+
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr|to_object_display_value }}</a>
3838
{% endif %}
3939
<br>
4040
{% if entry.content_type %}

django/contrib/admin/templates/admin/object_history.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{% extends "admin/base_site.html" %}
2-
{% load i18n admin_urls %}
2+
{% load i18n admin_urls admin_filters %}
33

44
{% block breadcrumbs %}
55
<ol class="breadcrumbs">
66
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
77
<li><a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a></li>
88
<li><a href="{% url opts|admin_urlname:'changelist' %}">{{ module_name }}</a></li>
9-
<li><a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|truncatewords:"18" }}</a></li>
9+
<li><a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}">{{ object|to_object_display_value|truncatewords:"18" }}</a></li>
1010
<li aria-current="page">{% translate 'History' %}</li>
1111
</ol>
1212
{% endblock %}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django import template
2+
from django.contrib.admin.options import EMPTY_VALUE_STRING
3+
from django.contrib.admin.utils import display_for_value
4+
from django.template.defaultfilters import stringfilter
5+
6+
register = template.Library()
7+
8+
9+
@register.filter
10+
@stringfilter
11+
def to_object_display_value(value):
12+
return display_for_value(str(value), EMPTY_VALUE_STRING)

django/contrib/admin/templatetags/admin_list.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,6 @@ def link_in_col(is_first, field_name, cl):
226226
empty_value_display = getattr(
227227
attr, "empty_value_display", empty_value_display
228228
)
229-
if isinstance(value, str) and value.strip() == "":
230-
value = ""
231229
if f is None or f.auto_created:
232230
if field_name == "action_checkbox":
233231
row_classes = ["action-checkbox"]

django/contrib/admin/utils.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from django.utils.hashable import make_hashable
1919
from django.utils.html import format_html
2020
from django.utils.regex_helper import _lazy_re_compile
21+
from django.utils.safestring import SafeString
2122
from django.utils.text import capfirst
2223
from django.utils.translation import ngettext
2324
from django.utils.translation import override as translation_override
@@ -131,6 +132,8 @@ def get_deleted_objects(objs, request, admin_site):
131132
Return a nested list of strings suitable for display in the
132133
template with the ``unordered_list`` filter.
133134
"""
135+
from django.contrib.admin.options import EMPTY_VALUE_STRING
136+
134137
try:
135138
obj = objs[0]
136139
except IndexError:
@@ -164,8 +167,12 @@ def format_callback(obj):
164167
return no_edit_link
165168

166169
# Display a link to the admin page.
170+
obj_display = display_for_value(str(obj), EMPTY_VALUE_STRING)
167171
return format_html(
168-
'{}: <a href="{}">{}</a>', capfirst(opts.verbose_name), admin_url, obj
172+
'{}: <a href="{}">{}</a>',
173+
capfirst(opts.verbose_name),
174+
admin_url,
175+
obj_display,
169176
)
170177
else:
171178
# 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):
468475

469476
if boolean:
470477
return _boolean_icon(value)
471-
elif value in EMPTY_VALUES:
478+
if isinstance(value, str) and not isinstance(value, SafeString):
479+
value = value.strip()
480+
if value in EMPTY_VALUES:
472481
return empty_value_display
473482
elif isinstance(value, bool):
474483
return str(value)

0 commit comments

Comments
 (0)