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:
commit
3395525121
1
AUTHORS
1
AUTHORS
@ -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>
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
"""
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -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())
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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`).
|
||||
|
@ -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
|
||||
---------------------------
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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"),
|
||||
[],
|
||||
)
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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"),
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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=[])
|
||||
|
@ -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")
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user