Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ answer newbie questions, and generally made Django that much better:
Kacper Wolkiewicz <kac.wolkiewicz@gmail.com>
Kadesarin Sanjek
Kapil Bansal <kapilbansal.gbpecdelhi@gmail.com>
Karan Suthar <karansthr97@gmail.com>
Karderio <karderio@gmail.com>
Karen Tracey <kmtracey@gmail.com>
Karol Sikora <elektrrrus@gmail.com>
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/admin/static/admin/js/urlify.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@
ú: "u",
ý: "y",
ž: "z",
Á: "a",
Á: "A",
Ä: "A",
Č: "C",
Ď: "D",
Expand Down
13 changes: 10 additions & 3 deletions django/db/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion django/db/models/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion django/db/models/fields/tuple_lookups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions django/forms/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions docs/releases/6.1.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
6 changes: 6 additions & 0 deletions tests/composite_pk/test_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 9 additions & 1 deletion tests/constraints/models.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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"}
27 changes: 27 additions & 0 deletions tests/constraints/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
GeneratedFieldVirtualProduct,
JSONFieldModel,
ModelWithDatabaseDefault,
ModelWithDatabaseDefaultExpression,
Product,
UniqueConstraintConditionProduct,
UniqueConstraintDeferrable,
Expand Down Expand Up @@ -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"),
)
51 changes: 51 additions & 0 deletions tests/forms_tests/tests/test_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/")
Expand Down Expand Up @@ -866,6 +867,56 @@ class InvalidType:
with self.assertRaises(TypeError):
Media() + InvalidType()

def test_html_safe_string_js(self):
tag = mark_safe('<script defer src="https://example.org/asset.js"></script>')
media = Media(js=[tag])
self.assertEqual(str(media), tag)

def test_html_safe_string_css(self):
tag = mark_safe('<link href="https://example.org/asset.css" rel="stylesheet">')
media = Media(css={"all": [tag]})
self.assertEqual(str(media), tag)

def test_html_safe_string_deduplication(self):
js_tag = mark_safe('<script defer src="https://example.org/asset.js"></script>')
css_tag = mark_safe(
'<link href="https://example.org/asset.css" rel="stylesheet">'
)
media = Media(
css={"all": [css_tag, css_tag, "/path/to/css1"]},
js=[js_tag, js_tag, Script("/path/to/js1")],
)
self.assertHTMLEqual(
str(media),
'<link href="https://example.org/asset.css" rel="stylesheet">\n'
'<link href="/path/to/css1" media="all" rel="stylesheet">\n'
'<script defer src="https://example.org/asset.js"></script>\n'
'<script src="/path/to/js1"></script>',
)

def test_html_safe_string_merging(self):
js_tag = mark_safe('<script defer src="https://example.org/asset.js"></script>')
css_tag = mark_safe(
'<link href="https://example.org/asset.css" rel="stylesheet">'
)
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),
'<link href="https://example.org/asset.css" rel="stylesheet">\n'
'<link href="/path/to/css1" media="all" rel="stylesheet">\n'
'<script src="/path/to/js1"></script>\n'
'<script defer src="https://example.org/asset.js"></script>\n'
'<script src="/path/to/js2"></script>',
)

def test_render_js_with_attrs(self):
media = Media(js=[Script("/path/to/js", integrity="sha256-abc")])
self.assertHTMLEqual(
Expand Down
18 changes: 17 additions & 1 deletion tests/validation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand Down
28 changes: 27 additions & 1 deletion tests/validation/test_unique.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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",
Expand Down
Loading