diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css index ee67fe7cd501..ec2ad6ab9370 100644 --- a/django/contrib/admin/static/admin/css/base.css +++ b/django/contrib/admin/static/admin/css/base.css @@ -767,7 +767,6 @@ ol.breadcrumbs li { display: inline-block; font-size: 0.875rem; padding: 0; - line-height: 0; } ol.breadcrumbs li:not([aria-current="page"])::after { @@ -807,7 +806,7 @@ ol.breadcrumbs a:focus, ol.breadcrumbs a:hover { .changelink, .inlinechangelink { padding-left: 16px; - background: url(../img/icon-changelink.svg) 0 1px no-repeat; + background: url(../img/icon-changelink.svg) 0 2px no-repeat; } .deletelink { diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css index 6249e55772b0..5d2c1d2018f4 100644 --- a/django/contrib/admin/static/admin/css/forms.css +++ b/django/contrib/admin/static/admin/css/forms.css @@ -516,6 +516,14 @@ form .related-widget-wrapper ul { padding-left: 0; } +form .related-widget-wrapper a.change-related img { + transform: scale(0.85); +} + +form .related-widget-wrapper a.view-related img { + transform: scale(0.95); +} + .clearable-file-input input { margin-top: 0; } diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css index 43271f8c0841..c0de045c6802 100644 --- a/django/contrib/admin/static/admin/css/widgets.css +++ b/django/contrib/admin/static/admin/css/widgets.css @@ -589,6 +589,10 @@ ul.timelist, .timelist li { margin-bottom: 5px; } +.related-widget-wrapper:has(select:not([multiple])) { + align-items: center; +} + .related-widget-wrapper-link { opacity: .6; filter: grayscale(1); diff --git a/django/contrib/admin/static/admin/img/icon-deletelink.svg b/django/contrib/admin/static/admin/img/icon-deletelink.svg index eac19d7507ad..9336c51ad1a9 100644 --- a/django/contrib/admin/static/admin/img/icon-deletelink.svg +++ b/django/contrib/admin/static/admin/img/icon-deletelink.svg @@ -6,6 +6,6 @@ Icon Family: classic Icon Style: solid --> - + diff --git a/django/contrib/contenttypes/management/__init__.py b/django/contrib/contenttypes/management/__init__.py index 929e44f390db..55b08870d83e 100644 --- a/django/contrib/contenttypes/management/__init__.py +++ b/django/contrib/contenttypes/management/__init__.py @@ -1,3 +1,5 @@ +import warnings + from django.apps import apps as global_apps from django.db import DEFAULT_DB_ALIAS, IntegrityError, migrations, router, transaction @@ -28,8 +30,14 @@ def _rename(self, apps, schema_editor, old_model, new_model): content_type.save(using=db, update_fields={"model"}) except IntegrityError: # Gracefully fallback if a stale content type causes a - # conflict as remove_stale_contenttypes will take care of - # asking the user what should be done next. + # conflict. Warn the user so they can run the + # remove_stale_contenttypes management command. + warnings.warn( + f"Could not rename content type '{self.app_label}.{old_model}' " + f"to '{new_model}' due to an existing conflicting content type. " + "Run 'remove_stale_contenttypes' to clean up stale entries.", + RuntimeWarning, + ) content_type.model = old_model else: # Clear the cache as the `get_by_natural_key()` call will cache diff --git a/django/utils/text.py b/django/utils/text.py index ef4baa935bf2..cfe6ceca9e4b 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -185,14 +185,8 @@ def process(self, data): class Truncator(SimpleLazyObject): """ An object used to truncate text, either by characters or words. - - When truncating HTML text (either chars or words), input will be limited to - at most `MAX_LENGTH_HTML` characters. """ - # 5 million characters are approximately 4000 text pages or 3 web pages. - MAX_LENGTH_HTML = 5_000_000 - def __init__(self, text): super().__init__(lambda: str(text)) diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 9eee51beba97..94ec4a6be5f4 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -2860,8 +2860,7 @@ Newlines in the HTML content will be preserved. .. admonition:: Size of input string Processing large, potentially malformed HTML strings can be - resource-intensive and impact service performance. ``truncatechars_html`` - limits input to the first five million characters. + resource-intensive and impact service performance. .. templatefilter:: truncatewords @@ -2908,8 +2907,7 @@ Newlines in the HTML content will be preserved. .. admonition:: Size of input string Processing large, potentially malformed HTML strings can be - resource-intensive and impact service performance. ``truncatewords_html`` - limits input to the first five million characters. + resource-intensive and impact service performance. .. templatefilter:: unordered_list diff --git a/tests/contenttypes_tests/test_operations.py b/tests/contenttypes_tests/test_operations.py index d44648d9fe6e..9f6506640ad6 100644 --- a/tests/contenttypes_tests/test_operations.py +++ b/tests/contenttypes_tests/test_operations.py @@ -154,13 +154,19 @@ def test_missing_content_type_rename_ignore(self): def test_content_type_rename_conflict(self): ContentType.objects.create(app_label="contenttypes_tests", model="foo") ContentType.objects.create(app_label="contenttypes_tests", model="renamedfoo") - call_command( - "migrate", - "contenttypes_tests", - database="default", - interactive=False, - verbosity=0, - ) + msg = ( + "Could not rename content type 'contenttypes_tests.foo' to " + "'renamedfoo' due to an existing conflicting content type. " + "Run 'remove_stale_contenttypes' to clean up stale entries." + ) + with self.assertWarnsMessage(RuntimeWarning, msg): + call_command( + "migrate", + "contenttypes_tests", + database="default", + interactive=False, + verbosity=0, + ) self.assertTrue( ContentType.objects.filter( app_label="contenttypes_tests", model="foo" @@ -171,14 +177,20 @@ def test_content_type_rename_conflict(self): app_label="contenttypes_tests", model="renamedfoo" ).exists() ) - call_command( - "migrate", - "contenttypes_tests", - "zero", - database="default", - interactive=False, - verbosity=0, - ) + msg = ( + "Could not rename content type 'contenttypes_tests.renamedfoo' to " + "'foo' due to an existing conflicting content type. " + "Run 'remove_stale_contenttypes' to clean up stale entries." + ) + with self.assertWarnsMessage(RuntimeWarning, msg): + call_command( + "migrate", + "contenttypes_tests", + "zero", + database="default", + interactive=False, + verbosity=0, + ) self.assertTrue( ContentType.objects.filter( app_label="contenttypes_tests", model="foo" diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py index 11c01874cb5d..50e205a25449 100644 --- a/tests/utils_tests/test_text.py +++ b/tests/utils_tests/test_text.py @@ -1,6 +1,5 @@ import json import sys -from unittest.mock import patch from django.core.exceptions import SuspiciousFileOperation from django.test import SimpleTestCase @@ -136,23 +135,6 @@ def test_truncate_chars_html(self): truncator = text.Truncator("foo

") self.assertEqual("foo

", truncator.chars(5, html=True)) - @patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000) - def test_truncate_chars_html_size_limit(self): - max_len = text.Truncator.MAX_LENGTH_HTML - bigger_len = text.Truncator.MAX_LENGTH_HTML + 1 - valid_html = "

Joel is a slug

" # 14 chars - perf_test_values = [ - ("", ""), - ("", "

"), - ("&" * bigger_len, ""), - ("_X<<<<<<<<<<<>", "_X<<<<<<<…"), - (valid_html * bigger_len, "

Joel is a…

"), # 10 chars - ] - for value, expected in perf_test_values: - with self.subTest(value=value): - truncator = text.Truncator(value) - self.assertEqual(expected, truncator.chars(10, html=True)) - def test_truncate_chars_html_with_newline_inside_tag(self): truncator = text.Truncator( '

The quick brown fox jumped over ' @@ -329,24 +311,6 @@ def test_truncate_html_words(self): self.assertEqual(truncator.words(3, html=True), "hello ><…") self.assertEqual(truncator.words(4, html=True), "hello >< world") - @patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000) - def test_truncate_words_html_size_limit(self): - max_len = text.Truncator.MAX_LENGTH_HTML - bigger_len = text.Truncator.MAX_LENGTH_HTML + 1 - valid_html = "

Joel is a slug

" # 4 words - perf_test_values = [ - ("", ""), - ("", "

"), - ("&" * max_len, ""), - ("&" * bigger_len, ""), - ("_X<<<<<<<<<<<>", "_X<<<<<<<<<<<>"), - (valid_html * bigger_len, valid_html * 12 + "

Joel is…

"), # 50 words - ] - for value, expected in perf_test_values: - with self.subTest(value=value): - truncator = text.Truncator(value) - self.assertEqual(expected, truncator.words(50, html=True)) - def test_wrap(self): digits = "1234 67 9" self.assertEqual(text.wrap(digits, 100), "1234 67 9")