From 2d3bb414cfb2778cc64f22e7203102d7389f81e6 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Sun, 14 Jul 2024 19:38:24 +0200 Subject: [PATCH 01/21] Refs #35560 -- Corrected required feature flags in GeneratedModelUniqueConstraint. --- tests/model_fields/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py index d9811ba164..5dfed00329 100644 --- a/tests/model_fields/models.py +++ b/tests/model_fields/models.py @@ -667,7 +667,7 @@ class GeneratedModelUniqueConstraint(GeneratedModelBase): class Meta: required_db_features = { "supports_stored_generated_columns", - "supports_table_check_constraints", + "supports_expression_indexes", } constraints = [ models.UniqueConstraint(F("a"), name="Generated model unique constraint a"), From 86b548a59bce88a274daa17f22b59ce63282d7ba Mon Sep 17 00:00:00 2001 From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> Date: Mon, 15 Jul 2024 10:51:41 +0200 Subject: [PATCH 02/21] Removed duplicate inline from tests.admin_inlines.admin.PhotographerAdmin. --- tests/admin_inlines/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py index 578142d192..3b018aa5d7 100644 --- a/tests/admin_inlines/admin.py +++ b/tests/admin_inlines/admin.py @@ -145,7 +145,6 @@ class PhotographerAdmin(admin.ModelAdmin): ), ] inlines = [ - PhotoTabularInline, PhotoTabularInline, PhotoStackedExtra2Inline, PhotoStackedExtra3Inline, From b5f4d76bc400b9f2017da0a52ee4ff0d7c09be15 Mon Sep 17 00:00:00 2001 From: Maryam Yusuf Date: Mon, 24 Jun 2024 00:38:02 +0100 Subject: [PATCH 03/21] Fixed #35464 -- Updated docs to note fieldsets have limited impact on TabularInlines. --- docs/ref/contrib/admin/index.txt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 504ab62368..6af2140717 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -437,9 +437,7 @@ subclass:: * ``description`` A string of optional extra text to be displayed at the top of each - fieldset, under the heading of the fieldset. This string is not - rendered for :class:`~django.contrib.admin.TabularInline` due to its - layout. + fieldset, under the heading of the fieldset. Note that this value is *not* HTML-escaped when it's displayed in the admin interface. This lets you include HTML if you so desire. @@ -447,6 +445,17 @@ subclass:: :func:`django.utils.html.escape` to escape any HTML special characters. + .. admonition:: :class:`~django.contrib.admin.TabularInline` has limited + support for ``fieldsets`` + + Using ``fieldsets`` with :class:`~django.contrib.admin.TabularInline` + has limited functionality. You can specify which fields will be + displayed and their order within the ``TabularInline`` layout by + defining ``fields`` in the ``field_options`` dictionary. + + All other features are not supported. This includes the use of ``name`` + to define a title for a group of fields. + .. attribute:: ModelAdmin.filter_horizontal By default, a :class:`~django.db.models.ManyToManyField` is displayed in From 65344f0e1ecfff3e9623c18c51eff8fe72f9a9df Mon Sep 17 00:00:00 2001 From: Maryam Yusuf Date: Sat, 13 Jul 2024 13:39:20 +0100 Subject: [PATCH 04/21] Refs #35464 -- Added test to cover layout of TabularInline fieldsets. --- tests/admin_inlines/admin.py | 10 +++++++--- tests/admin_inlines/tests.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py index 3b018aa5d7..c3983985c3 100644 --- a/tests/admin_inlines/admin.py +++ b/tests/admin_inlines/admin.py @@ -106,14 +106,18 @@ class PhotoInlineMixin: model = Photo extra = 2 fieldsets = [ - (None, {"fields": ["image", "title"]}), + (None, {"fields": ["image", "title"], "description": "First group"}), ( "Details", - {"fields": ["description", "creation_date"], "classes": ["collapse"]}, + { + "fields": ["description", "creation_date"], + "classes": ["collapse"], + "description": "Second group", + }, ), ( "Details", # Fieldset name intentionally duplicated - {"fields": ["update_date", "updated_by"]}, + {"fields": ["update_date", "updated_by"], "description": "Third group"}, ), ] diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 04f0a37e02..620aac10a8 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -2422,3 +2422,39 @@ class SeleniumTests(AdminSeleniumTestCase): ) self.assertEqual(available.text, "AVAILABLE ATTENDANT") self.assertEqual(chosen.text, "CHOSEN ATTENDANT") + + def test_tabular_inline_layout(self): + from selenium.webdriver.common.by import By + + self.admin_login(username="super", password="secret") + self.selenium.get( + self.live_server_url + reverse("admin:admin_inlines_photographer_add") + ) + tabular_inline = self.selenium.find_element( + By.CSS_SELECTOR, "[data-inline-type='tabular']" + ) + headers = tabular_inline.find_elements(By.TAG_NAME, "th") + self.assertEqual( + [h.get_attribute("innerText") for h in headers], + [ + "", + "IMAGE", + "TITLE", + "DESCRIPTION", + "CREATION DATE", + "UPDATE DATE", + "UPDATED BY", + "DELETE?", + ], + ) + # There are no fieldset section names rendered. + self.assertNotIn("Details", tabular_inline.text) + # There are no fieldset section descriptions rendered. + self.assertNotIn("First group", tabular_inline.text) + self.assertNotIn("Second group", tabular_inline.text) + self.assertNotIn("Third group", tabular_inline.text) + # There are no fieldset classes applied. + self.assertEqual( + tabular_inline.find_elements(By.CSS_SELECTOR, ".collapse"), + [], + ) From 27043bde5b795eb4a605aeca1d3bc4345d2ca478 Mon Sep 17 00:00:00 2001 From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:28:55 +0200 Subject: [PATCH 05/21] Refs #10941 -- Renamed query_string template tag to querystring. --- django/template/defaulttags.py | 12 ++-- docs/ref/templates/builtins.txt | 18 ++--- docs/releases/5.1.txt | 6 +- .../syntax_tests/test_query_string.py | 70 +++++++++---------- 4 files changed, 51 insertions(+), 55 deletions(-) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index dd0a6b3579..ae74679ec6 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -1169,8 +1169,8 @@ def now(parser, token): return NowNode(format_string, asvar) -@register.simple_tag(takes_context=True) -def query_string(context, query_dict=None, **kwargs): +@register.simple_tag(name="querystring", takes_context=True) +def querystring(context, query_dict=None, **kwargs): """ Add, remove, and change parameters of a ``QueryDict`` and return the result as a query string. If the ``query_dict`` argument is not provided, default @@ -1178,19 +1178,19 @@ def query_string(context, query_dict=None, **kwargs): For example:: - {% query_string foo=3 %} + {% querystring foo=3 %} To remove a key:: - {% query_string foo=None %} + {% querystring foo=None %} To use with pagination:: - {% query_string page=page_obj.next_page_number %} + {% querystring page=page_obj.next_page_number %} A custom ``QueryDict`` can also be used:: - {% query_string my_query_dict foo=3 %} + {% querystring my_query_dict foo=3 %} """ if query_dict is None: query_dict = context.request.GET diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 4cfd1d8f71..b920478700 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -952,9 +952,9 @@ output (as a string) inside a variable. This is useful if you want to use {% now "Y" as current_year %} {% blocktranslate %}Copyright {{ current_year }}{% endblocktranslate %} -.. templatetag:: query_string +.. templatetag:: querystring -``query_string`` +``querystring`` ---------------- .. versionadded:: 5.1 @@ -975,14 +975,14 @@ query string is ``?color=green&size=M``, the output would be .. code-block:: html+django - {% query_string %} + {% querystring %} You can also pass in a custom ``QueryDict`` that will be used instead of ``request.GET``: .. code-block:: html+django - {% query_string my_query_dict %} + {% querystring my_query_dict %} Each keyword argument will be added to the query string, replacing any existing value for that key. With the query string ``?color=blue``, the following would @@ -990,14 +990,14 @@ result in ``?color=red&size=S``: .. code-block:: html+django - {% query_string color="red" size="S" %} + {% querystring color="red" size="S" %} It is possible to remove parameters by passing ``None`` as a value. With the query string ``?color=blue&size=M``, the following would result in ``?size=M``: .. code-block:: html+django - {% query_string color=None %} + {% querystring color=None %} If the given parameter is a list, the value will remain as a list. For example, if ``my_list`` is set to ``["red", "blue"]``, the following would result in @@ -1005,7 +1005,7 @@ if ``my_list`` is set to ``["red", "blue"]``, the following would result in .. code-block:: html+django - {% query_string color=my_list %} + {% querystring color=my_list %} A common example of using this tag is to preserve the current query string when displaying a page of results, while adding a link to the next and previous @@ -1015,14 +1015,14 @@ would output ``?color=blue&size=M&page=4``: .. code-block:: html+django - {% query_string page=page.next_page_number %} + {% querystring page=page.next_page_number %} You can also store the value in a variable, for example, if you need multiple links to the same page with syntax such as: .. code-block:: html+django - {% query_string page=page.next_page_number as next_page %} + {% querystring page=page.next_page_number as next_page %} .. templatetag:: regroup diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 49741ca81c..db7ea41e52 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -26,10 +26,10 @@ only officially support the latest release of each series. What's new in Django 5.1 ======================== -``{% query_string %}`` template tag +``{% querystring %}`` template tag ----------------------------------- -Django 5.1 introduces the :ttag:`{% query_string %} ` template +Django 5.1 introduces the :ttag:`{% querystring %} ` template tag, simplifying the modification of query parameters in URLs, making it easier to generate links that maintain existing query parameters while adding or changing specific ones. @@ -53,7 +53,7 @@ When switching to using this new template tag, the above magically becomes: .. code-block:: html+django - Next page + Next page PostgreSQL Connection Pools --------------------------- diff --git a/tests/template_tests/syntax_tests/test_query_string.py b/tests/template_tests/syntax_tests/test_query_string.py index 13c0dc1d08..3f1cf3d281 100644 --- a/tests/template_tests/syntax_tests/test_query_string.py +++ b/tests/template_tests/syntax_tests/test_query_string.py @@ -9,92 +9,88 @@ class QueryStringTagTests(SimpleTestCase): def setUp(self): self.request_factory = RequestFactory() - @setup({"query_string_empty": "{% query_string %}"}) - def test_query_string_empty(self): + @setup({"querystring_empty": "{% querystring %}"}) + def test_querystring_empty(self): request = self.request_factory.get("/") - template = self.engine.get_template("query_string_empty") + template = self.engine.get_template("querystring_empty") context = RequestContext(request) output = template.render(context) self.assertEqual(output, "") - @setup({"query_string_non_empty": "{% query_string %}"}) - def test_query_string_non_empty(self): + @setup({"querystring_non_empty": "{% querystring %}"}) + def test_querystring_non_empty(self): request = self.request_factory.get("/", {"a": "b"}) - template = self.engine.get_template("query_string_non_empty") + template = self.engine.get_template("querystring_non_empty") context = RequestContext(request) output = template.render(context) self.assertEqual(output, "?a=b") - @setup({"query_string_multiple": "{% query_string %}"}) - def test_query_string_multiple(self): + @setup({"querystring_multiple": "{% querystring %}"}) + def test_querystring_multiple(self): request = self.request_factory.get("/", {"x": "y", "a": "b"}) - template = self.engine.get_template("query_string_multiple") + template = self.engine.get_template("querystring_multiple") context = RequestContext(request) output = template.render(context) self.assertEqual(output, "?x=y&a=b") - @setup({"query_string_replace": "{% query_string a=1 %}"}) - def test_query_string_replace(self): + @setup({"querystring_replace": "{% querystring a=1 %}"}) + def test_querystring_replace(self): request = self.request_factory.get("/", {"x": "y", "a": "b"}) - template = self.engine.get_template("query_string_replace") + template = self.engine.get_template("querystring_replace") context = RequestContext(request) output = template.render(context) self.assertEqual(output, "?x=y&a=1") - @setup({"query_string_add": "{% query_string test_new='something' %}"}) - def test_query_string_add(self): + @setup({"querystring_add": "{% querystring test_new='something' %}"}) + def test_querystring_add(self): request = self.request_factory.get("/", {"a": "b"}) - template = self.engine.get_template("query_string_add") + template = self.engine.get_template("querystring_add") context = RequestContext(request) output = template.render(context) self.assertEqual(output, "?a=b&test_new=something") - @setup({"query_string_remove": "{% query_string test=None a=1 %}"}) - def test_query_string_remove(self): + @setup({"querystring_remove": "{% querystring test=None a=1 %}"}) + def test_querystring_remove(self): request = self.request_factory.get("/", {"test": "value", "a": "1"}) - template = self.engine.get_template("query_string_remove") + template = self.engine.get_template("querystring_remove") context = RequestContext(request) output = template.render(context) self.assertEqual(output, "?a=1") - @setup( - {"query_string_remove_nonexistent": "{% query_string nonexistent=None a=1 %}"} - ) - def test_query_string_remove_nonexistent(self): + @setup({"querystring_remove_nonexistent": "{% querystring nonexistent=None a=1 %}"}) + def test_querystring_remove_nonexistent(self): request = self.request_factory.get("/", {"x": "y", "a": "1"}) - template = self.engine.get_template("query_string_remove_nonexistent") + template = self.engine.get_template("querystring_remove_nonexistent") context = RequestContext(request) output = template.render(context) self.assertEqual(output, "?x=y&a=1") - @setup({"query_string_list": "{% query_string a=my_list %}"}) - def test_query_string_add_list(self): + @setup({"querystring_list": "{% querystring a=my_list %}"}) + def test_querystring_add_list(self): request = self.request_factory.get("/") - template = self.engine.get_template("query_string_list") + template = self.engine.get_template("querystring_list") context = RequestContext(request, {"my_list": [2, 3]}) output = template.render(context) self.assertEqual(output, "?a=2&a=3") - @setup({"query_string_query_dict": "{% query_string request.GET a=2 %}"}) - def test_query_string_with_explicit_query_dict(self): + @setup({"querystring_query_dict": "{% querystring request.GET a=2 %}"}) + def test_querystring_with_explicit_query_dict(self): request = self.request_factory.get("/", {"a": 1}) output = self.engine.render_to_string( - "query_string_query_dict", {"request": request} + "querystring_query_dict", {"request": request} ) self.assertEqual(output, "?a=2") - @setup( - {"query_string_query_dict_no_request": "{% query_string my_query_dict a=2 %}"} - ) - def test_query_string_with_explicit_query_dict_and_no_request(self): + @setup({"querystring_query_dict_no_request": "{% querystring my_query_dict a=2 %}"}) + def test_querystring_with_explicit_query_dict_and_no_request(self): context = {"my_query_dict": QueryDict("a=1&b=2")} output = self.engine.render_to_string( - "query_string_query_dict_no_request", context + "querystring_query_dict_no_request", context ) self.assertEqual(output, "?a=2&b=2") - @setup({"query_string_no_request_no_query_dict": "{% query_string %}"}) - def test_query_string_without_request_or_explicit_query_dict(self): + @setup({"querystring_no_request_no_query_dict": "{% querystring %}"}) + def test_querystring_without_request_or_explicit_query_dict(self): msg = "'Context' object has no attribute 'request'" with self.assertRaisesMessage(AttributeError, msg): - self.engine.render_to_string("query_string_no_request_no_query_dict") + self.engine.render_to_string("querystring_no_request_no_query_dict") From 082fe2b5a83571dec4aa97580af0fe8cf2a5214e Mon Sep 17 00:00:00 2001 From: nessita <124304+nessita@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:20:24 -0300 Subject: [PATCH 06/21] Removed leftover KeyError handling after Query.tables attribute cleanup. Follow up from f7f5edd50d03e8482f8a6da5fb5202b895d68cd6. --- django/db/models/sql/compiler.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index f3aed06d81..fe22163961 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -1134,15 +1134,9 @@ class SQLCompiler: """ result = [] params = [] - for alias in tuple(self.query.alias_map): + for alias, from_clause in tuple(self.query.alias_map.items()): if not self.query.alias_refcount[alias]: continue - try: - from_clause = self.query.alias_map[alias] - except KeyError: - # Extra tables can end up in self.tables, but not in the - # alias_map if they aren't in a join. That's OK. We skip them. - continue clause_sql, clause_params = self.compile(from_clause) result.append(clause_sql) params.extend(clause_params) From 252eaca87fc0459dae71c771ca94bf2772127e5a Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 16 Jul 2024 14:00:30 -0400 Subject: [PATCH 07/21] Doc'd purpose of tuple() in SQLCompiler.get_from_clause(). It was added in 01d440fa1e6b5c62acfa8b3fde43dfa1505f93c6 to prevent "RuntimeError: OrderedDict mutated during iteration". That particular issue was fixed in d660cee5bc68b597503c2a16f3d9928d52f93fb4 but the issue could remain in Join.as_sql() subclasses. Co-authored-by: Simon Charette --- django/db/models/sql/compiler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index fe22163961..98feb42716 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -1134,6 +1134,9 @@ class SQLCompiler: """ result = [] params = [] + # Copy alias_map to a tuple in case Join.as_sql() subclasses (objects + # in alias_map) alter compiler.query.alias_map. That would otherwise + # raise "RuntimeError: dictionary changed size during iteration". for alias, from_clause in tuple(self.query.alias_map.items()): if not self.query.alias_refcount[alias]: continue From 5dc17177c38662d6f4408258ee117cd80e0cb933 Mon Sep 17 00:00:00 2001 From: nessita <124304+nessita@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:14:52 -0300 Subject: [PATCH 08/21] Refs #10941 -- Renamed test file test_query_string.py to test_querystring.py. This follows previous renames made in 27043bde5b795eb4a605aeca1d3bc4345d2ca478. --- .../syntax_tests/{test_query_string.py => test_querystring.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/template_tests/syntax_tests/{test_query_string.py => test_querystring.py} (100%) diff --git a/tests/template_tests/syntax_tests/test_query_string.py b/tests/template_tests/syntax_tests/test_querystring.py similarity index 100% rename from tests/template_tests/syntax_tests/test_query_string.py rename to tests/template_tests/syntax_tests/test_querystring.py From 13922580cccfb9ab2922ff4943dd39da56dfbd8c Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sat, 13 Jul 2024 22:09:50 -0400 Subject: [PATCH 09/21] Refs #30581 -- Made unattached UniqueConstraint(fields) validation testable. The logic allowing UniqueConstraint(fields).validate to preserve backward compatiblity with Model.unique_error_message failed to account for cases where the constraint might not be attached to a model which is a common pattern during testing. This changes allows for arbitrary UniqueConstraint(fields) to be tested in isolation without requiring actual models backing them up. Co-authored-by: Mark G --- django/db/models/constraints.py | 19 ++++++++----------- tests/constraints/tests.py | 9 ++++++++- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py index 3e6c5205c6..9bb407274c 100644 --- a/django/db/models/constraints.py +++ b/django/db/models/constraints.py @@ -653,19 +653,16 @@ class UniqueConstraint(BaseConstraint): queryset = queryset.exclude(pk=model_class_pk) if not self.condition: if queryset.exists(): - if self.expressions: + if self.fields: + # When fields are defined, use the unique_error_message() for + # backward compatibility. raise ValidationError( - self.get_violation_error_message(), - code=self.violation_error_code, + instance.unique_error_message(model, self.fields), ) - # When fields are defined, use the unique_error_message() for - # backward compatibility. - for model, constraints in instance.get_constraints(): - for constraint in constraints: - if constraint is self: - raise ValidationError( - instance.unique_error_message(model, self.fields), - ) + raise ValidationError( + self.get_violation_error_message(), + code=self.violation_error_code, + ) else: against = instance._get_field_value_map(meta=model._meta, exclude=exclude) try: diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py index 86efaa79e7..8b7599adc1 100644 --- a/tests/constraints/tests.py +++ b/tests/constraints/tests.py @@ -896,6 +896,13 @@ class UniqueConstraintTests(TestCase): ChildUniqueConstraintProduct(name=self.p1.name, color=self.p1.color), ) + def test_validate_fields_unattached(self): + Product.objects.create(price=42) + constraint = models.UniqueConstraint(fields=["price"], name="uniq_prices") + msg = "Product with this Price already exists." + with self.assertRaisesMessage(ValidationError, msg): + constraint.validate(Product, Product(price=42)) + @skipUnlessDBFeature("supports_partial_indexes") def test_validate_condition(self): p1 = UniqueConstraintConditionProduct.objects.create(name="p1") @@ -921,7 +928,7 @@ class UniqueConstraintTests(TestCase): ) @skipUnlessDBFeature("supports_partial_indexes") - def test_validate_conditon_custom_error(self): + def test_validate_condition_custom_error(self): p1 = UniqueConstraintConditionProduct.objects.create(name="p1") constraint = models.UniqueConstraint( fields=["name"], From adc0b6aac3f8a5c96e1ca282bc9f46e28d20281c Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sat, 13 Jul 2024 22:18:21 -0400 Subject: [PATCH 10/21] Fixed #35594 -- Added unique nulls distinct validation for expressions. Thanks Mark Gensler for the report. --- django/db/models/constraints.py | 12 ++++++++---- docs/releases/5.0.8.txt | 3 ++- tests/constraints/tests.py | 15 ++++++++++++++- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py index 9bb407274c..0a63e38d83 100644 --- a/django/db/models/constraints.py +++ b/django/db/models/constraints.py @@ -8,7 +8,7 @@ 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.indexes import IndexExpression -from django.db.models.lookups import Exact +from django.db.models.lookups import Exact, IsNull from django.db.models.query_utils import Q from django.db.models.sql.query import Query from django.db.utils import DEFAULT_DB_ALIAS @@ -642,12 +642,16 @@ class UniqueConstraint(BaseConstraint): meta=model._meta, exclude=exclude ).items() } - expressions = [] + filters = [] for expr in self.expressions: if hasattr(expr, "get_expression_for_validation"): expr = expr.get_expression_for_validation() - expressions.append(Exact(expr, expr.replace_expressions(replacements))) - queryset = queryset.filter(*expressions) + rhs = expr.replace_expressions(replacements) + condition = Exact(expr, rhs) + if self.nulls_distinct is False: + condition = Q(condition) | Q(IsNull(expr, True), IsNull(rhs, True)) + filters.append(condition) + queryset = queryset.filter(*filters) model_class_pk = instance._get_pk_val(model._meta) if not instance._state.adding and model_class_pk is not None: queryset = queryset.exclude(pk=model_class_pk) diff --git a/docs/releases/5.0.8.txt b/docs/releases/5.0.8.txt index 1c30ed4766..8e072049b2 100644 --- a/docs/releases/5.0.8.txt +++ b/docs/releases/5.0.8.txt @@ -9,4 +9,5 @@ Django 5.0.8 fixes several bugs in 5.0.7. Bugfixes ======== -* ... +* Added missing validation for ``UniqueConstraint(nulls_distinct=False)`` when + using ``*expressions`` (:ticket:`35594`). diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py index 8b7599adc1..fd68c5156d 100644 --- a/tests/constraints/tests.py +++ b/tests/constraints/tests.py @@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError from django.db import IntegrityError, connection, models from django.db.models import F from django.db.models.constraints import BaseConstraint, UniqueConstraint -from django.db.models.functions import Lower +from django.db.models.functions import Abs, Lower from django.db.transaction import atomic from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature from django.test.utils import ignore_warnings @@ -1070,6 +1070,19 @@ class UniqueConstraintTests(TestCase): is_not_null_constraint.validate(Product, Product(price=4, discounted_price=3)) is_not_null_constraint.validate(Product, Product(price=2, discounted_price=1)) + def test_validate_nulls_distinct_expressions(self): + Product.objects.create(price=42) + constraint = models.UniqueConstraint( + Abs("price"), + nulls_distinct=False, + name="uniq_prices_nulls_distinct", + ) + constraint.validate(Product, Product(price=None)) + Product.objects.create(price=None) + msg = f"Constraint “{constraint.name}” is violated." + with self.assertRaisesMessage(ValidationError, msg): + constraint.validate(Product, Product(price=None)) + def test_name(self): constraints = get_constraints(UniqueConstraintProduct._meta.db_table) expected_name = "name_color_uniq" From 0e49a8c3bd9119795525d9f076f73740741479b7 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sat, 13 Jul 2024 22:15:21 -0400 Subject: [PATCH 11/21] Refs #34701 -- Moved UniqueConstraint(nulls_distinct) validation tests. The original tests required the creation of a model that is no longer necessary and were exercising Model.full_clean(validate_constraints) which has nothing to do with the nulls_distinct feature. --- tests/constraints/tests.py | 13 +++++++++++++ tests/validation/models.py | 14 -------------- tests/validation/test_constraints.py | 23 ----------------------- 3 files changed, 13 insertions(+), 37 deletions(-) diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py index fd68c5156d..31c5d64652 100644 --- a/tests/constraints/tests.py +++ b/tests/constraints/tests.py @@ -1070,6 +1070,19 @@ class UniqueConstraintTests(TestCase): is_not_null_constraint.validate(Product, Product(price=4, discounted_price=3)) is_not_null_constraint.validate(Product, Product(price=2, discounted_price=1)) + def test_validate_nulls_distinct_fields(self): + Product.objects.create(price=42) + constraint = models.UniqueConstraint( + fields=["price"], + nulls_distinct=False, + name="uniq_prices_nulls_distinct", + ) + constraint.validate(Product, Product(price=None)) + Product.objects.create(price=None) + msg = "Product with this Price already exists." + with self.assertRaisesMessage(ValidationError, msg): + constraint.validate(Product, Product(price=None)) + def test_validate_nulls_distinct_expressions(self): Product.objects.create(price=42) constraint = models.UniqueConstraint( diff --git a/tests/validation/models.py b/tests/validation/models.py index f6b1e0cd62..653be4a239 100644 --- a/tests/validation/models.py +++ b/tests/validation/models.py @@ -217,17 +217,3 @@ class UniqueConstraintConditionProduct(models.Model): condition=models.Q(color__isnull=True), ), ] - - -class UniqueConstraintNullsDistinctProduct(models.Model): - name = models.CharField(max_length=255, blank=True, null=True) - - class Meta: - required_db_features = {"supports_nulls_distinct_unique_constraints"} - constraints = [ - models.UniqueConstraint( - fields=["name"], - name="name_nulls_not_distinct_uniq", - nulls_distinct=False, - ), - ] diff --git a/tests/validation/test_constraints.py b/tests/validation/test_constraints.py index eea2d0c533..0b1ee6518e 100644 --- a/tests/validation/test_constraints.py +++ b/tests/validation/test_constraints.py @@ -6,7 +6,6 @@ from .models import ( ChildUniqueConstraintProduct, Product, UniqueConstraintConditionProduct, - UniqueConstraintNullsDistinctProduct, UniqueConstraintProduct, ) @@ -94,25 +93,3 @@ class PerformConstraintChecksTest(TestCase): UniqueConstraintConditionProduct.objects.create(name="product") product = UniqueConstraintConditionProduct(name="product") product.full_clean(validate_constraints=False) - - @skipUnlessDBFeature("supports_nulls_distinct_unique_constraints") - def test_full_clean_with_nulls_distinct_unique_constraints(self): - UniqueConstraintNullsDistinctProduct.objects.create(name=None) - product = UniqueConstraintNullsDistinctProduct(name=None) - with self.assertRaises(ValidationError) as cm: - product.full_clean() - self.assertEqual( - cm.exception.message_dict, - { - "name": [ - "Unique constraint nulls distinct product with this Name " - "already exists." - ] - }, - ) - - @skipUnlessDBFeature("supports_nulls_distinct_unique_constraints") - def test_full_clean_with_nulls_distinct_unique_constraints_disabled(self): - UniqueConstraintNullsDistinctProduct.objects.create(name=None) - product = UniqueConstraintNullsDistinctProduct(name=None) - product.full_clean(validate_constraints=False) From 182f262b15882649bbc39d769f9b721cf3660f6f Mon Sep 17 00:00:00 2001 From: Hisham Mahmood Date: Wed, 17 Jul 2024 18:50:45 +0500 Subject: [PATCH 12/21] Fixed #35606, Refs #34045 -- Fixed rendering of ModelAdmin.action_checkbox for models with a __html__ method. Thank you Claude Paroz for the report. Regression in 85366fbca723c9b37d0ac9db1d44e3f1cb188db2. --- AUTHORS | 1 + django/contrib/admin/options.py | 4 +++- docs/releases/5.0.8.txt | 4 ++++ tests/admin_changelist/models.py | 6 ++++++ tests/admin_changelist/tests.py | 27 +++++++++++++++++++++++++++ 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 6e54cc1ea6..d394290728 100644 --- a/AUTHORS +++ b/AUTHORS @@ -416,6 +416,7 @@ answer newbie questions, and generally made Django that much better: Himanshu Chauhan hipertracker@gmail.com Hiroki Kiyohara + Hisham Mahmood Honza Král Horst Gutmann Hugo Osvaldo Barrera diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index e8760c2931..2257b3072e 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1026,7 +1026,9 @@ class ModelAdmin(BaseModelAdmin): """ attrs = { "class": "action-select", - "aria-label": format_html(_("Select this object for an action - {}"), obj), + "aria-label": format_html( + _("Select this object for an action - {}"), str(obj) + ), } checkbox = forms.CheckboxInput(attrs, lambda value: False) return checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk)) diff --git a/docs/releases/5.0.8.txt b/docs/releases/5.0.8.txt index 8e072049b2..1037b78f75 100644 --- a/docs/releases/5.0.8.txt +++ b/docs/releases/5.0.8.txt @@ -11,3 +11,7 @@ Bugfixes * Added missing validation for ``UniqueConstraint(nulls_distinct=False)`` when using ``*expressions`` (:ticket:`35594`). + +* Fixed a regression in Django 5.0 where ``ModelAdmin.action_checkbox`` could + break the admin changelist HTML page when rendering a model instance with a + ``__html__`` method (:ticket:`35606`). diff --git a/tests/admin_changelist/models.py b/tests/admin_changelist/models.py index 290a3ea4ec..78e65ab878 100644 --- a/tests/admin_changelist/models.py +++ b/tests/admin_changelist/models.py @@ -23,6 +23,12 @@ class GrandChild(models.Model): parent = models.ForeignKey(Child, models.SET_NULL, editable=False, null=True) name = models.CharField(max_length=30, blank=True) + def __str__(self): + return self.name + + def __html__(self): + return f'

{self.name}

' + class Genre(models.Model): name = models.CharField(max_length=20) diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index bf85cf038f..4d8845e11e 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -364,6 +364,33 @@ class ChangeListTests(TestCase): table_output, ) + def test_action_checkbox_for_model_with_dunder_html(self): + grandchild = GrandChild.objects.create(name="name") + request = self._mocked_authenticated_request("/grandchild/", self.superuser) + m = GrandChildAdmin(GrandChild, custom_site) + cl = m.get_changelist_instance(request) + cl.formset = None + template = Template( + "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}" + ) + context = Context({"cl": cl, "opts": GrandChild._meta}) + table_output = template.render(context) + link = reverse( + "admin:admin_changelist_grandchild_change", args=(grandchild.id,) + ) + row_html = build_tbody_html( + grandchild, + link, + "name", + '-' + '-', + ) + self.assertNotEqual( + table_output.find(row_html), + -1, + "Failed to find expected row element: %s" % table_output, + ) + def test_result_list_editable_html(self): """ Regression tests for #11791: Inclusion tag result_list generates a From 6b3f55446fdc62bd277903fd188a1781e4d92d29 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Wed, 17 Jul 2024 14:52:08 -0400 Subject: [PATCH 13/21] Fixed #35603 -- Prevented F.__contains__() from hanging. Regression in 94b6f101f7dc363a8e71593570b17527dbb9f77f. --- django/db/models/expressions.py | 5 +++++ tests/expressions/tests.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index dcba973ff4..ffb9f3c816 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -884,6 +884,11 @@ class F(Combinable): def __getitem__(self, subscript): return Sliced(self, subscript) + def __contains__(self, other): + # Disable old-style iteration protocol inherited from implementing + # __getitem__() to prevent this method from hanging. + raise TypeError(f"argument of type '{self.__class__.__name__}' is not iterable") + def resolve_expression( self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False ): diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index 3538900092..64103f14db 100644 --- a/tests/expressions/tests.py +++ b/tests/expressions/tests.py @@ -1302,6 +1302,11 @@ class FTests(SimpleTestCase): self.assertNotEqual(f, value) self.assertNotEqual(value, f) + def test_contains(self): + msg = "argument of type 'F' is not iterable" + with self.assertRaisesMessage(TypeError, msg): + "" in F("name") + class ExpressionsTests(TestCase): def test_F_reuse(self): From 9cb8baa0c4fa2c10789c5c8b65f4465932d4d172 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Tue, 25 Jun 2024 23:39:23 -0400 Subject: [PATCH 14/21] Fixed #35559 -- Avoided unnecessary query on sliced union of empty queries. While refs #34125 focused on the SQL correctness of slicing of union of potentially empty queries it missed an optimization opportunity to avoid performing a query at all when all queries are empty. Thanks Lucidiot for the report. --- django/db/models/sql/compiler.py | 72 +++++++++++++++------------- tests/queries/test_qs_combinators.py | 6 +++ 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 98feb42716..262d722dc1 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -583,50 +583,28 @@ class SQLCompiler: raise DatabaseError( "ORDER BY not allowed in subqueries of compound statements." ) - elif self.query.is_sliced and combinator == "union": - for compiler in compilers: - # A sliced union cannot have its parts elided as some of them - # might be sliced as well and in the event where only a single - # part produces a non-empty resultset it might be impossible to - # generate valid SQL. - compiler.elide_empty = False - parts = () - selected = self.query.selected + parts = [] + empty_compiler = None for compiler in compilers: try: - # If the columns list is limited, then all combined queries - # must have the same columns list. Set the selects defined on - # the query on all combined queries, if not already set. - if selected is not None and compiler.query.selected is None: - compiler.query = compiler.query.clone() - compiler.query.set_values(selected) - part_sql, part_args = compiler.as_sql(with_col_aliases=True) - if compiler.query.combinator: - # Wrap in a subquery if wrapping in parentheses isn't - # supported. - if not features.supports_parentheses_in_compound: - part_sql = "SELECT * FROM ({})".format(part_sql) - # Add parentheses when combining with compound query if not - # already added for all compound queries. - elif ( - self.query.subquery - or not features.supports_slicing_ordering_in_compound - ): - part_sql = "({})".format(part_sql) - elif ( - self.query.subquery - and features.supports_slicing_ordering_in_compound - ): - part_sql = "({})".format(part_sql) - parts += ((part_sql, part_args),) + parts.append(self._get_combinator_part_sql(compiler)) except EmptyResultSet: # Omit the empty queryset with UNION and with DIFFERENCE if the # first queryset is nonempty. if combinator == "union" or (combinator == "difference" and parts): + empty_compiler = compiler continue raise if not parts: raise EmptyResultSet + elif len(parts) == 1 and combinator == "union" and self.query.is_sliced: + # A sliced union cannot be composed of a single component because + # in the event the later is also sliced it might result in invalid + # SQL due to the usage of multiple LIMIT clauses. Prevent that from + # happening by always including an empty resultset query to force + # the creation of an union. + empty_compiler.elide_empty = False + parts.append(self._get_combinator_part_sql(empty_compiler)) combinator_sql = self.connection.ops.set_operators[combinator] if all and combinator == "union": combinator_sql += " ALL" @@ -642,6 +620,32 @@ class SQLCompiler: params.extend(part) return result, params + def _get_combinator_part_sql(self, compiler): + features = self.connection.features + # If the columns list is limited, then all combined queries + # must have the same columns list. Set the selects defined on + # the query on all combined queries, if not already set. + selected = self.query.selected + if selected is not None and compiler.query.selected is None: + compiler.query = compiler.query.clone() + compiler.query.set_values(selected) + part_sql, part_args = compiler.as_sql(with_col_aliases=True) + if compiler.query.combinator: + # Wrap in a subquery if wrapping in parentheses isn't + # supported. + if not features.supports_parentheses_in_compound: + part_sql = "SELECT * FROM ({})".format(part_sql) + # Add parentheses when combining with compound query if not + # already added for all compound queries. + elif ( + self.query.subquery + or not features.supports_slicing_ordering_in_compound + ): + part_sql = "({})".format(part_sql) + elif self.query.subquery and features.supports_slicing_ordering_in_compound: + part_sql = "({})".format(part_sql) + return part_sql, part_args + def get_qualify_sql(self): where_parts = [] if self.where: diff --git a/tests/queries/test_qs_combinators.py b/tests/queries/test_qs_combinators.py index eac1533803..ad1017c8af 100644 --- a/tests/queries/test_qs_combinators.py +++ b/tests/queries/test_qs_combinators.py @@ -76,6 +76,12 @@ class QuerySetSetOperationTests(TestCase): qs3 = qs1.union(qs2) self.assertNumbersEqual(qs3[:1], [0]) + def test_union_all_none_slice(self): + qs = Number.objects.filter(id__in=[]) + with self.assertNumQueries(0): + self.assertSequenceEqual(qs.union(qs), []) + self.assertSequenceEqual(qs.union(qs)[0:0], []) + def test_union_empty_filter_slice(self): qs1 = Number.objects.filter(num__lte=0) qs2 = Number.objects.filter(pk__in=[]) From ee20e2d0380e3f4f87c0ea386c7365056473d6ad Mon Sep 17 00:00:00 2001 From: "Muhammad N. Fadhil" <34566369+MuhammadNFadhil@users.noreply.github.com> Date: Mon, 15 Jul 2024 21:57:03 +0300 Subject: [PATCH 15/21] Fixed typos in Atomic docstring. --- django/db/transaction.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/django/db/transaction.py b/django/db/transaction.py index 4150cbcbbe..0c2eee8e73 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -156,7 +156,7 @@ class Atomic(ContextDecorator): It's possible to disable the creation of savepoints if the goal is to ensure that some code runs within a transaction without creating overhead. - A stack of savepoints identifiers is maintained as an attribute of the + A stack of savepoint identifiers is maintained as an attribute of the connection. None denotes the absence of a savepoint. This allows reentrancy even if the same AtomicWrapper is reused. For @@ -165,10 +165,10 @@ class Atomic(ContextDecorator): Since database connections are thread-local, this is thread-safe. - An atomic block can be tagged as durable. In this case, raise a - RuntimeError if it's nested within another atomic block. This guarantees + An atomic block can be tagged as durable. In this case, a RuntimeError is + raised if it's nested within another atomic block. This guarantees that database changes in a durable block are committed to the database when - the block exists without error. + the block exits without error. This is a private API. """ From b21f6d7ee41ff09e1ff16d2bbb929464f6939d37 Mon Sep 17 00:00:00 2001 From: Ellen <38250543+ellen364@users.noreply.github.com> Date: Tue, 2 Jul 2024 20:29:14 +0100 Subject: [PATCH 16/21] Fixed broken link in django.core.files.temp docstring. --- django/core/files/temp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/core/files/temp.py b/django/core/files/temp.py index 5bd31dd5f2..b719d94a84 100644 --- a/django/core/files/temp.py +++ b/django/core/files/temp.py @@ -12,7 +12,7 @@ processes in a manner that works across platforms. The custom version of NamedTemporaryFile doesn't support the same keyword arguments available in tempfile.NamedTemporaryFile. -1: https://mail.python.org/pipermail/python-list/2005-December/336957.html +1: https://mail.python.org/pipermail/python-list/2005-December/336955.html 2: https://bugs.python.org/issue14243 """ From bdd538488c837edec74cbfd4b0064228373319a1 Mon Sep 17 00:00:00 2001 From: Bendeguz Csirmaz Date: Thu, 18 Jul 2024 18:33:09 +0800 Subject: [PATCH 17/21] Fixed #35614 -- Prevented SQLCompiler.as_subquery_condition() from mutating a query. --- django/db/models/sql/compiler.py | 9 +++++---- tests/foreign_object/tests.py | 7 +++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 262d722dc1..1d426f49b6 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -1616,14 +1616,15 @@ class SQLCompiler: def as_subquery_condition(self, alias, columns, compiler): qn = compiler.quote_name_unless_alias qn2 = self.connection.ops.quote_name + query = self.query.clone() - for index, select_col in enumerate(self.query.select): + for index, select_col in enumerate(query.select): lhs_sql, lhs_params = self.compile(select_col) rhs = "%s.%s" % (qn(alias), qn2(columns[index])) - self.query.where.add(RawSQL("%s = %s" % (lhs_sql, rhs), lhs_params), AND) + query.where.add(RawSQL("%s = %s" % (lhs_sql, rhs), lhs_params), AND) - sql, params = self.as_sql() - return "EXISTS (%s)" % sql, params + sql, params = query.as_sql(compiler, self.connection) + return "EXISTS %s" % sql, params def explain_query(self): result = list(self.execute_sql()) diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py index c9e8da5792..2d3aa800f7 100644 --- a/tests/foreign_object/tests.py +++ b/tests/foreign_object/tests.py @@ -223,6 +223,13 @@ class MultiColumnFKTests(TestCase): [m2], ) + def test_query_does_not_mutate(self): + """ + Recompiling the same subquery doesn't mutate it. + """ + queryset = Friendship.objects.filter(to_friend__in=Person.objects.all()) + self.assertEqual(str(queryset.query), str(queryset.query)) + def test_select_related_foreignkey_forward_works(self): Membership.objects.create( membership_country=self.usa, person=self.bob, group=self.cia From 1029a4694e5390e8c6ec88a6a2192c2e32bb5430 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 2 Jun 2024 11:42:35 -0400 Subject: [PATCH 18/21] Fixed typo in django/test/testcases.py docstring. --- django/test/testcases.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/test/testcases.py b/django/test/testcases.py index f1c6b5ae9c..6027332cd5 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -405,8 +405,8 @@ class SimpleTestCase(unittest.TestCase): def modify_settings(self, **kwargs): """ - A context manager that temporarily applies changes a list setting and - reverts back to the original value when exiting the context. + A context manager that temporarily applies changes to a list setting + and reverts back to the original value when exiting the context. """ return modify_settings(**kwargs) From 2ff917fd06e5155920e085837a2cfab73604e3e4 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 20 Jul 2024 09:56:22 +0300 Subject: [PATCH 19/21] Applied optimizations to template.utils.get_app_template_dirs(). --- django/template/utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/django/template/utils.py b/django/template/utils.py index 2b118f900e..05e9d46cad 100644 --- a/django/template/utils.py +++ b/django/template/utils.py @@ -102,10 +102,9 @@ def get_app_template_dirs(dirname): dirname is the name of the subdirectory containing templates inside installed applications. """ - template_dirs = [ - Path(app_config.path) / dirname - for app_config in apps.get_app_configs() - if app_config.path and (Path(app_config.path) / dirname).is_dir() - ] # Immutable return value because it will be cached and shared by callers. - return tuple(template_dirs) + return tuple( + path + for app_config in apps.get_app_configs() + if app_config.path and (path := Path(app_config.path) / dirname).is_dir() + ) From b06cf62c88218292209427f67dc679761d7d24fc Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Sun, 21 Jul 2024 07:44:38 +0200 Subject: [PATCH 20/21] Cleaned up temporary test directories in tests. --- tests/forms_tests/tests/tests.py | 1 + tests/model_forms/models.py | 2 +- tests/model_forms/tests.py | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/forms_tests/tests/tests.py b/tests/forms_tests/tests/tests.py index 196085ceb2..38735bfb78 100644 --- a/tests/forms_tests/tests/tests.py +++ b/tests/forms_tests/tests/tests.py @@ -259,6 +259,7 @@ class FormsModelTestCase(TestCase): m.file.name, "tests/\u6211\u96bb\u6c23\u588a\u8239\u88dd\u6eff\u6652\u9c54.txt", ) + m.file.delete() m.delete() def test_boundary_conditions(self): diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py index c28461d862..f9441a4c77 100644 --- a/tests/model_forms/models.py +++ b/tests/model_forms/models.py @@ -135,7 +135,7 @@ class WriterProfile(models.Model): class Document(models.Model): - myfile = models.FileField(upload_to="unused", blank=True) + myfile = models.FileField(storage=temp_storage, upload_to="unused", blank=True) class TextFile(models.Model): diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index c6e12e1aab..3c4c510440 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -1,5 +1,6 @@ import datetime import os +import shutil from decimal import Decimal from unittest import mock, skipUnless @@ -72,6 +73,7 @@ from .models import ( Triple, Writer, WriterProfile, + temp_storage_dir, test_images, ) @@ -2482,6 +2484,12 @@ class ModelOneToOneFieldTests(TestCase): class FileAndImageFieldTests(TestCase): + def setUp(self): + if os.path.exists(temp_storage_dir): + shutil.rmtree(temp_storage_dir) + os.mkdir(temp_storage_dir) + self.addCleanup(shutil.rmtree, temp_storage_dir) + def test_clean_false(self): """ If the ``clean`` method on a non-required FileField receives False as From cf03aa4e94625971852a09e869f7ee7c328b573f Mon Sep 17 00:00:00 2001 From: nessita <124304+nessita@users.noreply.github.com> Date: Mon, 22 Jul 2024 10:31:54 -0300 Subject: [PATCH 21/21] Refs #10941 -- Reorganized querystring template tag docs. --- docs/ref/templates/builtins.txt | 85 +++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 26 deletions(-) diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index b920478700..2413cb1d82 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -959,66 +959,99 @@ output (as a string) inside a variable. This is useful if you want to use .. versionadded:: 5.1 -Outputs the query string from a given :class:`~django.http.QueryDict` instance, -if provided, or ``request.GET`` if not and the -``django.template.context_processors.request`` context processor is enabled. -If the ``QueryDict`` is empty, then the output will be an empty string. -Otherwise, the query string will be returned with a leading ``"?"``. +Outputs a URL-encoded formatted query string based on the provided parameters. -If not using the ``django.template.context_processors.request`` context -processor, you must pass either the ``request`` into the template context or a -``QueryDict`` instance into this tag. +This tag requires a :class:`~django.http.QueryDict` instance, which defaults to +:attr:`request.GET ` if none is provided. -The following example outputs the current query string verbatim. So if the -query string is ``?color=green&size=M``, the output would be -``?color=green&size=M``: +If the :class:`~django.http.QueryDict` is empty and no additional parameters +are provided, an empty string is returned. A non-empty result includes a +leading ``"?"``. + +.. admonition:: Using ``request.GET`` as default + + To use ``request.GET`` as the default ``QueryDict`` instance, the + ``django.template.context_processors.request`` context processor should be + enabled. If it's not enabled, you must either explicitly pass the + ``request`` object into the template context, or provide a ``QueryDict`` + instance to this tag. + +Basic usage +~~~~~~~~~~~ .. code-block:: html+django {% querystring %} -You can also pass in a custom ``QueryDict`` that will be used instead of -``request.GET``: +Outputs the current query string verbatim. So if the query string is +``?color=green``, the output would be ``?color=green``. + +.. code-block:: html+django + + {% querystring size="M" %} + +Outputs the current query string with the addition of the ``size`` parameter. +Following the previous example, the output would be ``?color=green&size=M``. + +Custom QueryDict +~~~~~~~~~~~~~~~~ .. code-block:: html+django {% querystring my_query_dict %} -Each keyword argument will be added to the query string, replacing any existing -value for that key. With the query string ``?color=blue``, the following would -result in ``?color=red&size=S``: +You can provide a custom ``QueryDict`` to be used instead of ``request.GET``. +So if ``my_query_dict`` is ````, this outputs +``?color=blue``. + +Setting items +~~~~~~~~~~~~~ .. code-block:: html+django {% querystring color="red" size="S" %} -It is possible to remove parameters by passing ``None`` as a value. With the -query string ``?color=blue&size=M``, the following would result in ``?size=M``: +Adds or modifies parameters in the query string. Each keyword argument will be +added to the query string, replacing any existing value for that key. For +instance, if the current query string is ``?color=green``, the output will be +``?color=red&size=S``. + +Removing items +~~~~~~~~~~~~~~ .. code-block:: html+django {% querystring color=None %} -If the given parameter is a list, the value will remain as a list. For example, -if ``my_list`` is set to ``["red", "blue"]``, the following would result in -``?color=red&color=blue``: +Passing ``None`` as the value removes the parameter from the query string. For +example, if the current query string is ``?color=green&size=M``, the output +will be ``?size=M``. + +Handling lists +~~~~~~~~~~~~~~ .. code-block:: html+django {% querystring color=my_list %} +If ``my_list`` is ``["red", "blue"]``, the output will be +``?color=red&color=blue``, preserving the list structure in the query string. + +Dynamic usage +~~~~~~~~~~~~~ + A common example of using this tag is to preserve the current query string when displaying a page of results, while adding a link to the next and previous -pages of results. For example, if the paginator is currently on page 3, and -the current query string is ``?color=blue&size=M&page=3``, the following code -would output ``?color=blue&size=M&page=4``: +pages of results. For example, if the paginator is currently on page 3, and the +current query string is ``?color=blue&size=M&page=3``, the following code would +output ``?color=blue&size=M&page=4``: .. code-block:: html+django {% querystring page=page.next_page_number %} -You can also store the value in a variable, for example, if you need multiple -links to the same page with syntax such as: +You can also store the value in a variable. For example, if you need multiple +links to the same page, define it as: .. code-block:: html+django