0
0
mirror of https://github.com/django/django.git synced 2024-11-21 19:09:18 +01:00

Merge branch 'main' into ticket_21961

This commit is contained in:
Akash Kumar Sen 2024-07-23 11:44:52 +05:30 committed by GitHub
commit 3395525121
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 352 additions and 204 deletions

View File

@ -416,6 +416,7 @@ answer newbie questions, and generally made Django that much better:
Himanshu Chauhan <hchauhan1404@outlook.com>
hipertracker@gmail.com
Hiroki Kiyohara <hirokiky@gmail.com>
Hisham Mahmood <hishammahmood41@gmail.com>
Honza Král <honza.kral@gmail.com>
Horst Gutmann <zerok@zerokspot.com>
Hugo Osvaldo Barrera <hugo@barrera.io>

View File

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

View File

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

View File

@ -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,30 +642,31 @@ 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)
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:

View File

@ -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
):

View File

@ -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:
@ -1134,15 +1138,12 @@ class SQLCompiler:
"""
result = []
params = []
for alias in tuple(self.query.alias_map):
# 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
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)
@ -1615,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())

View File

@ -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.
"""

View File

@ -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

View File

@ -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()
)

View File

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

View File

@ -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

View File

@ -952,77 +952,110 @@ 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
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 <django.http.HttpRequest.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
{% query_string %}
{% 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
{% query_string my_query_dict %}
{% querystring size="M" %}
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``:
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
{% query_string color="red" size="S" %}
{% querystring my_query_dict %}
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``:
You can provide a custom ``QueryDict`` to be used instead of ``request.GET``.
So if ``my_query_dict`` is ``<QueryDict: {'color': ['blue']}>``, this outputs
``?color=blue``.
Setting items
~~~~~~~~~~~~~
.. code-block:: html+django
{% query_string color=None %}
{% querystring color="red" size="S" %}
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``:
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
{% query_string color=my_list %}
{% querystring color=None %}
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
{% 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:
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
{% query_string page=page.next_page_number as next_page %}
{% querystring page=page.next_page_number as next_page %}
.. templatetag:: regroup

View File

@ -9,4 +9,9 @@ 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`).
* 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`).

View File

@ -37,10 +37,10 @@ database-level actions:
* ``DB_SET_NULL`` - set the referring foreign key to SQL ``NULL``.
* ``DB_SET_DEFAULT`` - set the referring column to its ``db_default`` value.
``{% query_string %}`` template tag
``{% querystring %}`` template tag
-----------------------------------
Django 5.1 introduces the :ttag:`{% query_string %} <query_string>` template
Django 5.1 introduces the :ttag:`{% querystring %} <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.
@ -64,7 +64,7 @@ When switching to using this new template tag, the above magically becomes:
.. code-block:: html+django
<a href="{% query_string page=page.next_page_number %}">Next page</a>
<a href="{% querystring page=page.next_page_number %}">Next page</a>
PostgreSQL Connection Pools
---------------------------

View File

@ -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'<h2 class="main">{self.name}</h2>'
class Genre(models.Model):
name = models.CharField(max_length=20)

View File

@ -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",
'<td class="field-parent__name">-</td>'
'<td class="field-parent__parent__name">-</td>',
)
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

View File

@ -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"},
),
]
@ -145,7 +149,6 @@ class PhotographerAdmin(admin.ModelAdmin):
),
]
inlines = [
PhotoTabularInline,
PhotoTabularInline,
PhotoStackedExtra2Inline,
PhotoStackedExtra3Inline,

View File

@ -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"),
[],
)

View File

@ -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
@ -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"],
@ -1063,6 +1070,32 @@ 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(
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"

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

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

View File

@ -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):

View File

@ -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

View File

@ -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=[])

View File

@ -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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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")

View File

@ -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,
),
]

View File

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