1
0
mirror of https://github.com/django/django.git synced 2025-06-05 03:29:12 +00:00

Merge remote-tracking branch 'django/main'

This commit is contained in:
tanaydin 2024-12-10 00:14:53 +01:00
commit 0f7fc9da59
54 changed files with 1336 additions and 528 deletions

View File

@ -169,7 +169,7 @@ class ArrayField(CheckFieldDefaultMixin, Field):
else: else:
obj = AttributeSetter(base_field.attname, val) obj = AttributeSetter(base_field.attname, val)
values.append(base_field.value_to_string(obj)) values.append(base_field.value_to_string(obj))
return json.dumps(values) return json.dumps(values, ensure_ascii=False)
def get_transform(self, name): def get_transform(self, name):
transform = super().get_transform(name) transform = super().get_transform(name)

View File

@ -43,7 +43,7 @@ class HStoreField(CheckFieldDefaultMixin, Field):
return value return value
def value_to_string(self, obj): def value_to_string(self, obj):
return json.dumps(self.value_from_object(obj)) return json.dumps(self.value_from_object(obj), ensure_ascii=False)
def formfield(self, **kwargs): def formfield(self, **kwargs):
return super().formfield( return super().formfield(

View File

@ -40,7 +40,7 @@ def check_programs(*programs):
def is_valid_locale(locale): def is_valid_locale(locale):
return re.match(r"^[a-z]+$", locale) or re.match(r"^[a-z]+_[A-Z].*$", locale) return re.match(r"^[a-z]+$", locale) or re.match(r"^[a-z]+_[A-Z0-9].*$", locale)
@total_ordering @total_ordering

View File

@ -32,10 +32,9 @@ class Command(BaseCommand):
) )
def execute(self, *args, **options): def execute(self, *args, **options):
# sqlmigrate doesn't support coloring its output but we need to force # sqlmigrate doesn't support coloring its output, so make the
# no_color=True so that the BEGIN/COMMIT statements added by # BEGIN/COMMIT statements added by output_transaction colorless also.
# output_transaction don't get colored either. self.style.SQL_KEYWORD = lambda noop: noop
options["no_color"] = True
return super().execute(*args, **options) return super().execute(*args, **options)
def handle(self, *args, **options): def handle(self, *args, **options):

View File

@ -8,7 +8,6 @@ import sqlparse
from django.conf import settings from django.conf import settings
from django.db import NotSupportedError, transaction from django.db import NotSupportedError, transaction
from django.db.backends import utils
from django.db.models.expressions import Col from django.db.models.expressions import Col
from django.utils import timezone from django.utils import timezone
from django.utils.deprecation import RemovedInDjango60Warning from django.utils.deprecation import RemovedInDjango60Warning
@ -586,7 +585,7 @@ class BaseDatabaseOperations:
Transform a decimal.Decimal value to an object compatible with what is Transform a decimal.Decimal value to an object compatible with what is
expected by the backend driver for decimal (numeric) columns. expected by the backend driver for decimal (numeric) columns.
""" """
return utils.format_number(value, max_digits, decimal_places) return value
def adapt_ipaddressfield_value(self, value): def adapt_ipaddressfield_value(self, value):
""" """

View File

@ -166,9 +166,6 @@ class DatabaseOperations(BaseDatabaseOperations):
""" """
return [(None, ("NULL", [], False))] return [(None, ("NULL", [], False))]
def adapt_decimalfield_value(self, value, max_digits=None, decimal_places=None):
return value
def last_executed_query(self, cursor, sql, params): def last_executed_query(self, cursor, sql, params):
# With MySQLdb, cursor objects have an (undocumented) "_executed" # With MySQLdb, cursor objects have an (undocumented) "_executed"
# attribute where the exact query sent to the database is saved. # attribute where the exact query sent to the database is saved.

View File

@ -629,9 +629,6 @@ END;
1900, 1, 1, value.hour, value.minute, value.second, value.microsecond 1900, 1, 1, value.hour, value.minute, value.second, value.microsecond
) )
def adapt_decimalfield_value(self, value, max_digits=None, decimal_places=None):
return value
def combine_expression(self, connector, sub_expressions): def combine_expression(self, connector, sub_expressions):
lhs, rhs = sub_expressions lhs, rhs = sub_expressions
if connector == "%%": if connector == "%%":

View File

@ -346,9 +346,6 @@ class DatabaseOperations(BaseDatabaseOperations):
def adapt_timefield_value(self, value): def adapt_timefield_value(self, value):
return value return value
def adapt_decimalfield_value(self, value, max_digits=None, decimal_places=None):
return value
def adapt_ipaddressfield_value(self, value): def adapt_ipaddressfield_value(self, value):
if value: if value:
return Inet(value) return Inet(value)

View File

@ -50,6 +50,10 @@ class DatabaseFeatures(BaseDatabaseFeatures):
# The django_format_dtdelta() function doesn't properly handle mixed # The django_format_dtdelta() function doesn't properly handle mixed
# Date/DateTime fields and timedeltas. # Date/DateTime fields and timedeltas.
"expressions.tests.FTimeDeltaTests.test_mixed_comparisons1", "expressions.tests.FTimeDeltaTests.test_mixed_comparisons1",
# SQLite doesn't parse escaped double quotes in the JSON path notation,
# so it cannot match keys that contains double quotes (#35842).
"model_fields.test_jsonfield.TestQuerying."
"test_lookups_special_chars_double_quotes",
} }
create_test_table_with_composite_primary_key = """ create_test_table_with_composite_primary_key = """
CREATE TABLE test_table_composite_pk ( CREATE TABLE test_table_composite_pk (

View File

@ -726,12 +726,13 @@ class Model(AltersData, metaclass=ModelBase):
if fields is not None: if fields is not None:
db_instance_qs = db_instance_qs.only(*fields) db_instance_qs = db_instance_qs.only(*fields)
elif deferred_fields: elif deferred_fields:
fields = { db_instance_qs = db_instance_qs.only(
f.attname *{
for f in self._meta.concrete_fields f.attname
if f.attname not in deferred_fields for f in self._meta.concrete_fields
} if f.attname not in deferred_fields
db_instance_qs = db_instance_qs.only(*fields) }
)
db_instance = db_instance_qs.get() db_instance = db_instance_qs.get()
non_loaded_fields = db_instance.get_deferred_fields() non_loaded_fields = db_instance.get_deferred_fields()
@ -748,9 +749,9 @@ class Model(AltersData, metaclass=ModelBase):
field.delete_cached_value(self) field.delete_cached_value(self)
# Clear cached relations. # Clear cached relations.
for field in self._meta.related_objects: for rel in self._meta.related_objects:
if (fields is None or field.name in fields) and field.is_cached(self): if (fields is None or rel.name in fields) and rel.is_cached(self):
field.delete_cached_value(self) rel.delete_cached_value(self)
# Clear cached private relations. # Clear cached private relations.
for field in self._meta.private_fields: for field in self._meta.private_fields:

View File

@ -1828,9 +1828,8 @@ class DecimalField(Field):
) )
return decimal_value return decimal_value
def get_db_prep_save(self, value, connection): def get_db_prep_value(self, value, connection, prepared=False):
if hasattr(value, "as_sql"): value = super().get_db_prep_value(value, connection, prepared)
return value
return connection.ops.adapt_decimalfield_value( return connection.ops.adapt_decimalfield_value(
self.to_python(value), self.max_digits, self.decimal_places self.to_python(value), self.max_digits, self.decimal_places
) )

View File

@ -193,20 +193,18 @@ class HasKeyLookup(PostgresOperatorLookup):
# Compile the final key without interpreting ints as array elements. # Compile the final key without interpreting ints as array elements.
return ".%s" % json.dumps(key_transform) return ".%s" % json.dumps(key_transform)
def as_sql(self, compiler, connection, template=None): def _as_sql_parts(self, compiler, connection):
# Process JSON path from the left-hand side. # Process JSON path from the left-hand side.
if isinstance(self.lhs, KeyTransform): if isinstance(self.lhs, KeyTransform):
lhs, lhs_params, lhs_key_transforms = self.lhs.preprocess_lhs( lhs_sql, lhs_params, lhs_key_transforms = self.lhs.preprocess_lhs(
compiler, connection compiler, connection
) )
lhs_json_path = compile_json_path(lhs_key_transforms) lhs_json_path = compile_json_path(lhs_key_transforms)
else: else:
lhs, lhs_params = self.process_lhs(compiler, connection) lhs_sql, lhs_params = self.process_lhs(compiler, connection)
lhs_json_path = "$" lhs_json_path = "$"
sql = template % lhs
# Process JSON path from the right-hand side. # Process JSON path from the right-hand side.
rhs = self.rhs rhs = self.rhs
rhs_params = []
if not isinstance(rhs, (list, tuple)): if not isinstance(rhs, (list, tuple)):
rhs = [rhs] rhs = [rhs]
for key in rhs: for key in rhs:
@ -217,24 +215,45 @@ class HasKeyLookup(PostgresOperatorLookup):
*rhs_key_transforms, final_key = rhs_key_transforms *rhs_key_transforms, final_key = rhs_key_transforms
rhs_json_path = compile_json_path(rhs_key_transforms, include_root=False) rhs_json_path = compile_json_path(rhs_key_transforms, include_root=False)
rhs_json_path += self.compile_json_path_final_key(final_key) rhs_json_path += self.compile_json_path_final_key(final_key)
rhs_params.append(lhs_json_path + rhs_json_path) yield lhs_sql, lhs_params, lhs_json_path + rhs_json_path
def _combine_sql_parts(self, parts):
# Add condition for each key. # Add condition for each key.
if self.logical_operator: if self.logical_operator:
sql = "(%s)" % self.logical_operator.join([sql] * len(rhs_params)) return "(%s)" % self.logical_operator.join(parts)
return sql, tuple(lhs_params) + tuple(rhs_params) return "".join(parts)
def as_sql(self, compiler, connection, template=None):
sql_parts = []
params = []
for lhs_sql, lhs_params, rhs_json_path in self._as_sql_parts(
compiler, connection
):
sql_parts.append(template % (lhs_sql, "%s"))
params.extend(lhs_params + [rhs_json_path])
return self._combine_sql_parts(sql_parts), tuple(params)
def as_mysql(self, compiler, connection): def as_mysql(self, compiler, connection):
return self.as_sql( return self.as_sql(
compiler, connection, template="JSON_CONTAINS_PATH(%s, 'one', %%s)" compiler, connection, template="JSON_CONTAINS_PATH(%s, 'one', %s)"
) )
def as_oracle(self, compiler, connection): def as_oracle(self, compiler, connection):
sql, params = self.as_sql( # Use a custom delimiter to prevent the JSON path from escaping the SQL
compiler, connection, template="JSON_EXISTS(%s, '%%s')" # literal. See comment in KeyTransform.
) template = "JSON_EXISTS(%s, q'\uffff%s\uffff')"
# Add paths directly into SQL because path expressions cannot be passed sql_parts = []
# as bind variables on Oracle. params = []
return sql % tuple(params), [] for lhs_sql, lhs_params, rhs_json_path in self._as_sql_parts(
compiler, connection
):
# Add right-hand-side directly into SQL because it cannot be passed
# as bind variables to JSON_EXISTS. It might result in invalid
# queries but it is assumed that it cannot be evaded because the
# path is JSON serialized.
sql_parts.append(template % (lhs_sql, rhs_json_path))
params.extend(lhs_params)
return self._combine_sql_parts(sql_parts), tuple(params)
def as_postgresql(self, compiler, connection): def as_postgresql(self, compiler, connection):
if isinstance(self.rhs, KeyTransform): if isinstance(self.rhs, KeyTransform):
@ -246,7 +265,7 @@ class HasKeyLookup(PostgresOperatorLookup):
def as_sqlite(self, compiler, connection): def as_sqlite(self, compiler, connection):
return self.as_sql( return self.as_sql(
compiler, connection, template="JSON_TYPE(%s, %%s) IS NOT NULL" compiler, connection, template="JSON_TYPE(%s, %s) IS NOT NULL"
) )
@ -362,10 +381,24 @@ class KeyTransform(Transform):
json_path = compile_json_path(key_transforms) json_path = compile_json_path(key_transforms)
if connection.features.supports_primitives_in_json_field: if connection.features.supports_primitives_in_json_field:
sql = ( sql = (
"COALESCE(JSON_VALUE(%s, '%s'), JSON_QUERY(%s, '%s' DISALLOW SCALARS))" "COALESCE("
"JSON_VALUE(%s, q'\uffff%s\uffff'),"
"JSON_QUERY(%s, q'\uffff%s\uffff' DISALLOW SCALARS)"
")"
) )
else: else:
sql = "COALESCE(JSON_QUERY(%s, '%s'), JSON_VALUE(%s, '%s'))" sql = (
"COALESCE("
"JSON_QUERY(%s, q'\uffff%s\uffff'),"
"JSON_VALUE(%s, q'\uffff%s\uffff')"
")"
)
# Add paths directly into SQL because path expressions cannot be passed
# as bind variables on Oracle. Use a custom delimiter to prevent the
# JSON path from escaping the SQL literal. Each key in the JSON path is
# passed through json.dumps() with ensure_ascii=True (the default),
# which converts the delimiter into the escaped \uffff format. This
# ensures that the delimiter is not present in the JSON path.
return sql % ((lhs, json_path) * 2), tuple(params) * 2 return sql % ((lhs, json_path) * 2), tuple(params) * 2
def as_postgresql(self, compiler, connection): def as_postgresql(self, compiler, connection):
@ -455,9 +488,9 @@ class KeyTransformIsNull(lookups.IsNull):
return "(NOT %s OR %s IS NULL)" % (sql, lhs), tuple(params) + tuple(lhs_params) return "(NOT %s OR %s IS NULL)" % (sql, lhs), tuple(params) + tuple(lhs_params)
def as_sqlite(self, compiler, connection): def as_sqlite(self, compiler, connection):
template = "JSON_TYPE(%s, %%s) IS NULL" template = "JSON_TYPE(%s, %s) IS NULL"
if not self.rhs: if not self.rhs:
template = "JSON_TYPE(%s, %%s) IS NOT NULL" template = "JSON_TYPE(%s, %s) IS NOT NULL"
return HasKeyOrArrayIndex(self.lhs.lhs, self.lhs.key_name).as_sql( return HasKeyOrArrayIndex(self.lhs.lhs, self.lhs.key_name).as_sql(
compiler, compiler,
connection, connection,

View File

@ -298,7 +298,10 @@ class BaseForm(RenderableFormMixin):
error_class="nonfield", renderer=self.renderer error_class="nonfield", renderer=self.renderer
) )
else: else:
self._errors[field] = self.error_class(renderer=self.renderer) self._errors[field] = self.error_class(
renderer=self.renderer,
field_id=self[field].auto_id,
)
self._errors[field].extend(error_list) self._errors[field].extend(error_list)
if field in self.cleaned_data: if field in self.cleaned_data:
del self.cleaned_data[field] del self.cleaned_data[field]

View File

@ -1 +1 @@
{% if errors %}<ul class="{{ error_class }}">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %} {% if errors %}<ul class="{{ error_class }}"{% if errors.field_id %} id="{{ errors.field_id }}_error"{% endif %}>{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}

View File

@ -1 +1 @@
{% if errors %}<ul class="{{ error_class }}">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %} {% if errors %}<ul class="{{ error_class }}"{% if errors.field_id %} id="{{ errors.field_id }}_error"{% endif %}>{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}

View File

@ -147,7 +147,7 @@ class ErrorList(UserList, list, RenderableErrorMixin):
template_name_text = "django/forms/errors/list/text.txt" template_name_text = "django/forms/errors/list/text.txt"
template_name_ul = "django/forms/errors/list/ul.html" template_name_ul = "django/forms/errors/list/ul.html"
def __init__(self, initlist=None, error_class=None, renderer=None): def __init__(self, initlist=None, error_class=None, renderer=None, field_id=None):
super().__init__(initlist) super().__init__(initlist)
if error_class is None: if error_class is None:
@ -155,6 +155,7 @@ class ErrorList(UserList, list, RenderableErrorMixin):
else: else:
self.error_class = "errorlist {}".format(error_class) self.error_class = "errorlist {}".format(error_class)
self.renderer = renderer or get_default_renderer() self.renderer = renderer or get_default_renderer()
self.field_id = field_id
def as_data(self): def as_data(self):
return ValidationError(self.data).error_list return ValidationError(self.data).error_list

View File

@ -242,7 +242,11 @@ def do_block(parser, token):
return BlockNode(block_name, nodelist) return BlockNode(block_name, nodelist)
def construct_relative_path(current_template_name, relative_name): def construct_relative_path(
current_template_name,
relative_name,
allow_recursion=False,
):
""" """
Convert a relative path (starting with './' or '../') to the full template Convert a relative path (starting with './' or '../') to the full template
name based on the current_template_name. name based on the current_template_name.
@ -264,7 +268,7 @@ def construct_relative_path(current_template_name, relative_name):
"The relative path '%s' points outside the file hierarchy that " "The relative path '%s' points outside the file hierarchy that "
"template '%s' is in." % (relative_name, current_template_name) "template '%s' is in." % (relative_name, current_template_name)
) )
if current_template_name.lstrip("/") == new_name: if not allow_recursion and current_template_name.lstrip("/") == new_name:
raise TemplateSyntaxError( raise TemplateSyntaxError(
"The relative path '%s' was translated to template name '%s', the " "The relative path '%s' was translated to template name '%s', the "
"same template in which the tag appears." "same template in which the tag appears."
@ -346,7 +350,11 @@ def do_include(parser, token):
options[option] = value options[option] = value
isolated_context = options.get("only", False) isolated_context = options.get("only", False)
namemap = options.get("with", {}) namemap = options.get("with", {})
bits[1] = construct_relative_path(parser.origin.template_name, bits[1]) bits[1] = construct_relative_path(
parser.origin.template_name,
bits[1],
allow_recursion=True,
)
return IncludeNode( return IncludeNode(
parser.compile_filter(bits[1]), parser.compile_filter(bits[1]),
extra_context=namemap, extra_context=namemap,

View File

@ -8,6 +8,7 @@ from collections.abc import Mapping
from html.parser import HTMLParser from html.parser import HTMLParser
from urllib.parse import parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit from urllib.parse import parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit
from django.core.exceptions import SuspiciousOperation
from django.utils.deprecation import RemovedInDjango60Warning from django.utils.deprecation import RemovedInDjango60Warning
from django.utils.encoding import punycode from django.utils.encoding import punycode
from django.utils.functional import Promise, cached_property, keep_lazy, keep_lazy_text from django.utils.functional import Promise, cached_property, keep_lazy, keep_lazy_text
@ -40,6 +41,7 @@ VOID_ELEMENTS = frozenset(
) )
MAX_URL_LENGTH = 2048 MAX_URL_LENGTH = 2048
MAX_STRIP_TAGS_DEPTH = 50
@keep_lazy(SafeString) @keep_lazy(SafeString)
@ -211,15 +213,19 @@ def _strip_once(value):
@keep_lazy_text @keep_lazy_text
def strip_tags(value): def strip_tags(value):
"""Return the given HTML with all tags stripped.""" """Return the given HTML with all tags stripped."""
# Note: in typical case this loop executes _strip_once once. Loop condition
# is redundant, but helps to reduce number of executions of _strip_once.
value = str(value) value = str(value)
# Note: in typical case this loop executes _strip_once twice (the second
# execution does not remove any more tags).
strip_tags_depth = 0
while "<" in value and ">" in value: while "<" in value and ">" in value:
if strip_tags_depth >= MAX_STRIP_TAGS_DEPTH:
raise SuspiciousOperation
new_value = _strip_once(value) new_value = _strip_once(value)
if value.count("<") == new_value.count("<"): if value.count("<") == new_value.count("<"):
# _strip_once wasn't able to detect more tags. # _strip_once wasn't able to detect more tags.
break break
value = new_value value = new_value
strip_tags_depth += 1
return value return value

View File

@ -234,9 +234,7 @@ Now let's update our ``index`` view in ``polls/views.py`` to use the template:
def index(request): def index(request):
latest_question_list = Question.objects.order_by("-pub_date")[:5] latest_question_list = Question.objects.order_by("-pub_date")[:5]
template = loader.get_template("polls/index.html") template = loader.get_template("polls/index.html")
context = { context = {"latest_question_list": latest_question_list}
"latest_question_list": latest_question_list,
}
return HttpResponse(template.render(context, request)) return HttpResponse(template.render(context, request))
That code loads the template called ``polls/index.html`` and passes it a That code loads the template called ``polls/index.html`` and passes it a

View File

@ -407,6 +407,7 @@ Function PostGIS Oracle MariaDB MySQL
:class:`FromWKB` X X X X X :class:`FromWKB` X X X X X
:class:`FromWKT` X X X X X :class:`FromWKT` X X X X X
:class:`GeoHash` X X (≥ 11.7) X X (LWGEOM/RTTOPO) :class:`GeoHash` X X (≥ 11.7) X X (LWGEOM/RTTOPO)
:class:`GeometryDistance` X
:class:`Intersection` X X X X X :class:`Intersection` X X X X X
:class:`IsEmpty` X :class:`IsEmpty` X
:class:`IsValid` X X X (≥ 11.7) X X :class:`IsValid` X X X (≥ 11.7) X X

View File

@ -1025,13 +1025,17 @@ method you're using:
Customizing the error list format Customizing the error list format
--------------------------------- ---------------------------------
.. class:: ErrorList(initlist=None, error_class=None, renderer=None) .. class:: ErrorList(initlist=None, error_class=None, renderer=None, field_id=None)
By default, forms use ``django.forms.utils.ErrorList`` to format validation By default, forms use ``django.forms.utils.ErrorList`` to format validation
errors. ``ErrorList`` is a list like object where ``initlist`` is the errors. ``ErrorList`` is a list like object where ``initlist`` is the
list of errors. In addition this class has the following attributes and list of errors. In addition this class has the following attributes and
methods. methods.
.. versionchanged:: 5.2
The ``field_id`` argument was added.
.. attribute:: error_class .. attribute:: error_class
The CSS classes to be used when rendering the error list. Any provided The CSS classes to be used when rendering the error list. Any provided
@ -1043,6 +1047,16 @@ Customizing the error list format
Defaults to ``None`` which means to use the default renderer Defaults to ``None`` which means to use the default renderer
specified by the :setting:`FORM_RENDERER` setting. specified by the :setting:`FORM_RENDERER` setting.
.. attribute:: field_id
.. versionadded:: 5.2
An ``id`` for the field for which the errors relate. This allows an
HTML ``id`` attribute to be added in the error template and is useful
to associate the errors with the field. The default template uses the
format ``id="{{ field_id }}_error"`` and a value is provided by
:meth:`.Form.add_error` using the field's :attr:`~.BoundField.auto_id`.
.. attribute:: template_name .. attribute:: template_name
The name of the template used when calling ``__str__`` or The name of the template used when calling ``__str__`` or

View File

@ -554,12 +554,21 @@ a subclass of dictionary. Exceptions are outlined here:
.. method:: QueryDict.__getitem__(key) .. method:: QueryDict.__getitem__(key)
Returns the value for the given key. If the key has more than one value, Returns the last value for the given key; or an empty list (``[]``) if the
it returns the last value. Raises key exists but has no values. Raises
``django.utils.datastructures.MultiValueDictKeyError`` if the key does not ``django.utils.datastructures.MultiValueDictKeyError`` if the key does not
exist. (This is a subclass of Python's standard :exc:`KeyError`, so you can exist. (This is a subclass of Python's standard :exc:`KeyError`, so you can
stick to catching ``KeyError``.) stick to catching ``KeyError``.)
.. code-block:: pycon
>>> q = QueryDict("a=1&a=2&a=3", mutable=True)
>>> q.__getitem__("a")
'3'
>>> q.__setitem__("b", [])
>>> q.__getitem__("b")
[]
.. method:: QueryDict.__setitem__(key, value) .. method:: QueryDict.__setitem__(key, value)
Sets the given key to ``[value]`` (a list whose single element is Sets the given key to ``[value]`` (a list whose single element is

View File

@ -521,7 +521,7 @@ https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004
The cached value can be treated like an ordinary attribute of the instance:: The cached value can be treated like an ordinary attribute of the instance::
# clear it, requiring re-computation next time it's called # clear it, requiring re-computation next time it's called
del person.friends # or delattr(person, "friends") person.__dict__.pop("friends", None)
# set a value manually, that will persist on the instance until cleared # set a value manually, that will persist on the instance until cleared
person.friends = ["Huckleberry Finn", "Tom Sawyer"] person.friends = ["Huckleberry Finn", "Tom Sawyer"]

View File

@ -6,3 +6,28 @@ Django 4.2.17 release notes
Django 4.2.17 fixes one security issue with severity "high" and one security Django 4.2.17 fixes one security issue with severity "high" and one security
issue with severity "moderate" in 4.2.16. issue with severity "moderate" in 4.2.16.
CVE-2024-53907: Denial-of-service possibility in ``strip_tags()``
=================================================================
:func:`~django.utils.html.strip_tags` would be extremely slow to evaluate
certain inputs containing large sequences of nested incomplete HTML entities.
The ``strip_tags()`` method is used to implement the corresponding
:tfilter:`striptags` template filter, which was thus also vulnerable.
``strip_tags()`` now has an upper limit of recursive calls to ``HTMLParser``
before raising a :exc:`.SuspiciousOperation` exception.
Remember that absolutely NO guarantee is provided about the results of
``strip_tags()`` being HTML safe. So NEVER mark safe the result of a
``strip_tags()`` call without escaping it first, for example with
:func:`django.utils.html.escape`.
CVE-2024-53908: Potential SQL injection via ``HasKey(lhs, rhs)`` on Oracle
==========================================================================
Direct usage of the ``django.db.models.fields.json.HasKey`` lookup on Oracle
was subject to SQL injection if untrusted data was used as a ``lhs`` value.
Applications that use the :lookup:`has_key <jsonfield.has_key>` lookup through
the ``__`` syntax are unaffected.

View File

@ -6,3 +6,28 @@ Django 5.0.10 release notes
Django 5.0.10 fixes one security issue with severity "high" and one security Django 5.0.10 fixes one security issue with severity "high" and one security
issue with severity "moderate" in 5.0.9. issue with severity "moderate" in 5.0.9.
CVE-2024-53907: Denial-of-service possibility in ``strip_tags()``
=================================================================
:func:`~django.utils.html.strip_tags` would be extremely slow to evaluate
certain inputs containing large sequences of nested incomplete HTML entities.
The ``strip_tags()`` method is used to implement the corresponding
:tfilter:`striptags` template filter, which was thus also vulnerable.
``strip_tags()`` now has an upper limit of recursive calls to ``HTMLParser``
before raising a :exc:`.SuspiciousOperation` exception.
Remember that absolutely NO guarantee is provided about the results of
``strip_tags()`` being HTML safe. So NEVER mark safe the result of a
``strip_tags()`` call without escaping it first, for example with
:func:`django.utils.html.escape`.
CVE-2024-53908: Potential SQL injection via ``HasKey(lhs, rhs)`` on Oracle
==========================================================================
Direct usage of the ``django.db.models.fields.json.HasKey`` lookup on Oracle
was subject to SQL injection if untrusted data was used as a ``lhs`` value.
Applications that use the :lookup:`has_key <jsonfield.has_key>` lookup through
the ``__`` syntax are unaffected.

View File

@ -7,8 +7,37 @@ Django 5.1.4 release notes
Django 5.1.4 fixes one security issue with severity "high", one security issue Django 5.1.4 fixes one security issue with severity "high", one security issue
with severity "moderate", and several bugs in 5.1.3. with severity "moderate", and several bugs in 5.1.3.
CVE-2024-53907: Denial-of-service possibility in ``strip_tags()``
=================================================================
:func:`~django.utils.html.strip_tags` would be extremely slow to evaluate
certain inputs containing large sequences of nested incomplete HTML entities.
The ``strip_tags()`` method is used to implement the corresponding
:tfilter:`striptags` template filter, which was thus also vulnerable.
``strip_tags()`` now has an upper limit of recursive calls to ``HTMLParser``
before raising a :exc:`.SuspiciousOperation` exception.
Remember that absolutely NO guarantee is provided about the results of
``strip_tags()`` being HTML safe. So NEVER mark safe the result of a
``strip_tags()`` call without escaping it first, for example with
:func:`django.utils.html.escape`.
CVE-2024-53908: Potential SQL injection via ``HasKey(lhs, rhs)`` on Oracle
==========================================================================
Direct usage of the ``django.db.models.fields.json.HasKey`` lookup on Oracle
was subject to SQL injection if untrusted data was used as a ``lhs`` value.
Applications that use the :lookup:`has_key <jsonfield.has_key>` lookup through
the ``__`` syntax are unaffected.
Bugfixes Bugfixes
======== ========
* Fixed a crash in ``createsuperuser`` on Python 3.13+ caused by an unhandled * Fixed a crash in ``createsuperuser`` on Python 3.13+ caused by an unhandled
``OSError`` when the username could not be determined (:ticket:`35942`). ``OSError`` when the username could not be determined (:ticket:`35942`).
* Fixed a regression in Django 5.1 where relational fields were not updated
when calling ``Model.refresh_from_db()`` on instances with deferred fields
(:ticket:`35950`).

12
docs/releases/5.1.5.txt Normal file
View File

@ -0,0 +1,12 @@
==========================
Django 5.1.5 release notes
==========================
*Expected January 7, 2025*
Django 5.1.5 fixes several bugs in 5.1.4.
Bugfixes
========
* ...

View File

@ -249,6 +249,10 @@ Forms
* The new :class:`~django.forms.TelInput` form widget is for entering telephone * The new :class:`~django.forms.TelInput` form widget is for entering telephone
numbers and renders as ``<input type="tel" ...>``. numbers and renders as ``<input type="tel" ...>``.
* The new ``field_id`` argument for :class:`~django.forms.ErrorList` allows an
HTML ``id`` attribute to be added in the error template. See
:attr:`.ErrorList.field_id` for details.
Generic Views Generic Views
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
@ -395,6 +399,8 @@ backends.
* The new :meth:`Model._is_pk_set() <django.db.models.Model._is_pk_set>` method * The new :meth:`Model._is_pk_set() <django.db.models.Model._is_pk_set>` method
allows checking if a Model instance's primary key is defined. allows checking if a Model instance's primary key is defined.
* ``BaseDatabaseOperations.adapt_decimalfield_value()`` is now a no-op, simply
returning the given value.
:mod:`django.contrib.gis` :mod:`django.contrib.gis`
------------------------- -------------------------

View File

@ -32,6 +32,7 @@ versions of the documentation contain the release notes for any later releases.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
5.1.5
5.1.4 5.1.4
5.1.3 5.1.3
5.1.2 5.1.2

View File

@ -36,6 +36,28 @@ Issues under Django's security process
All security issues have been handled under versions of Django's security All security issues have been handled under versions of Django's security
process. These are listed below. process. These are listed below.
December 4, 2024 - :cve:`2024-53907`
------------------------------------
Potential denial-of-service in ``django.utils.html.strip_tags()``.
`Full description
<https://www.djangoproject.com/weblog/2024/dec/04/security-releases/>`__
* Django 5.1 :commit:`(patch) <bbc74a7f7eb7335e913bdb4787f22e83a9be947e>`
* Django 5.0 :commit:`(patch) <a5a89ea28cc550c1b29b03f9e14ef3c128ec1e84>`
* Django 4.2 :commit:`(patch) <790eb058b0716c536a2f2e8d1c6d5079d776c22b>`
December 4, 2024 - :cve:`2024-53908`
------------------------------------
Potential SQL injection in ``HasKey(lhs, rhs)`` on Oracle.
`Full description
<https://www.djangoproject.com/weblog/2024/dec/04/security-releases/>`__
* Django 5.1 :commit:`(patch) <6943d61818e63e77b65d8b1ae65941e8f04bd87b>`
* Django 5.0 :commit:`(patch) <ff08bb6c70aa45f83a5ef3bd0b601c7c9d1a7642>`
* Django 4.2 :commit:`(patch) <7376bcbf508883282ffcc0f0fac5cf0ed2d6cbc5>`
September 3, 2024 - :cve:`2024-45231` September 3, 2024 - :cve:`2024-45231`
------------------------------------- -------------------------------------

View File

@ -3,14 +3,12 @@ accessor
accessors accessors
Aceh Aceh
admindocs admindocs
affine
affordances affordances
Ai Ai
Alchin Alchin
allowlist allowlist
alphanumerics alphanumerics
amet amet
analytics
arccosine arccosine
architected architected
arcsine arcsine
@ -60,7 +58,6 @@ Bokmål
Bonham Bonham
bookmarklet bookmarklet
bookmarklets bookmarklets
boolean
booleans booleans
bpython bpython
Bronn Bronn
@ -114,23 +111,18 @@ Danga
Darussalam Darussalam
databrowse databrowse
datafile datafile
dataset
datasets
datetimes datetimes
declaratively declaratively
decrementing
deduplicates deduplicates
deduplication deduplication
deepcopy deepcopy
deferrable deferrable
DEP
deprecations deprecations
deserialization deserialization
deserialize deserialize
deserialized deserialized
deserializer deserializer
deserializing deserializing
deterministically
Deutsch Deutsch
dev dev
dictConfig dictConfig
@ -142,7 +134,6 @@ Disqus
distro distro
django django
djangoproject djangoproject
djangotutorial
dm dm
docstring docstring
docstrings docstrings
@ -165,9 +156,7 @@ esque
Ess Ess
ETag ETag
ETags ETags
exe
exfiltration exfiltration
extensibility
fallbacks fallbacks
favicon favicon
fieldset fieldset
@ -177,7 +166,6 @@ filesystems
flatpage flatpage
flatpages flatpages
focusable focusable
fooapp
formatter formatter
formatters formatters
formfield formfield
@ -210,7 +198,6 @@ hashable
hasher hasher
hashers hashers
headerlist headerlist
hoc
Hoerner Hoerner
Holovaty Holovaty
Homebrew Homebrew
@ -223,7 +210,6 @@ Hypercorn
ies ies
iframe iframe
Igbo Igbo
incrementing
indexable indexable
ing ing
ini ini
@ -240,7 +226,6 @@ iterable
iterables iterables
iteratively iteratively
ize ize
Jazzband
Jinja Jinja
jQuery jQuery
Jupyter Jupyter
@ -283,7 +268,6 @@ manouche
Marino Marino
memcache memcache
memcached memcached
mentorship
metaclass metaclass
metaclasses metaclasses
metre metre
@ -338,8 +322,6 @@ orm
Outdim Outdim
outfile outfile
paginator paginator
parallelization
parallelized
parameterization parameterization
params params
parens parens
@ -361,9 +343,7 @@ pluggable
pluralizations pluralizations
pooler pooler
postfix postfix
postgis
postgres postgres
postgresql
pragma pragma
pre pre
precisions precisions
@ -376,7 +356,6 @@ prefetches
prefetching prefetching
preload preload
preloaded preloaded
prepend
prepended prepended
prepending prepending
prepends prepends
@ -429,7 +408,6 @@ reflow
registrable registrable
reimplement reimplement
reindent reindent
reindex
releaser releaser
releasers releasers
reloader reloader
@ -439,7 +417,6 @@ repo
reportable reportable
reprojection reprojection
reraising reraising
resampling
reST reST
reStructuredText reStructuredText
reusability reusability
@ -447,7 +424,6 @@ reverter
roadmap roadmap
Roald Roald
rss rss
runtime
Sandvik Sandvik
savepoint savepoint
savepoints savepoints
@ -458,7 +434,6 @@ screencasts
semimajor semimajor
semiminor semiminor
serializability serializability
serializable
serializer serializer
serializers serializers
shapefile shapefile
@ -467,7 +442,6 @@ sharding
sitewide sitewide
sliceable sliceable
SMTP SMTP
solaris
Sorani Sorani
sortable sortable
Spectre Spectre
@ -535,7 +509,6 @@ toolkits
toolset toolset
trac trac
tracebacks tracebacks
transactional
Transifex Transifex
Tredinnick Tredinnick
triager triager

View File

@ -4,28 +4,25 @@
Fixtures Fixtures
======== ========
.. seealso::
* :doc:`/howto/initial-data`
What is a fixture?
==================
A *fixture* is a collection of files that contain the serialized contents of A *fixture* is a collection of files that contain the serialized contents of
the database. Each fixture has a unique name, and the files that comprise the the database. Each fixture has a unique name, and the files that comprise the
fixture can be distributed over multiple directories, in multiple applications. fixture can be distributed over multiple directories, in multiple applications.
How to produce a fixture? .. seealso::
=========================
* :doc:`/howto/initial-data`
How to produce a fixture
========================
Fixtures can be generated by :djadmin:`manage.py dumpdata <dumpdata>`. It's Fixtures can be generated by :djadmin:`manage.py dumpdata <dumpdata>`. It's
also possible to generate custom fixtures by directly using :doc:`serialization also possible to generate custom fixtures by directly using :doc:`serialization
tools </topics/serialization>` or even by handwriting them. tools </topics/serialization>` or even by handwriting them.
How to use a fixture? How to use a fixture
===================== ====================
Fixtures can be used to pre-populate database with data for Fixtures can be used to pre-populate the database with data for
:ref:`tests <topics-testing-fixtures>`: :ref:`tests <topics-testing-fixtures>`:
.. code-block:: python .. code-block:: python
@ -40,8 +37,8 @@ or to provide some :ref:`initial data <initial-data-via-fixtures>` using the
django-admin loaddata <fixture label> django-admin loaddata <fixture label>
Where Django looks for fixtures? How fixtures are discovered
================================ ===========================
Django will search in these locations for fixtures: Django will search in these locations for fixtures:
@ -116,8 +113,8 @@ example).
.. _MySQL: https://dev.mysql.com/doc/refman/en/constraint-foreign-key.html .. _MySQL: https://dev.mysql.com/doc/refman/en/constraint-foreign-key.html
How fixtures are saved to the database? How fixtures are saved to the database
======================================= ======================================
When fixture files are processed, the data is saved to the database as is. When fixture files are processed, the data is saved to the database as is.
Model defined :meth:`~django.db.models.Model.save` methods are not called, and Model defined :meth:`~django.db.models.Model.save` methods are not called, and

View File

@ -571,14 +571,12 @@ happen when the user changes these values:
... {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)}, ... {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
... ], ... ],
... ) ... )
>>> formset.is_valid()
True
>>> for form in formset.ordered_forms: >>> for form in formset.ordered_forms:
... print(form.cleaned_data) ... print(form.cleaned_data)
... ...
{'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0, 'title': 'Article #3'} {'title': 'Article #3', 'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0}
{'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': 'Article #2'} {'title': 'Article #2', 'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1}
{'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2, 'title': 'Article #1'} {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2}
:class:`~django.forms.formsets.BaseFormSet` also provides an :class:`~django.forms.formsets.BaseFormSet` also provides an
:attr:`~django.forms.formsets.BaseFormSet.ordering_widget` attribute and :attr:`~django.forms.formsets.BaseFormSet.ordering_widget` attribute and
@ -690,7 +688,7 @@ delete fields you can access them with ``deleted_forms``:
... ], ... ],
... ) ... )
>>> [form.cleaned_data for form in formset.deleted_forms] >>> [form.cleaned_data for form in formset.deleted_forms]
[{'DELETE': True, 'pub_date': datetime.date(2008, 5, 10), 'title': 'Article #1'}] [{'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10), 'DELETE': True}]
If you are using a :class:`ModelFormSet<django.forms.models.BaseModelFormSet>`, If you are using a :class:`ModelFormSet<django.forms.models.BaseModelFormSet>`,
model instances for deleted forms will be deleted when you call model instances for deleted forms will be deleted when you call

View File

@ -1,7 +1,7 @@
from django.core import checks from django.core import checks
from django.db import connection, models from django.db import connection, models
from django.db.models import F from django.db.models import F
from django.test import TestCase from django.test import TestCase, skipUnlessAnyDBFeature
from django.test.utils import isolate_apps from django.test.utils import isolate_apps
@ -217,16 +217,18 @@ class CompositePKChecksTests(TestCase):
], ],
) )
@skipUnlessAnyDBFeature(
"supports_virtual_generated_columns",
"supports_stored_generated_columns",
)
def test_composite_pk_cannot_include_generated_field(self): def test_composite_pk_cannot_include_generated_field(self):
is_oracle = connection.vendor == "oracle"
class Foo(models.Model): class Foo(models.Model):
pk = models.CompositePrimaryKey("id", "foo") pk = models.CompositePrimaryKey("id", "foo")
id = models.IntegerField() id = models.IntegerField()
foo = models.GeneratedField( foo = models.GeneratedField(
expression=F("id"), expression=F("id"),
output_field=models.IntegerField(), output_field=models.IntegerField(),
db_persist=not is_oracle, db_persist=connection.features.supports_stored_generated_columns,
) )
self.assertEqual( self.assertEqual(

View File

@ -2,7 +2,12 @@ import json
import unittest import unittest
from uuid import UUID from uuid import UUID
import yaml try:
import yaml # NOQA
HAS_YAML = True
except ImportError:
HAS_YAML = False
from django import forms from django import forms
from django.core import serializers from django.core import serializers
@ -35,9 +40,9 @@ class CompositePKTests(TestCase):
cls.comment = Comment.objects.create(tenant=cls.tenant, id=1, user=cls.user) cls.comment = Comment.objects.create(tenant=cls.tenant, id=1, user=cls.user)
@staticmethod @staticmethod
def get_constraints(table): def get_primary_key_columns(table):
with connection.cursor() as cursor: with connection.cursor() as cursor:
return connection.introspection.get_constraints(cursor, table) return connection.introspection.get_primary_key_columns(cursor, table)
def test_pk_updated_if_field_updated(self): def test_pk_updated_if_field_updated(self):
user = User.objects.get(pk=self.user.pk) user = User.objects.get(pk=self.user.pk)
@ -125,53 +130,15 @@ class CompositePKTests(TestCase):
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
Comment.objects.create(tenant=self.tenant, id=self.comment.id) Comment.objects.create(tenant=self.tenant, id=self.comment.id)
@unittest.skipUnless(connection.vendor == "postgresql", "PostgreSQL specific test") def test_get_primary_key_columns(self):
def test_get_constraints_postgresql(self): self.assertEqual(
user_constraints = self.get_constraints(User._meta.db_table) self.get_primary_key_columns(User._meta.db_table),
user_pk = user_constraints["composite_pk_user_pkey"] ["tenant_id", "id"],
self.assertEqual(user_pk["columns"], ["tenant_id", "id"]) )
self.assertIs(user_pk["primary_key"], True) self.assertEqual(
self.get_primary_key_columns(Comment._meta.db_table),
comment_constraints = self.get_constraints(Comment._meta.db_table) ["tenant_id", "comment_id"],
comment_pk = comment_constraints["composite_pk_comment_pkey"] )
self.assertEqual(comment_pk["columns"], ["tenant_id", "comment_id"])
self.assertIs(comment_pk["primary_key"], True)
@unittest.skipUnless(connection.vendor == "sqlite", "SQLite specific test")
def test_get_constraints_sqlite(self):
user_constraints = self.get_constraints(User._meta.db_table)
user_pk = user_constraints["__primary__"]
self.assertEqual(user_pk["columns"], ["tenant_id", "id"])
self.assertIs(user_pk["primary_key"], True)
comment_constraints = self.get_constraints(Comment._meta.db_table)
comment_pk = comment_constraints["__primary__"]
self.assertEqual(comment_pk["columns"], ["tenant_id", "comment_id"])
self.assertIs(comment_pk["primary_key"], True)
@unittest.skipUnless(connection.vendor == "mysql", "MySQL specific test")
def test_get_constraints_mysql(self):
user_constraints = self.get_constraints(User._meta.db_table)
user_pk = user_constraints["PRIMARY"]
self.assertEqual(user_pk["columns"], ["tenant_id", "id"])
self.assertIs(user_pk["primary_key"], True)
comment_constraints = self.get_constraints(Comment._meta.db_table)
comment_pk = comment_constraints["PRIMARY"]
self.assertEqual(comment_pk["columns"], ["tenant_id", "comment_id"])
self.assertIs(comment_pk["primary_key"], True)
@unittest.skipUnless(connection.vendor == "oracle", "Oracle specific test")
def test_get_constraints_oracle(self):
user_constraints = self.get_constraints(User._meta.db_table)
user_pk = next(c for c in user_constraints.values() if c["primary_key"])
self.assertEqual(user_pk["columns"], ["tenant_id", "id"])
self.assertEqual(user_pk["primary_key"], 1)
comment_constraints = self.get_constraints(Comment._meta.db_table)
comment_pk = next(c for c in comment_constraints.values() if c["primary_key"])
self.assertEqual(comment_pk["columns"], ["tenant_id", "comment_id"])
self.assertEqual(comment_pk["primary_key"], 1)
def test_in_bulk(self): def test_in_bulk(self):
""" """
@ -291,6 +258,7 @@ class CompositePKFixturesTests(TestCase):
}, },
) )
@unittest.skipUnless(HAS_YAML, "No yaml library detected")
def test_serialize_user_yaml(self): def test_serialize_user_yaml(self):
users = User.objects.filter(pk=(2, 3)) users = User.objects.filter(pk=(2, 3))
result = serializers.serialize("yaml", users) result = serializers.serialize("yaml", users)

View File

@ -57,6 +57,15 @@ class GenericForeignKeyTests(TestCase):
self.assertIsNot(answer.question, old_question_obj) self.assertIsNot(answer.question, old_question_obj)
self.assertEqual(answer.question, old_question_obj) self.assertEqual(answer.question, old_question_obj)
def test_clear_cached_generic_relation_when_deferred(self):
question = Question.objects.create(text="question")
Answer.objects.create(text="answer", question=question)
answer = Answer.objects.defer("text").get()
old_question_obj = answer.question
# The reverse relation is refreshed even when the text field is deferred.
answer.refresh_from_db()
self.assertIsNot(answer.question, old_question_obj)
class GenericRelationTests(TestCase): class GenericRelationTests(TestCase):
def test_value_to_string(self): def test_value_to_string(self):

View File

@ -290,6 +290,14 @@ class TestDefer2(AssertionMixin, TestCase):
self.assertEqual(rf2.name, "new foo") self.assertEqual(rf2.name, "new foo")
self.assertEqual(rf2.value, "new bar") self.assertEqual(rf2.value, "new bar")
def test_refresh_when_one_field_deferred(self):
s = Secondary.objects.create()
PrimaryOneToOne.objects.create(name="foo", value="bar", related=s)
s = Secondary.objects.defer("first").get()
p_before = s.primary_o2o
s.refresh_from_db()
self.assertIsNot(s.primary_o2o, p_before)
class InvalidDeferTests(SimpleTestCase): class InvalidDeferTests(SimpleTestCase):
def test_invalid_defer(self): def test_invalid_defer(self):

View File

@ -249,7 +249,8 @@ class FormsErrorMessagesTestCase(SimpleTestCase, AssertFormErrorsMixin):
form1 = TestForm({"first_name": "John"}) form1 = TestForm({"first_name": "John"})
self.assertHTMLEqual( self.assertHTMLEqual(
str(form1["last_name"].errors), str(form1["last_name"].errors),
'<ul class="errorlist"><li>This field is required.</li></ul>', '<ul class="errorlist" id="id_last_name_error"><li>'
"This field is required.</li></ul>",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
str(form1.errors["__all__"]), str(form1.errors["__all__"]),
@ -280,7 +281,7 @@ class FormsErrorMessagesTestCase(SimpleTestCase, AssertFormErrorsMixin):
f = SomeForm({"field": "<script>"}) f = SomeForm({"field": "<script>"})
self.assertHTMLEqual( self.assertHTMLEqual(
t.render(Context({"form": f})), t.render(Context({"form": f})),
'<ul class="errorlist"><li>field<ul class="errorlist">' '<ul class="errorlist"><li>field<ul class="errorlist" id="id_field_error">'
"<li>Select a valid choice. &lt;script&gt; is not one of the " "<li>Select a valid choice. &lt;script&gt; is not one of the "
"available choices.</li></ul></li></ul>", "available choices.</li></ul></li></ul>",
) )
@ -291,7 +292,7 @@ class FormsErrorMessagesTestCase(SimpleTestCase, AssertFormErrorsMixin):
f = SomeForm({"field": ["<script>"]}) f = SomeForm({"field": ["<script>"]})
self.assertHTMLEqual( self.assertHTMLEqual(
t.render(Context({"form": f})), t.render(Context({"form": f})),
'<ul class="errorlist"><li>field<ul class="errorlist">' '<ul class="errorlist"><li>field<ul class="errorlist" id="id_field_error">'
"<li>Select a valid choice. &lt;script&gt; is not one of the " "<li>Select a valid choice. &lt;script&gt; is not one of the "
"available choices.</li></ul></li></ul>", "available choices.</li></ul></li></ul>",
) )
@ -302,7 +303,7 @@ class FormsErrorMessagesTestCase(SimpleTestCase, AssertFormErrorsMixin):
f = SomeForm({"field": ["<script>"]}) f = SomeForm({"field": ["<script>"]})
self.assertHTMLEqual( self.assertHTMLEqual(
t.render(Context({"form": f})), t.render(Context({"form": f})),
'<ul class="errorlist"><li>field<ul class="errorlist">' '<ul class="errorlist"><li>field<ul class="errorlist" id="id_field_error">'
"<li>“&lt;script&gt;” is not a valid value.</li>" "<li>“&lt;script&gt;” is not a valid value.</li>"
"</ul></li></ul>", "</ul></li></ul>",
) )

View File

@ -181,53 +181,55 @@ class FormsTestCase(SimpleTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
str(p), str(p),
'<div><label for="id_first_name">First name:</label>' '<div><label for="id_first_name">First name:</label>'
'<ul class="errorlist"><li>This field is required.</li></ul>' '<ul class="errorlist" id="id_first_name_error"><li>This field is required.'
'<input type="text" name="first_name" aria-invalid="true" required ' '</li></ul><input type="text" name="first_name" aria-invalid="true" '
'id="id_first_name"></div>' 'required id="id_first_name"></div>'
'<div><label for="id_last_name">Last name:</label>' '<div><label for="id_last_name">Last name:</label>'
'<ul class="errorlist"><li>This field is required.</li></ul>' '<ul class="errorlist" id="id_last_name_error"><li>This field is required.'
'<input type="text" name="last_name" aria-invalid="true" required ' '</li></ul><input type="text" name="last_name" aria-invalid="true" '
'id="id_last_name"></div><div>' 'required id="id_last_name"></div><div>'
'<label for="id_birthday">Birthday:</label>' '<label for="id_birthday">Birthday:</label>'
'<ul class="errorlist"><li>This field is required.</li></ul>' '<ul class="errorlist" id="id_birthday_error"><li>This field is required.'
'<input type="text" name="birthday" aria-invalid="true" required ' '</li></ul><input type="text" name="birthday" aria-invalid="true" required '
'id="id_birthday"></div>', 'id="id_birthday"></div>',
) )
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_table(), p.as_table(),
"""<tr><th><label for="id_first_name">First name:</label></th><td> """<tr><th><label for="id_first_name">First name:</label></th><td>
<ul class="errorlist"><li>This field is required.</li></ul> <ul class="errorlist" id="id_first_name_error"><li>This field is required.</li></ul>
<input type="text" name="first_name" id="id_first_name" aria-invalid="true" required> <input type="text" name="first_name" id="id_first_name" aria-invalid="true" required>
</td></tr><tr><th><label for="id_last_name">Last name:</label></th> </td></tr><tr><th><label for="id_last_name">Last name:</label></th>
<td><ul class="errorlist"><li>This field is required.</li></ul> <td><ul class="errorlist" id="id_last_name_error"><li>This field is required.</li></ul>
<input type="text" name="last_name" id="id_last_name" aria-invalid="true" required> <input type="text" name="last_name" id="id_last_name" aria-invalid="true" required>
</td></tr><tr><th><label for="id_birthday">Birthday:</label></th> </td></tr><tr><th><label for="id_birthday">Birthday:</label></th>
<td><ul class="errorlist"><li>This field is required.</li></ul> <td><ul class="errorlist" id="id_birthday_error"><li>This field is required.</li></ul>
<input type="text" name="birthday" id="id_birthday" aria-invalid="true" required> <input type="text" name="birthday" id="id_birthday" aria-invalid="true" required>
</td></tr>""", </td></tr>""",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_ul(), p.as_ul(),
"""<li><ul class="errorlist"><li>This field is required.</li></ul> """<li><ul class="errorlist" id="id_first_name_error">
<li>This field is required.</li></ul>
<label for="id_first_name">First name:</label> <label for="id_first_name">First name:</label>
<input type="text" name="first_name" id="id_first_name" aria-invalid="true" required> <input type="text" name="first_name" id="id_first_name" aria-invalid="true" required>
</li><li><ul class="errorlist"><li>This field is required.</li></ul> </li><li><ul class="errorlist" id="id_last_name_error"><li>This field is required.</li>
<label for="id_last_name">Last name:</label> </ul><label for="id_last_name">Last name:</label>
<input type="text" name="last_name" id="id_last_name" aria-invalid="true" required> <input type="text" name="last_name" id="id_last_name" aria-invalid="true" required>
</li><li><ul class="errorlist"><li>This field is required.</li></ul> </li><li><ul class="errorlist" id="id_birthday_error"><li>This field is required.</li>
<label for="id_birthday">Birthday:</label> </ul><label for="id_birthday">Birthday:</label>
<input type="text" name="birthday" id="id_birthday" aria-invalid="true" required> <input type="text" name="birthday" id="id_birthday" aria-invalid="true" required>
</li>""", </li>""",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_p(), p.as_p(),
"""<ul class="errorlist"><li>This field is required.</li></ul> """<ul class="errorlist" id="id_first_name_error"><li>
This field is required.</li></ul>
<p><label for="id_first_name">First name:</label> <p><label for="id_first_name">First name:</label>
<input type="text" name="first_name" id="id_first_name" aria-invalid="true" required> <input type="text" name="first_name" id="id_first_name" aria-invalid="true" required>
</p><ul class="errorlist"><li>This field is required.</li></ul> </p><ul class="errorlist" id="id_last_name_error"><li>This field is required.</li></ul>
<p><label for="id_last_name">Last name:</label> <p><label for="id_last_name">Last name:</label>
<input type="text" name="last_name" id="id_last_name" aria-invalid="true" required> <input type="text" name="last_name" id="id_last_name" aria-invalid="true" required>
</p><ul class="errorlist"><li>This field is required.</li></ul> </p><ul class="errorlist" id="id_birthday_error"><li>This field is required.</li></ul>
<p><label for="id_birthday">Birthday:</label> <p><label for="id_birthday">Birthday:</label>
<input type="text" name="birthday" id="id_birthday" aria-invalid="true" required> <input type="text" name="birthday" id="id_birthday" aria-invalid="true" required>
</p>""", </p>""",
@ -235,16 +237,16 @@ class FormsTestCase(SimpleTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_div(), p.as_div(),
'<div><label for="id_first_name">First name:</label>' '<div><label for="id_first_name">First name:</label>'
'<ul class="errorlist"><li>This field is required.</li></ul>' '<ul class="errorlist" id="id_first_name_error"><li>This field is required.'
'<input type="text" name="first_name" aria-invalid="true" required ' '</li></ul><input type="text" name="first_name" aria-invalid="true" '
'id="id_first_name"></div>' 'required id="id_first_name"></div>'
'<div><label for="id_last_name">Last name:</label>' '<div><label for="id_last_name">Last name:</label>'
'<ul class="errorlist"><li>This field is required.</li></ul>' '<ul class="errorlist" id="id_last_name_error"><li>This field is required.'
'<input type="text" name="last_name" aria-invalid="true" required ' '</li></ul><input type="text" name="last_name" aria-invalid="true" '
'id="id_last_name"></div><div>' 'required id="id_last_name"></div><div>'
'<label for="id_birthday">Birthday:</label>' '<label for="id_birthday">Birthday:</label>'
'<ul class="errorlist"><li>This field is required.</li></ul>' '<ul class="errorlist" id="id_birthday_error"><li>This field is required.'
'<input type="text" name="birthday" aria-invalid="true" required ' '</li></ul><input type="text" name="birthday" aria-invalid="true" required '
'id="id_birthday"></div>', 'id="id_birthday"></div>',
) )
@ -387,7 +389,8 @@ class FormsTestCase(SimpleTestCase):
self.assertEqual(p["first_name"].errors, ["This field is required."]) self.assertEqual(p["first_name"].errors, ["This field is required."])
self.assertHTMLEqual( self.assertHTMLEqual(
p["first_name"].errors.as_ul(), p["first_name"].errors.as_ul(),
'<ul class="errorlist"><li>This field is required.</li></ul>', '<ul class="errorlist" id="id_first_name_error">'
"<li>This field is required.</li></ul>",
) )
self.assertEqual(p["first_name"].errors.as_text(), "* This field is required.") self.assertEqual(p["first_name"].errors.as_text(), "* This field is required.")
@ -3706,7 +3709,7 @@ Options: <select multiple name="options" aria-invalid="true" required>
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_ul(), p.as_ul(),
""" """
<li class="required error"><ul class="errorlist"> <li class="required error"><ul class="errorlist" id="id_name_error">
<li>This field is required.</li></ul> <li>This field is required.</li></ul>
<label class="required" for="id_name">Name:</label> <label class="required" for="id_name">Name:</label>
<input type="text" name="name" id="id_name" aria-invalid="true" required> <input type="text" name="name" id="id_name" aria-invalid="true" required>
@ -3719,7 +3722,7 @@ Options: <select multiple name="options" aria-invalid="true" required>
</select></li> </select></li>
<li><label for="id_email">Email:</label> <li><label for="id_email">Email:</label>
<input type="email" name="email" id="id_email" maxlength="320"></li> <input type="email" name="email" id="id_email" maxlength="320"></li>
<li class="required error"><ul class="errorlist"> <li class="required error"><ul class="errorlist" id="id_age_error">
<li>This field is required.</li></ul> <li>This field is required.</li></ul>
<label class="required" for="id_age">Age:</label> <label class="required" for="id_age">Age:</label>
<input type="number" name="age" id="id_age" aria-invalid="true" required> <input type="number" name="age" id="id_age" aria-invalid="true" required>
@ -3729,8 +3732,8 @@ Options: <select multiple name="options" aria-invalid="true" required>
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_p(), p.as_p(),
""" """
<ul class="errorlist"><li>This field is required.</li></ul> <ul class="errorlist" id="id_name_error"><li>This field is required.</li>
<p class="required error"> </ul><p class="required error">
<label class="required" for="id_name">Name:</label> <label class="required" for="id_name">Name:</label>
<input type="text" name="name" id="id_name" aria-invalid="true" required> <input type="text" name="name" id="id_name" aria-invalid="true" required>
</p><p class="required"> </p><p class="required">
@ -3742,17 +3745,17 @@ Options: <select multiple name="options" aria-invalid="true" required>
</select></p> </select></p>
<p><label for="id_email">Email:</label> <p><label for="id_email">Email:</label>
<input type="email" name="email" id="id_email" maxlength="320"></p> <input type="email" name="email" id="id_email" maxlength="320"></p>
<ul class="errorlist"><li>This field is required.</li></ul> <ul class="errorlist" id="id_age_error"><li>This field is required.</li>
<p class="required error"><label class="required" for="id_age">Age:</label> </ul><p class="required error"><label class="required" for="id_age">
<input type="number" name="age" id="id_age" aria-invalid="true" required> Age:</label><input type="number" name="age" id="id_age" aria-invalid="true"
</p>""", required></p>""",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_table(), p.as_table(),
"""<tr class="required error"> """<tr class="required error">
<th><label class="required" for="id_name">Name:</label></th> <th><label class="required" for="id_name">Name:</label></th>
<td><ul class="errorlist"><li>This field is required.</li></ul> <td><ul class="errorlist" id="id_name_error"><li>This field is required.</li></ul>
<input type="text" name="name" id="id_name" aria-invalid="true" required></td></tr> <input type="text" name="name" id="id_name" aria-invalid="true" required></td></tr>
<tr class="required"><th><label class="required" for="id_is_cool">Is cool:</label></th> <tr class="required"><th><label class="required" for="id_is_cool">Is cool:</label></th>
<td><select name="is_cool" id="id_is_cool"> <td><select name="is_cool" id="id_is_cool">
@ -3763,14 +3766,14 @@ Options: <select multiple name="options" aria-invalid="true" required>
<tr><th><label for="id_email">Email:</label></th><td> <tr><th><label for="id_email">Email:</label></th><td>
<input type="email" name="email" id="id_email" maxlength="320"></td></tr> <input type="email" name="email" id="id_email" maxlength="320"></td></tr>
<tr class="required error"><th><label class="required" for="id_age">Age:</label></th> <tr class="required error"><th><label class="required" for="id_age">Age:</label></th>
<td><ul class="errorlist"><li>This field is required.</li></ul> <td><ul class="errorlist" id="id_age_error"><li>This field is required.</li></ul>
<input type="number" name="age" id="id_age" aria-invalid="true" required></td></tr>""", <input type="number" name="age" id="id_age" aria-invalid="true" required></td></tr>""",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_div(), p.as_div(),
'<div class="required error"><label for="id_name" class="required">Name:' '<div class="required error"><label for="id_name" class="required">Name:'
'</label><ul class="errorlist"><li>This field is required.</li></ul>' '</label><ul class="errorlist" id="id_name_error"><li>This field is '
'<input type="text" name="name" required id="id_name" ' 'required.</li></ul><input type="text" name="name" required id="id_name" '
'aria-invalid="true" /></div>' 'aria-invalid="true" /></div>'
'<div class="required"><label for="id_is_cool" class="required">Is cool:' '<div class="required"><label for="id_is_cool" class="required">Is cool:'
'</label><select name="is_cool" id="id_is_cool">' '</label><select name="is_cool" id="id_is_cool">'
@ -3779,8 +3782,8 @@ Options: <select multiple name="options" aria-invalid="true" required>
'</select></div><div><label for="id_email">Email:</label>' '</select></div><div><label for="id_email">Email:</label>'
'<input type="email" name="email" id="id_email" maxlength="320"/></div>' '<input type="email" name="email" id="id_email" maxlength="320"/></div>'
'<div class="required error"><label for="id_age" class="required">Age:' '<div class="required error"><label for="id_age" class="required">Age:'
'</label><ul class="errorlist"><li>This field is required.</li></ul>' '</label><ul class="errorlist" id="id_age_error"><li>This field is '
'<input type="number" name="age" required id="id_age" ' 'required.</li></ul><input type="number" name="age" required id="id_age" '
'aria-invalid="true" /></div>', 'aria-invalid="true" /></div>',
) )
@ -4255,8 +4258,10 @@ Options: <select multiple name="options" aria-invalid="true" required>
errors = form.errors.as_ul() errors = form.errors.as_ul()
control = [ control = [
'<li>foo<ul class="errorlist"><li>This field is required.</li></ul></li>', '<li>foo<ul class="errorlist" id="id_foo_error"><li>This field is required.'
'<li>bar<ul class="errorlist"><li>This field is required.</li></ul></li>', "</li></ul></li>",
'<li>bar<ul class="errorlist" id="id_bar_error"><li>This field is required.'
"</li></ul></li>",
'<li>__all__<ul class="errorlist nonfield"><li>Non-field error.</li></ul>' '<li>__all__<ul class="errorlist nonfield"><li>Non-field error.</li></ul>'
"</li>", "</li>",
] ]
@ -4461,7 +4466,8 @@ Options: <select multiple name="options" aria-invalid="true" required>
form.as_ul(), form.as_ul(),
'<li><ul class="errorlist nonfield">' '<li><ul class="errorlist nonfield">'
"<li>(Hidden field hidden) Foo &amp; &quot;bar&quot;!</li></ul></li>" "<li>(Hidden field hidden) Foo &amp; &quot;bar&quot;!</li></ul></li>"
'<li><ul class="errorlist"><li>Foo &amp; &quot;bar&quot;!</li></ul>' '<li><ul class="errorlist" id="id_visible_error"><li>Foo &amp; '
"&quot;bar&quot;!</li></ul>"
'<label for="id_visible">Visible:</label> ' '<label for="id_visible">Visible:</label> '
'<input type="text" name="visible" aria-invalid="true" value="b" ' '<input type="text" name="visible" aria-invalid="true" value="b" '
'id="id_visible" required>' 'id="id_visible" required>'

View File

@ -97,7 +97,7 @@ class FormsI18nTests(SimpleTestCase):
f = SomeForm({}) f = SomeForm({})
self.assertHTMLEqual( self.assertHTMLEqual(
f.as_p(), f.as_p(),
'<ul class="errorlist"><li>' '<ul class="errorlist" id="id_somechoice_error"><li>'
"\u041e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c" "\u041e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c"
"\u043d\u043e\u0435 \u043f\u043e\u043b\u0435.</li></ul>\n" "\u043d\u043e\u0435 \u043f\u043e\u043b\u0435.</li></ul>\n"
"<p><label>\xc5\xf8\xdf:</label>" "<p><label>\xc5\xf8\xdf:</label>"

View File

@ -179,6 +179,15 @@ class BasicExtractorTests(ExtractorTests):
self.assertIn("processing locale en_GB", out.getvalue()) self.assertIn("processing locale en_GB", out.getvalue())
self.assertIs(Path("locale/en_GB/LC_MESSAGES/django.po").exists(), True) self.assertIs(Path("locale/en_GB/LC_MESSAGES/django.po").exists(), True)
def test_valid_locale_with_numeric_region_code(self):
out = StringIO()
management.call_command(
"makemessages", locale=["ar_002"], stdout=out, verbosity=1
)
self.assertNotIn("invalid locale ar_002", out.getvalue())
self.assertIn("processing locale ar_002", out.getvalue())
self.assertIs(Path("locale/ar_002/LC_MESSAGES/django.po").exists(), True)
def test_valid_locale_tachelhit_latin_morocco(self): def test_valid_locale_tachelhit_latin_morocco(self):
out = StringIO() out = StringIO()
management.call_command( management.call_command(

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ from unittest import mock
from django.apps import apps from django.apps import apps
from django.core.management import CommandError, call_command from django.core.management import CommandError, call_command
from django.core.management.base import SystemCheckError
from django.core.management.commands.makemigrations import ( from django.core.management.commands.makemigrations import (
Command as MakeMigrationsCommand, Command as MakeMigrationsCommand,
) )
@ -859,7 +860,7 @@ class MigrateTests(MigrationTestBase):
sqlmigrate outputs forward looking SQL. sqlmigrate outputs forward looking SQL.
""" """
out = io.StringIO() out = io.StringIO()
call_command("sqlmigrate", "migrations", "0001", stdout=out) call_command("sqlmigrate", "migrations", "0001", stdout=out, no_color=True)
lines = out.getvalue().splitlines() lines = out.getvalue().splitlines()
@ -921,7 +922,14 @@ class MigrateTests(MigrationTestBase):
call_command("migrate", "migrations", verbosity=0) call_command("migrate", "migrations", verbosity=0)
out = io.StringIO() out = io.StringIO()
call_command("sqlmigrate", "migrations", "0001", stdout=out, backwards=True) call_command(
"sqlmigrate",
"migrations",
"0001",
stdout=out,
backwards=True,
no_color=True,
)
lines = out.getvalue().splitlines() lines = out.getvalue().splitlines()
try: try:
@ -1098,6 +1106,30 @@ class MigrateTests(MigrationTestBase):
], ],
) )
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
def test_sqlmigrate_transaction_keywords_not_colorized(self):
out = io.StringIO()
with mock.patch(
"django.core.management.color.supports_color", lambda *args: True
):
call_command("sqlmigrate", "migrations", "0001", stdout=out, no_color=False)
self.assertNotIn("\x1b", out.getvalue())
@override_settings(
MIGRATION_MODULES={"migrations": "migrations.test_migrations_no_operations"},
INSTALLED_APPS=["django.contrib.auth"],
)
def test_sqlmigrate_system_checks_colorized(self):
with (
mock.patch(
"django.core.management.color.supports_color", lambda *args: True
),
self.assertRaisesMessage(SystemCheckError, "\x1b"),
):
call_command(
"sqlmigrate", "migrations", "0001", skip_checks=False, no_color=False
)
@override_settings( @override_settings(
INSTALLED_APPS=[ INSTALLED_APPS=[
"migrations.migrations_test_apps.migrated_app", "migrations.migrations_test_apps.migrated_app",

View File

@ -1,9 +1,10 @@
import math import math
from decimal import Decimal from decimal import Decimal
from unittest import mock
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import connection, models
from django.test import TestCase from django.test import TestCase
from .models import BigD, Foo from .models import BigD, Foo
@ -48,6 +49,20 @@ class DecimalFieldTests(TestCase):
self.assertIsNone(f.get_prep_value(None)) self.assertIsNone(f.get_prep_value(None))
self.assertEqual(f.get_prep_value("2.4"), Decimal("2.4")) self.assertEqual(f.get_prep_value("2.4"), Decimal("2.4"))
def test_get_db_prep_value(self):
"""
DecimalField.get_db_prep_value() must call
DatabaseOperations.adapt_decimalfield_value().
"""
f = models.DecimalField(max_digits=5, decimal_places=1)
# None of the built-in database backends implement
# adapt_decimalfield_value(), so this must be confirmed with mocking.
with mock.patch.object(
connection.ops.__class__, "adapt_decimalfield_value"
) as adapt_decimalfield_value:
f.get_db_prep_value("2.4", connection)
adapt_decimalfield_value.assert_called_with(Decimal("2.4"), 5, 1)
def test_filter_with_strings(self): def test_filter_with_strings(self):
""" """
Should be able to filter decimal fields using strings (#8023). Should be able to filter decimal fields using strings (#8023).

View File

@ -29,6 +29,7 @@ from django.db.models import (
from django.db.models.expressions import RawSQL from django.db.models.expressions import RawSQL
from django.db.models.fields.json import ( from django.db.models.fields.json import (
KT, KT,
HasKey,
KeyTextTransform, KeyTextTransform,
KeyTransform, KeyTransform,
KeyTransformFactory, KeyTransformFactory,
@ -582,6 +583,14 @@ class TestQuerying(TestCase):
[expected], [expected],
) )
def test_has_key_literal_lookup(self):
self.assertSequenceEqual(
NullableJSONModel.objects.filter(
HasKey(Value({"foo": "bar"}, JSONField()), "foo")
).order_by("id"),
self.objs,
)
def test_has_key_list(self): def test_has_key_list(self):
obj = NullableJSONModel.objects.create(value=[{"a": 1}, {"b": "x"}]) obj = NullableJSONModel.objects.create(value=[{"a": 1}, {"b": "x"}])
tests = [ tests = [
@ -808,6 +817,59 @@ class TestQuerying(TestCase):
) )
self.assertIs(NullableJSONModel.objects.filter(value__c__lt=5).exists(), False) self.assertIs(NullableJSONModel.objects.filter(value__c__lt=5).exists(), False)
def test_lookups_special_chars(self):
test_keys = [
"CONTROL",
"single'",
"dollar$",
"dot.dot",
"with space",
"back\\slash",
"question?mark",
"user@name",
"emo🤡'ji",
"com,ma",
"curly{{{brace}}}s",
"escape\uffff'seq'\uffffue\uffff'nce",
]
json_value = {key: "some value" for key in test_keys}
obj = NullableJSONModel.objects.create(value=json_value)
obj.refresh_from_db()
self.assertEqual(obj.value, json_value)
for key in test_keys:
lookups = {
"has_key": Q(value__has_key=key),
"has_keys": Q(value__has_keys=[key, "CONTROL"]),
"has_any_keys": Q(value__has_any_keys=[key, "does_not_exist"]),
"exact": Q(**{f"value__{key}": "some value"}),
}
for lookup, condition in lookups.items():
results = NullableJSONModel.objects.filter(condition)
with self.subTest(key=key, lookup=lookup):
self.assertSequenceEqual(results, [obj])
def test_lookups_special_chars_double_quotes(self):
test_keys = [
'double"',
"m\\i@x. m🤡'a,t{{{ch}}}e?d$\"'es\uffff'ca\uffff'pe",
]
json_value = {key: "some value" for key in test_keys}
obj = NullableJSONModel.objects.create(value=json_value)
obj.refresh_from_db()
self.assertEqual(obj.value, json_value)
self.assertSequenceEqual(
NullableJSONModel.objects.filter(value__has_keys=test_keys), [obj]
)
for key in test_keys:
with self.subTest(key=key):
results = NullableJSONModel.objects.filter(
Q(value__has_key=key),
Q(value__has_any_keys=[key, "does_not_exist"]),
Q(**{f"value__{key}": "some value"}),
)
self.assertSequenceEqual(results, [obj])
def test_lookup_exclude(self): def test_lookup_exclude(self):
tests = [ tests = [
(Q(value__a="b"), [self.objs[0]]), (Q(value__a="b"), [self.objs[0]]),

View File

@ -3207,11 +3207,13 @@ class ModelFormCustomErrorTests(SimpleTestCase):
errors = CustomErrorMessageForm(data).errors errors = CustomErrorMessageForm(data).errors
self.assertHTMLEqual( self.assertHTMLEqual(
str(errors["name1"]), str(errors["name1"]),
'<ul class="errorlist"><li>Form custom error message.</li></ul>', '<ul class="errorlist" id="id_name1_error">'
"<li>Form custom error message.</li></ul>",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
str(errors["name2"]), str(errors["name2"]),
'<ul class="errorlist"><li>Model custom error message.</li></ul>', '<ul class="errorlist" id="id_name2_error">'
"<li>Model custom error message.</li></ul>",
) )
def test_model_clean_error_messages(self): def test_model_clean_error_messages(self):
@ -3220,14 +3222,15 @@ class ModelFormCustomErrorTests(SimpleTestCase):
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
self.assertHTMLEqual( self.assertHTMLEqual(
str(form.errors["name1"]), str(form.errors["name1"]),
'<ul class="errorlist"><li>Model.clean() error messages.</li></ul>', '<ul class="errorlist" id="id_name1_error">'
"<li>Model.clean() error messages.</li></ul>",
) )
data = {"name1": "FORBIDDEN_VALUE2", "name2": "ABC"} data = {"name1": "FORBIDDEN_VALUE2", "name2": "ABC"}
form = CustomErrorMessageForm(data) form = CustomErrorMessageForm(data)
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
self.assertHTMLEqual( self.assertHTMLEqual(
str(form.errors["name1"]), str(form.errors["name1"]),
'<ul class="errorlist">' '<ul class="errorlist" id="id_name1_error">'
"<li>Model.clean() error messages (simpler syntax).</li></ul>", "<li>Model.clean() error messages (simpler syntax).</li></ul>",
) )
data = {"name1": "GLOBAL_ERROR", "name2": "ABC"} data = {"name1": "GLOBAL_ERROR", "name2": "ABC"}

View File

@ -1008,6 +1008,32 @@ class TestSerialization(PostgreSQLSimpleTestCase):
self.assertEqual(instance.field, [1, 2, None]) self.assertEqual(instance.field, [1, 2, None])
class TestStringSerialization(PostgreSQLSimpleTestCase):
field_values = [["Django", "Python", None], ["Джанго", "פייתון", None, "król"]]
@staticmethod
def create_json_data(array_field_value):
fields = {"field": json.dumps(array_field_value, ensure_ascii=False)}
return json.dumps(
[{"model": "postgres_tests.chararraymodel", "pk": None, "fields": fields}]
)
def test_encode(self):
for field_value in self.field_values:
with self.subTest(field_value=field_value):
instance = CharArrayModel(field=field_value)
data = serializers.serialize("json", [instance])
json_data = self.create_json_data(field_value)
self.assertEqual(json.loads(data), json.loads(json_data))
def test_decode(self):
for field_value in self.field_values:
with self.subTest(field_value=field_value):
json_data = self.create_json_data(field_value)
instance = list(serializers.deserialize("json", json_data))[0].object
self.assertEqual(instance.field, field_value)
class TestValidation(PostgreSQLSimpleTestCase): class TestValidation(PostgreSQLSimpleTestCase):
def test_unbounded(self): def test_unbounded(self):
field = ArrayField(models.IntegerField()) field = ArrayField(models.IntegerField())

View File

@ -297,39 +297,53 @@ class TestChecks(PostgreSQLSimpleTestCase):
class TestSerialization(PostgreSQLSimpleTestCase): class TestSerialization(PostgreSQLSimpleTestCase):
test_data = json.dumps( field_values = [
[ ({"a": "b"}, [{"a": "b"}, {"b": "a"}]),
{ (
"model": "postgres_tests.hstoremodel", {"все": "Трурль и Клапауций"},
"pk": None, [{"Трурль": "Клапауций"}, {"Клапауций": "Трурль"}],
"fields": { ),
"field": json.dumps({"a": "b"}), ]
"array_field": json.dumps(
[ @staticmethod
json.dumps({"a": "b"}), def create_json_data(field_value, array_field_value):
json.dumps({"b": "a"}), fields = {
] "field": json.dumps(field_value, ensure_ascii=False),
), "array_field": json.dumps(
}, [json.dumps(item, ensure_ascii=False) for item in array_field_value],
} ensure_ascii=False,
] ),
) }
return json.dumps(
[{"model": "postgres_tests.hstoremodel", "pk": None, "fields": fields}]
)
def test_dumping(self): def test_dumping(self):
instance = HStoreModel(field={"a": "b"}, array_field=[{"a": "b"}, {"b": "a"}]) for field_value, array_field_value in self.field_values:
data = serializers.serialize("json", [instance]) with self.subTest(field_value=field_value, array_value=array_field_value):
self.assertEqual(json.loads(data), json.loads(self.test_data)) instance = HStoreModel(field=field_value, array_field=array_field_value)
data = serializers.serialize("json", [instance])
json_data = self.create_json_data(field_value, array_field_value)
self.assertEqual(json.loads(data), json.loads(json_data))
def test_loading(self): def test_loading(self):
instance = list(serializers.deserialize("json", self.test_data))[0].object for field_value, array_field_value in self.field_values:
self.assertEqual(instance.field, {"a": "b"}) with self.subTest(field_value=field_value, array_value=array_field_value):
self.assertEqual(instance.array_field, [{"a": "b"}, {"b": "a"}]) json_data = self.create_json_data(field_value, array_field_value)
instance = list(serializers.deserialize("json", json_data))[0].object
self.assertEqual(instance.field, field_value)
self.assertEqual(instance.array_field, array_field_value)
def test_roundtrip_with_null(self): def test_roundtrip_with_null(self):
instance = HStoreModel(field={"a": "b", "c": None}) for field_value in [
data = serializers.serialize("json", [instance]) {"a": "b", "c": None},
new_instance = list(serializers.deserialize("json", data))[0].object {"Енеїда": "Ти знаєш, він який суціга", "Зефір": None},
self.assertEqual(instance.field, new_instance.field) ]:
with self.subTest(field_value=field_value):
instance = HStoreModel(field=field_value)
data = serializers.serialize("json", [instance])
new_instance = list(serializers.deserialize("json", data))[0].object
self.assertEqual(instance.field, new_instance.field)
class TestValidation(PostgreSQLSimpleTestCase): class TestValidation(PostgreSQLSimpleTestCase):

View File

@ -32,9 +32,11 @@ else:
RemovedInDjango60Warning, RemovedInDjango60Warning,
RemovedInDjango61Warning, RemovedInDjango61Warning,
) )
from django.utils.functional import classproperty
from django.utils.log import DEFAULT_LOGGING from django.utils.log import DEFAULT_LOGGING
from django.utils.version import PY312, PYPY from django.utils.version import PY312, PYPY
try: try:
import MySQLdb import MySQLdb
except ImportError: except ImportError:
@ -307,12 +309,12 @@ def setup_run_tests(verbosity, start_at, start_after, test_labels=None):
apps.set_installed_apps(settings.INSTALLED_APPS) apps.set_installed_apps(settings.INSTALLED_APPS)
# Force declaring available_apps in TransactionTestCase for faster tests. # Force declaring available_apps in TransactionTestCase for faster tests.
def no_available_apps(self): def no_available_apps(cls):
raise Exception( raise Exception(
"Please define available_apps in TransactionTestCase and its subclasses." "Please define available_apps in TransactionTestCase and its subclasses."
) )
TransactionTestCase.available_apps = property(no_available_apps) TransactionTestCase.available_apps = classproperty(no_available_apps)
TestCase.available_apps = None TestCase.available_apps = None
# Set an environment variable that other code may consult to see if # Set an environment variable that other code may consult to see if

View File

@ -13,6 +13,15 @@ from django.db import models
from .base import BaseModel from .base import BaseModel
try:
from PIL import Image # NOQA
except ImportError:
ImageData = None
else:
class ImageData(models.Model):
data = models.ImageField(null=True)
class BinaryData(models.Model): class BinaryData(models.Model):
data = models.BinaryField(null=True) data = models.BinaryField(null=True)
@ -62,10 +71,6 @@ class BigIntegerData(models.Model):
data = models.BigIntegerField(null=True) data = models.BigIntegerField(null=True)
# class ImageData(models.Model):
# data = models.ImageField(null=True)
class GenericIPAddressData(models.Model): class GenericIPAddressData(models.Model):
data = models.GenericIPAddressField(null=True) data = models.GenericIPAddressField(null=True)

View File

@ -10,10 +10,11 @@ forward, backwards and self references.
import datetime import datetime
import decimal import decimal
import uuid import uuid
from collections import namedtuple
from django.core import serializers from django.core import serializers
from django.db import connection, models from django.db import connection, models
from django.test import TestCase from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
from .models import ( from .models import (
Anchor, Anchor,
@ -46,6 +47,7 @@ from .models import (
GenericData, GenericData,
GenericIPAddressData, GenericIPAddressData,
GenericIPAddressPKData, GenericIPAddressPKData,
ImageData,
InheritAbstractModel, InheritAbstractModel,
InheritBaseModel, InheritBaseModel,
IntegerData, IntegerData,
@ -239,24 +241,23 @@ def inherited_compare(testcase, pk, klass, data):
testcase.assertEqual(value, getattr(instance, key)) testcase.assertEqual(value, getattr(instance, key))
# Define some data types. Each data type is # Define some test helpers. Each has a pair of functions: one to create objects and one
# actually a pair of functions; one to create # to make assertions against objects of a particular type.
# and one to compare objects of that type TestHelper = namedtuple("TestHelper", ["create_object", "compare_object"])
data_obj = (data_create, data_compare) data_obj = TestHelper(data_create, data_compare)
generic_obj = (generic_create, generic_compare) generic_obj = TestHelper(generic_create, generic_compare)
fk_obj = (fk_create, fk_compare) fk_obj = TestHelper(fk_create, fk_compare)
m2m_obj = (m2m_create, m2m_compare) m2m_obj = TestHelper(m2m_create, m2m_compare)
im2m_obj = (im2m_create, im2m_compare) im2m_obj = TestHelper(im2m_create, im2m_compare)
im_obj = (im_create, im_compare) im_obj = TestHelper(im_create, im_compare)
o2o_obj = (o2o_create, o2o_compare) o2o_obj = TestHelper(o2o_create, o2o_compare)
pk_obj = (pk_create, pk_compare) pk_obj = TestHelper(pk_create, pk_compare)
inherited_obj = (inherited_create, inherited_compare) inherited_obj = TestHelper(inherited_create, inherited_compare)
uuid_obj = uuid.uuid4() uuid_obj = uuid.uuid4()
test_data = [ test_data = [
# Format: (data type, PK value, Model Class, data) # Format: (test helper, PK value, Model Class, data)
(data_obj, 1, BinaryData, memoryview(b"\x05\xFD\x00")), (data_obj, 1, BinaryData, memoryview(b"\x05\xFD\x00")),
(data_obj, 2, BinaryData, None),
(data_obj, 5, BooleanData, True), (data_obj, 5, BooleanData, True),
(data_obj, 6, BooleanData, False), (data_obj, 6, BooleanData, False),
(data_obj, 7, BooleanData, None), (data_obj, 7, BooleanData, None),
@ -265,7 +266,6 @@ test_data = [
(data_obj, 12, CharData, "None"), (data_obj, 12, CharData, "None"),
(data_obj, 13, CharData, "null"), (data_obj, 13, CharData, "null"),
(data_obj, 14, CharData, "NULL"), (data_obj, 14, CharData, "NULL"),
(data_obj, 15, CharData, None),
# (We use something that will fit into a latin1 database encoding here, # (We use something that will fit into a latin1 database encoding here,
# because that is still the default used on many system setups.) # because that is still the default used on many system setups.)
(data_obj, 16, CharData, "\xa5"), (data_obj, 16, CharData, "\xa5"),
@ -274,13 +274,11 @@ test_data = [
(data_obj, 30, DateTimeData, datetime.datetime(2006, 6, 16, 10, 42, 37)), (data_obj, 30, DateTimeData, datetime.datetime(2006, 6, 16, 10, 42, 37)),
(data_obj, 31, DateTimeData, None), (data_obj, 31, DateTimeData, None),
(data_obj, 40, EmailData, "hovercraft@example.com"), (data_obj, 40, EmailData, "hovercraft@example.com"),
(data_obj, 41, EmailData, None),
(data_obj, 42, EmailData, ""), (data_obj, 42, EmailData, ""),
(data_obj, 50, FileData, "file:///foo/bar/whiz.txt"), (data_obj, 50, FileData, "file:///foo/bar/whiz.txt"),
# (data_obj, 51, FileData, None), # (data_obj, 51, FileData, None),
(data_obj, 52, FileData, ""), (data_obj, 52, FileData, ""),
(data_obj, 60, FilePathData, "/foo/bar/whiz.txt"), (data_obj, 60, FilePathData, "/foo/bar/whiz.txt"),
(data_obj, 61, FilePathData, None),
(data_obj, 62, FilePathData, ""), (data_obj, 62, FilePathData, ""),
(data_obj, 70, DecimalData, decimal.Decimal("12.345")), (data_obj, 70, DecimalData, decimal.Decimal("12.345")),
(data_obj, 71, DecimalData, decimal.Decimal("-12.345")), (data_obj, 71, DecimalData, decimal.Decimal("-12.345")),
@ -294,7 +292,6 @@ test_data = [
(data_obj, 81, IntegerData, -123456789), (data_obj, 81, IntegerData, -123456789),
(data_obj, 82, IntegerData, 0), (data_obj, 82, IntegerData, 0),
(data_obj, 83, IntegerData, None), (data_obj, 83, IntegerData, None),
# (XX, ImageData
(data_obj, 95, GenericIPAddressData, "fe80:1424:2223:6cff:fe8a:2e8a:2151:abcd"), (data_obj, 95, GenericIPAddressData, "fe80:1424:2223:6cff:fe8a:2e8a:2151:abcd"),
(data_obj, 96, GenericIPAddressData, None), (data_obj, 96, GenericIPAddressData, None),
(data_obj, 110, PositiveBigIntegerData, 9223372036854775807), (data_obj, 110, PositiveBigIntegerData, 9223372036854775807),
@ -304,7 +301,6 @@ test_data = [
(data_obj, 130, PositiveSmallIntegerData, 12), (data_obj, 130, PositiveSmallIntegerData, 12),
(data_obj, 131, PositiveSmallIntegerData, None), (data_obj, 131, PositiveSmallIntegerData, None),
(data_obj, 140, SlugData, "this-is-a-slug"), (data_obj, 140, SlugData, "this-is-a-slug"),
(data_obj, 141, SlugData, None),
(data_obj, 142, SlugData, ""), (data_obj, 142, SlugData, ""),
(data_obj, 150, SmallData, 12), (data_obj, 150, SmallData, 12),
(data_obj, 151, SmallData, -12), (data_obj, 151, SmallData, -12),
@ -320,7 +316,6 @@ Several of them.
The end.""", The end.""",
), ),
(data_obj, 161, TextData, ""), (data_obj, 161, TextData, ""),
(data_obj, 162, TextData, None),
(data_obj, 170, TimeData, datetime.time(10, 42, 37)), (data_obj, 170, TimeData, datetime.time(10, 42, 37)),
(data_obj, 171, TimeData, None), (data_obj, 171, TimeData, None),
(generic_obj, 200, GenericData, ["Generic Object 1", "tag1", "tag2"]), (generic_obj, 200, GenericData, ["Generic Object 1", "tag1", "tag2"]),
@ -388,15 +383,6 @@ The end.""",
(pk_obj, 750, SmallPKData, 12), (pk_obj, 750, SmallPKData, 12),
(pk_obj, 751, SmallPKData, -12), (pk_obj, 751, SmallPKData, -12),
(pk_obj, 752, SmallPKData, 0), (pk_obj, 752, SmallPKData, 0),
(
pk_obj,
760,
TextPKData,
"""This is a long piece of text.
It contains line breaks.
Several of them.
The end.""",
),
(pk_obj, 770, TimePKData, datetime.time(10, 42, 37)), (pk_obj, 770, TimePKData, datetime.time(10, 42, 37)),
(pk_obj, 791, UUIDData, uuid_obj), (pk_obj, 791, UUIDData, uuid_obj),
(fk_obj, 792, FKToUUID, uuid_obj), (fk_obj, 792, FKToUUID, uuid_obj),
@ -419,71 +405,107 @@ The end.""",
(data_obj, 1005, LengthModel, 1), (data_obj, 1005, LengthModel, 1),
] ]
if ImageData is not None:
# Because Oracle treats the empty string as NULL, Oracle is expected to fail test_data.extend(
# when field.empty_strings_allowed is True and the value is None; skip these [
# tests. (data_obj, 86, ImageData, "file:///foo/bar/whiz.png"),
if connection.features.interprets_empty_strings_as_nulls: # (data_obj, 87, ImageData, None),
test_data = [ (data_obj, 88, ImageData, ""),
data ]
for data in test_data )
if not (
data[0] == data_obj
and data[2]._meta.get_field("data").empty_strings_allowed
and data[3] is None
)
]
if not connection.features.supports_index_on_text_field:
test_data = [data for data in test_data if data[2] != TextPKData]
class SerializerDataTests(TestCase): class SerializerDataTests(TestCase):
pass pass
def serializerTest(self, format): def assert_serializer(self, format, data):
# FK to an object with PK of 0. This won't work on MySQL without the # Create all the objects defined in the test data.
# NO_AUTO_VALUE_ON_ZERO SQL mode since it won't let you create an object
# with an autoincrement primary key of 0.
if connection.features.allows_auto_pk_0:
test_data.extend(
[
(data_obj, 0, Anchor, "Anchor 0"),
(fk_obj, 465, FKData, 0),
]
)
# Create all the objects defined in the test data
objects = [] objects = []
instance_count = {} for test_helper, pk, model, data_value in data:
for func, pk, klass, datum in test_data:
with connection.constraint_checks_disabled(): with connection.constraint_checks_disabled():
objects.extend(func[0](pk, klass, datum)) objects.extend(test_helper.create_object(pk, model, data_value))
# Get a count of the number of objects created for each class # Get a count of the number of objects created for each model class.
for klass in instance_count: instance_counts = {}
instance_count[klass] = klass.objects.count() for _, _, model, _ in data:
if model not in instance_counts:
instance_counts[model] = model.objects.count()
# Add the generic tagged objects to the object list # Add the generic tagged objects to the object list.
objects.extend(Tag.objects.all()) objects.extend(Tag.objects.all())
# Serialize the test database # Serialize the test database.
serialized_data = serializers.serialize(format, objects, indent=2) serialized_data = serializers.serialize(format, objects, indent=2)
for obj in serializers.deserialize(format, serialized_data): for obj in serializers.deserialize(format, serialized_data):
obj.save() obj.save()
# Assert that the deserialized data is the same # Assert that the deserialized data is the same as the original source.
# as the original source for test_helper, pk, model, data_value in data:
for func, pk, klass, datum in test_data: with self.subTest(model=model, data_value=data_value):
func[1](self, pk, klass, datum) test_helper.compare_object(self, pk, model, data_value)
# Assert that the number of objects deserialized is the # Assert no new objects were created.
# same as the number that was serialized. for model, count in instance_counts.items():
for klass, count in instance_count.items(): with self.subTest(model=model, count=count):
self.assertEqual(count, klass.objects.count()) self.assertEqual(count, model.objects.count())
def serializerTest(self, format):
assert_serializer(self, format, test_data)
@skipUnlessDBFeature("allows_auto_pk_0")
def serializerTestPK0(self, format):
# FK to an object with PK of 0. This won't work on MySQL without the
# NO_AUTO_VALUE_ON_ZERO SQL mode since it won't let you create an object
# with an autoincrement primary key of 0.
data = [
(data_obj, 0, Anchor, "Anchor 0"),
(fk_obj, 1, FKData, 0),
]
assert_serializer(self, format, data)
@skipIfDBFeature("interprets_empty_strings_as_nulls")
def serializerTestNullValueStingField(self, format):
data = [
(data_obj, 1, BinaryData, None),
(data_obj, 2, CharData, None),
(data_obj, 3, EmailData, None),
(data_obj, 4, FilePathData, None),
(data_obj, 5, SlugData, None),
(data_obj, 6, TextData, None),
]
assert_serializer(self, format, data)
@skipUnlessDBFeature("supports_index_on_text_field")
def serializerTestTextFieldPK(self, format):
data = [
(
pk_obj,
1,
TextPKData,
"""This is a long piece of text.
It contains line breaks.
Several of them.
The end.""",
),
]
assert_serializer(self, format, data)
register_tests(SerializerDataTests, "test_%s_serializer", serializerTest) register_tests(SerializerDataTests, "test_%s_serializer", serializerTest)
register_tests(SerializerDataTests, "test_%s_serializer_pk_0", serializerTestPK0)
register_tests(
SerializerDataTests,
"test_%s_serializer_null_value_string_field",
serializerTestNullValueStingField,
)
register_tests(
SerializerDataTests,
"test_%s_serializer_text_field_pk",
serializerTestTextFieldPK,
)

View File

@ -330,15 +330,43 @@ class IncludeTests(SimpleTestCase):
], ],
} }
] ]
engine = Engine(app_dirs=True) with self.subTest(template="recursive_include.html"):
t = engine.get_template("recursive_include.html") engine = Engine(app_dirs=True)
self.assertEqual( t = engine.get_template("recursive_include.html")
"Recursion! A1 Recursion! B1 B2 B3 Recursion! C1", self.assertEqual(
t.render(Context({"comments": comments})) "Recursion! A1 Recursion! B1 B2 B3 Recursion! C1",
.replace(" ", "") t.render(Context({"comments": comments}))
.replace("\n", " ") .replace(" ", "")
.strip(), .replace("\n", " ")
) .strip(),
)
with self.subTest(template="recursive_relative_include.html"):
engine = Engine(app_dirs=True)
t = engine.get_template("recursive_relative_include.html")
self.assertEqual(
"Recursion! A1 Recursion! B1 B2 B3 Recursion! C1",
t.render(Context({"comments": comments}))
.replace(" ", "")
.replace("\n", " ")
.strip(),
)
with self.subTest(template="tmpl"):
engine = Engine()
template = """
Recursion!
{% for c in comments %}
{{ c.comment }}
{% if c.children %}{% include tmpl with comments=c.children %}{% endif %}
{% endfor %}
"""
outer_tmpl = engine.from_string("{% include tmpl %}")
output = outer_tmpl.render(
Context({"tmpl": engine.from_string(template), "comments": comments})
)
self.assertEqual(
"Recursion! A1 Recursion! B1 B2 B3 Recursion! C1",
output.replace(" ", "").replace("\n", " ").strip(),
)
def test_include_cache(self): def test_include_cache(self):
""" """

View File

@ -0,0 +1,7 @@
Recursion!
{% for comment in comments %}
{{ comment.comment }}
{% if comment.children %}
{% include "./recursive_relative_include.html" with comments=comment.children %}
{% endif %}
{% endfor %}

View File

@ -1,6 +1,7 @@
import os import os
from datetime import datetime from datetime import datetime
from django.core.exceptions import SuspiciousOperation
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.utils.deprecation import RemovedInDjango60Warning from django.utils.deprecation import RemovedInDjango60Warning
@ -145,12 +146,18 @@ class TestUtilsHtml(SimpleTestCase):
("<script>alert()</script>&h", "alert()h"), ("<script>alert()</script>&h", "alert()h"),
("><!" + ("&" * 16000) + "D", "><!" + ("&" * 16000) + "D"), ("><!" + ("&" * 16000) + "D", "><!" + ("&" * 16000) + "D"),
("X<<<<br>br>br>br>X", "XX"), ("X<<<<br>br>br>br>X", "XX"),
("<" * 50 + "a>" * 50, ""),
) )
for value, output in items: for value, output in items:
with self.subTest(value=value, output=output): with self.subTest(value=value, output=output):
self.check_output(strip_tags, value, output) self.check_output(strip_tags, value, output)
self.check_output(strip_tags, lazystr(value), output) self.check_output(strip_tags, lazystr(value), output)
def test_strip_tags_suspicious_operation(self):
value = "<" * 51 + "a>" * 51, "<a>"
with self.assertRaises(SuspiciousOperation):
strip_tags(value)
def test_strip_tags_files(self): def test_strip_tags_files(self):
# Test with more lengthy content (also catching performance regressions) # Test with more lengthy content (also catching performance regressions)
for filename in ("strip_tags1.html", "strip_tags2.txt"): for filename in ("strip_tags1.html", "strip_tags2.txt"):