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"