From 99d56e5cf15839995f62f24294584f1e2976b1ea Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 8 Aug 2026 23:19:07 +0100 Subject: [PATCH 1/4] Fixed #37262 -- Restored rendering of html-safe strings in form media. `Media.__init__()` normalized every string js/css entry into `Script` or `Stylesheet` objects. `SafeString` is a `str` subclass, so html-safe strings such as `mark_safe("")`, a previously-documented idiom for including complete asset tags, were treated as asset paths, run through `static()`, and percent-encoded instead of being rendered verbatim. Leave objects providing `__html__()` un-normalized so that they take the verbatim rendering path, restoring the Django 6.0 behavior. Regression in 8096b5251090bf7539c59956e398b027c7525529. co-authored-by: Johannes Maron --- django/forms/widgets.py | 4 +-- docs/releases/6.1.1.txt | 5 +++ tests/forms_tests/tests/test_media.py | 51 +++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 82498aa662f2..a52ad18b09c8 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -152,13 +152,13 @@ def __init__(self, media=None, css=None, js=None): @staticmethod def _normalize_js(js): - return [Script(path) if isinstance(path, str) else path for path in js] + return [(path if hasattr(path, "__html__") else Script(path)) for path in js] @staticmethod def _normalize_css(css): return { medium: [ - Stylesheet(path, media=medium) if isinstance(path, str) else path + (path if hasattr(path, "__html__") else Stylesheet(path, media=medium)) for path in paths ] for medium, paths in css.items() diff --git a/docs/releases/6.1.1.txt b/docs/releases/6.1.1.txt index f53eb8c68de3..4e13e8441ee9 100644 --- a/docs/releases/6.1.1.txt +++ b/docs/releases/6.1.1.txt @@ -20,3 +20,8 @@ Bugfixes * Fixed a bug in Django 6.1 where the ``fields.E323`` system check did not detect mixed ``on_delete`` variants for auto-created intermediate models for ``ManyToManyField``\s (:ticket:`37254`). + +* Fixed a regression in Django 6.1 where HTML-safe strings, such as those + created with :func:`~django.utils.safestring.mark_safe`, used as form media + assets were treated as asset paths rather than being rendered verbatim + (:ticket:`37262`). diff --git a/tests/forms_tests/tests/test_media.py b/tests/forms_tests/tests/test_media.py index 854e52d95c9f..ddc4ac78957e 100644 --- a/tests/forms_tests/tests/test_media.py +++ b/tests/forms_tests/tests/test_media.py @@ -3,6 +3,7 @@ from django.template import Context, Template from django.test import SimpleTestCase, override_settings from django.utils.html import html_safe +from django.utils.safestring import mark_safe @override_settings(STATIC_URL="http://media.example.com/static/") @@ -866,6 +867,56 @@ class InvalidType: with self.assertRaises(TypeError): Media() + InvalidType() + def test_html_safe_string_js(self): + tag = mark_safe('') + media = Media(js=[tag]) + self.assertEqual(str(media), tag) + + def test_html_safe_string_css(self): + tag = mark_safe('') + media = Media(css={"all": [tag]}) + self.assertEqual(str(media), tag) + + def test_html_safe_string_deduplication(self): + js_tag = mark_safe('') + css_tag = mark_safe( + '' + ) + media = Media( + css={"all": [css_tag, css_tag, "/path/to/css1"]}, + js=[js_tag, js_tag, Script("/path/to/js1")], + ) + self.assertHTMLEqual( + str(media), + '\n' + '\n' + '\n' + '', + ) + + def test_html_safe_string_merging(self): + js_tag = mark_safe('') + css_tag = mark_safe( + '' + ) + m1 = Media( + css={"all": [css_tag, "/path/to/css1"]}, + js=["/path/to/js1", js_tag], + ) + m2 = Media( + css={"all": [css_tag]}, + js=[js_tag, Script("/path/to/js2")], + ) + merged = m1 + m2 + self.assertHTMLEqual( + str(merged), + '\n' + '\n' + '\n' + '\n' + '', + ) + def test_render_js_with_attrs(self): media = Media(js=[Script("/path/to/js", integrity="sha256-abc")]) self.assertHTMLEqual( From d6b73a6613f5e7de95fd8719198d0347c85ddf42 Mon Sep 17 00:00:00 2001 From: Jens Spanier <42373861+JensSpanier@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:47:42 +0200 Subject: [PATCH 2/4] =?UTF-8?q?Fixed=20#37276=20--=20Fixed=20Slovak=20case?= =?UTF-8?q?=20mapping=20for=20'=C3=81'=20in=20urlify.js.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- django/contrib/admin/static/admin/js/urlify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/admin/static/admin/js/urlify.js b/django/contrib/admin/static/admin/js/urlify.js index 3a23ec1647ab..005e97291ca0 100644 --- a/django/contrib/admin/static/admin/js/urlify.js +++ b/django/contrib/admin/static/admin/js/urlify.js @@ -286,7 +286,7 @@ ú: "u", ý: "y", ž: "z", - Á: "a", + Á: "A", Ä: "A", Č: "C", Ď: "D", From 812c08bd4e9da7b74ab9ee0db83da58a6da48d19 Mon Sep 17 00:00:00 2001 From: Pravin Kamble Date: Thu, 13 Aug 2026 15:34:15 +0530 Subject: [PATCH 3/4] Fixed #37273 -- Allowed F() expressions on the RHS of CompositePK lookups. --- django/db/models/fields/tuple_lookups.py | 2 +- tests/composite_pk/test_filter.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/django/db/models/fields/tuple_lookups.py b/django/db/models/fields/tuple_lookups.py index 69db53180267..19f05963bcb4 100644 --- a/django/db/models/fields/tuple_lookups.py +++ b/django/db/models/fields/tuple_lookups.py @@ -74,7 +74,7 @@ def check_rhs_length_equals_lhs_length(self): ) def check_rhs_is_supported_expression(self): - if not isinstance(self.rhs, (ResolvedOuterRef, Query)): + if not isinstance(self.rhs, (ColPairs, ResolvedOuterRef, Query)): lhs_str = self.get_lhs_str() rhs_cls = self.rhs.__class__.__name__ raise ValueError( diff --git a/tests/composite_pk/test_filter.py b/tests/composite_pk/test_filter.py index fdaa323fd872..98153e3975ee 100644 --- a/tests/composite_pk/test_filter.py +++ b/tests/composite_pk/test_filter.py @@ -539,6 +539,12 @@ def test_unsupported_rhs(self): with self.assertRaisesMessage(ValueError, msg): Comment.objects.filter(pk=pk) + def test_filter_by_pk_exact_rhs_f_object(self): + self.assertEqual( + Comment.objects.filter(pk=F("pk")).count(), + Comment.objects.count(), + ) + @skipUnlessDBFeature("allow_sliced_subqueries_with_in") def test_filter_comments_by_pk_exact_subquery(self): self.assertSequenceEqual( From febefb175e03352e5aeb2ed827024bacab96cf16 Mon Sep 17 00:00:00 2001 From: Karan Suthar Date: Fri, 14 Aug 2026 23:08:03 +0530 Subject: [PATCH 4/4] Fixed #37248 -- Skipped unique validation of a dynamic DatabaseDefault expression. --- AUTHORS | 1 + django/db/models/base.py | 13 ++++++++++--- django/db/models/constraints.py | 15 ++++++++++++++- tests/constraints/models.py | 10 +++++++++- tests/constraints/tests.py | 27 +++++++++++++++++++++++++++ tests/validation/models.py | 18 +++++++++++++++++- tests/validation/test_unique.py | 28 +++++++++++++++++++++++++++- 7 files changed, 105 insertions(+), 7 deletions(-) diff --git a/AUTHORS b/AUTHORS index 26b8d73c5517..e2aca4ea3b98 100644 --- a/AUTHORS +++ b/AUTHORS @@ -602,6 +602,7 @@ answer newbie questions, and generally made Django that much better: Kacper Wolkiewicz Kadesarin Sanjek Kapil Bansal + Karan Suthar Karderio Karen Tracey Karol Sikora diff --git a/django/db/models/base.py b/django/db/models/base.py index 61a5ee91856a..a2dae61b999e 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1562,9 +1562,16 @@ def _perform_unique_checks(self, unique_checks): f = self._meta.get_field(field_name) lookup_value = getattr(self, f.attname) # TODO: Handle multiple backends with different feature flags. - if lookup_value is None or ( - lookup_value == "" - and connection.features.interprets_empty_strings_as_nulls + if ( + lookup_value is None + or ( + lookup_value == "" + and connection.features.interprets_empty_strings_as_nulls + ) + or ( + isinstance(lookup_value, DatabaseDefault) + and not isinstance(lookup_value.expression, Value) + ) ): # no value, skip the lookup continue diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py index a4e8ab8ab119..f95876ae2d37 100644 --- a/django/db/models/constraints.py +++ b/django/db/models/constraints.py @@ -5,7 +5,14 @@ from django.core.exceptions import FieldDoesNotExist, ValidationError from django.db import connections from django.db.models.constants import LOOKUP_SEP -from django.db.models.expressions import Exists, ExpressionList, F, RawSQL +from django.db.models.expressions import ( + DatabaseDefault, + Exists, + ExpressionList, + F, + RawSQL, + Value, +) from django.db.models.fields import BooleanField from django.db.models.functions import Coalesce from django.db.models.indexes import IndexExpression @@ -605,6 +612,12 @@ def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS): # A composite constraint containing NULL value cannot # cause a violation since NULL != NULL in SQL. return + if isinstance(lookup_value, DatabaseDefault) and not isinstance( + lookup_value.expression, Value + ): + # The value is generated by the database on INSERT and + # cannot be known beforehand. + return lookup_kwargs[field.name] = lookup_value lookup_args = [] if generated_field_names: diff --git a/tests/constraints/models.py b/tests/constraints/models.py index 41b827640efa..86e6dba6cb7d 100644 --- a/tests/constraints/models.py +++ b/tests/constraints/models.py @@ -1,5 +1,5 @@ from django.db import models -from django.db.models.functions import Coalesce, Lower +from django.db.models.functions import Coalesce, Lower, Now class Product(models.Model): @@ -175,3 +175,11 @@ class ModelWithDatabaseDefault(models.Model): field_with_db_default = models.CharField( max_length=255, db_default=models.Value("field_with_db_default") ) + + +class ModelWithDatabaseDefaultExpression(models.Model): + field = models.CharField(max_length=255) + field_with_db_default_expression = models.DateTimeField(db_default=Now()) + + class Meta: + required_db_features = {"supports_expression_defaults"} diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py index b6e7c8883987..fb3ed6967048 100644 --- a/tests/constraints/tests.py +++ b/tests/constraints/tests.py @@ -16,6 +16,7 @@ GeneratedFieldVirtualProduct, JSONFieldModel, ModelWithDatabaseDefault, + ModelWithDatabaseDefaultExpression, Product, UniqueConstraintConditionProduct, UniqueConstraintDeferrable, @@ -1503,3 +1504,29 @@ def test_database_default(self): Upper("field_with_db_default"), name="unique_field_with_db_default_expression", ).validate(ModelWithDatabaseDefault, ModelWithDatabaseDefault()) + + @skipUnlessDBFeature("supports_expression_defaults") + def test_database_default_expression(self): + """ + A field whose db_default is a non-constant expression cannot be + validated before the value is generated on INSERT, so the constraint + check is skipped. + """ + ModelWithDatabaseDefaultExpression.objects.create() + with self.assertNumQueries(0): + models.UniqueConstraint( + fields=["field_with_db_default_expression"], + name="unique_field_with_db_default_expression_field", + ).validate( + ModelWithDatabaseDefaultExpression, ModelWithDatabaseDefaultExpression() + ) + # A multi-field constraint containing such a field is skipped + # entirely, even if the other fields have concrete values. + with self.assertNumQueries(0): + models.UniqueConstraint( + fields=["field", "field_with_db_default_expression"], + name="unique_field_and_db_default_expression_field", + ).validate( + ModelWithDatabaseDefaultExpression, + ModelWithDatabaseDefaultExpression(field="value"), + ) diff --git a/tests/validation/models.py b/tests/validation/models.py index ed8875036452..3fc45cbd4b2e 100644 --- a/tests/validation/models.py +++ b/tests/validation/models.py @@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError from django.db import models -from django.db.models.functions import Lower +from django.db.models.functions import Lower, Now def validate_answer_to_universe(value): @@ -52,6 +52,22 @@ class UniqueFieldsModel(models.Model): non_unique_field = models.IntegerField() +class UniqueDbDefaultExpressionModel(models.Model): + unique_created = models.DateTimeField(unique=True, db_default=Now()) + + class Meta: + required_db_features = {"supports_expression_defaults"} + + +class UniqueTogetherDbDefaultExpressionModel(models.Model): + number = models.IntegerField() + created = models.DateTimeField(db_default=Now()) + + class Meta: + required_db_features = {"supports_expression_defaults"} + unique_together = [("number", "created")] + + class CustomPKModel(models.Model): my_pk_field = models.CharField(max_length=100, primary_key=True) diff --git a/tests/validation/test_unique.py b/tests/validation/test_unique.py index 36ee6e9da0cb..6eb39adfd8d2 100644 --- a/tests/validation/test_unique.py +++ b/tests/validation/test_unique.py @@ -4,17 +4,19 @@ from django.apps.registry import Apps from django.core.exceptions import ValidationError from django.db import models -from django.test import TestCase +from django.test import TestCase, skipUnlessDBFeature from .models import ( CustomPKModel, FlexibleDatePost, ModelToValidate, Post, + UniqueDbDefaultExpressionModel, UniqueErrorsModel, UniqueFieldsModel, UniqueForDateModel, UniqueFuncConstraintModel, + UniqueTogetherDbDefaultExpressionModel, UniqueTogetherModel, ) @@ -160,6 +162,30 @@ def test_unique_db_default(self): }, ) + @skipUnlessDBFeature("supports_expression_defaults") + def test_unique_db_default_expression(self): + """ + A unique field whose db_default is a non-constant expression cannot + be validated before the value is generated on INSERT, so the unique + check is skipped (uniqueness is enforced by the database constraint). + """ + UniqueDbDefaultExpressionModel.objects.create() + m = UniqueDbDefaultExpressionModel() + with self.assertNumQueries(0): + m.full_clean() + + @skipUnlessDBFeature("supports_expression_defaults") + def test_unique_together_db_default_expression(self): + """ + A unique check containing a field with a non-constant db_default is + skipped entirely; a partial lookup on the remaining fields would be + incorrect. + """ + UniqueTogetherDbDefaultExpressionModel.objects.create(number=1) + m = UniqueTogetherDbDefaultExpressionModel(number=1) + with self.assertNumQueries(0): + m.full_clean() + def test_unique_for_date(self): Post.objects.create( title="Django 1.0 is released",