diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index 0bc65b9d104b..27cac36b23f9 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Benchmark Repo
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: django/django-asv
path: "."
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 0e0958ea97d1..c7ec6da8e2cd 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -26,7 +26,7 @@ jobs:
name: docs
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
@@ -44,7 +44,7 @@ jobs:
name: blacken-docs
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index 0a77a7f7addc..28526264a919 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
@@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
@@ -57,6 +57,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: black
uses: psf/black@stable
diff --git a/.github/workflows/schedule_tests.yml b/.github/workflows/schedule_tests.yml
index d758642ef778..be1fa50b0804 100644
--- a/.github/workflows/schedule_tests.yml
+++ b/.github/workflows/schedule_tests.yml
@@ -25,7 +25,7 @@ jobs:
continue-on-error: true
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
@@ -43,7 +43,7 @@ jobs:
name: JavaScript tests
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 1afd11b0dddf..44ea149cd160 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -27,7 +27,7 @@ jobs:
name: Windows, SQLite, Python ${{ matrix.python-version }}
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
@@ -45,7 +45,7 @@ jobs:
name: JavaScript tests
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
diff --git a/django/__init__.py b/django/__init__.py
index 816d9a0ed8a2..176c5bb81cd0 100644
--- a/django/__init__.py
+++ b/django/__init__.py
@@ -1,6 +1,6 @@
from django.utils.version import get_version
-VERSION = (4, 2, 5, "final", 0)
+VERSION = (4, 2, 6, "final", 0)
__version__ = get_version(VERSION)
diff --git a/django/conf/__init__.py b/django/conf/__init__.py
index f63df722c24c..72ec964d23a0 100644
--- a/django/conf/__init__.py
+++ b/django/conf/__init__.py
@@ -271,8 +271,9 @@ def __init__(self, settings_module):
raise ImproperlyConfigured(
"DEFAULT_FILE_STORAGE/STORAGES are mutually exclusive."
)
- self.STORAGES[DEFAULT_STORAGE_ALIAS] = {
- "BACKEND": self.DEFAULT_FILE_STORAGE
+ self.STORAGES = {
+ **self.STORAGES,
+ DEFAULT_STORAGE_ALIAS: {"BACKEND": self.DEFAULT_FILE_STORAGE},
}
warnings.warn(DEFAULT_FILE_STORAGE_DEPRECATED_MSG, RemovedInDjango51Warning)
@@ -281,8 +282,9 @@ def __init__(self, settings_module):
raise ImproperlyConfigured(
"STATICFILES_STORAGE/STORAGES are mutually exclusive."
)
- self.STORAGES[STATICFILES_STORAGE_ALIAS] = {
- "BACKEND": self.STATICFILES_STORAGE
+ self.STORAGES = {
+ **self.STORAGES,
+ STATICFILES_STORAGE_ALIAS: {"BACKEND": self.STATICFILES_STORAGE},
}
warnings.warn(STATICFILES_STORAGE_DEPRECATED_MSG, RemovedInDjango51Warning)
# RemovedInDjango51Warning.
diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py
index 14fde6b08e6b..2cda788c2ce1 100644
--- a/django/db/backends/mysql/features.py
+++ b/django/db/backends/mysql/features.py
@@ -154,6 +154,21 @@ def django_test_skips(self):
},
}
)
+ if self.connection.mysql_is_mariadb and self.connection.mysql_version >= (
+ 10,
+ 5,
+ 2,
+ ):
+ skips.update(
+ {
+ "ALTER TABLE ... RENAME COLUMN statement doesn't rename inline "
+ "constraints on MariaDB 10.5.2+, this is fixed in Django 5.0+ "
+ "(#34320).": {
+ "schema.tests.SchemaTests."
+ "test_rename_field_with_check_to_truncated_name",
+ },
+ }
+ )
return skips
@cached_property
diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py
index 18cfcb29cbc5..c4d90b56ab0e 100644
--- a/django/db/backends/postgresql/operations.py
+++ b/django/db/backends/postgresql/operations.py
@@ -153,17 +153,6 @@ def fetch_returned_insert_rows(self, cursor):
def lookup_cast(self, lookup_type, internal_type=None):
lookup = "%s"
-
- if lookup_type == "isnull" and internal_type in (
- "CharField",
- "EmailField",
- "TextField",
- "CICharField",
- "CIEmailField",
- "CITextField",
- ):
- return "%s::text"
-
# Cast text lookups to text to allow things like filter(x__contains=4)
if lookup_type in (
"iexact",
diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py
index d3697b200300..a4729c640a92 100644
--- a/django/db/models/lookups.py
+++ b/django/db/models/lookups.py
@@ -568,11 +568,15 @@ def as_sql(self, compiler, connection):
raise ValueError(
"The QuerySet value for an isnull lookup must be True or False."
)
- if isinstance(self.lhs, Value) and self.lhs.value is None:
- if self.rhs:
- raise FullResultSet
+ if isinstance(self.lhs, Value):
+ if self.lhs.value is None or (
+ self.lhs.value == ""
+ and connection.features.interprets_empty_strings_as_nulls
+ ):
+ result_exception = FullResultSet if self.rhs else EmptyResultSet
else:
- raise EmptyResultSet
+ result_exception = EmptyResultSet if self.rhs else FullResultSet
+ raise result_exception
sql, params = self.process_lhs(compiler, connection)
if self.rhs:
return "%s IS NULL" % sql, params
diff --git a/django/utils/text.py b/django/utils/text.py
index 86d3b5274103..26631641e941 100644
--- a/django/utils/text.py
+++ b/django/utils/text.py
@@ -67,8 +67,14 @@ def _generator():
class Truncator(SimpleLazyObject):
"""
An object used to truncate text, either by characters or words.
+
+ When truncating HTML text (either chars or words), input will be limited to
+ at most `MAX_LENGTH_HTML` characters.
"""
+ # 5 million characters are approximately 4000 text pages or 3 web pages.
+ MAX_LENGTH_HTML = 5_000_000
+
def __init__(self, text):
super().__init__(lambda: str(text))
@@ -164,6 +170,11 @@ def _truncate_html(self, length, truncate, text, truncate_len, words):
if words and length <= 0:
return ""
+ size_limited = False
+ if len(text) > self.MAX_LENGTH_HTML:
+ text = text[: self.MAX_LENGTH_HTML]
+ size_limited = True
+
html4_singlets = (
"br",
"col",
@@ -220,10 +231,14 @@ def _truncate_html(self, length, truncate, text, truncate_len, words):
# Add it to the start of the open tags list
open_tags.insert(0, tagname)
+ truncate_text = self.add_truncation_text("", truncate)
+
if current_len <= length:
+ if size_limited and truncate_text:
+ text += truncate_text
return text
+
out = text[:end_text_pos]
- truncate_text = self.add_truncation_text("", truncate)
if truncate_text:
out += truncate_text
# Close any tags still open
diff --git a/docs/howto/custom-file-storage.txt b/docs/howto/custom-file-storage.txt
index 881ef70bdf8f..4e51548bc7bd 100644
--- a/docs/howto/custom-file-storage.txt
+++ b/docs/howto/custom-file-storage.txt
@@ -85,8 +85,8 @@ Called by ``Storage.save()``. The ``name`` will already have gone through
``get_valid_name()`` and ``get_available_name()``, and the ``content`` will be a
``File`` object itself.
-Should return the actual name of name of the file saved (usually the ``name``
-passed in, but if the storage needs to change the file name return the new name
+Should return the actual name of the file saved (usually the ``name`` passed
+in, but if the storage needs to change the file name return the new name
instead).
.. method:: get_valid_name(name)
diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt
index 4b63f6ec82bc..5c2d0b7451dd 100644
--- a/docs/internals/howto-release-django.txt
+++ b/docs/internals/howto-release-django.txt
@@ -432,6 +432,10 @@ You're almost done! All that's left to do now is:
__ https://github.com/django/code.djangoproject.com/blob/main/trac-env/conf/trac.ini
+#. If it's a final release, update the current stable branch and remove the
+ pre-release branch in the `Django release process
+ `_ on Trac.
+
#. If this was a security release, update :doc:`/releases/security` with
details of the issues addressed.
@@ -479,6 +483,10 @@ need to be done by the releaser.
`_. For example
``Framework :: Django :: 3.1``.
+#. Update the current branch under active development and add pre-release
+ branch in the `Django release process
+ `_ on Trac.
+
Notes on setting the VERSION tuple
==================================
diff --git a/docs/ref/contrib/flatpages.txt b/docs/ref/contrib/flatpages.txt
index d68257bfd1fd..c82fb5de85c0 100644
--- a/docs/ref/contrib/flatpages.txt
+++ b/docs/ref/contrib/flatpages.txt
@@ -164,6 +164,13 @@ For more on middleware, read the :doc:`middleware docs
How to add, change and delete flatpages
=======================================
+.. warning::
+
+ Permissions to add or edit flatpages should be restricted to trusted users.
+ Flatpages are defined by raw HTML and are **not sanitized** by Django. As a
+ consequence, a malicious flatpage can lead to various security
+ vulnerabilities, including permission escalation.
+
.. _flatpages-admin:
Via the admin interface
diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
index d02e2173ef06..0d7e9884c317 100644
--- a/docs/ref/models/querysets.txt
+++ b/docs/ref/models/querysets.txt
@@ -1161,10 +1161,11 @@ one-to-one.
``prefetch_related``, on the other hand, does a separate lookup for each
relationship, and does the 'joining' in Python. This allows it to prefetch
-many-to-many and many-to-one objects, which cannot be done using
-``select_related``, in addition to the foreign key and one-to-one relationships
-that are supported by ``select_related``. It also supports prefetching of
-:class:`~django.contrib.contenttypes.fields.GenericRelation` and
+many-to-many, many-to-one, and
+:class:`~django.contrib.contenttypes.fields.GenericRelation` objects which
+cannot be done using ``select_related``, in addition to the foreign key and
+one-to-one relationships that are supported by ``select_related``. It also
+supports prefetching of
:class:`~django.contrib.contenttypes.fields.GenericForeignKey`, however, it
must be restricted to a homogeneous set of results. For example, prefetching
objects referenced by a ``GenericForeignKey`` is only supported if the query
@@ -3924,14 +3925,15 @@ documentation to learn how to create your aggregates.
currently emulates these features using a text field. Attempts to use
aggregation on date/time fields in SQLite will raise ``NotSupportedError``.
-.. admonition:: Empty queryset
+.. admonition:: Empty querysets or groups
- Aggregation functions return ``None`` when used with an empty
- ``QuerySet``. For example, the ``Sum`` aggregation function returns ``None``
- instead of ``0`` if the ``QuerySet`` contains no entries. To return another
- value instead, pass a value to the ``default`` argument. An exception is
- ``Count``, which does return ``0`` if the ``QuerySet`` is empty. ``Count``
- does not support the ``default`` argument.
+ Aggregation functions return ``None`` when used with an empty ``QuerySet``
+ or group. For example, the ``Sum`` aggregation function returns ``None``
+ instead of ``0`` if the ``QuerySet`` contains no entries or for any empty
+ group in a non-empty ``QuerySet``. To return another value instead, define
+ the ``default`` argument. ``Count`` is an exception to this behavior; it
+ returns ``0`` if the ``QuerySet`` is empty since ``Count`` does not support
+ the ``default`` argument.
All aggregates have the following parameters in common:
diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
index c8e55ea37036..6612befdc602 100644
--- a/docs/ref/request-response.txt
+++ b/docs/ref/request-response.txt
@@ -786,10 +786,16 @@ Attributes
A bytestring representing the content, encoded from a string if necessary.
+.. attribute:: HttpResponse.cookies
+
+ A :py:obj:`http.cookies.SimpleCookie` object holding the cookies included
+ in the response.
+
.. attribute:: HttpResponse.headers
A case insensitive, dict-like object that provides an interface to all
- HTTP headers on the response. See :ref:`setting-header-fields`.
+ HTTP headers on the response, except a ``Set-Cookie`` header. See
+ :ref:`setting-header-fields` and :attr:`HttpResponse.cookies`.
.. attribute:: HttpResponse.charset
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index 9d6bc57a92fe..39aa39833885 100644
--- a/docs/ref/templates/builtins.txt
+++ b/docs/ref/templates/builtins.txt
@@ -2652,6 +2652,16 @@ If ``value`` is ``"Joel is a slug
"``, the output will be
Newlines in the HTML content will be preserved.
+.. admonition:: Size of input string
+
+ Processing large, potentially malformed HTML strings can be
+ resource-intensive and impact service performance. ``truncatechars_html``
+ limits input to the first five million characters.
+
+.. versionchanged:: 3.2.22
+
+ In older versions, strings over five million characters were processed.
+
.. templatefilter:: truncatewords
``truncatewords``
@@ -2694,6 +2704,16 @@ If ``value`` is ``"Joel is a slug
"``, the output will be
Newlines in the HTML content will be preserved.
+.. admonition:: Size of input string
+
+ Processing large, potentially malformed HTML strings can be
+ resource-intensive and impact service performance. ``truncatewords_html``
+ limits input to the first five million characters.
+
+.. versionchanged:: 3.2.22
+
+ In older versions, strings over five million characters were processed.
+
.. templatefilter:: unordered_list
``unordered_list``
diff --git a/docs/releases/3.2.22.txt b/docs/releases/3.2.22.txt
new file mode 100644
index 000000000000..cfedc41de8bf
--- /dev/null
+++ b/docs/releases/3.2.22.txt
@@ -0,0 +1,25 @@
+===========================
+Django 3.2.22 release notes
+===========================
+
+*October 4, 2023*
+
+Django 3.2.22 fixes a security issue with severity "moderate" in 3.2.21.
+
+CVE-2023-43665: Denial-of-service possibility in ``django.utils.text.Truncator``
+================================================================================
+
+Following the fix for :cve:`2019-14232`, the regular expressions used in the
+implementation of ``django.utils.text.Truncator``'s ``chars()`` and ``words()``
+methods (with ``html=True``) were revised and improved. However, these regular
+expressions still exhibited linear backtracking complexity, so when given a
+very long, potentially malformed HTML input, the evaluation would still be
+slow, leading to a potential denial of service vulnerability.
+
+The ``chars()`` and ``words()`` methods are used to implement the
+:tfilter:`truncatechars_html` and :tfilter:`truncatewords_html` template
+filters, which were thus also vulnerable.
+
+The input processed by ``Truncator``, when operating in HTML mode, has been
+limited to the first five million characters in order to avoid potential
+performance and memory issues.
diff --git a/docs/releases/4.1.12.txt b/docs/releases/4.1.12.txt
new file mode 100644
index 000000000000..6c331dd318e6
--- /dev/null
+++ b/docs/releases/4.1.12.txt
@@ -0,0 +1,25 @@
+===========================
+Django 4.1.12 release notes
+===========================
+
+*October 4, 2023*
+
+Django 4.1.12 fixes a security issue with severity "moderate" in 4.1.11.
+
+CVE-2023-43665: Denial-of-service possibility in ``django.utils.text.Truncator``
+================================================================================
+
+Following the fix for :cve:`2019-14232`, the regular expressions used in the
+implementation of ``django.utils.text.Truncator``'s ``chars()`` and ``words()``
+methods (with ``html=True``) were revised and improved. However, these regular
+expressions still exhibited linear backtracking complexity, so when given a
+very long, potentially malformed HTML input, the evaluation would still be
+slow, leading to a potential denial of service vulnerability.
+
+The ``chars()`` and ``words()`` methods are used to implement the
+:tfilter:`truncatechars_html` and :tfilter:`truncatewords_html` template
+filters, which were thus also vulnerable.
+
+The input processed by ``Truncator``, when operating in HTML mode, has been
+limited to the first five million characters in order to avoid potential
+performance and memory issues.
diff --git a/docs/releases/4.2.6.txt b/docs/releases/4.2.6.txt
new file mode 100644
index 000000000000..9b99d8c6227f
--- /dev/null
+++ b/docs/releases/4.2.6.txt
@@ -0,0 +1,43 @@
+==========================
+Django 4.2.6 release notes
+==========================
+
+*October 4, 2023*
+
+Django 4.2.6 fixes a security issue with severity "moderate" and several bugs
+in 4.2.5.
+
+CVE-2023-43665: Denial-of-service possibility in ``django.utils.text.Truncator``
+================================================================================
+
+Following the fix for :cve:`2019-14232`, the regular expressions used in the
+implementation of ``django.utils.text.Truncator``'s ``chars()`` and ``words()``
+methods (with ``html=True``) were revised and improved. However, these regular
+expressions still exhibited linear backtracking complexity, so when given a
+very long, potentially malformed HTML input, the evaluation would still be
+slow, leading to a potential denial of service vulnerability.
+
+The ``chars()`` and ``words()`` methods are used to implement the
+:tfilter:`truncatechars_html` and :tfilter:`truncatewords_html` template
+filters, which were thus also vulnerable.
+
+The input processed by ``Truncator``, when operating in HTML mode, has been
+limited to the first five million characters in order to avoid potential
+performance and memory issues.
+
+Bugfixes
+========
+
+* Fixed a regression in Django 4.2.5 where overriding the deprecated
+ ``DEFAULT_FILE_STORAGE`` and ``STATICFILES_STORAGE`` settings in tests caused
+ the main ``STORAGES`` to mutate (:ticket:`34821`).
+
+* Fixed a regression in Django 4.2 that caused unnecessary casting of string
+ based fields (``CharField``, ``EmailField``, ``TextField``, ``CICharField``,
+ ``CIEmailField``, and ``CITextField``) used with the ``__isnull`` lookup on
+ PostgreSQL. As a consequence, the pre-Django 4.2 indexes didn't match and
+ were not used by the query planner (:ticket:`34840`).
+
+ You may need to recreate indexes propagated to the database with Django
+ 4.2 - 4.2.5 as they contain unnecessary ``::text`` casting that is avoided as
+ of this release.
diff --git a/docs/releases/index.txt b/docs/releases/index.txt
index d5bb0483a796..a270310895d9 100644
--- a/docs/releases/index.txt
+++ b/docs/releases/index.txt
@@ -26,6 +26,7 @@ versions of the documentation contain the release notes for any later releases.
.. toctree::
:maxdepth: 1
+ 4.2.6
4.2.5
4.2.4
4.2.3
@@ -38,6 +39,7 @@ versions of the documentation contain the release notes for any later releases.
.. toctree::
:maxdepth: 1
+ 4.1.12
4.1.11
4.1.10
4.1.9
@@ -73,6 +75,7 @@ versions of the documentation contain the release notes for any later releases.
.. toctree::
:maxdepth: 1
+ 3.2.22
3.2.21
3.2.20
3.2.19
diff --git a/docs/releases/security.txt b/docs/releases/security.txt
index 48586c8a6e90..34394c50b0bc 100644
--- a/docs/releases/security.txt
+++ b/docs/releases/security.txt
@@ -36,6 +36,17 @@ Issues under Django's security process
All security issues have been handled under versions of Django's security
process. These are listed below.
+September 4, 2023 - :cve:`2023-41164`
+-------------------------------------
+
+Potential denial of service vulnerability in
+``django.utils.encoding.uri_to_iri()``. `Full description
+`__
+
+* Django 4.2 :commit:`(patch) <9c51b4dcfa0cefcb48231f4d71cafa80821f87b9>`
+* Django 4.1 :commit:`(patch) `
+* Django 3.2 :commit:`(patch) <6f030b1149bd8fa4ba90452e77cb3edc095ce54e>`
+
July 3, 2023 - :cve:`2023-36053`
--------------------------------
diff --git a/tests/backends/postgresql/tests.py b/tests/backends/postgresql/tests.py
index 947d51ea1e4b..3a1d2da35d67 100644
--- a/tests/backends/postgresql/tests.py
+++ b/tests/backends/postgresql/tests.py
@@ -376,6 +376,20 @@ def test_lookup_cast(self):
"::citext", do.lookup_cast(lookup, internal_type=field_type)
)
+ def test_lookup_cast_isnull_noop(self):
+ from django.db.backends.postgresql.operations import DatabaseOperations
+
+ do = DatabaseOperations(connection=None)
+ # Using __isnull lookup doesn't require casting.
+ tests = [
+ "CharField",
+ "EmailField",
+ "TextField",
+ ]
+ for field_type in tests:
+ with self.subTest(field_type=field_type):
+ self.assertEqual(do.lookup_cast("isnull", field_type), "%s")
+
def test_correct_extraction_psycopg_version(self):
from django.db.backends.postgresql.base import Database, psycopg_version
diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py
index 5f59b3a47ef9..8578aa85d539 100644
--- a/tests/constraints/tests.py
+++ b/tests/constraints/tests.py
@@ -779,6 +779,42 @@ def test_validate_expression_str(self):
exclude={"name"},
)
+ def test_validate_nullable_textfield_with_isnull_true(self):
+ is_null_constraint = models.UniqueConstraint(
+ "price",
+ "discounted_price",
+ condition=models.Q(unit__isnull=True),
+ name="uniq_prices_no_unit",
+ )
+ is_not_null_constraint = models.UniqueConstraint(
+ "price",
+ "discounted_price",
+ condition=models.Q(unit__isnull=False),
+ name="uniq_prices_unit",
+ )
+
+ Product.objects.create(price=2, discounted_price=1)
+ Product.objects.create(price=4, discounted_price=3, unit="ng/mL")
+
+ msg = "Constraint “uniq_prices_no_unit” is violated."
+ with self.assertRaisesMessage(ValidationError, msg):
+ is_null_constraint.validate(
+ Product, Product(price=2, discounted_price=1, unit=None)
+ )
+ is_null_constraint.validate(
+ Product, Product(price=2, discounted_price=1, unit="ng/mL")
+ )
+ is_null_constraint.validate(Product, Product(price=4, discounted_price=3))
+
+ msg = "Constraint “uniq_prices_unit” is violated."
+ with self.assertRaisesMessage(ValidationError, msg):
+ is_not_null_constraint.validate(
+ Product,
+ Product(price=4, discounted_price=3, unit="μg/mL"),
+ )
+ is_not_null_constraint.validate(Product, Product(price=4, discounted_price=3))
+ is_not_null_constraint.validate(Product, Product(price=2, discounted_price=1))
+
def test_name(self):
constraints = get_constraints(UniqueConstraintProduct._meta.db_table)
expected_name = "name_color_uniq"
diff --git a/tests/deprecation/test_storages.py b/tests/deprecation/test_storages.py
index 0574f3e880fc..99a1fc1884af 100644
--- a/tests/deprecation/test_storages.py
+++ b/tests/deprecation/test_storages.py
@@ -32,6 +32,7 @@ def test_override_settings_warning(self):
pass
def test_settings_init(self):
+ old_staticfiles_storage = settings.STORAGES.get(STATICFILES_STORAGE_ALIAS)
settings_module = ModuleType("fake_settings_module")
settings_module.USE_TZ = True
settings_module.STATICFILES_STORAGE = (
@@ -49,6 +50,11 @@ def test_settings_init(self):
),
},
)
+ # settings.STORAGES is not mutated.
+ self.assertEqual(
+ settings.STORAGES.get(STATICFILES_STORAGE_ALIAS),
+ old_staticfiles_storage,
+ )
finally:
del sys.modules["fake_settings_module"]
@@ -161,6 +167,7 @@ def test_override_settings_warning(self):
pass
def test_settings_init(self):
+ old_default_storage = settings.STORAGES.get(DEFAULT_STORAGE_ALIAS)
settings_module = ModuleType("fake_settings_module")
settings_module.USE_TZ = True
settings_module.DEFAULT_FILE_STORAGE = "django.core.files.storage.Storage"
@@ -172,6 +179,11 @@ def test_settings_init(self):
fake_settings.STORAGES[DEFAULT_STORAGE_ALIAS],
{"BACKEND": "django.core.files.storage.Storage"},
)
+ # settings.STORAGES is not mutated.
+ self.assertEqual(
+ settings.STORAGES.get(DEFAULT_STORAGE_ALIAS),
+ old_default_storage,
+ )
finally:
del sys.modules["fake_settings_module"]
diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py
index ab3d968aaceb..9ab8eb87e62f 100644
--- a/tests/lookup/tests.py
+++ b/tests/lookup/tests.py
@@ -1309,6 +1309,16 @@ def test_isnull_non_boolean_value(self):
with self.assertRaisesMessage(ValueError, msg):
qs.exists()
+ def test_isnull_textfield(self):
+ self.assertSequenceEqual(
+ Author.objects.filter(bio__isnull=True),
+ [self.au2],
+ )
+ self.assertSequenceEqual(
+ Author.objects.filter(bio__isnull=False),
+ [self.au1],
+ )
+
def test_lookup_rhs(self):
product = Product.objects.create(name="GME", qty_target=5000)
stock_1 = Stock.objects.create(product=product, short=True, qty_available=180)
diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py
index cb2959fe1572..7d20445b1e8e 100644
--- a/tests/utils_tests/test_text.py
+++ b/tests/utils_tests/test_text.py
@@ -1,5 +1,6 @@
import json
import sys
+from unittest.mock import patch
from django.core.exceptions import SuspiciousFileOperation
from django.test import SimpleTestCase
@@ -94,11 +95,17 @@ def test_truncate_chars(self):
text.Truncator(lazystr("The quick brown fox")).chars(10), "The quick…"
)
- def test_truncate_chars_html(self):
+ @patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
+ def test_truncate_chars_html_size_limit(self):
+ max_len = text.Truncator.MAX_LENGTH_HTML
+ bigger_len = text.Truncator.MAX_LENGTH_HTML + 1
+ valid_html = "Joel is a slug
" # 14 chars
perf_test_values = [
- (("", None),
- ("&" * 50000, "&" * 9 + "…"),
+ ("", None),
+ ("
", "", None),
+ (valid_html * bigger_len, "Joel is a…
"), # 10 chars
]
for value, expected in perf_test_values:
with self.subTest(value=value):
@@ -176,15 +183,25 @@ def test_truncate_html_words(self):
truncator = text.Truncator("I <3 python, what about you?
")
self.assertEqual("I <3 python,…
", truncator.words(3, html=True))
+ @patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
+ def test_truncate_words_html_size_limit(self):
+ max_len = text.Truncator.MAX_LENGTH_HTML
+ bigger_len = text.Truncator.MAX_LENGTH_HTML + 1
+ valid_html = "Joel is a slug
" # 4 words
perf_test_values = [
- ("",
- "&" * 50000,
- "_X<<<<<<<<<<<>",
+ ("", None),
+ ("", "", None),
+ (valid_html * bigger_len, valid_html * 12 + "Joel is…
"), # 50 words
]
- for value in perf_test_values:
+ for value, expected in perf_test_values:
with self.subTest(value=value):
truncator = text.Truncator(value)
- self.assertEqual(value, truncator.words(50, html=True))
+ self.assertEqual(
+ expected if expected else value, truncator.words(50, html=True)
+ )
def test_wrap(self):
digits = "1234 67 9"