-
-
Couldn't load subscription status.
- Fork 33.2k
Fixed #36127 -- Fixed to display whitespace in object str in admin. #19105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(is_safe=True) | ||
| @stringfilter | ||
| def to_object_display_value(value): | ||
| return display_for_value(str(value), EMPTY_VALUE_STRING) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,7 @@ | |
| from django.urls import NoReverseMatch, reverse | ||
| from django.utils import formats, timezone | ||
| from django.utils.hashable import make_hashable | ||
| from django.utils.html import format_html | ||
| from django.utils.html import format_html, strip_tags | ||
| from django.utils.regex_helper import _lazy_re_compile | ||
| from django.utils.text import capfirst | ||
| from django.utils.translation import ngettext | ||
|
|
@@ -130,6 +130,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: | ||
|
|
@@ -163,8 +165,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 | ||
|
|
@@ -428,6 +434,40 @@ def help_text_for_field(name, model): | |
| return help_text | ||
|
|
||
|
|
||
| def convert_to_nbsp(value): | ||
| """ | ||
| Converts spaces in a string to non-breaking spaces (nbsp) | ||
| for visual preservation. | ||
|
|
||
| Exactly leading and trailing spaces, as well as consecutive | ||
| spaces between words, are converted to non-breaking spaces. | ||
| """ | ||
| result = "" | ||
| nbsp = "\xa0" | ||
| if not value.strip(): | ||
| return value.replace(" ", nbsp) | ||
|
|
||
| value_length = len(value) | ||
| left_space_length = value_length - len(value.lstrip()) | ||
| right_space_length = value_length - len(value.rstrip()) | ||
| left = left_space_length * nbsp | ||
| right = right_space_length * nbsp | ||
| space_cnt = 0 | ||
| for char in value.strip(): | ||
| if char == " ": | ||
| space_cnt += 1 | ||
| else: | ||
| # Consecutive spaces between words, replaced with nbsp | ||
| if space_cnt > 1: | ||
| result = result[:-space_cnt] | ||
| nbsps = space_cnt * nbsp | ||
| result += nbsps | ||
| space_cnt = 0 | ||
| result += char | ||
| result = left + result + right | ||
| return result | ||
|
|
||
|
|
||
| def display_for_field(value, field, empty_value_display, avoid_link=False): | ||
| from django.contrib.admin.templatetags.admin_list import _boolean_icon | ||
|
|
||
|
|
@@ -469,7 +509,7 @@ def display_for_field(value, field, empty_value_display, avoid_link=False): | |
| return display_for_value(value, empty_value_display) | ||
|
|
||
|
|
||
| def display_for_value(value, empty_value_display, boolean=False): | ||
| def display_for_value(value, empty_value_display, boolean=False, avoid_quote=False): | ||
| from django.contrib.admin.templatetags.admin_list import _boolean_icon | ||
|
|
||
| if boolean: | ||
|
|
@@ -486,6 +526,11 @@ def display_for_value(value, empty_value_display, boolean=False): | |
| return formats.number_format(value) | ||
| elif isinstance(value, (list, tuple)): | ||
| return ", ".join(str(v) for v in value) | ||
| elif strip_tags(value) == value and isinstance(value, str): | ||
|
||
| converted_value = convert_to_nbsp(value) | ||
| if value.strip() != value and not avoid_quote: | ||
| return f"“{converted_value}”" | ||
| return converted_value | ||
| else: | ||
| return str(value) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1078,14 +1078,26 @@ def test_link_field_display_links(self): | |
|
|
||
| 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, | ||
| ) | ||
| cases = [ | ||
| (" ", "“ ”"), | ||
| ("Antoliny ", "“Antoliny ”"), | ||
| (" Antoliny", "“ Antoliny”"), | ||
| (" Antoliny ", "“ Antoliny ”"), | ||
| ("Anto liny", "Anto liny"), | ||
| ("A n t o l i n y", "A n t o l i n y"), | ||
| ("A n t o l i n y", "A n t o l i n y"), | ||
| ] | ||
| for value, expect_display_value in cases: | ||
| with self.subTest(value=value): | ||
| gc = GrandChild.objects.create(name=value) | ||
| response = self.client.get( | ||
| reverse("admin:admin_changelist_grandchild_changelist") | ||
| ) | ||
| self.assertContains( | ||
| response, | ||
| '<a href="/admin/admin_changelist/grandchild/%s/change/">%s</a>' | ||
| % (gc.pk, expect_display_value), | ||
| ) | ||
|
|
||
| def test_clear_all_filters_link(self): | ||
| self.client.force_login(self.superuser) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should update the unit tests for display_for_value with the new cases
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add the following test cases....