Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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('<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,
Expand Down Expand Up @@ -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('<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(
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion django/contrib/admin/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 %}
Expand All @@ -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 %}
Expand Down
4 changes: 2 additions & 2 deletions django/contrib/admin/templates/admin/change_form.html
Original file line number Diff line number Diff line change
@@ -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 }}
Expand All @@ -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 %}
Expand Down
10 changes: 5 additions & 5 deletions django/contrib/admin/templates/admin/delete_confirmation.html
Original file line number Diff line number Diff line change
@@ -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 }}
Expand All @@ -14,25 +14,25 @@
<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 %}

{% 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>
Expand Down
6 changes: 3 additions & 3 deletions django/contrib/admin/templates/admin/index.html
Original file line number Diff line number Diff line change
@@ -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 %}

Expand Down Expand Up @@ -32,9 +32,9 @@ <h3>{% translate 'My actions' %}</h3>
<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 %}
Expand Down
4 changes: 2 additions & 2 deletions django/contrib/admin/templates/admin/object_history.html
Original file line number Diff line number Diff line change
@@ -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 %}
Expand Down
12 changes: 12 additions & 0 deletions django/contrib/admin/templatetags/admin_filters.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 0 additions & 2 deletions django/contrib/admin/templatetags/admin_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
13 changes: 11 additions & 2 deletions django/contrib/admin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
'{}: <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
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions django/db/backends/oracle/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/6.0.3.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<tuple-for-params>` ``as_sql()`` to accept any sequence
(:ticket:`36934`).
2 changes: 2 additions & 0 deletions docs/releases/6.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------------------------------------------

Expand Down
11 changes: 0 additions & 11 deletions tests/admin_changelist/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1193,17 +1193,6 @@ def test_link_field_display_links(self):
"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")
Expand Down
13 changes: 13 additions & 0 deletions tests/admin_utils/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(" <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
Expand Down
2 changes: 2 additions & 0 deletions tests/admin_views/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Loading