From 9482a4116cc50961e8093151c87ba0719bcb33be Mon Sep 17 00:00:00 2001
From: Antoliny0919
Date: Tue, 10 Feb 2026 19:47:52 +0900
Subject: [PATCH 1/5] Fixed #36873 -- Aligned and normalized stroke width for
non-multiple select icons in related-widget-wrapper.
---
django/contrib/admin/static/admin/css/forms.css | 8 ++++++++
django/contrib/admin/static/admin/css/widgets.css | 4 ++++
django/contrib/admin/static/admin/img/icon-deletelink.svg | 2 +-
3 files changed, 13 insertions(+), 1 deletion(-)
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
-->
-
+
From 69d47f921979aba5ad5d876bff015b55121fc49c Mon Sep 17 00:00:00 2001
From: Antoliny0919
Date: Tue, 24 Feb 2026 09:13:38 +0900
Subject: [PATCH 2/5] Refs #36873 -- Fixed changelink background image Y
position.
---
django/contrib/admin/static/admin/css/base.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css
index ee67fe7cd501..8cb965984cf6 100644
--- a/django/contrib/admin/static/admin/css/base.css
+++ b/django/contrib/admin/static/admin/css/base.css
@@ -807,7 +807,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 {
From 169152f8f5ac586e19779faf9943086fe0f416e9 Mon Sep 17 00:00:00 2001
From: AbhimanyuGit2507
Date: Sat, 14 Feb 2026 01:59:03 +0530
Subject: [PATCH 3/5] Fixed #36839 -- Warned when model renames encounter
conflicts from stale ContentTypes.
---
.../contenttypes/management/__init__.py | 12 +++++-
tests/contenttypes_tests/test_operations.py | 42 ++++++++++++-------
2 files changed, 37 insertions(+), 17 deletions(-)
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/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"
From 07d6fa5a95b79a70cf4ba4b725ce37e488ceb7ca Mon Sep 17 00:00:00 2001
From: Amar <100243770+aadeina@users.noreply.github.com>
Date: Wed, 25 Feb 2026 16:00:39 +0000
Subject: [PATCH 4/5] Fixed #36948 -- Fixed breadcrumb text overlap at small
widths in admin.
Visual regression in bc03f1064e10fa247a46d4e8a98ba9b26aa4790d.
---
django/contrib/admin/static/admin/css/base.css | 1 -
1 file changed, 1 deletion(-)
diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css
index 8cb965984cf6..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 {
From bbc6818bc12f14c1764a7eb68556018195f56b59 Mon Sep 17 00:00:00 2001
From: Natalia <124304+nessita@users.noreply.github.com>
Date: Wed, 25 Feb 2026 10:37:38 -0300
Subject: [PATCH 5/5] Fixed #36944 -- Removed MAX_LENGTH_HTML and related 5M
chars limit references from HTML truncation docs.
---
django/utils/text.py | 6 ------
docs/ref/templates/builtins.txt | 6 ++----
tests/utils_tests/test_text.py | 36 ---------------------------------
3 files changed, 2 insertions(+), 46 deletions(-)
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/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")