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")