From b9a25b69ae93dcd3c4971cdeb5dabd3fb710de15 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 8 Sep 2023 10:52:06 +0200 Subject: [PATCH 001/748] Bumped version; main is now 5.1 pre-alpha. --- django/__init__.py | 2 +- docs/conf.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/django/__init__.py b/django/__init__.py index 42317d407ad7..af19c36b411f 100644 --- a/django/__init__.py +++ b/django/__init__.py @@ -1,6 +1,6 @@ from django.utils.version import get_version -VERSION = (5, 0, 0, "alpha", 0) +VERSION = (5, 1, 0, "alpha", 0) __version__ = get_version(VERSION) diff --git a/docs/conf.py b/docs/conf.py index 31478ae79dd1..3c71ced6bf86 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -111,7 +111,7 @@ # built documents. # # The short X.Y version. -version = "5.0" +version = "5.1" # The full version, including alpha/beta/rc tags. try: from django import VERSION, get_version @@ -128,7 +128,7 @@ def django_release(): release = django_release() # The "development version" of Django -django_next_version = "5.0" +django_next_version = "5.1" extlinks = { "bpo": ("https://bugs.python.org/issue?@action=redirect&bpo=%s", "bpo-%s"), From 590a31eb105292510ce94adebc85d691b30f49ca Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 8 Sep 2023 10:54:07 +0200 Subject: [PATCH 002/748] Added stub release notes for 5.1. --- docs/faq/install.txt | 1 + docs/releases/5.1.txt | 254 ++++++++++++++++++++++++++++++++++++++++ docs/releases/index.txt | 7 ++ 3 files changed, 262 insertions(+) create mode 100644 docs/releases/5.1.txt diff --git a/docs/faq/install.txt b/docs/faq/install.txt index 621b314734c3..6e09ee8d3fd0 100644 --- a/docs/faq/install.txt +++ b/docs/faq/install.txt @@ -55,6 +55,7 @@ Django version Python versions 4.1 3.8, 3.9, 3.10, 3.11 (added in 4.1.3) 4.2 3.8, 3.9, 3.10, 3.11 5.0 3.10, 3.11, 3.12 +5.1 3.10, 3.11, 3.12 ============== =============== For each version of Python, only the latest micro release (A.B.C) is officially diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt new file mode 100644 index 000000000000..c6d17cc3c7b0 --- /dev/null +++ b/docs/releases/5.1.txt @@ -0,0 +1,254 @@ +============================================ +Django 5.1 release notes - UNDER DEVELOPMENT +============================================ + +*Expected August 2024* + +Welcome to Django 5.1! + +These release notes cover the :ref:`new features `, as well as +some :ref:`backwards incompatible changes ` you'll +want to be aware of when upgrading from Django 5.0 or earlier. We've +:ref:`begun the deprecation process for some features +`. + +See the :doc:`/howto/upgrade-version` guide if you're updating an existing +project. + +Python compatibility +==================== + +Django 5.1 supports Python 3.10, 3.11, and 3.12. We **highly recommend** and +only officially support the latest release of each series. + +.. _whats-new-5.1: + +What's new in Django 5.1 +======================== + +Minor features +-------------- + +:mod:`django.contrib.admin` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +:mod:`django.contrib.admindocs` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +:mod:`django.contrib.auth` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +:mod:`django.contrib.contenttypes` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +:mod:`django.contrib.gis` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +:mod:`django.contrib.messages` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +:mod:`django.contrib.postgres` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +:mod:`django.contrib.redirects` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +:mod:`django.contrib.sessions` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +:mod:`django.contrib.sitemaps` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +:mod:`django.contrib.sites` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +:mod:`django.contrib.staticfiles` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +:mod:`django.contrib.syndication` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +Asynchronous views +~~~~~~~~~~~~~~~~~~ + +* ... + +Cache +~~~~~ + +* ... + +CSRF +~~~~ + +* ... + +Decorators +~~~~~~~~~~ + +* ... + +Email +~~~~~ + +* ... + +Error Reporting +~~~~~~~~~~~~~~~ + +* ... + +File Storage +~~~~~~~~~~~~ + +* ... + +File Uploads +~~~~~~~~~~~~ + +* ... + +Forms +~~~~~ + +* ... + +Generic Views +~~~~~~~~~~~~~ + +* ... + +Internationalization +~~~~~~~~~~~~~~~~~~~~ + +* ... + +Logging +~~~~~~~ + +* ... + +Management Commands +~~~~~~~~~~~~~~~~~~~ + +* ... + +Migrations +~~~~~~~~~~ + +* ... + +Models +~~~~~~ + +* ... + +Requests and Responses +~~~~~~~~~~~~~~~~~~~~~~ + +* ... + +Security +~~~~~~~~ + +* ... + +Serialization +~~~~~~~~~~~~~ + +* ... + +Signals +~~~~~~~ + +* ... + +Templates +~~~~~~~~~ + +* ... + +Tests +~~~~~ + +* ... + +URLs +~~~~ + +* ... + +Utilities +~~~~~~~~~ + +* ... + +Validators +~~~~~~~~~~ + +* ... + +.. _backwards-incompatible-5.1: + +Backwards incompatible changes in 5.1 +===================================== + +Database backend API +-------------------- + +This section describes changes that may be needed in third-party database +backends. + +* ... + +Miscellaneous +------------- + +* ... + +.. _deprecated-features-5.1: + +Features deprecated in 5.1 +========================== + +Miscellaneous +------------- + +* ... + +Features removed in 5.1 +======================= + +These features have reached the end of their deprecation cycle and are removed +in Django 5.1. + +See :ref:`deprecated-features-4.2` for details on these changes, including how +to remove usage of these features. + +* ... diff --git a/docs/releases/index.txt b/docs/releases/index.txt index b2b5f8f3ec79..665ff32cd57c 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -20,6 +20,13 @@ versions of the documentation contain the release notes for any later releases. .. _development_release_notes: +5.1 release +----------- +.. toctree:: + :maxdepth: 1 + + 5.1 + 5.0 release ----------- .. toctree:: From 295467c04ab4c26a1a9d3798b1e941003fa116cf Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Mon, 11 Sep 2023 09:57:44 +0200 Subject: [PATCH 003/748] Removed versionadded/changed annotations for 4.2. This also removes remaining versionadded/changed annotations for older versions. --- docs/howto/custom-file-storage.txt | 2 - docs/howto/error-reporting.txt | 4 -- docs/howto/static-files/deployment.txt | 4 -- .../contributing/writing-documentation.txt | 5 --- docs/ref/contrib/admin/index.txt | 4 -- docs/ref/contrib/auth.txt | 4 -- docs/ref/contrib/gis/functions.txt | 6 --- docs/ref/contrib/gis/gdal.txt | 4 -- docs/ref/contrib/gis/geoip2.txt | 4 -- docs/ref/contrib/gis/geoquerysets.txt | 2 - docs/ref/contrib/gis/install/postgis.txt | 4 -- docs/ref/contrib/gis/serializers.txt | 5 --- docs/ref/contrib/postgres/fields.txt | 5 --- docs/ref/contrib/postgres/lookups.txt | 2 - docs/ref/contrib/postgres/search.txt | 4 -- docs/ref/contrib/sitemaps.txt | 2 - docs/ref/contrib/staticfiles.txt | 7 ---- docs/ref/databases.txt | 12 ------ docs/ref/django-admin.txt | 7 ---- docs/ref/exceptions.txt | 6 --- docs/ref/files/storage.txt | 4 -- docs/ref/forms/fields.txt | 4 -- docs/ref/logging.txt | 5 --- docs/ref/middleware.txt | 4 -- docs/ref/migration-operations.txt | 2 - docs/ref/models/database-functions.txt | 5 --- docs/ref/models/expressions.txt | 10 ----- docs/ref/models/fields.txt | 11 ----- docs/ref/models/instances.txt | 12 ------ docs/ref/models/lookups.txt | 5 --- docs/ref/models/options.txt | 2 - docs/ref/models/querysets.txt | 5 --- docs/ref/models/relations.txt | 16 -------- docs/ref/request-response.txt | 6 --- docs/ref/schema-editor.txt | 2 - docs/ref/settings.txt | 4 -- docs/ref/templates/builtins.txt | 10 ----- docs/ref/urls.txt | 5 --- docs/ref/utils.txt | 6 --- docs/ref/validators.txt | 12 ------ docs/topics/async.txt | 4 -- docs/topics/auth/customizing.txt | 7 ---- docs/topics/auth/default.txt | 9 ----- docs/topics/auth/passwords.txt | 9 ----- docs/topics/db/queries.txt | 7 ---- docs/topics/db/transactions.txt | 8 ---- docs/topics/files.txt | 4 -- docs/topics/forms/modelforms.txt | 4 -- docs/topics/http/file-uploads.txt | 8 ---- docs/topics/http/middleware.txt | 4 -- docs/topics/i18n/translation.txt | 5 --- docs/topics/migrations.txt | 4 -- docs/topics/testing/advanced.txt | 8 ---- docs/topics/testing/overview.txt | 2 - docs/topics/testing/tools.txt | 40 ------------------- 55 files changed, 346 deletions(-) diff --git a/docs/howto/custom-file-storage.txt b/docs/howto/custom-file-storage.txt index 4e51548bc7bd..b7bd22d9c101 100644 --- a/docs/howto/custom-file-storage.txt +++ b/docs/howto/custom-file-storage.txt @@ -125,8 +125,6 @@ obtain an alternative name. Use your custom storage engine ============================== -.. versionadded:: 4.2 - The first step to using your custom storage with Django is to tell Django about the file storage backend you'll be using. This is done using the :setting:`STORAGES` setting. This setting maps storage aliases, which are a way diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index 875e56a51d87..74e11a6951ab 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -306,10 +306,6 @@ following attributes and methods: re.compile(r"API|TOKEN|KEY|SECRET|PASS|SIGNATURE|HTTP_COOKIE", flags=re.IGNORECASE) - .. versionchanged:: 4.2 - - ``HTTP_COOKIE`` was added. - .. method:: is_active(request) Returns ``True`` to activate the filtering in diff --git a/docs/howto/static-files/deployment.txt b/docs/howto/static-files/deployment.txt index 67ecf59a71a8..d6d1158249d4 100644 --- a/docs/howto/static-files/deployment.txt +++ b/docs/howto/static-files/deployment.txt @@ -106,10 +106,6 @@ provide storage backends for many common file storage APIs. A good starting point is the `overview at djangopackages.org `_. -.. versionchanged:: 4.2 - - The :setting:`STORAGES` setting was added. - Learn more ========== diff --git a/docs/internals/contributing/writing-documentation.txt b/docs/internals/contributing/writing-documentation.txt index fa3cd749f9a5..7943dc173d22 100644 --- a/docs/internals/contributing/writing-documentation.txt +++ b/docs/internals/contributing/writing-documentation.txt @@ -262,11 +262,6 @@ documentation: also need to define a reference to the documentation for that environment variable using :rst:dir:`.. envvar:: `. -.. versionchanged:: 4.2 - - All Python code blocks in the Django documentation were reformatted with - :pypi:`blacken-docs`. - Django-specific markup ====================== diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 318a6c6ad51d..6d5465dac9fa 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -2142,10 +2142,6 @@ forms or widgets depending on ``django.jQuery`` must specify ``js=['admin/js/jquery.init.js', …]`` when :ref:`declaring form media assets `. -.. versionchanged:: 4.2 - - jQuery was upgraded from 3.6.0 to 3.6.4. - .. versionchanged:: 5.0 jQuery was upgraded from 3.6.4 to 3.7.1. diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index be4960f1bf65..9d56631923d5 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -716,10 +716,6 @@ Utility functions backend's ``get_user()`` method, or if the session auth hash doesn't validate. - .. versionchanged:: 4.1.8 - - Fallback verification with :setting:`SECRET_KEY_FALLBACKS` was added. - .. versionchanged:: 5.0 ``aget_user()`` function was added. diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index 31af52bb4e0e..36ef651cfe72 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/docs/ref/contrib/gis/functions.txt @@ -354,8 +354,6 @@ are returned unchanged. ``FromWKB`` =========== -.. versionadded:: 4.2 - .. class:: FromWKB(expression, **extra) *Availability*: MariaDB, `MySQL @@ -367,8 +365,6 @@ Creates geometry from `Well-known binary (WKB)`_ representation. ``FromWKT`` =========== -.. versionadded:: 4.2 - .. class:: FromWKT(expression, **extra) *Availability*: MariaDB, `MySQL @@ -421,8 +417,6 @@ intersection between them. ``IsEmpty`` =========== -.. versionadded:: 4.2 - .. class:: IsEmpty(expr) *Availability*: `PostGIS `__ diff --git a/docs/ref/contrib/gis/gdal.txt b/docs/ref/contrib/gis/gdal.txt index 2eeb3818c679..3692c51c34b9 100644 --- a/docs/ref/contrib/gis/gdal.txt +++ b/docs/ref/contrib/gis/gdal.txt @@ -1282,10 +1282,6 @@ blue. >>> rst.name # Stored in a random path in the vsimem filesystem. '/vsimem/da300bdb-129d-49a8-b336-e410a9428dad' - .. versionchanged:: 4.2 - - Support for :class:`pathlib.Path` ``ds_input`` was added. - .. attribute:: name The name of the source which is equivalent to the input file path or the name diff --git a/docs/ref/contrib/gis/geoip2.txt b/docs/ref/contrib/gis/geoip2.txt index 6390c3de3a0e..5468cff29d89 100644 --- a/docs/ref/contrib/gis/geoip2.txt +++ b/docs/ref/contrib/gis/geoip2.txt @@ -23,10 +23,6 @@ __ https://dev.maxmind.com/geoip/geolite2-free-geolocation-data __ https://db-ip.com/db/lite.php __ https://github.com/maxmind/libmaxminddb/ -.. versionchanged:: 4.2 - - Support for ``.mmdb`` files downloaded from DB-IP was added. - Example ======= diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index 2c95e32b4d60..99b8638a65b3 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -351,8 +351,6 @@ SpatiaLite ``Intersects(poly, geom)`` ``isempty`` ----------- -.. versionadded:: 4.2 - *Availability*: `PostGIS `__ Tests if the geometry is empty. diff --git a/docs/ref/contrib/gis/install/postgis.txt b/docs/ref/contrib/gis/install/postgis.txt index 6af1f7e3fab6..4a828817d105 100644 --- a/docs/ref/contrib/gis/install/postgis.txt +++ b/docs/ref/contrib/gis/install/postgis.txt @@ -22,10 +22,6 @@ platform-specific instructions if you are on :ref:`macos` or :ref:`windows`. .. _PostGIS requirements: https://postgis.net/docs/postgis_installation.html#install_requirements .. _build from source: https://postgis.net/docs/postgis_installation.html#install_short_version -.. versionchanged:: 4.2 - - Support for ``psycopg`` 3.1.8+ was added. - Post-installation ================= diff --git a/docs/ref/contrib/gis/serializers.txt b/docs/ref/contrib/gis/serializers.txt index 9f7ccd260dfc..e62b6ef3063b 100644 --- a/docs/ref/contrib/gis/serializers.txt +++ b/docs/ref/contrib/gis/serializers.txt @@ -60,8 +60,3 @@ Would output:: When the ``fields`` parameter is not specified, the ``geojson`` serializer adds a ``pk`` key to the ``properties`` dictionary with the primary key of the object as the value. - -.. versionchanged:: 4.2 - - The ``id`` key for serialized features was added. Also, the ``id_field`` - option was added to the ``geojson`` serializer. diff --git a/docs/ref/contrib/postgres/fields.txt b/docs/ref/contrib/postgres/fields.txt index b53ce8669645..0348bb235f13 100644 --- a/docs/ref/contrib/postgres/fields.txt +++ b/docs/ref/contrib/postgres/fields.txt @@ -196,11 +196,6 @@ the SQL operator ``&&``. For example: >>> Post.objects.filter(tags__overlap=Post.objects.values_list("tags")) , , ]> -.. versionchanged:: 4.2 - - Support for ``QuerySet.values()`` and ``values_list()`` as a right-hand - side was added. - .. fieldlookup:: arrayfield.len ``len`` diff --git a/docs/ref/contrib/postgres/lookups.txt b/docs/ref/contrib/postgres/lookups.txt index b9b92fc7fa12..e7ccf1c21b93 100644 --- a/docs/ref/contrib/postgres/lookups.txt +++ b/docs/ref/contrib/postgres/lookups.txt @@ -61,8 +61,6 @@ The ``trigram_word_similar`` lookup can be used on ``trigram_strict_word_similar`` ------------------------------- -.. versionadded:: 4.2 - Similar to :lookup:`trigram_word_similar`, except that it forces extent boundaries to match word boundaries. diff --git a/docs/ref/contrib/postgres/search.txt b/docs/ref/contrib/postgres/search.txt index 699f81bd11a6..8b1313709e64 100644 --- a/docs/ref/contrib/postgres/search.txt +++ b/docs/ref/contrib/postgres/search.txt @@ -378,8 +378,6 @@ Usage example: .. class:: TrigramStrictWordSimilarity(string, expression, **extra) -.. versionadded:: 4.2 - Accepts a string or expression, and a field name or expression. Returns the trigram strict word similarity between the two arguments. Similar to :class:`TrigramWordSimilarity() `, except that it forces @@ -436,7 +434,5 @@ Usage example: .. class:: TrigramStrictWordDistance(string, expression, **extra) -.. versionadded:: 4.2 - Accepts a string or expression, and a field name or expression. Returns the trigram strict word distance between the two arguments. diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index 020eaf03ffaa..de8d53218013 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -316,8 +316,6 @@ Note: .. method:: Sitemap.get_languages_for_item(item) - .. versionadded:: 4.2 - **Optional.** A method that returns the sequence of language codes for which the item is displayed. By default :meth:`~Sitemap.get_languages_for_item` returns diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index d446f833ea6d..18f5bb402087 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -345,15 +345,8 @@ argument. For example:: commented out. This :ticket:`may crash on the nonexistent paths <21080>`. You should check and eventually strip comments. -.. versionchanged:: 4.2 - - Experimental optional support for finding paths to JavaScript modules in - ``import`` and ``export`` statements was added. - .. attribute:: storage.ManifestStaticFilesStorage.manifest_hash -.. versionadded:: 4.2 - This attribute provides a single hash that changes whenever a file in the manifest changes. This can be useful to communicate to SPAs that the assets on the server have changed (due to a new deployment). diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index f859f5137732..a4c93be0768b 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -122,10 +122,6 @@ Django supports PostgreSQL 12 and higher. `psycopg`_ 3.1.8+ or `psycopg2`_ Support for ``psycopg2`` is likely to be deprecated and removed at some point in the future. -.. versionchanged:: 4.2 - - Support for ``psycopg`` 3.1.8+ was added. - .. _postgresql-connection-settings: PostgreSQL connection settings @@ -223,17 +219,11 @@ configuration in :setting:`DATABASES`:: .. _isolation level: https://www.postgresql.org/docs/current/transaction-iso.html -.. versionchanged:: 4.2 - - ``IsolationLevel`` was added. - .. _database-role: Role ---- -.. versionadded:: 4.2 - If you need to use a different role for database connections than the role use to establish the connection, set it in the :setting:`OPTIONS` part of your database configuration in :setting:`DATABASES`:: @@ -253,8 +243,6 @@ database configuration in :setting:`DATABASES`:: Server-side parameters binding ------------------------------ -.. versionadded:: 4.2 - With `psycopg`_ 3.1.8+, Django defaults to the :ref:`client-side binding cursors `. If you want to use the :ref:`server-side binding ` set it in the diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index d63c049b0b08..eb77e2a17d4e 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -744,11 +744,6 @@ Generate migration files without Django version and timestamp header. Makes ``makemigrations`` exit with a non-zero status when model changes without migrations are detected. -.. versionchanged:: 4.2 - - In older versions, the missing migrations were also created when using the - ``--check`` option. - .. django-admin-option:: --scriptable Diverts log output and input prompts to ``stderr``, writing only paths of @@ -756,8 +751,6 @@ generated migration files to ``stdout``. .. django-admin-option:: --update -.. versionadded:: 4.2 - Merges model changes into the latest migration and optimize the resulting operations. diff --git a/docs/ref/exceptions.txt b/docs/ref/exceptions.txt index 54a31115f0ff..f9f7c3c49891 100644 --- a/docs/ref/exceptions.txt +++ b/docs/ref/exceptions.txt @@ -47,8 +47,6 @@ Django core exception classes are defined in ``django.core.exceptions``. .. exception:: FullResultSet -.. versionadded:: 4.2 - ``FullResultSet`` may be raised during query generation if a query will match everything. Most Django projects won't encounter this exception, but it might be useful for implementing custom lookups and expressions. @@ -102,10 +100,6 @@ Django core exception classes are defined in ``django.core.exceptions``. a :class:`~django.http.HttpResponseBadRequest`. See the :doc:`logging documentation ` for more information. -.. versionchanged:: 3.2.18 - - ``SuspiciousOperation`` is raised when too many files are submitted. - ``PermissionDenied`` -------------------- diff --git a/docs/ref/files/storage.txt b/docs/ref/files/storage.txt index 47a578857430..afc2476e8549 100644 --- a/docs/ref/files/storage.txt +++ b/docs/ref/files/storage.txt @@ -11,8 +11,6 @@ Django provides convenient ways to access the default storage class: .. data:: storages - .. versionadded:: 4.2 - Storage instances as defined by :setting:`STORAGES`. .. class:: DefaultStorage @@ -88,8 +86,6 @@ The ``FileSystemStorage`` class The ``InMemoryStorage`` class ============================= -.. versionadded:: 4.2 - .. class:: InMemoryStorage(location=None, base_url=None, file_permissions_mode=None, directory_permissions_mode=None) The :class:`~django.core.files.storage.InMemoryStorage` class implements diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 5e42404f94e7..35fc4b4b2779 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -656,10 +656,6 @@ For each field, we describe the default widget used if you don't specify ``empty_value`` which work just as they do for :class:`CharField`. The ``max_length`` argument defaults to 320 (see :rfc:`3696#section-3`). - .. versionchanged:: 3.2.20 - - The default value for ``max_length`` was changed to 320 characters. - ``FileField`` ------------- diff --git a/docs/ref/logging.txt b/docs/ref/logging.txt index b11fb752f795..a15e2ac91f1d 100644 --- a/docs/ref/logging.txt +++ b/docs/ref/logging.txt @@ -199,11 +199,6 @@ This logging does not include framework-level initialization (e.g. ``SET TIMEZONE``). Turn on query logging in your database if you wish to view all database queries. -.. versionchanged:: 4.2 - - Support for logging transaction management queries (``BEGIN``, ``COMMIT``, - and ``ROLLBACK``) was added. - .. _django-security-logger: ``django.security.*`` diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 73f315e7fc73..63b38da0a0c2 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -118,10 +118,6 @@ GZip middleware .. _breachattack.com: https://www.breachattack.com/ .. _Heal The Breach (HTB) paper: https://ieeexplore.ieee.org/document/9754554 -.. versionchanged:: 4.2 - - Mitigation for the BREACH attack was added. - The ``django.middleware.gzip.GZipMiddleware`` compresses content for browsers that understand GZip compression (all modern browsers). diff --git a/docs/ref/migration-operations.txt b/docs/ref/migration-operations.txt index 96a8e4bc8c44..7cc3a3926fd3 100644 --- a/docs/ref/migration-operations.txt +++ b/docs/ref/migration-operations.txt @@ -91,8 +91,6 @@ option on the ``Meta`` subclass). ``AlterModelTableComment`` -------------------------- -.. versionadded:: 4.2 - .. class:: AlterModelTableComment(name, table_comment) Changes the model's table comment (the diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt index 489c969b560b..42ae8c34e731 100644 --- a/docs/ref/models/database-functions.txt +++ b/docs/ref/models/database-functions.txt @@ -567,11 +567,6 @@ Usage example: On Oracle, the SQL ``LOCALTIMESTAMP`` is used to avoid issues with casting ``CURRENT_TIMESTAMP`` to ``DateTimeField``. -.. versionchanged:: 4.2 - - Support for microsecond precision on MySQL and millisecond precision on - SQLite were added. - .. versionchanged:: 5.0 In older versions, the SQL ``CURRENT_TIMESTAMP`` was used on Oracle instead diff --git a/docs/ref/models/expressions.txt b/docs/ref/models/expressions.txt index abae25f09c4a..fe26e7a35fb0 100644 --- a/docs/ref/models/expressions.txt +++ b/docs/ref/models/expressions.txt @@ -282,8 +282,6 @@ is null) after companies that have been contacted:: Using ``F()`` with logical operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.2 - ``F()`` expressions that output ``BooleanField`` can be logically negated with the inversion operator ``~F()``. For example, to swap the activation status of companies:: @@ -876,10 +874,6 @@ from groups to be included: NotImplementedError: Heterogeneous disjunctive predicates against window functions are not implemented when performing conditional aggregation. -.. versionchanged:: 4.2 - - Support for filtering against window functions was added. - Among Django's built-in database backends, MySQL, PostgreSQL, and Oracle support window expressions. Support for different window expression features varies among the different databases. For example, the options in @@ -1100,10 +1094,6 @@ calling the appropriate methods on the wrapped expression. nested expressions. ``F()`` objects, in particular, hold a reference to a column. - .. versionchanged:: 4.2 - - The ``alias=None`` keyword argument was removed. - .. method:: asc(nulls_first=None, nulls_last=None) Returns the expression ready to be sorted in ascending order. diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 7ad9f77741a8..1c166443be87 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -392,8 +392,6 @@ scenes. ``db_comment`` -------------- -.. versionadded:: 4.2 - .. attribute:: Field.db_comment The comment on the database column to use for this field. It is useful for @@ -740,10 +738,6 @@ The default form widget for this field is a :class:`~django.forms.TextInput`. ``max_length`` for some backends. Refer to the :doc:`database backend notes ` for details. - .. versionchanged:: 4.2 - - Support for unlimited ``VARCHAR`` columns was added on PostgreSQL. - .. attribute:: CharField.db_collation Optional. The database collation name of the field. @@ -2469,11 +2463,6 @@ Registering and fetching lookups The API can be used to customize which lookups are available for a field class and its instances, and how lookups are fetched from a field. -.. versionchanged:: 4.2 - - Support for registering lookups on :class:`~django.db.models.Field` - instances was added. - .. _model-field-attributes: ========================= diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 6ceb0703abf6..ae5dec2e656b 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -200,10 +200,6 @@ all of the instance's fields when a deferred field is reloaded:: A helper method that returns a set containing the attribute names of all those fields that are currently deferred for this model. -.. versionchanged:: 4.2 - - ``arefresh_from_db()`` method was added. - .. _validating-objects: Validating objects @@ -426,10 +422,6 @@ method. See :ref:`overriding-model-methods` for more details. The model save process also has some subtleties; see the sections below. -.. versionchanged:: 4.2 - - ``asave()`` method was added. - Auto-incrementing primary keys ------------------------------ @@ -709,10 +701,6 @@ Sometimes with :ref:`multi-table inheritance ` you may want to delete only a child model's data. Specifying ``keep_parents=True`` will keep the parent model's data. -.. versionchanged:: 4.2 - - ``adelete()`` method was added. - Pickling objects ================ diff --git a/docs/ref/models/lookups.txt b/docs/ref/models/lookups.txt index 3998eb370564..07d6e03783ff 100644 --- a/docs/ref/models/lookups.txt +++ b/docs/ref/models/lookups.txt @@ -82,11 +82,6 @@ For a class to be a lookup, it must follow the :ref:`Query Expression API `. :class:`~Lookup` and :class:`~Transform` naturally follow this API. -.. versionchanged:: 4.2 - - Support for registering lookups on :class:`~django.db.models.Field` - instances was added. - .. _query-expression: The Query Expression API diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index 6f318b2df8d5..2a383b2b888f 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -94,8 +94,6 @@ Django quotes column and table names behind the scenes. ``db_table_comment`` -------------------- -.. versionadded:: 4.2 - .. attribute:: Options.db_table_comment The comment on the database table to use for this model. It is useful for diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index cd6c13fc05d9..547bd4b4ff26 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -2353,11 +2353,6 @@ Like :meth:`get_or_create` and :meth:`create`, if you're using manually specified primary keys and an object needs to be created but the key already exists in the database, an :exc:`~django.db.IntegrityError` is raised. -.. versionchanged:: 4.2 - - In older versions, ``update_or_create()`` didn't specify ``update_fields`` - when calling :meth:`Model.save() `. - .. versionchanged:: 5.0 The ``create_defaults`` argument was added. diff --git a/docs/ref/models/relations.txt b/docs/ref/models/relations.txt index 98177010b8c8..f941b435cca4 100644 --- a/docs/ref/models/relations.txt +++ b/docs/ref/models/relations.txt @@ -84,10 +84,6 @@ Related objects reference dictionary and they will be evaluated once before creating any intermediate instance(s). - .. versionchanged:: 4.2 - - ``aadd()`` method was added. - .. method:: create(through_defaults=None, **kwargs) .. method:: acreate(through_defaults=None, **kwargs) @@ -168,10 +164,6 @@ Related objects reference For many-to-many relationships, the ``bulk`` keyword argument doesn't exist. - .. versionchanged:: 4.2 - - ``aremove()`` method was added. - .. method:: clear(bulk=True) .. method:: aclear(bulk=True) @@ -194,10 +186,6 @@ Related objects reference For many-to-many relationships, the ``bulk`` keyword argument doesn't exist. - .. versionchanged:: 4.2 - - ``aclear()`` method was added. - .. method:: set(objs, bulk=True, clear=False, through_defaults=None) .. method:: aset(objs, bulk=True, clear=False, through_defaults=None) @@ -236,10 +224,6 @@ Related objects reference dictionary and they will be evaluated once before creating any intermediate instance(s). - .. versionchanged:: 4.2 - - ``aset()`` method was added. - .. note:: Note that ``add()``, ``aadd()``, ``create()``, ``acreate()``, diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 036f7004d2c8..1837b4be27db 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -1246,10 +1246,6 @@ with the following notable differences: The :class:`HttpResponseBase` base class is common between :class:`HttpResponse` and :class:`StreamingHttpResponse`. -.. versionchanged:: 4.2 - - Support for asynchronous iteration was added. - Attributes ---------- @@ -1280,8 +1276,6 @@ Attributes .. attribute:: StreamingHttpResponse.is_async - .. versionadded:: 4.2 - Boolean indicating whether :attr:`StreamingHttpResponse.streaming_content` is an asynchronous iterator or not. diff --git a/docs/ref/schema-editor.txt b/docs/ref/schema-editor.txt index 3526e1ffff74..635272c982f7 100644 --- a/docs/ref/schema-editor.txt +++ b/docs/ref/schema-editor.txt @@ -128,8 +128,6 @@ Renames the model's table from ``old_db_table`` to ``new_db_table``. ``alter_db_table_comment()`` ---------------------------- -.. versionadded:: 4.2 - .. method:: BaseDatabaseSchemaEditor.alter_db_table_comment(model, old_db_table_comment, new_db_table_comment) Change the ``model``’s table comment to ``new_db_table_comment``. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 3dc6edf3331c..5f8395563d16 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1067,8 +1067,6 @@ perform a similar check at that level. ``DATA_UPLOAD_MAX_NUMBER_FILES`` -------------------------------- -.. versionadded:: 3.2.18 - Default: ``100`` The maximum number of files that may be received via POST in a @@ -2597,8 +2595,6 @@ See also the :doc:`/ref/checks` documentation. ``STORAGES`` ------------ -.. versionadded:: 4.2 - Default:: { diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 65579677cabc..4798663bf037 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -244,11 +244,6 @@ Outputs a whole load of debugging information, including the current context and imported modules. ``{% debug %}`` outputs nothing when the :setting:`DEBUG` setting is ``False``. -.. versionchanged:: 2.2.27 - - In older versions, debugging information was displayed when the - :setting:`DEBUG` setting was ``False``. - .. templatetag:: extends ``extends`` @@ -1803,11 +1798,6 @@ produce empty output: Ordering by elements at specified index is not supported on dictionaries. -.. versionchanged:: 2.2.26 - - In older versions, ordering elements at specified index was supported on - dictionaries. - .. templatefilter:: dictsortreversed ``dictsortreversed`` diff --git a/docs/ref/urls.txt b/docs/ref/urls.txt index be8380ed5a52..e8d51eeda22b 100644 --- a/docs/ref/urls.txt +++ b/docs/ref/urls.txt @@ -79,11 +79,6 @@ pattern (:py:func:`re.fullmatch` is used). The ``view``, ``kwargs`` and ``name`` arguments are the same as for :func:`~django.urls.path()`. -.. versionchanged:: 2.2.25 - - In older versions, a full-match wasn't required for a ``route`` which ends - with ``$``. - ``include()`` ============= diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index acbe5d51a621..4e28690f44ac 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -685,10 +685,6 @@ escaping HTML. serialize the data. See :ref:`JSON serialization ` for more details about this serializer. - .. versionchanged:: 4.2 - - The ``encoder`` argument was added. - .. function:: strip_tags(value) Tries to remove anything that looks like an HTML tag from the string, that @@ -742,8 +738,6 @@ escaping HTML. .. function:: content_disposition_header(as_attachment, filename) - .. versionadded:: 4.2 - Constructs a ``Content-Disposition`` HTTP header value from the given ``filename`` as specified by :rfc:`6266`. Returns ``None`` if ``as_attachment`` is ``False`` and ``filename`` is ``None``, otherwise diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt index 8323b55909e4..789d47d93504 100644 --- a/docs/ref/validators.txt +++ b/docs/ref/validators.txt @@ -159,11 +159,6 @@ to, or in lieu of custom ``field.clean()`` methods. validation, so you'd need to add them to the ``allowlist`` as necessary. - .. versionchanged:: 3.2.20 - - In older versions, values longer than 320 characters could be - considered valid. - ``URLValidator`` ---------------- @@ -190,16 +185,9 @@ to, or in lieu of custom ``field.clean()`` methods. .. attribute:: max_length - .. versionadded:: 3.2.20 - The maximum length of values that could be considered valid. Defaults to 2048 characters. - .. versionchanged:: 3.2.20 - - In older versions, values longer than 2048 characters could be - considered valid. - ``validate_email`` ------------------ diff --git a/docs/topics/async.txt b/docs/topics/async.txt index 0e33753e682c..8a9857a0df2d 100644 --- a/docs/topics/async.txt +++ b/docs/topics/async.txt @@ -150,10 +150,6 @@ Transactions do not yet work in async mode. If you have a piece of code that needs transactions behavior, we recommend you write that piece as a single synchronous function and call it using :func:`sync_to_async`. -.. versionchanged:: 4.2 - - Asynchronous model and related manager interfaces were added. - .. _async_performance: Performance diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 78bee37a0f31..bd0c07427348 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -731,8 +731,6 @@ The following attributes and methods are available on any subclass of .. method:: models.AbstractBaseUser.get_session_auth_fallback_hash() - .. versionadded:: 4.1.8 - Yields the HMAC of the password field using :setting:`SECRET_KEY_FALLBACKS`. Used by ``get_user()``. @@ -871,11 +869,6 @@ extend these forms in this manner:: model = CustomUser fields = UserCreationForm.Meta.fields + ("custom_field",) -.. versionchanged:: 4.2 - - In older versions, :class:`~django.contrib.auth.forms.UserCreationForm` - didn't save many-to-many form fields for a custom user model. - Custom users and :mod:`django.contrib.admin` -------------------------------------------- diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index 4aee26d9cae2..c8140e4d228a 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -1723,8 +1723,6 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`: .. class:: BaseUserCreationForm - .. versionadded:: 4.2 - A :class:`~django.forms.ModelForm` for creating a new user. This is the recommended base class if you need to customize the user creation form. @@ -1741,13 +1739,6 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`: similar usernames, the form doesn't allow usernames that differ only in case. - .. versionchanged:: 4.2 - - In older versions, :class:`UserCreationForm` didn't save many-to-many - form fields for a custom user model. - - In older versions, usernames that differ only in case are allowed. - .. currentmodule:: django.contrib.auth Authentication data in templates diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt index 0876ac4f6ea3..d0a812f39812 100644 --- a/docs/topics/auth/passwords.txt +++ b/docs/topics/auth/passwords.txt @@ -620,10 +620,6 @@ Django includes four validators: ``user_attributes``, whereas a value of 1.0 rejects only passwords that are identical to an attribute's value. - .. versionchanged:: 2.2.26 - - The ``max_similarity`` parameter was limited to a minimum value of 0.1. - .. class:: CommonPasswordValidator(password_list_path=DEFAULT_PASSWORD_LIST_PATH) Validates that the password is not a common password. This converts the @@ -635,11 +631,6 @@ Django includes four validators: common passwords. This file should contain one lowercase password per line and may be plain text or gzipped. - .. versionchanged:: 4.2 - - The list of 20,000 common passwords was updated to the most recent - version. - .. class:: NumericPasswordValidator() Validate that the password is not entirely numeric. diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 6dfe97f31795..b975f6cdfe54 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -1085,11 +1085,6 @@ Unless you are sure you wish to work with SQL ``NULL`` values, consider setting Storing JSON scalar ``null`` does not violate :attr:`null=False `. -.. versionchanged:: 4.2 - - Support for expressing JSON ``null`` using ``Value(None, JSONField())`` was - added. - .. deprecated:: 4.2 Passing ``Value("null")`` to express JSON ``null`` is deprecated. @@ -1162,8 +1157,6 @@ To query for missing keys, use the ``isnull`` lookup: ``KT()`` expressions ~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.2 - .. module:: django.db.models.fields.json .. class:: KT(lookup) diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index c43312ec0f85..951ce54a6852 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -332,10 +332,6 @@ are caught and logged to the ``django.db.backends.base`` logger. You can use :meth:`.TestCase.captureOnCommitCallbacks` to test callbacks registered with :func:`on_commit`. -.. versionchanged:: 4.2 - - The ``robust`` argument was added. - Savepoints ---------- @@ -382,10 +378,6 @@ transaction raises an uncaught exception, no later registered functions in that same transaction will run. This is the same behavior as if you'd executed the functions sequentially yourself without :func:`on_commit`. -.. versionchanged:: 4.2 - - The ``robust`` argument was added. - Timing of execution ------------------- diff --git a/docs/topics/files.txt b/docs/topics/files.txt index 9ec4b0dc6653..6ae1603f07ef 100644 --- a/docs/topics/files.txt +++ b/docs/topics/files.txt @@ -272,7 +272,3 @@ use :data:`~django.core.files.storage.storages`:: class MyModel(models.Model): upload = models.FileField(storage=select_storage) - -.. versionchanged:: 4.2 - - Support for ``storages`` was added. diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index fbd5695c17df..53ce716a4d9b 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -676,10 +676,6 @@ the field declaratively and setting its ``validators`` parameter:: See the :doc:`form field documentation ` for more information on fields and their arguments. -.. versionchanged:: 4.2 - - The ``Meta.formfield_callback`` attribute was added. - Enabling localization of fields ------------------------------- diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt index cb0fb5b7fbd6..0e696badc00b 100644 --- a/docs/topics/http/file-uploads.txt +++ b/docs/topics/http/file-uploads.txt @@ -230,14 +230,6 @@ uploads: instance (in a single field), for example, even if the custom widget is used with a form field related to a model ``FileField``. -.. versionchanged:: 3.2.19 - - In previous versions, there was no support for the ``allow_multiple_selected`` - class attribute, and users were advised to create the widget with the HTML - attribute ``multiple`` set through the ``attrs`` argument. However, this - caused validation of the form field to be applied only to the last file - submitted, which could have adverse security implications. - Upload Handlers =============== diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt index 9b4bd12a7bd2..1c3b3c2a26cd 100644 --- a/docs/topics/http/middleware.txt +++ b/docs/topics/http/middleware.txt @@ -273,10 +273,6 @@ asynchronous iterators. The wrapping function must match. Check ` if your middleware needs to support both types of iterator. -.. versionchanged:: 4.2 - - Support for streaming responses with asynchronous iterators was added. - Exception handling ================== diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 6fe126f59da4..41bee79204e4 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -741,11 +741,6 @@ In practice you'll use this to get a string you can use in multiple places in a template or so you can use the output as an argument for other template tags or filters. -.. versionchanged:: 4.2 - - In older versions, ``asvar`` instances weren't marked as safe for (HTML) - output purposes. - ``{% blocktranslate %}`` also supports :ref:`contextual markers` using the ``context`` keyword: diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt index b7cd7043c2bc..248a7addbe4e 100644 --- a/docs/topics/migrations.txt +++ b/docs/topics/migrations.txt @@ -795,10 +795,6 @@ Django can serialize the following: - Any class reference (must be in module's top-level scope) - Anything with a custom ``deconstruct()`` method (:ref:`see below `) -.. versionchanged:: 4.2 - - Serialization support for ``enum.Flag`` was added. - .. versionchanged:: 5.0 Serialization support for functions decorated with :func:`functools.cache` diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index 54b9d7d133d2..fda77e766197 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -32,10 +32,6 @@ restricted subset of the test client API: attributes must be supplied by the test itself if required for the view to function properly. -.. versionchanged:: 4.2 - - The ``headers`` parameter was added. - Example ------- @@ -89,10 +85,6 @@ difference being that it returns ``ASGIRequest`` instances rather than Arbitrary keyword arguments in ``defaults`` are added directly into the ASGI scope. -.. versionchanged:: 4.2 - - The ``headers`` parameter was added. - Testing class-based views ========================= diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 5dbe46ceb2ee..30f4a75edc02 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -381,8 +381,6 @@ time to run tests. Avoiding disk access for media files ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 4.2 - The :class:`~django.core.files.storage.InMemoryStorage` is a convenient way to prevent disk access for media files. All data is kept in memory, then it gets discarded after tests run. diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index a9373ff10855..e9b12d94f0b6 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -158,10 +158,6 @@ Use the ``django.test.Client`` class to make requests. Once you have a ``Client`` instance, you can call any of the following methods: - .. versionchanged:: 4.2 - - The ``headers`` parameter was added. - .. method:: Client.get(path, data=None, follow=False, secure=False, *, headers=None, **extra) Makes a GET request on the provided ``path`` and returns a ``Response`` @@ -234,10 +230,6 @@ Use the ``django.test.Client`` class to make requests. If you set ``secure`` to ``True`` the client will emulate an HTTPS request. - .. versionchanged:: 4.2 - - The ``headers`` parameter was added. - .. method:: Client.post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, *, headers=None, **extra) Makes a POST request on the provided ``path`` and returns a @@ -351,10 +343,6 @@ Use the ``django.test.Client`` class to make requests. If you set ``secure`` to ``True`` the client will emulate an HTTPS request. - .. versionchanged:: 4.2 - - The ``headers`` parameter was added. - .. method:: Client.head(path, data=None, follow=False, secure=False, *, headers=None, **extra) Makes a HEAD request on the provided ``path`` and returns a @@ -362,10 +350,6 @@ Use the ``django.test.Client`` class to make requests. including the ``follow``, ``secure``, ``headers``, and ``extra`` parameters, except it does not return a message body. - .. versionchanged:: 4.2 - - The ``headers`` parameter was added. - .. method:: Client.options(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra) Makes an OPTIONS request on the provided ``path`` and returns a @@ -377,10 +361,6 @@ Use the ``django.test.Client`` class to make requests. The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act the same as for :meth:`Client.get`. - .. versionchanged:: 4.2 - - The ``headers`` parameter was added. - .. method:: Client.put(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra) Makes a PUT request on the provided ``path`` and returns a @@ -392,10 +372,6 @@ Use the ``django.test.Client`` class to make requests. The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act the same as for :meth:`Client.get`. - .. versionchanged:: 4.2 - - The ``headers`` parameter was added. - .. method:: Client.patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra) Makes a PATCH request on the provided ``path`` and returns a @@ -404,10 +380,6 @@ Use the ``django.test.Client`` class to make requests. The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act the same as for :meth:`Client.get`. - .. versionchanged:: 4.2 - - The ``headers`` parameter was added. - .. method:: Client.delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra) Makes a DELETE request on the provided ``path`` and returns a @@ -419,10 +391,6 @@ Use the ``django.test.Client`` class to make requests. The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act the same as for :meth:`Client.get`. - .. versionchanged:: 4.2 - - The ``headers`` parameter was added. - .. method:: Client.trace(path, follow=False, secure=False, *, headers=None, **extra) Makes a TRACE request on the provided ``path`` and returns a @@ -435,10 +403,6 @@ Use the ``django.test.Client`` class to make requests. The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act the same as for :meth:`Client.get`. - .. versionchanged:: 4.2 - - The ``headers`` parameter was added. - .. method:: Client.login(**credentials) .. method:: Client.alogin(**credentials) @@ -2041,10 +2005,6 @@ test client, with the following exceptions: >>> c = AsyncClient() >>> c.get("/customers/details/", {"name": "fred", "age": 7}, ACCEPT="application/json") -.. versionchanged:: 4.2 - - The ``headers`` parameter was added. - .. versionchanged:: 5.0 Support for the ``follow`` parameter was added to the ``AsyncClient``. From 00e187961059a0e77403151d2bb38c217101d5af Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Mon, 11 Sep 2023 21:57:31 +0200 Subject: [PATCH 004/748] Refs #33764 -- Removed BaseUserManager.make_random_password() per deprecation timeline. --- django/contrib/auth/base_user.py | 21 +-------------------- docs/releases/5.1.txt | 2 +- docs/topics/auth/customizing.txt | 13 ------------- tests/auth_tests/test_models.py | 15 --------------- 4 files changed, 2 insertions(+), 49 deletions(-) diff --git a/django/contrib/auth/base_user.py b/django/contrib/auth/base_user.py index da0eac731fb3..aa8e9f8a849a 100644 --- a/django/contrib/auth/base_user.py +++ b/django/contrib/auth/base_user.py @@ -3,7 +3,6 @@ not in INSTALLED_APPS. """ import unicodedata -import warnings from django.conf import settings from django.contrib.auth import password_validation @@ -14,8 +13,7 @@ make_password, ) from django.db import models -from django.utils.crypto import get_random_string, salted_hmac -from django.utils.deprecation import RemovedInDjango51Warning +from django.utils.crypto import salted_hmac from django.utils.translation import gettext_lazy as _ @@ -34,23 +32,6 @@ def normalize_email(cls, email): email = email_name + "@" + domain_part.lower() return email - def make_random_password( - self, - length=10, - allowed_chars="abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", - ): - """ - Generate a random password with the given length and given - allowed_chars. The default value of allowed_chars does not have "I" or - "O" or letters and digits that look similar -- just to avoid confusion. - """ - warnings.warn( - "BaseUserManager.make_random_password() is deprecated.", - category=RemovedInDjango51Warning, - stacklevel=2, - ) - return get_random_string(length, allowed_chars) - def get_by_natural_key(self, username): return self.get(**{self.model.USERNAME_FIELD: username}) diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index c6d17cc3c7b0..4560e1fabfce 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -251,4 +251,4 @@ in Django 5.1. See :ref:`deprecated-features-4.2` for details on these changes, including how to remove usage of these features. -* ... +* The ``BaseUserManager.make_random_password()`` method is removed. diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index bd0c07427348..52fa3515b8e0 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -799,19 +799,6 @@ utility methods: Retrieves a user instance using the contents of the field nominated by ``USERNAME_FIELD``. - .. method:: models.BaseUserManager.make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789') - - .. deprecated:: 4.2 - - Returns a random password with the given length and given string of - allowed characters. Note that the default value of ``allowed_chars`` - doesn't contain letters that can cause user confusion, including: - - * ``i``, ``l``, ``I``, and ``1`` (lowercase letter i, lowercase - letter L, uppercase letter i, and the number one) - * ``o``, ``O``, and ``0`` (lowercase letter o, uppercase letter o, - and zero) - Extending Django's default ``User`` ----------------------------------- diff --git a/tests/auth_tests/test_models.py b/tests/auth_tests/test_models.py index 19454ade65f8..b9a006f96dcd 100644 --- a/tests/auth_tests/test_models.py +++ b/tests/auth_tests/test_models.py @@ -20,8 +20,6 @@ from django.db.migrations.state import ModelState, ProjectState from django.db.models.signals import post_save from django.test import SimpleTestCase, TestCase, TransactionTestCase, override_settings -from django.test.utils import ignore_warnings -from django.utils.deprecation import RemovedInDjango51Warning from .models import CustomEmailField, IntegerUsernameUser @@ -168,19 +166,6 @@ def test_create_superuser_raises_error_on_false_is_staff(self): is_staff=False, ) - @ignore_warnings(category=RemovedInDjango51Warning) - def test_make_random_password(self): - allowed_chars = "abcdefg" - password = UserManager().make_random_password(5, allowed_chars) - self.assertEqual(len(password), 5) - for char in password: - self.assertIn(char, allowed_chars) - - def test_make_random_password_warning(self): - msg = "BaseUserManager.make_random_password() is deprecated." - with self.assertWarnsMessage(RemovedInDjango51Warning, msg): - UserManager().make_random_password() - def test_runpython_manager_methods(self): def forwards(apps, schema_editor): UserModel = apps.get_model("auth", "User") From 2abf417c815c20f41c0868d6f66520b32347106e Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Tue, 12 Sep 2023 05:56:16 +0200 Subject: [PATCH 005/748] Refs #27236 -- Removed Meta.index_together per deprecation timeline. --- django/db/backends/base/schema.py | 12 +- django/db/backends/sqlite3/schema.py | 10 - django/db/migrations/autodetector.py | 37 +- django/db/migrations/operations/models.py | 27 - django/db/migrations/state.py | 13 +- django/db/models/base.py | 31 -- django/db/models/options.py | 11 - docs/ref/checks.txt | 18 +- docs/ref/migration-operations.txt | 2 +- docs/ref/models/options.txt | 23 - docs/ref/schema-editor.txt | 5 +- docs/releases/1.11.txt | 4 +- docs/releases/1.5.txt | 3 +- docs/releases/1.7.txt | 4 +- docs/releases/4.1.txt | 4 +- docs/releases/4.2.txt | 5 +- docs/releases/5.1.txt | 2 + tests/indexes/tests.py | 29 +- tests/invalid_models_tests/test_models.py | 131 +---- tests/migrations/test_autodetector.py | 589 +--------------------- tests/migrations/test_base.py | 3 - tests/migrations/test_operations.py | 172 ------- tests/migrations/test_optimizer.py | 75 --- tests/migrations/test_state.py | 8 +- tests/schema/tests.py | 131 +---- 25 files changed, 41 insertions(+), 1308 deletions(-) diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index 493a47548a56..191809441afd 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -501,8 +501,7 @@ def create_model(self, model): model, field, field_type, field.db_comment ) ) - # Add any field index and index_together's (deferred as SQLite - # _remake_table needs it). + # Add any field index (deferred as SQLite _remake_table needs it). self.deferred_sql.extend(self._model_indexes_sql(model)) # Make M2M tables @@ -1585,8 +1584,8 @@ def _index_columns(self, table, columns, col_suffixes, opclasses): def _model_indexes_sql(self, model): """ - Return a list of all index SQL statements (field indexes, - index_together, Meta.indexes) for the specified model. + Return a list of all index SQL statements (field indexes, Meta.indexes) + for the specified model. """ if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] @@ -1594,11 +1593,6 @@ def _model_indexes_sql(self, model): for field in model._meta.local_fields: output.extend(self._field_indexes_sql(model, field)) - # RemovedInDjango51Warning. - for field_names in model._meta.index_together: - fields = [model._meta.get_field(field) for field in field_names] - output.append(self._create_index_sql(model, fields=fields, suffix="_idx")) - for index in model._meta.indexes: if ( not index.contains_expressions diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index f311e0b7459f..a8200a1a8c70 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -180,14 +180,6 @@ def is_self_referential(f): for unique in model._meta.unique_together ] - # RemovedInDjango51Warning. - # Work out the new value for index_together, taking renames into - # account - index_together = [ - [rename_mapping.get(n, n) for n in index] - for index in model._meta.index_together - ] - indexes = model._meta.indexes if delete_field: indexes = [ @@ -210,7 +202,6 @@ def is_self_referential(f): "app_label": model._meta.app_label, "db_table": model._meta.db_table, "unique_together": unique_together, - "index_together": index_together, # RemovedInDjango51Warning. "indexes": indexes, "constraints": constraints, "apps": apps, @@ -226,7 +217,6 @@ def is_self_referential(f): "app_label": model._meta.app_label, "db_table": "new__%s" % strip_quotes(model._meta.db_table), "unique_together": unique_together, - "index_together": index_together, # RemovedInDjango51Warning. "indexes": indexes, "constraints": constraints, "apps": apps, diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index 3a0ee511ff45..a823b0373847 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -190,14 +190,12 @@ def _detect_changes(self, convert_apps=None, graph=None): self.generate_renamed_indexes() # Generate removal of foo together. self.generate_removed_altered_unique_together() - self.generate_removed_altered_index_together() # RemovedInDjango51Warning. # Generate field operations. self.generate_removed_fields() self.generate_added_fields() self.generate_altered_fields() self.generate_altered_order_with_respect_to() self.generate_altered_unique_together() - self.generate_altered_index_together() # RemovedInDjango51Warning. self.generate_added_indexes() self.generate_added_constraints() self.generate_altered_db_table() @@ -584,7 +582,7 @@ def generate_created_models(self): possible). Defer any model options that refer to collections of fields that might - be deferred (e.g. unique_together, index_together). + be deferred (e.g. unique_together). """ old_keys = self.old_model_keys | self.old_unmanaged_keys added_models = self.new_model_keys - old_keys @@ -608,12 +606,10 @@ def generate_created_models(self): if getattr(field.remote_field, "through", None): related_fields[field_name] = field - # Are there indexes/unique|index_together to defer? + # Are there indexes/unique_together to defer? indexes = model_state.options.pop("indexes") constraints = model_state.options.pop("constraints") unique_together = model_state.options.pop("unique_together", None) - # RemovedInDjango51Warning. - index_together = model_state.options.pop("index_together", None) order_with_respect_to = model_state.options.pop( "order_with_respect_to", None ) @@ -742,16 +738,6 @@ def generate_created_models(self): ), dependencies=related_dependencies, ) - # RemovedInDjango51Warning. - if index_together: - self.add_operation( - app_label, - operations.AlterIndexTogether( - name=model_name, - index_together=index_together, - ), - dependencies=related_dependencies, - ) # Fix relationships if the model changed from a proxy model to a # concrete model. relations = self.to_state.relations @@ -832,8 +818,6 @@ def generate_deleted_models(self): related_fields[field_name] = field # Generate option removal first unique_together = model_state.options.pop("unique_together", None) - # RemovedInDjango51Warning. - index_together = model_state.options.pop("index_together", None) if unique_together: self.add_operation( app_label, @@ -842,15 +826,6 @@ def generate_deleted_models(self): unique_together=None, ), ) - # RemovedInDjango51Warning. - if index_together: - self.add_operation( - app_label, - operations.AlterIndexTogether( - name=model_name, - index_together=None, - ), - ) # Then remove each related field for name in sorted(related_fields): self.add_operation( @@ -1525,10 +1500,6 @@ def _generate_removed_altered_foo_together(self, operation): def generate_removed_altered_unique_together(self): self._generate_removed_altered_foo_together(operations.AlterUniqueTogether) - # RemovedInDjango51Warning. - def generate_removed_altered_index_together(self): - self._generate_removed_altered_foo_together(operations.AlterIndexTogether) - def _generate_altered_foo_together(self, operation): for ( old_value, @@ -1548,10 +1519,6 @@ def _generate_altered_foo_together(self, operation): def generate_altered_unique_together(self): self._generate_altered_foo_together(operations.AlterUniqueTogether) - # RemovedInDjango51Warning. - def generate_altered_index_together(self): - self._generate_altered_foo_together(operations.AlterIndexTogether) - def generate_altered_db_table(self): models_to_check = self.kept_model_keys.union( self.kept_proxy_keys, self.kept_unmanaged_keys diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py index b18ef553695e..d616cafb4597 100644 --- a/django/db/migrations/operations/models.py +++ b/django/db/migrations/operations/models.py @@ -341,33 +341,6 @@ def reduce(self, operation, app_label): managers=self.managers, ), ] - elif isinstance(operation, RenameIndex) and operation.old_fields: - options_index_together = { - fields - for fields in self.options.get("index_together", []) - if fields != operation.old_fields - } - if options_index_together: - self.options["index_together"] = options_index_together - else: - self.options.pop("index_together", None) - return [ - CreateModel( - self.name, - fields=self.fields, - options={ - **self.options, - "indexes": [ - *self.options.get("indexes", []), - models.Index( - fields=operation.old_fields, name=operation.new_name - ), - ], - }, - bases=self.bases, - managers=self.managers, - ), - ] return super().reduce(operation, app_label) diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index ae55967383f1..4aa6e1f6cc64 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -310,14 +310,13 @@ def rename_field(self, app_label, model_name, old_name, new_name): for from_field_name in from_fields ] ) - # Fix index/unique_together to refer to the new field. + # Fix unique_together to refer to the new field. options = model_state.options - for option in ("index_together", "unique_together"): - if option in options: - options[option] = [ - [new_name if n == old_name else n for n in together] - for together in options[option] - ] + if "unique_together" in options: + options["unique_together"] = [ + [new_name if n == old_name else n for n in together] + for together in options["unique_together"] + ] # Fix to_fields to refer to the new field. delay = True references = get_references(self, model_key, (old_name, found)) diff --git a/django/db/models/base.py b/django/db/models/base.py index 80503d118af0..64d54380dad6 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1595,7 +1595,6 @@ def check(cls, **kwargs): if not clash_errors: errors.extend(cls._check_column_name_clashes()) errors += [ - *cls._check_index_together(), *cls._check_unique_together(), *cls._check_indexes(databases), *cls._check_ordering(), @@ -1928,36 +1927,6 @@ def _check_single_primary_key(cls): ) return errors - # RemovedInDjango51Warning. - @classmethod - def _check_index_together(cls): - """Check the value of "index_together" option.""" - if not isinstance(cls._meta.index_together, (tuple, list)): - return [ - checks.Error( - "'index_together' must be a list or tuple.", - obj=cls, - id="models.E008", - ) - ] - - elif any( - not isinstance(fields, (tuple, list)) for fields in cls._meta.index_together - ): - return [ - checks.Error( - "All 'index_together' elements must be lists or tuples.", - obj=cls, - id="models.E009", - ) - ] - - else: - errors = [] - for fields in cls._meta.index_together: - errors.extend(cls._check_local_fields(fields, "index_together")) - return errors - @classmethod def _check_unique_together(cls): """Check the value of "unique_together" option.""" diff --git a/django/db/models/options.py b/django/db/models/options.py index 76017fc8b523..9b3106f67ef6 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -1,7 +1,6 @@ import bisect import copy import inspect -import warnings from collections import defaultdict from django.apps import apps @@ -11,7 +10,6 @@ from django.db.models import AutoField, Manager, OrderWrt, UniqueConstraint from django.db.models.query_utils import PathInfo from django.utils.datastructures import ImmutableList, OrderedSet -from django.utils.deprecation import RemovedInDjango51Warning from django.utils.functional import cached_property from django.utils.module_loading import import_string from django.utils.text import camel_case_to_spaces, format_lazy @@ -43,7 +41,6 @@ "proxy", "swappable", "auto_created", - "index_together", # RemovedInDjango51Warning. "apps", "default_permissions", "select_on_save", @@ -119,7 +116,6 @@ def __init__(self, meta, app_label=None): self.indexes = [] self.constraints = [] self.unique_together = [] - self.index_together = [] # RemovedInDjango51Warning. self.select_on_save = False self.default_permissions = ("add", "change", "delete", "view") self.permissions = [] @@ -205,13 +201,6 @@ def contribute_to_class(self, cls, name): self.original_attrs[attr_name] = getattr(self, attr_name) self.unique_together = normalize_together(self.unique_together) - self.index_together = normalize_together(self.index_together) - if self.index_together: - warnings.warn( - f"'index_together' is deprecated. Use 'Meta.indexes' in " - f"{self.label!r} instead.", - RemovedInDjango51Warning, - ) # App label/class name interpolation for names of constraints and # indexes. if not getattr(cls._meta, "abstract", False): diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 72699ac136cf..e641c989e3b2 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -345,21 +345,23 @@ Models ```` from model ````. * **models.E007**: Field ```` has column name ```` that is used by another field. -* **models.E008**: ``index_together`` must be a list or tuple. +* **models.E008**: ``index_together`` must be a list or tuple. *This check + appeared before Django 5.1.* * **models.E009**: All ``index_together`` elements must be lists or tuples. + *This check appeared before Django 5.1.* * **models.E010**: ``unique_together`` must be a list or tuple. * **models.E011**: All ``unique_together`` elements must be lists or tuples. -* **models.E012**: ``constraints/indexes/index_together/unique_together`` - refers to the nonexistent field ````. -* **models.E013**: ``constraints/indexes/index_together/unique_together`` - refers to a ``ManyToManyField`` ````, but ``ManyToManyField``\s - are not supported for that option. +* **models.E012**: ``constraints/indexes/unique_together`` refers to the + nonexistent field ````. +* **models.E013**: ``constraints/indexes/unique_together`` refers to a + ``ManyToManyField`` ````, but ``ManyToManyField``\s are not + supported for that option. * **models.E014**: ``ordering`` must be a tuple or list (even if you want to order by only one field). * **models.E015**: ``ordering`` refers to the nonexistent field, related field, or lookup ````. -* **models.E016**: ``constraints/indexes/index_together/unique_together`` - refers to field ```` which is not local to model ````. +* **models.E016**: ``constraints/indexes/unique_together`` refers to field + ```` which is not local to model ````. * **models.E017**: Proxy model ```` contains model fields. * **models.E018**: Autogenerated column name too long for field ````. Maximum length is ```` for database ````. diff --git a/docs/ref/migration-operations.txt b/docs/ref/migration-operations.txt index 7cc3a3926fd3..e8d863085167 100644 --- a/docs/ref/migration-operations.txt +++ b/docs/ref/migration-operations.txt @@ -247,7 +247,7 @@ Removes the index named ``name`` from the model with ``model_name``. Renames an index in the database table for the model with ``model_name``. Exactly one of ``old_name`` and ``old_fields`` can be provided. ``old_fields`` is an iterable of the strings, often corresponding to fields of -:attr:`~django.db.models.Options.index_together`. +``index_together`` (pre-Django 5.1 option). On databases that don't support an index renaming statement (SQLite and MariaDB < 10.5.2), the operation will drop and recreate the index, which can be diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index 2a383b2b888f..909577be6cea 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -451,29 +451,6 @@ not be looking at your Django code. For example:: The ``ValidationError`` raised during model validation when the constraint is violated has the ``unique_together`` error code. -``index_together`` ------------------- - -.. attribute:: Options.index_together - - Sets of field names that, taken together, are indexed:: - - index_together = [ - ["pub_date", "deadline"], - ] - - This list of fields will be indexed together (i.e. the appropriate - ``CREATE INDEX`` statement will be issued.) - - For convenience, ``index_together`` can be a single list when dealing with a single - set of fields:: - - index_together = ["pub_date", "deadline"] - - .. deprecated:: 4.2 - - Use the :attr:`~Options.indexes` option instead. - ``constraints`` --------------- diff --git a/docs/ref/schema-editor.txt b/docs/ref/schema-editor.txt index 635272c982f7..b459e646bcad 100644 --- a/docs/ref/schema-editor.txt +++ b/docs/ref/schema-editor.txt @@ -114,9 +114,8 @@ the new value. .. method:: BaseDatabaseSchemaEditor.alter_index_together(model, old_index_together, new_index_together) -Changes a model's :attr:`~django.db.models.Options.index_together` value; this -will add or remove indexes from the model's table until they match the new -value. +Changes a model's ``index_together`` value; this will add or remove indexes +from the model's table until they match the new value. ``alter_db_table()`` -------------------- diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index 74db5fcf0d0a..a178257cd0ad 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -59,8 +59,8 @@ creating database indexes. Indexes are added to models using the The :class:`~django.db.models.Index` class creates a b-tree index, as if you used :attr:`~django.db.models.Field.db_index` on the model field or -:attr:`~django.db.models.Options.index_together` on the model ``Meta`` class. -It can be subclassed to support different index types, such as +``index_together`` on the model ``Meta`` class. It can be subclassed to support +different index types, such as :class:`~django.contrib.postgres.indexes.GinIndex`. It also allows defining the order (ASC/DESC) for the columns of the index. diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 09284e6b353e..76d41a9ab8a8 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -329,8 +329,7 @@ Django 1.5 also includes several smaller improvements worth noting: session data in a non-default cache. * Multi-column indexes can now be created on models. Read the - :attr:`~django.db.models.Options.index_together` documentation for more - information. + ``index_together`` documentation for more information. * During Django's logging configuration verbose Deprecation warnings are enabled and warnings are captured into the logging system. Logged warnings diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index e311e902d855..87246be6e4f1 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -778,8 +778,8 @@ Models an error (before that, it would either result in a database error or incorrect data). -* You can use a single list for :attr:`~django.db.models.Options.index_together` - (rather than a list of lists) when specifying a single set of fields. +* You can use a single list for ``index_together`` (rather than a list of + lists) when specifying a single set of fields. * Custom intermediate models having more than one foreign key to any of the models participating in a many-to-many relationship are now permitted, diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt index d93833b7f7b2..c840db4a7f57 100644 --- a/docs/releases/4.1.txt +++ b/docs/releases/4.1.txt @@ -317,7 +317,7 @@ Migrations * The new :class:`~django.db.migrations.operations.RenameIndex` operation allows renaming indexes defined in the :attr:`Meta.indexes ` or - :attr:`~django.db.models.Options.index_together` options. + ``index_together`` options. * The migrations autodetector now generates :class:`~django.db.migrations.operations.RenameIndex` operations instead of @@ -327,7 +327,7 @@ Migrations * The migrations autodetector now generates :class:`~django.db.migrations.operations.RenameIndex` operations instead of ``AlterIndexTogether`` and ``AddIndex``, when moving indexes defined in the - :attr:`Meta.index_together ` to the + ``Meta.index_together`` to the :attr:`Meta.indexes `. Models diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index 7d7848708de9..0a52f3c813f1 100644 --- a/docs/releases/4.2.txt +++ b/docs/releases/4.2.txt @@ -477,9 +477,8 @@ Features deprecated in 4.2 ``index_together`` option is deprecated in favor of ``indexes`` --------------------------------------------------------------- -The :attr:`Meta.index_together ` -option is deprecated in favor of the :attr:`~django.db.models.Options.indexes` -option. +The ``Meta.index_together`` option is deprecated in favor of the +:attr:`~django.db.models.Options.indexes` option. Migrating existing ``index_together`` should be handled as a migration. For example:: diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 4560e1fabfce..c8d6f5f3f6e8 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -252,3 +252,5 @@ See :ref:`deprecated-features-4.2` for details on these changes, including how to remove usage of these features. * The ``BaseUserManager.make_random_password()`` method is removed. + +* The model's ``Meta.index_together`` option is removed. diff --git a/tests/indexes/tests.py b/tests/indexes/tests.py index 8fdaec25f59d..107703c39a20 100644 --- a/tests/indexes/tests.py +++ b/tests/indexes/tests.py @@ -3,26 +3,16 @@ from django.conf import settings from django.db import connection -from django.db.models import ( - CASCADE, - CharField, - DateTimeField, - ForeignKey, - Index, - Model, - Q, -) +from django.db.models import CASCADE, ForeignKey, Index, Q from django.db.models.functions import Lower from django.test import ( TestCase, TransactionTestCase, - ignore_warnings, skipIfDBFeature, skipUnlessDBFeature, ) -from django.test.utils import isolate_apps, override_settings +from django.test.utils import override_settings from django.utils import timezone -from django.utils.deprecation import RemovedInDjango51Warning from .models import Article, ArticleTranslation, IndexedArticle2 @@ -80,21 +70,6 @@ def test_quoted_index_name(self): index_sql[0], ) - @ignore_warnings(category=RemovedInDjango51Warning) - @isolate_apps("indexes") - def test_index_together_single_list(self): - class IndexTogetherSingleList(Model): - headline = CharField(max_length=100) - pub_date = DateTimeField() - - class Meta: - index_together = ["headline", "pub_date"] - - index_sql = connection.schema_editor()._model_indexes_sql( - IndexTogetherSingleList - ) - self.assertEqual(len(index_sql), 1) - def test_columns_list_sql(self): index = Index(fields=["headline"], name="whitespace_idx") editor = connection.schema_editor() diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py index dc52f58c4431..f22f273c9a55 100644 --- a/tests/invalid_models_tests/test_models.py +++ b/tests/invalid_models_tests/test_models.py @@ -5,9 +5,8 @@ from django.db import connection, connections, models from django.db.models.functions import Abs, Lower, Round from django.db.models.signals import post_init -from django.test import SimpleTestCase, TestCase, ignore_warnings, skipUnlessDBFeature +from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature from django.test.utils import isolate_apps, override_settings, register_lookup -from django.utils.deprecation import RemovedInDjango51Warning class EmptyRouter: @@ -29,134 +28,6 @@ def get_max_column_name_length(): return (allowed_len, db_alias) -@isolate_apps("invalid_models_tests") -@ignore_warnings(category=RemovedInDjango51Warning) -class IndexTogetherTests(SimpleTestCase): - def test_non_iterable(self): - class Model(models.Model): - class Meta: - index_together = 42 - - self.assertEqual( - Model.check(), - [ - Error( - "'index_together' must be a list or tuple.", - obj=Model, - id="models.E008", - ), - ], - ) - - def test_non_list(self): - class Model(models.Model): - class Meta: - index_together = "not-a-list" - - self.assertEqual( - Model.check(), - [ - Error( - "'index_together' must be a list or tuple.", - obj=Model, - id="models.E008", - ), - ], - ) - - def test_list_containing_non_iterable(self): - class Model(models.Model): - class Meta: - index_together = [("a", "b"), 42] - - self.assertEqual( - Model.check(), - [ - Error( - "All 'index_together' elements must be lists or tuples.", - obj=Model, - id="models.E009", - ), - ], - ) - - def test_pointing_to_missing_field(self): - class Model(models.Model): - class Meta: - index_together = [["missing_field"]] - - self.assertEqual( - Model.check(), - [ - Error( - "'index_together' refers to the nonexistent field 'missing_field'.", - obj=Model, - id="models.E012", - ), - ], - ) - - def test_pointing_to_non_local_field(self): - class Foo(models.Model): - field1 = models.IntegerField() - - class Bar(Foo): - field2 = models.IntegerField() - - class Meta: - index_together = [["field2", "field1"]] - - self.assertEqual( - Bar.check(), - [ - Error( - "'index_together' refers to field 'field1' which is not " - "local to model 'Bar'.", - hint="This issue may be caused by multi-table inheritance.", - obj=Bar, - id="models.E016", - ), - ], - ) - - def test_pointing_to_m2m_field(self): - class Model(models.Model): - m2m = models.ManyToManyField("self") - - class Meta: - index_together = [["m2m"]] - - self.assertEqual( - Model.check(), - [ - Error( - "'index_together' refers to a ManyToManyField 'm2m', but " - "ManyToManyFields are not permitted in 'index_together'.", - obj=Model, - id="models.E013", - ), - ], - ) - - def test_pointing_to_fk(self): - class Foo(models.Model): - pass - - class Bar(models.Model): - foo_1 = models.ForeignKey( - Foo, on_delete=models.CASCADE, related_name="bar_1" - ) - foo_2 = models.ForeignKey( - Foo, on_delete=models.CASCADE, related_name="bar_2" - ) - - class Meta: - index_together = [["foo_1_id", "foo_2"]] - - self.assertEqual(Bar.check(), []) - - -# unique_together tests are very similar to index_together tests. @isolate_apps("invalid_models_tests") class UniqueTogetherTests(SimpleTestCase): def test_non_iterable(self): diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index 85674e552ade..c54349313e71 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -13,9 +13,8 @@ from django.db.migrations.loader import MigrationLoader from django.db.migrations.questioner import MigrationQuestioner from django.db.migrations.state import ModelState, ProjectState -from django.test import SimpleTestCase, TestCase, ignore_warnings, override_settings +from django.test import SimpleTestCase, TestCase, override_settings from django.test.utils import isolate_lru_cache -from django.utils.deprecation import RemovedInDjango51Warning from .models import FoodManager, FoodQuerySet @@ -4872,592 +4871,6 @@ def deconstruct(self): self.assertOperationAttributes(changes, "testapp", 0, 0, name="Book") -@ignore_warnings(category=RemovedInDjango51Warning) -class AutodetectorIndexTogetherTests(BaseAutodetectorTests): - book_index_together = ModelState( - "otherapp", - "Book", - [ - ("id", models.AutoField(primary_key=True)), - ("author", models.ForeignKey("testapp.Author", models.CASCADE)), - ("title", models.CharField(max_length=200)), - ], - { - "index_together": {("author", "title")}, - }, - ) - book_index_together_2 = ModelState( - "otherapp", - "Book", - [ - ("id", models.AutoField(primary_key=True)), - ("author", models.ForeignKey("testapp.Author", models.CASCADE)), - ("title", models.CharField(max_length=200)), - ], - { - "index_together": {("title", "author")}, - }, - ) - book_index_together_3 = ModelState( - "otherapp", - "Book", - [ - ("id", models.AutoField(primary_key=True)), - ("newfield", models.IntegerField()), - ("author", models.ForeignKey("testapp.Author", models.CASCADE)), - ("title", models.CharField(max_length=200)), - ], - { - "index_together": {("title", "newfield")}, - }, - ) - book_index_together_4 = ModelState( - "otherapp", - "Book", - [ - ("id", models.AutoField(primary_key=True)), - ("newfield2", models.IntegerField()), - ("author", models.ForeignKey("testapp.Author", models.CASCADE)), - ("title", models.CharField(max_length=200)), - ], - { - "index_together": {("title", "newfield2")}, - }, - ) - - def test_empty_index_together(self): - """Empty index_together shouldn't generate a migration.""" - # Explicitly testing for not specified, since this is the case after - # a CreateModel operation w/o any definition on the original model - model_state_not_specified = ModelState( - "a", "model", [("id", models.AutoField(primary_key=True))] - ) - # Explicitly testing for None, since this was the issue in #23452 after - # an AlterIndexTogether operation with e.g. () as value - model_state_none = ModelState( - "a", - "model", - [("id", models.AutoField(primary_key=True))], - { - "index_together": None, - }, - ) - # Explicitly testing for the empty set, since we now always have sets. - # During removal (('col1', 'col2'),) --> () this becomes set([]) - model_state_empty = ModelState( - "a", - "model", - [("id", models.AutoField(primary_key=True))], - { - "index_together": set(), - }, - ) - - def test(from_state, to_state, msg): - changes = self.get_changes([from_state], [to_state]) - if changes: - ops = ", ".join( - o.__class__.__name__ for o in changes["a"][0].operations - ) - self.fail("Created operation(s) %s from %s" % (ops, msg)) - - tests = ( - ( - model_state_not_specified, - model_state_not_specified, - '"not specified" to "not specified"', - ), - (model_state_not_specified, model_state_none, '"not specified" to "None"'), - ( - model_state_not_specified, - model_state_empty, - '"not specified" to "empty"', - ), - (model_state_none, model_state_not_specified, '"None" to "not specified"'), - (model_state_none, model_state_none, '"None" to "None"'), - (model_state_none, model_state_empty, '"None" to "empty"'), - ( - model_state_empty, - model_state_not_specified, - '"empty" to "not specified"', - ), - (model_state_empty, model_state_none, '"empty" to "None"'), - (model_state_empty, model_state_empty, '"empty" to "empty"'), - ) - - for t in tests: - test(*t) - - def test_rename_index_together_to_index(self): - changes = self.get_changes( - [AutodetectorTests.author_empty, self.book_index_together], - [AutodetectorTests.author_empty, AutodetectorTests.book_indexes], - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes(changes, "otherapp", 0, ["RenameIndex"]) - self.assertOperationAttributes( - changes, - "otherapp", - 0, - 0, - model_name="book", - new_name="book_title_author_idx", - old_fields=("author", "title"), - ) - - def test_rename_index_together_to_index_extra_options(self): - # Indexes with extra options don't match indexes in index_together. - book_partial_index = ModelState( - "otherapp", - "Book", - [ - ("id", models.AutoField(primary_key=True)), - ("author", models.ForeignKey("testapp.Author", models.CASCADE)), - ("title", models.CharField(max_length=200)), - ], - { - "indexes": [ - models.Index( - fields=["author", "title"], - condition=models.Q(title__startswith="The"), - name="book_title_author_idx", - ) - ], - }, - ) - changes = self.get_changes( - [AutodetectorTests.author_empty, self.book_index_together], - [AutodetectorTests.author_empty, book_partial_index], - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes( - changes, - "otherapp", - 0, - ["AlterIndexTogether", "AddIndex"], - ) - - def test_rename_index_together_to_index_order_fields(self): - # Indexes with reordered fields don't match indexes in index_together. - changes = self.get_changes( - [AutodetectorTests.author_empty, self.book_index_together], - [AutodetectorTests.author_empty, AutodetectorTests.book_unordered_indexes], - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes( - changes, - "otherapp", - 0, - ["AlterIndexTogether", "AddIndex"], - ) - - def test_add_index_together(self): - changes = self.get_changes( - [AutodetectorTests.author_empty, AutodetectorTests.book], - [AutodetectorTests.author_empty, self.book_index_together], - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes(changes, "otherapp", 0, ["AlterIndexTogether"]) - self.assertOperationAttributes( - changes, "otherapp", 0, 0, name="book", index_together={("author", "title")} - ) - - def test_remove_index_together(self): - changes = self.get_changes( - [AutodetectorTests.author_empty, self.book_index_together], - [AutodetectorTests.author_empty, AutodetectorTests.book], - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes(changes, "otherapp", 0, ["AlterIndexTogether"]) - self.assertOperationAttributes( - changes, "otherapp", 0, 0, name="book", index_together=set() - ) - - def test_index_together_remove_fk(self): - changes = self.get_changes( - [AutodetectorTests.author_empty, self.book_index_together], - [AutodetectorTests.author_empty, AutodetectorTests.book_with_no_author], - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes( - changes, - "otherapp", - 0, - ["AlterIndexTogether", "RemoveField"], - ) - self.assertOperationAttributes( - changes, "otherapp", 0, 0, name="book", index_together=set() - ) - self.assertOperationAttributes( - changes, "otherapp", 0, 1, model_name="book", name="author" - ) - - def test_index_together_no_changes(self): - """ - index_together doesn't generate a migration if no changes have been - made. - """ - changes = self.get_changes( - [AutodetectorTests.author_empty, self.book_index_together], - [AutodetectorTests.author_empty, self.book_index_together], - ) - self.assertEqual(len(changes), 0) - - def test_index_together_ordering(self): - """index_together triggers on ordering changes.""" - changes = self.get_changes( - [AutodetectorTests.author_empty, self.book_index_together], - [AutodetectorTests.author_empty, self.book_index_together_2], - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes( - changes, - "otherapp", - 0, - ["AlterIndexTogether"], - ) - self.assertOperationAttributes( - changes, - "otherapp", - 0, - 0, - name="book", - index_together={("title", "author")}, - ) - - def test_add_field_and_index_together(self): - """ - Added fields will be created before using them in index_together. - """ - changes = self.get_changes( - [AutodetectorTests.author_empty, AutodetectorTests.book], - [AutodetectorTests.author_empty, self.book_index_together_3], - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes( - changes, - "otherapp", - 0, - ["AddField", "AlterIndexTogether"], - ) - self.assertOperationAttributes( - changes, - "otherapp", - 0, - 1, - name="book", - index_together={("title", "newfield")}, - ) - - def test_create_model_and_index_together(self): - author = ModelState( - "otherapp", - "Author", - [ - ("id", models.AutoField(primary_key=True)), - ("name", models.CharField(max_length=200)), - ], - ) - book_with_author = ModelState( - "otherapp", - "Book", - [ - ("id", models.AutoField(primary_key=True)), - ("author", models.ForeignKey("otherapp.Author", models.CASCADE)), - ("title", models.CharField(max_length=200)), - ], - { - "index_together": {("title", "author")}, - }, - ) - changes = self.get_changes( - [AutodetectorTests.book_with_no_author], [author, book_with_author] - ) - self.assertEqual(len(changes["otherapp"]), 1) - migration = changes["otherapp"][0] - self.assertEqual(len(migration.operations), 3) - self.assertOperationTypes( - changes, - "otherapp", - 0, - ["CreateModel", "AddField", "AlterIndexTogether"], - ) - - def test_remove_field_and_index_together(self): - """ - Removed fields will be removed after updating index_together. - """ - changes = self.get_changes( - [AutodetectorTests.author_empty, self.book_index_together_3], - [AutodetectorTests.author_empty, self.book_index_together], - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes( - changes, - "otherapp", - 0, - ["AlterIndexTogether", "RemoveField"], - ) - self.assertOperationAttributes( - changes, - "otherapp", - 0, - 0, - name="book", - index_together={("author", "title")}, - ) - self.assertOperationAttributes( - changes, - "otherapp", - 0, - 1, - model_name="book", - name="newfield", - ) - - def test_alter_field_and_index_together(self): - """Fields are altered after deleting some index_together.""" - initial_author = ModelState( - "testapp", - "Author", - [ - ("id", models.AutoField(primary_key=True)), - ("name", models.CharField(max_length=200)), - ("age", models.IntegerField(db_index=True)), - ], - { - "index_together": {("name",)}, - }, - ) - author_reversed_constraints = ModelState( - "testapp", - "Author", - [ - ("id", models.AutoField(primary_key=True)), - ("name", models.CharField(max_length=200, unique=True)), - ("age", models.IntegerField()), - ], - { - "index_together": {("age",)}, - }, - ) - changes = self.get_changes([initial_author], [author_reversed_constraints]) - - self.assertNumberMigrations(changes, "testapp", 1) - self.assertOperationTypes( - changes, - "testapp", - 0, - [ - "AlterIndexTogether", - "AlterField", - "AlterField", - "AlterIndexTogether", - ], - ) - self.assertOperationAttributes( - changes, - "testapp", - 0, - 0, - name="author", - index_together=set(), - ) - self.assertOperationAttributes( - changes, - "testapp", - 0, - 1, - model_name="author", - name="age", - ) - self.assertOperationAttributes( - changes, - "testapp", - 0, - 2, - model_name="author", - name="name", - ) - self.assertOperationAttributes( - changes, - "testapp", - 0, - 3, - name="author", - index_together={("age",)}, - ) - - def test_partly_alter_index_together_increase(self): - initial_author = ModelState( - "testapp", - "Author", - [ - ("id", models.AutoField(primary_key=True)), - ("name", models.CharField(max_length=200)), - ("age", models.IntegerField()), - ], - { - "index_together": {("name",)}, - }, - ) - author_new_constraints = ModelState( - "testapp", - "Author", - [ - ("id", models.AutoField(primary_key=True)), - ("name", models.CharField(max_length=200)), - ("age", models.IntegerField()), - ], - { - "index_together": {("name",), ("age",)}, - }, - ) - changes = self.get_changes([initial_author], [author_new_constraints]) - - self.assertNumberMigrations(changes, "testapp", 1) - self.assertOperationTypes( - changes, - "testapp", - 0, - ["AlterIndexTogether"], - ) - self.assertOperationAttributes( - changes, - "testapp", - 0, - 0, - name="author", - index_together={("name",), ("age",)}, - ) - - def test_partly_alter_index_together_decrease(self): - initial_author = ModelState( - "testapp", - "Author", - [ - ("id", models.AutoField(primary_key=True)), - ("name", models.CharField(max_length=200)), - ("age", models.IntegerField()), - ], - { - "index_together": {("name",), ("age",)}, - }, - ) - author_new_constraints = ModelState( - "testapp", - "Author", - [ - ("id", models.AutoField(primary_key=True)), - ("name", models.CharField(max_length=200)), - ("age", models.IntegerField()), - ], - { - "index_together": {("age",)}, - }, - ) - changes = self.get_changes([initial_author], [author_new_constraints]) - - self.assertNumberMigrations(changes, "testapp", 1) - self.assertOperationTypes( - changes, - "testapp", - 0, - ["AlterIndexTogether"], - ) - self.assertOperationAttributes( - changes, - "testapp", - 0, - 0, - name="author", - index_together={("age",)}, - ) - - def test_rename_field_and_index_together(self): - """Fields are renamed before updating index_together.""" - changes = self.get_changes( - [AutodetectorTests.author_empty, self.book_index_together_3], - [AutodetectorTests.author_empty, self.book_index_together_4], - MigrationQuestioner({"ask_rename": True}), - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes( - changes, - "otherapp", - 0, - ["RenameField", "AlterIndexTogether"], - ) - self.assertOperationAttributes( - changes, - "otherapp", - 0, - 1, - name="book", - index_together={("title", "newfield2")}, - ) - - def test_add_model_order_with_respect_to_index_together(self): - changes = self.get_changes( - [], - [ - AutodetectorTests.book, - ModelState( - "testapp", - "Author", - [ - ("id", models.AutoField(primary_key=True)), - ("name", models.CharField(max_length=200)), - ("book", models.ForeignKey("otherapp.Book", models.CASCADE)), - ], - options={ - "order_with_respect_to": "book", - "index_together": {("name", "_order")}, - }, - ), - ], - ) - self.assertNumberMigrations(changes, "testapp", 1) - self.assertOperationTypes(changes, "testapp", 0, ["CreateModel"]) - self.assertOperationAttributes( - changes, - "testapp", - 0, - 0, - name="Author", - options={ - "order_with_respect_to": "book", - "index_together": {("name", "_order")}, - }, - ) - - def test_set_alter_order_with_respect_to_index_together(self): - after = ModelState( - "testapp", - "Author", - [ - ("id", models.AutoField(primary_key=True)), - ("name", models.CharField(max_length=200)), - ("book", models.ForeignKey("otherapp.Book", models.CASCADE)), - ], - options={ - "order_with_respect_to": "book", - "index_together": {("name", "_order")}, - }, - ) - changes = self.get_changes( - [AutodetectorTests.book, AutodetectorTests.author_with_book], - [AutodetectorTests.book, after], - ) - self.assertNumberMigrations(changes, "testapp", 1) - self.assertOperationTypes( - changes, - "testapp", - 0, - ["AlterOrderWithRespectTo", "AlterIndexTogether"], - ) - - class MigrationSuggestNameTests(SimpleTestCase): def test_no_operations(self): class Migration(migrations.Migration): diff --git a/tests/migrations/test_base.py b/tests/migrations/test_base.py index b5228ad44538..0ff1dda1d92c 100644 --- a/tests/migrations/test_base.py +++ b/tests/migrations/test_base.py @@ -269,7 +269,6 @@ def set_up_test_model( unique_together=False, options=False, db_table=None, - index_together=False, # RemovedInDjango51Warning. constraints=None, indexes=None, ): @@ -277,8 +276,6 @@ def set_up_test_model( # Make the "current" state. model_options = { "swappable": "TEST_SWAP_MODEL", - # RemovedInDjango51Warning. - "index_together": [["weight", "pink"]] if index_together else [], "unique_together": [["pink", "weight"]] if unique_together else [], } if options: diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 01197d5fc82d..c77197cc5604 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -11,13 +11,11 @@ from django.db.transaction import atomic from django.test import ( SimpleTestCase, - ignore_warnings, override_settings, skipIfDBFeature, skipUnlessDBFeature, ) from django.test.utils import CaptureQueriesContext -from django.utils.deprecation import RemovedInDjango51Warning from .models import FoodManager, FoodQuerySet, UnicodeModel from .test_base import OperationTestBase @@ -3094,37 +3092,6 @@ def test_rename_field_unique_together(self): self.assertColumnExists("test_rnflut_pony", "pink") self.assertColumnNotExists("test_rnflut_pony", "blue") - @ignore_warnings(category=RemovedInDjango51Warning) - def test_rename_field_index_together(self): - project_state = self.set_up_test_model("test_rnflit", index_together=True) - operation = migrations.RenameField("Pony", "pink", "blue") - new_state = project_state.clone() - operation.state_forwards("test_rnflit", new_state) - self.assertIn("blue", new_state.models["test_rnflit", "pony"].fields) - self.assertNotIn("pink", new_state.models["test_rnflit", "pony"].fields) - # index_together has the renamed column. - self.assertIn( - "blue", new_state.models["test_rnflit", "pony"].options["index_together"][0] - ) - self.assertNotIn( - "pink", new_state.models["test_rnflit", "pony"].options["index_together"][0] - ) - # Rename field. - self.assertColumnExists("test_rnflit_pony", "pink") - self.assertColumnNotExists("test_rnflit_pony", "blue") - with connection.schema_editor() as editor: - operation.database_forwards("test_rnflit", editor, project_state, new_state) - self.assertColumnExists("test_rnflit_pony", "blue") - self.assertColumnNotExists("test_rnflit_pony", "pink") - # The index constraint has been ported over. - self.assertIndexExists("test_rnflit_pony", ["weight", "blue"]) - # Reversal. - with connection.schema_editor() as editor: - operation.database_backwards( - "test_rnflit", editor, new_state, project_state - ) - self.assertIndexExists("test_rnflit_pony", ["weight", "pink"]) - def test_rename_field_with_db_column(self): project_state = self.apply_operations( "test_rfwdbc", @@ -3601,52 +3568,6 @@ def test_rename_index_arguments(self): with self.assertRaisesMessage(ValueError, msg): migrations.RenameIndex("Pony", new_name="new_idx_name") - @ignore_warnings(category=RemovedInDjango51Warning) - def test_rename_index_unnamed_index(self): - app_label = "test_rninui" - project_state = self.set_up_test_model(app_label, index_together=True) - table_name = app_label + "_pony" - self.assertIndexNameNotExists(table_name, "new_pony_test_idx") - operation = migrations.RenameIndex( - "Pony", new_name="new_pony_test_idx", old_fields=("weight", "pink") - ) - self.assertEqual( - operation.describe(), - "Rename unnamed index for ('weight', 'pink') on Pony to new_pony_test_idx", - ) - self.assertEqual( - operation.migration_name_fragment, - "rename_pony_weight_pink_new_pony_test_idx", - ) - - new_state = project_state.clone() - operation.state_forwards(app_label, new_state) - # Rename index. - with connection.schema_editor() as editor: - operation.database_forwards(app_label, editor, project_state, new_state) - self.assertIndexNameExists(table_name, "new_pony_test_idx") - # Reverse is a no-op. - with connection.schema_editor() as editor, self.assertNumQueries(0): - operation.database_backwards(app_label, editor, new_state, project_state) - self.assertIndexNameExists(table_name, "new_pony_test_idx") - # Reapply, RenameIndex operation is a noop when the old and new name - # match. - with connection.schema_editor() as editor: - operation.database_forwards(app_label, editor, new_state, project_state) - self.assertIndexNameExists(table_name, "new_pony_test_idx") - # Deconstruction. - definition = operation.deconstruct() - self.assertEqual(definition[0], "RenameIndex") - self.assertEqual(definition[1], []) - self.assertEqual( - definition[2], - { - "model_name": "Pony", - "new_name": "new_pony_test_idx", - "old_fields": ("weight", "pink"), - }, - ) - def test_rename_index_unknown_unnamed_index(self): app_label = "test_rninuui" project_state = self.set_up_test_model(app_label) @@ -3697,22 +3618,6 @@ def test_rename_index_state_forwards(self): self.assertIsNot(old_model, new_model) self.assertEqual(new_model._meta.indexes[0].name, "new_pony_pink_idx") - @ignore_warnings(category=RemovedInDjango51Warning) - def test_rename_index_state_forwards_unnamed_index(self): - app_label = "test_rnidsfui" - project_state = self.set_up_test_model(app_label, index_together=True) - old_model = project_state.apps.get_model(app_label, "Pony") - new_state = project_state.clone() - - operation = migrations.RenameIndex( - "Pony", new_name="new_pony_pink_idx", old_fields=("weight", "pink") - ) - operation.state_forwards(app_label, new_state) - new_model = new_state.apps.get_model(app_label, "Pony") - self.assertIsNot(old_model, new_model) - self.assertEqual(new_model._meta.index_together, tuple()) - self.assertEqual(new_model._meta.indexes[0].name, "new_pony_pink_idx") - @skipUnlessDBFeature("supports_expression_indexes") def test_add_func_index(self): app_label = "test_addfuncin" @@ -3832,89 +3737,12 @@ def test_alter_field_with_index(self): # Ensure the index is still there self.assertIndexExists("test_alflin_pony", ["pink"]) - @ignore_warnings(category=RemovedInDjango51Warning) - def test_alter_index_together(self): - """ - Tests the AlterIndexTogether operation. - """ - project_state = self.set_up_test_model("test_alinto") - # Test the state alteration - operation = migrations.AlterIndexTogether("Pony", [("pink", "weight")]) - self.assertEqual( - operation.describe(), "Alter index_together for Pony (1 constraint(s))" - ) - self.assertEqual( - operation.migration_name_fragment, - "alter_pony_index_together", - ) - new_state = project_state.clone() - operation.state_forwards("test_alinto", new_state) - self.assertEqual( - len( - project_state.models["test_alinto", "pony"].options.get( - "index_together", set() - ) - ), - 0, - ) - self.assertEqual( - len( - new_state.models["test_alinto", "pony"].options.get( - "index_together", set() - ) - ), - 1, - ) - # Make sure there's no matching index - self.assertIndexNotExists("test_alinto_pony", ["pink", "weight"]) - # Test the database alteration - with connection.schema_editor() as editor: - operation.database_forwards("test_alinto", editor, project_state, new_state) - self.assertIndexExists("test_alinto_pony", ["pink", "weight"]) - # And test reversal - with connection.schema_editor() as editor: - operation.database_backwards( - "test_alinto", editor, new_state, project_state - ) - self.assertIndexNotExists("test_alinto_pony", ["pink", "weight"]) - # And deconstruction - definition = operation.deconstruct() - self.assertEqual(definition[0], "AlterIndexTogether") - self.assertEqual(definition[1], []) - self.assertEqual( - definition[2], {"name": "Pony", "index_together": {("pink", "weight")}} - ) - def test_alter_index_together_remove(self): operation = migrations.AlterIndexTogether("Pony", None) self.assertEqual( operation.describe(), "Alter index_together for Pony (0 constraint(s))" ) - @skipUnlessDBFeature("allows_multiple_constraints_on_same_fields") - @ignore_warnings(category=RemovedInDjango51Warning) - def test_alter_index_together_remove_with_unique_together(self): - app_label = "test_alintoremove_wunto" - table_name = "%s_pony" % app_label - project_state = self.set_up_test_model(app_label, unique_together=True) - self.assertUniqueConstraintExists(table_name, ["pink", "weight"]) - # Add index together. - new_state = project_state.clone() - operation = migrations.AlterIndexTogether("Pony", [("pink", "weight")]) - operation.state_forwards(app_label, new_state) - with connection.schema_editor() as editor: - operation.database_forwards(app_label, editor, project_state, new_state) - self.assertIndexExists(table_name, ["pink", "weight"]) - # Remove index together. - project_state = new_state - new_state = project_state.clone() - operation = migrations.AlterIndexTogether("Pony", set()) - operation.state_forwards(app_label, new_state) - with connection.schema_editor() as editor: - operation.database_forwards(app_label, editor, project_state, new_state) - self.assertIndexNotExists(table_name, ["pink", "weight"]) - self.assertUniqueConstraintExists(table_name, ["pink", "weight"]) - def test_add_constraint(self): project_state = self.set_up_test_model("test_addconstraint") gt_check = models.Q(pink__gt=2) diff --git a/tests/migrations/test_optimizer.py b/tests/migrations/test_optimizer.py index ec5f6b4ce98b..ece6580ad8b3 100644 --- a/tests/migrations/test_optimizer.py +++ b/tests/migrations/test_optimizer.py @@ -1293,81 +1293,6 @@ def test_create_model_remove_index(self): ], ) - def test_create_model_remove_index_together_rename_index(self): - self.assertOptimizesTo( - [ - migrations.CreateModel( - name="Pony", - fields=[ - ("weight", models.IntegerField()), - ("age", models.IntegerField()), - ], - options={ - "index_together": [("age", "weight")], - }, - ), - migrations.RenameIndex( - "Pony", new_name="idx_pony_age_weight", old_fields=("age", "weight") - ), - ], - [ - migrations.CreateModel( - name="Pony", - fields=[ - ("weight", models.IntegerField()), - ("age", models.IntegerField()), - ], - options={ - "indexes": [ - models.Index( - fields=["age", "weight"], name="idx_pony_age_weight" - ), - ], - }, - ), - ], - ) - - def test_create_model_index_together_rename_index(self): - self.assertOptimizesTo( - [ - migrations.CreateModel( - name="Pony", - fields=[ - ("weight", models.IntegerField()), - ("age", models.IntegerField()), - ("height", models.IntegerField()), - ("rank", models.IntegerField()), - ], - options={ - "index_together": [("age", "weight"), ("height", "rank")], - }, - ), - migrations.RenameIndex( - "Pony", new_name="idx_pony_age_weight", old_fields=("age", "weight") - ), - ], - [ - migrations.CreateModel( - name="Pony", - fields=[ - ("weight", models.IntegerField()), - ("age", models.IntegerField()), - ("height", models.IntegerField()), - ("rank", models.IntegerField()), - ], - options={ - "index_together": {("height", "rank")}, - "indexes": [ - models.Index( - fields=["age", "weight"], name="idx_pony_age_weight" - ), - ], - }, - ), - ], - ) - def test_create_model_rename_index_no_old_fields(self): self.assertOptimizesTo( [ diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py index 55de9b3a6722..f1ac78c9926e 100644 --- a/tests/migrations/test_state.py +++ b/tests/migrations/test_state.py @@ -13,9 +13,8 @@ ProjectState, get_related_models_recursive, ) -from django.test import SimpleTestCase, ignore_warnings, override_settings +from django.test import SimpleTestCase, override_settings from django.test.utils import isolate_apps -from django.utils.deprecation import RemovedInDjango51Warning from .models import ( FoodManager, @@ -31,9 +30,6 @@ class StateTests(SimpleTestCase): Tests state construction, rendering and modification by operations. """ - # RemovedInDjango51Warning, when deprecation ends, only remove - # Meta.index_together from inline models. - @ignore_warnings(category=RemovedInDjango51Warning) def test_create(self): """ Tests making a ProjectState from an Apps @@ -50,7 +46,6 @@ class Meta: app_label = "migrations" apps = new_apps unique_together = ["name", "bio"] - index_together = ["bio", "age"] # RemovedInDjango51Warning. class AuthorProxy(Author): class Meta: @@ -142,7 +137,6 @@ class Meta: author_state.options, { "unique_together": {("name", "bio")}, - "index_together": {("bio", "age")}, # RemovedInDjango51Warning. "indexes": [], "constraints": [], }, diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 9f620331bcf0..e20b875a8ecf 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -54,14 +54,8 @@ from django.db.models.functions import Abs, Cast, Collate, Lower, Random, Upper from django.db.models.indexes import IndexExpression from django.db.transaction import TransactionManagementError, atomic -from django.test import ( - TransactionTestCase, - ignore_warnings, - skipIfDBFeature, - skipUnlessDBFeature, -) +from django.test import TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature from django.test.utils import CaptureQueriesContext, isolate_apps, register_lookup -from django.utils.deprecation import RemovedInDjango51Warning from .fields import CustomManyToManyField, InheritedManyToManyField, MediumBlobField from .models import ( @@ -3351,7 +3345,6 @@ def test_unique_constraint_nulls_distinct_unsupported(self): self.assertIsNone(editor.add_constraint(Author, constraint)) self.assertIsNone(editor.remove_constraint(Author, constraint)) - @ignore_warnings(category=RemovedInDjango51Warning) def test_index_together(self): """ Tests removing and adding index_together constraints on a model. @@ -3395,128 +3388,6 @@ def test_index_together(self): False, ) - @ignore_warnings(category=RemovedInDjango51Warning) - def test_index_together_with_fk(self): - """ - Tests removing and adding index_together constraints that include - a foreign key. - """ - # Create the table - with connection.schema_editor() as editor: - editor.create_model(Author) - editor.create_model(Book) - # Ensure the fields are unique to begin with - self.assertEqual(Book._meta.index_together, ()) - # Add the unique_together constraint - with connection.schema_editor() as editor: - editor.alter_index_together(Book, [], [["author", "title"]]) - # Alter it back - with connection.schema_editor() as editor: - editor.alter_index_together(Book, [["author", "title"]], []) - - @ignore_warnings(category=RemovedInDjango51Warning) - @isolate_apps("schema") - def test_create_index_together(self): - """ - Tests creating models with index_together already defined - """ - - class TagIndexed(Model): - title = CharField(max_length=255) - slug = SlugField(unique=True) - - class Meta: - app_label = "schema" - index_together = [["slug", "title"]] - - # Create the table - with connection.schema_editor() as editor: - editor.create_model(TagIndexed) - self.isolated_local_models = [TagIndexed] - # Ensure there is an index - self.assertIs( - any( - c["index"] - for c in self.get_constraints("schema_tagindexed").values() - if c["columns"] == ["slug", "title"] - ), - True, - ) - - @skipUnlessDBFeature("allows_multiple_constraints_on_same_fields") - @ignore_warnings(category=RemovedInDjango51Warning) - @isolate_apps("schema") - def test_remove_index_together_does_not_remove_meta_indexes(self): - class AuthorWithIndexedNameAndBirthday(Model): - name = CharField(max_length=255) - birthday = DateField() - - class Meta: - app_label = "schema" - index_together = [["name", "birthday"]] - - with connection.schema_editor() as editor: - editor.create_model(AuthorWithIndexedNameAndBirthday) - self.isolated_local_models = [AuthorWithIndexedNameAndBirthday] - # Add the custom index - index = Index(fields=["name", "birthday"], name="author_name_birthday_idx") - custom_index_name = index.name - AuthorWithIndexedNameAndBirthday._meta.indexes = [index] - with connection.schema_editor() as editor: - editor.add_index(AuthorWithIndexedNameAndBirthday, index) - # Ensure the indexes exist - constraints = self.get_constraints( - AuthorWithIndexedNameAndBirthday._meta.db_table - ) - self.assertIn(custom_index_name, constraints) - other_constraints = [ - name - for name, details in constraints.items() - if details["columns"] == ["name", "birthday"] - and details["index"] - and name != custom_index_name - ] - self.assertEqual(len(other_constraints), 1) - # Remove index together - index_together = AuthorWithIndexedNameAndBirthday._meta.index_together - with connection.schema_editor() as editor: - editor.alter_index_together( - AuthorWithIndexedNameAndBirthday, index_together, [] - ) - constraints = self.get_constraints( - AuthorWithIndexedNameAndBirthday._meta.db_table - ) - self.assertIn(custom_index_name, constraints) - other_constraints = [ - name - for name, details in constraints.items() - if details["columns"] == ["name", "birthday"] - and details["index"] - and name != custom_index_name - ] - self.assertEqual(len(other_constraints), 0) - # Re-add index together - with connection.schema_editor() as editor: - editor.alter_index_together( - AuthorWithIndexedNameAndBirthday, [], index_together - ) - constraints = self.get_constraints( - AuthorWithIndexedNameAndBirthday._meta.db_table - ) - self.assertIn(custom_index_name, constraints) - other_constraints = [ - name - for name, details in constraints.items() - if details["columns"] == ["name", "birthday"] - and details["index"] - and name != custom_index_name - ] - self.assertEqual(len(other_constraints), 1) - # Drop the index - with connection.schema_editor() as editor: - AuthorWithIndexedNameAndBirthday._meta.indexes = [] - editor.remove_index(AuthorWithIndexedNameAndBirthday, index) - @isolate_apps("schema") def test_db_table(self): """ From 14ef92fa9e87a77cd3642235387e6f683048046b Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Tue, 12 Sep 2023 21:34:40 +0200 Subject: [PATCH 006/748] Refs #33864 -- Removed length_is template filter per deprecation timeline. --- django/template/defaultfilters.py | 16 --- docs/ref/templates/builtins.txt | 18 --- docs/releases/5.1.txt | 2 + .../filter_tests/test_length_is.py | 130 ------------------ 4 files changed, 2 insertions(+), 164 deletions(-) delete mode 100644 tests/template_tests/filter_tests/test_length_is.py diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 589ca3841423..1e1bdbc5c90a 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -2,7 +2,6 @@ import random as random_module import re import types -import warnings from decimal import ROUND_HALF_UP, Context, Decimal, InvalidOperation, getcontext from functools import wraps from inspect import unwrap @@ -12,7 +11,6 @@ from django.utils import formats from django.utils.dateformat import format, time_format -from django.utils.deprecation import RemovedInDjango51Warning from django.utils.encoding import iri_to_uri from django.utils.html import avoid_wrapping, conditional_escape, escape, escapejs from django.utils.html import json_script as _json_script @@ -622,20 +620,6 @@ def length(value): return 0 -@register.filter(is_safe=False) -def length_is(value, arg): - """Return a boolean of whether the value's length is the argument.""" - warnings.warn( - "The length_is template filter is deprecated in favor of the length template " - "filter and the == operator within an {% if %} tag.", - RemovedInDjango51Warning, - ) - try: - return len(value) == int(arg) - except (ValueError, TypeError): - return "" - - @register.filter(is_safe=True) def random(value): """Return a random item from the list.""" diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 4798663bf037..e0e06dafe89d 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -2161,24 +2161,6 @@ If ``value`` is ``['a', 'b', 'c', 'd']`` or ``"abcd"``, the output will be The filter returns ``0`` for an undefined variable. -.. templatefilter:: length_is - -``length_is`` -------------- - -.. deprecated:: 4.2 - -Returns ``True`` if the value's length is the argument, or ``False`` otherwise. - -For example: - -.. code-block:: html+django - - {{ value|length_is:"4" }} - -If ``value`` is ``['a', 'b', 'c', 'd']`` or ``"abcd"``, the output will be -``True``. - .. templatefilter:: linebreaks ``linebreaks`` diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index c8d6f5f3f6e8..c6c4e2217d05 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -254,3 +254,5 @@ to remove usage of these features. * The ``BaseUserManager.make_random_password()`` method is removed. * The model's ``Meta.index_together`` option is removed. + +* The ``length_is`` template filter is removed. diff --git a/tests/template_tests/filter_tests/test_length_is.py b/tests/template_tests/filter_tests/test_length_is.py deleted file mode 100644 index 5f24b2ab592c..000000000000 --- a/tests/template_tests/filter_tests/test_length_is.py +++ /dev/null @@ -1,130 +0,0 @@ -from django.template.defaultfilters import length_is -from django.test import SimpleTestCase, ignore_warnings -from django.utils.deprecation import RemovedInDjango51Warning - -from ..utils import setup - - -@ignore_warnings(category=RemovedInDjango51Warning) -class LengthIsTests(SimpleTestCase): - @setup({"length_is01": '{% if some_list|length_is:"4" %}Four{% endif %}'}) - def test_length_is01(self): - output = self.engine.render_to_string( - "length_is01", {"some_list": ["4", None, True, {}]} - ) - self.assertEqual(output, "Four") - - @setup( - { - "length_is02": ( - '{% if some_list|length_is:"4" %}Four{% else %}Not Four{% endif %}' - ) - } - ) - def test_length_is02(self): - output = self.engine.render_to_string( - "length_is02", {"some_list": ["4", None, True, {}, 17]} - ) - self.assertEqual(output, "Not Four") - - @setup({"length_is03": '{% if mystring|length_is:"4" %}Four{% endif %}'}) - def test_length_is03(self): - output = self.engine.render_to_string("length_is03", {"mystring": "word"}) - self.assertEqual(output, "Four") - - @setup( - { - "length_is04": ( - '{% if mystring|length_is:"4" %}Four{% else %}Not Four{% endif %}' - ) - } - ) - def test_length_is04(self): - output = self.engine.render_to_string("length_is04", {"mystring": "Python"}) - self.assertEqual(output, "Not Four") - - @setup( - { - "length_is05": ( - '{% if mystring|length_is:"4" %}Four{% else %}Not Four{% endif %}' - ) - } - ) - def test_length_is05(self): - output = self.engine.render_to_string("length_is05", {"mystring": ""}) - self.assertEqual(output, "Not Four") - - @setup( - { - "length_is06": ( - "{% with var|length as my_length %}{{ my_length }}{% endwith %}" - ) - } - ) - def test_length_is06(self): - output = self.engine.render_to_string("length_is06", {"var": "django"}) - self.assertEqual(output, "6") - - # Boolean return value from length_is should not be coerced to a string - @setup( - { - "length_is07": ( - '{% if "X"|length_is:0 %}Length is 0{% else %}Length not 0{% endif %}' - ) - } - ) - def test_length_is07(self): - output = self.engine.render_to_string("length_is07", {}) - self.assertEqual(output, "Length not 0") - - @setup( - { - "length_is08": ( - '{% if "X"|length_is:1 %}Length is 1{% else %}Length not 1{% endif %}' - ) - } - ) - def test_length_is08(self): - output = self.engine.render_to_string("length_is08", {}) - self.assertEqual(output, "Length is 1") - - # Invalid uses that should fail silently. - @setup({"length_is09": '{{ var|length_is:"fish" }}'}) - def test_length_is09(self): - output = self.engine.render_to_string("length_is09", {"var": "django"}) - self.assertEqual(output, "") - - @setup({"length_is10": '{{ int|length_is:"1" }}'}) - def test_length_is10(self): - output = self.engine.render_to_string("length_is10", {"int": 7}) - self.assertEqual(output, "") - - @setup({"length_is11": '{{ none|length_is:"1" }}'}) - def test_length_is11(self): - output = self.engine.render_to_string("length_is11", {"none": None}) - self.assertEqual(output, "") - - -@ignore_warnings(category=RemovedInDjango51Warning) -class FunctionTests(SimpleTestCase): - def test_empty_list(self): - self.assertIs(length_is([], 0), True) - self.assertIs(length_is([], 1), False) - - def test_string(self): - self.assertIs(length_is("a", 1), True) - self.assertIs(length_is("a", 10), False) - - -class DeprecationTests(SimpleTestCase): - @setup( - {"length_is_warning": "{{ string|length_is:3 }}"}, - test_once=True, - ) - def test_length_is_warning(self): - msg = ( - "The length_is template filter is deprecated in favor of the length " - "template filter and the == operator within an {% if %} tag." - ) - with self.assertRaisesMessage(RemovedInDjango51Warning, msg): - self.engine.render_to_string("length_is_warning", {"string": "good"}) From 6e4e5523a8f40b63f3e74889266a7d638f6364dc Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Tue, 12 Sep 2023 21:44:53 +0200 Subject: [PATCH 007/748] Refs #33691 -- Removed insecure password hashers per deprecation timeline. --- django/contrib/auth/hashers.py | 160 ------------------------------- docs/releases/5.1.txt | 4 + tests/auth_tests/test_hashers.py | 116 +--------------------- 3 files changed, 5 insertions(+), 275 deletions(-) diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index b63904cd75dc..95b6e000bc4b 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -16,7 +16,6 @@ get_random_string, pbkdf2, ) -from django.utils.deprecation import RemovedInDjango51Warning from django.utils.module_loading import import_string from django.utils.translation import gettext_noop as _ @@ -641,57 +640,6 @@ def harden_runtime(self, password, encoded): pass -# RemovedInDjango51Warning. -class SHA1PasswordHasher(BasePasswordHasher): - """ - The SHA1 password hashing algorithm (not recommended) - """ - - algorithm = "sha1" - - def __init__(self, *args, **kwargs): - warnings.warn( - "django.contrib.auth.hashers.SHA1PasswordHasher is deprecated.", - RemovedInDjango51Warning, - stacklevel=2, - ) - super().__init__(*args, **kwargs) - - def encode(self, password, salt): - self._check_encode_args(password, salt) - hash = hashlib.sha1((salt + password).encode()).hexdigest() - return "%s$%s$%s" % (self.algorithm, salt, hash) - - def decode(self, encoded): - algorithm, salt, hash = encoded.split("$", 2) - assert algorithm == self.algorithm - return { - "algorithm": algorithm, - "hash": hash, - "salt": salt, - } - - def verify(self, password, encoded): - decoded = self.decode(encoded) - encoded_2 = self.encode(password, decoded["salt"]) - return constant_time_compare(encoded, encoded_2) - - def safe_summary(self, encoded): - decoded = self.decode(encoded) - return { - _("algorithm"): decoded["algorithm"], - _("salt"): mask_hash(decoded["salt"], show=2), - _("hash"): mask_hash(decoded["hash"]), - } - - def must_update(self, encoded): - decoded = self.decode(encoded) - return must_update_salt(decoded["salt"], self.salt_entropy) - - def harden_runtime(self, password, encoded): - pass - - class MD5PasswordHasher(BasePasswordHasher): """ The Salted MD5 password hashing algorithm (not recommended) @@ -732,111 +680,3 @@ def must_update(self, encoded): def harden_runtime(self, password, encoded): pass - - -# RemovedInDjango51Warning. -class UnsaltedSHA1PasswordHasher(BasePasswordHasher): - """ - Very insecure algorithm that you should *never* use; store SHA1 hashes - with an empty salt. - - This class is implemented because Django used to accept such password - hashes. Some older Django installs still have these values lingering - around so we need to handle and upgrade them properly. - """ - - algorithm = "unsalted_sha1" - - def __init__(self, *args, **kwargs): - warnings.warn( - "django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher is deprecated.", - RemovedInDjango51Warning, - stacklevel=2, - ) - super().__init__(*args, **kwargs) - - def salt(self): - return "" - - def encode(self, password, salt): - if salt != "": - raise ValueError("salt must be empty.") - hash = hashlib.sha1(password.encode()).hexdigest() - return "sha1$$%s" % hash - - def decode(self, encoded): - assert encoded.startswith("sha1$$") - return { - "algorithm": self.algorithm, - "hash": encoded[6:], - "salt": None, - } - - def verify(self, password, encoded): - encoded_2 = self.encode(password, "") - return constant_time_compare(encoded, encoded_2) - - def safe_summary(self, encoded): - decoded = self.decode(encoded) - return { - _("algorithm"): decoded["algorithm"], - _("hash"): mask_hash(decoded["hash"]), - } - - def harden_runtime(self, password, encoded): - pass - - -# RemovedInDjango51Warning. -class UnsaltedMD5PasswordHasher(BasePasswordHasher): - """ - Incredibly insecure algorithm that you should *never* use; stores unsalted - MD5 hashes without the algorithm prefix, also accepts MD5 hashes with an - empty salt. - - This class is implemented because Django used to store passwords this way - and to accept such password hashes. Some older Django installs still have - these values lingering around so we need to handle and upgrade them - properly. - """ - - algorithm = "unsalted_md5" - - def __init__(self, *args, **kwargs): - warnings.warn( - "django.contrib.auth.hashers.UnsaltedMD5PasswordHasher is deprecated.", - RemovedInDjango51Warning, - stacklevel=2, - ) - super().__init__(*args, **kwargs) - - def salt(self): - return "" - - def encode(self, password, salt): - if salt != "": - raise ValueError("salt must be empty.") - return hashlib.md5(password.encode()).hexdigest() - - def decode(self, encoded): - return { - "algorithm": self.algorithm, - "hash": encoded, - "salt": None, - } - - def verify(self, password, encoded): - if len(encoded) == 37: - encoded = encoded.removeprefix("md5$$") - encoded_2 = self.encode(password, "") - return constant_time_compare(encoded, encoded_2) - - def safe_summary(self, encoded): - decoded = self.decode(encoded) - return { - _("algorithm"): decoded["algorithm"], - _("hash"): mask_hash(decoded["hash"], show=3), - } - - def harden_runtime(self, password, encoded): - pass diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index c6c4e2217d05..9a0cd94f7ee5 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -256,3 +256,7 @@ to remove usage of these features. * The model's ``Meta.index_together`` option is removed. * The ``length_is`` template filter is removed. + +* The ``django.contrib.auth.hashers.SHA1PasswordHasher``, + ``django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher``, and + ``django.contrib.auth.hashers.UnsaltedMD5PasswordHasher`` are removed. diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py index e10992b25c66..643f60ffc78e 100644 --- a/tests/auth_tests/test_hashers.py +++ b/tests/auth_tests/test_hashers.py @@ -18,9 +18,8 @@ is_password_usable, make_password, ) -from django.test import SimpleTestCase, ignore_warnings +from django.test import SimpleTestCase from django.test.utils import override_settings -from django.utils.deprecation import RemovedInDjango51Warning try: import bcrypt @@ -103,40 +102,6 @@ def test_pbkdf2(self): self.assertIs(hasher.must_update(encoded_weak_salt), True) self.assertIs(hasher.must_update(encoded_strong_salt), False) - @ignore_warnings(category=RemovedInDjango51Warning) - @override_settings( - PASSWORD_HASHERS=["django.contrib.auth.hashers.SHA1PasswordHasher"] - ) - def test_sha1(self): - encoded = make_password("lètmein", "seasalt", "sha1") - self.assertEqual( - encoded, "sha1$seasalt$cff36ea83f5706ce9aa7454e63e431fc726b2dc8" - ) - self.assertTrue(is_password_usable(encoded)) - self.assertTrue(check_password("lètmein", encoded)) - self.assertFalse(check_password("lètmeinz", encoded)) - self.assertEqual(identify_hasher(encoded).algorithm, "sha1") - # Blank passwords - blank_encoded = make_password("", "seasalt", "sha1") - self.assertTrue(blank_encoded.startswith("sha1$")) - self.assertTrue(is_password_usable(blank_encoded)) - self.assertTrue(check_password("", blank_encoded)) - self.assertFalse(check_password(" ", blank_encoded)) - # Salt entropy check. - hasher = get_hasher("sha1") - encoded_weak_salt = make_password("lètmein", "iodizedsalt", "sha1") - encoded_strong_salt = make_password("lètmein", hasher.salt(), "sha1") - self.assertIs(hasher.must_update(encoded_weak_salt), True) - self.assertIs(hasher.must_update(encoded_strong_salt), False) - - @override_settings( - PASSWORD_HASHERS=["django.contrib.auth.hashers.SHA1PasswordHasher"] - ) - def test_sha1_deprecation_warning(self): - msg = "django.contrib.auth.hashers.SHA1PasswordHasher is deprecated." - with self.assertRaisesMessage(RemovedInDjango51Warning, msg): - get_hasher("sha1") - @override_settings( PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"] ) @@ -160,85 +125,6 @@ def test_md5(self): self.assertIs(hasher.must_update(encoded_weak_salt), True) self.assertIs(hasher.must_update(encoded_strong_salt), False) - @ignore_warnings(category=RemovedInDjango51Warning) - @override_settings( - PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"] - ) - def test_unsalted_md5(self): - encoded = make_password("lètmein", "", "unsalted_md5") - self.assertEqual(encoded, "88a434c88cca4e900f7874cd98123f43") - self.assertTrue(is_password_usable(encoded)) - self.assertTrue(check_password("lètmein", encoded)) - self.assertFalse(check_password("lètmeinz", encoded)) - self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_md5") - # Alternate unsalted syntax - alt_encoded = "md5$$%s" % encoded - self.assertTrue(is_password_usable(alt_encoded)) - self.assertTrue(check_password("lètmein", alt_encoded)) - self.assertFalse(check_password("lètmeinz", alt_encoded)) - # Blank passwords - blank_encoded = make_password("", "", "unsalted_md5") - self.assertTrue(is_password_usable(blank_encoded)) - self.assertTrue(check_password("", blank_encoded)) - self.assertFalse(check_password(" ", blank_encoded)) - - @ignore_warnings(category=RemovedInDjango51Warning) - @override_settings( - PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"] - ) - def test_unsalted_md5_encode_invalid_salt(self): - hasher = get_hasher("unsalted_md5") - msg = "salt must be empty." - with self.assertRaisesMessage(ValueError, msg): - hasher.encode("password", salt="salt") - - @override_settings( - PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"] - ) - def test_unsalted_md5_deprecation_warning(self): - msg = "django.contrib.auth.hashers.UnsaltedMD5PasswordHasher is deprecated." - with self.assertRaisesMessage(RemovedInDjango51Warning, msg): - get_hasher("unsalted_md5") - - @ignore_warnings(category=RemovedInDjango51Warning) - @override_settings( - PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"] - ) - def test_unsalted_sha1(self): - encoded = make_password("lètmein", "", "unsalted_sha1") - self.assertEqual(encoded, "sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b") - self.assertTrue(is_password_usable(encoded)) - self.assertTrue(check_password("lètmein", encoded)) - self.assertFalse(check_password("lètmeinz", encoded)) - self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_sha1") - # Raw SHA1 isn't acceptable - alt_encoded = encoded[6:] - self.assertFalse(check_password("lètmein", alt_encoded)) - # Blank passwords - blank_encoded = make_password("", "", "unsalted_sha1") - self.assertTrue(blank_encoded.startswith("sha1$")) - self.assertTrue(is_password_usable(blank_encoded)) - self.assertTrue(check_password("", blank_encoded)) - self.assertFalse(check_password(" ", blank_encoded)) - - @ignore_warnings(category=RemovedInDjango51Warning) - @override_settings( - PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"] - ) - def test_unsalted_sha1_encode_invalid_salt(self): - hasher = get_hasher("unsalted_sha1") - msg = "salt must be empty." - with self.assertRaisesMessage(ValueError, msg): - hasher.encode("password", salt="salt") - - @override_settings( - PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"] - ) - def test_unsalted_sha1_deprecation_warning(self): - msg = "django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher is deprecated." - with self.assertRaisesMessage(RemovedInDjango51Warning, msg): - get_hasher("unsalted_sha1") - @skipUnless(bcrypt, "bcrypt not installed") def test_bcrypt_sha256(self): encoded = make_password("lètmein", hasher="bcrypt_sha256") From 04eb1b4567c96ccb167c16a95ca12c336b0c791b Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Wed, 13 Sep 2023 14:03:45 +0200 Subject: [PATCH 008/748] Refs #33872 -- Removed django.contrib.postgres.fields.CIText/CICharField/CIEmailField/CITextField. Per deprecation timeline. --- django/contrib/postgres/fields/citext.py | 65 ++++--------- django/db/backends/postgresql/operations.py | 6 -- docs/ref/checks.txt | 12 ++- docs/ref/contrib/postgres/fields.txt | 65 ------------- docs/releases/5.1.txt | 7 ++ tests/backends/postgresql/tests.py | 7 -- .../test_deprecated_fields.py | 38 ++++---- tests/postgres_tests/fields.py | 6 -- .../migrations/0002_create_test_models.py | 20 ---- tests/postgres_tests/models.py | 11 --- tests/postgres_tests/test_citext.py | 91 ------------------- tests/runtests.py | 7 -- 12 files changed, 49 insertions(+), 286 deletions(-) delete mode 100644 tests/postgres_tests/test_citext.py diff --git a/django/contrib/postgres/fields/citext.py b/django/contrib/postgres/fields/citext.py index 2dac2577d171..01df5411abaa 100644 --- a/django/contrib/postgres/fields/citext.py +++ b/django/contrib/postgres/fields/citext.py @@ -1,78 +1,45 @@ -import warnings - from django.db.models import CharField, EmailField, TextField -from django.test.utils import ignore_warnings -from django.utils.deprecation import RemovedInDjango51Warning - -__all__ = ["CICharField", "CIEmailField", "CIText", "CITextField"] - - -# RemovedInDjango51Warning. -class CIText: - def __init__(self, *args, **kwargs): - warnings.warn( - "django.contrib.postgres.fields.CIText mixin is deprecated.", - RemovedInDjango51Warning, - stacklevel=2, - ) - super().__init__(*args, **kwargs) - def get_internal_type(self): - return "CI" + super().get_internal_type() +__all__ = ["CICharField", "CIEmailField", "CITextField"] - def db_type(self, connection): - return "citext" - -class CICharField(CIText, CharField): - system_check_deprecated_details = { +class CICharField(CharField): + system_check_removed_details = { "msg": ( - "django.contrib.postgres.fields.CICharField is deprecated. Support for it " - "(except in historical migrations) will be removed in Django 5.1." + "django.contrib.postgres.fields.CICharField is removed except for support " + "in historical migrations." ), "hint": ( 'Use CharField(db_collation="…") with a case-insensitive non-deterministic ' "collation instead." ), - "id": "fields.W905", + "id": "fields.E905", } - def __init__(self, *args, **kwargs): - with ignore_warnings(category=RemovedInDjango51Warning): - super().__init__(*args, **kwargs) - -class CIEmailField(CIText, EmailField): - system_check_deprecated_details = { +class CIEmailField(EmailField): + system_check_removed_details = { "msg": ( - "django.contrib.postgres.fields.CIEmailField is deprecated. Support for it " - "(except in historical migrations) will be removed in Django 5.1." + "django.contrib.postgres.fields.CIEmailField is removed except for support " + "in historical migrations." ), "hint": ( 'Use EmailField(db_collation="…") with a case-insensitive ' "non-deterministic collation instead." ), - "id": "fields.W906", + "id": "fields.E906", } - def __init__(self, *args, **kwargs): - with ignore_warnings(category=RemovedInDjango51Warning): - super().__init__(*args, **kwargs) - -class CITextField(CIText, TextField): - system_check_deprecated_details = { +class CITextField(TextField): + system_check_removed_details = { "msg": ( - "django.contrib.postgres.fields.CITextField is deprecated. Support for it " - "(except in historical migrations) will be removed in Django 5.1." + "django.contrib.postgres.fields.CITextField is removed except for support " + "in historical migrations." ), "hint": ( 'Use TextField(db_collation="…") with a case-insensitive non-deterministic ' "collation instead." ), - "id": "fields.W907", + "id": "fields.E907", } - - def __init__(self, *args, **kwargs): - with ignore_warnings(category=RemovedInDjango51Warning): - super().__init__(*args, **kwargs) diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py index aa839f5634f8..93eb982f4b69 100644 --- a/django/db/backends/postgresql/operations.py +++ b/django/db/backends/postgresql/operations.py @@ -159,9 +159,6 @@ def lookup_cast(self, lookup_type, internal_type=None): "CharField", "EmailField", "TextField", - "CICharField", - "CIEmailField", - "CITextField", ): return "%s::text" @@ -179,9 +176,6 @@ def lookup_cast(self, lookup_type, internal_type=None): ): if internal_type in ("IPAddressField", "GenericIPAddressField"): lookup = "HOST(%s)" - # RemovedInDjango51Warning. - elif internal_type in ("CICharField", "CIEmailField", "CITextField"): - lookup = "%s::citext" else: lookup = "%s::text" diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index e641c989e3b2..9170ff83a830 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -237,13 +237,19 @@ Model fields except for support in historical migrations. * **fields.W905**: ``django.contrib.postgres.fields.CICharField`` is deprecated. Support for it (except in historical migrations) will be removed - in Django 5.1. + in Django 5.1. *This check appeared in Django 4.2 and 5.0*. +* **fields.E905**: ``django.contrib.postgres.fields.CICharField`` is removed + except for support in historical migrations. * **fields.W906**: ``django.contrib.postgres.fields.CIEmailField`` is deprecated. Support for it (except in historical migrations) will be removed - in Django 5.1. + in Django 5.1. *This check appeared in Django 4.2 and 5.0*. +* **fields.E906**: ``django.contrib.postgres.fields.CIEmailField`` is removed + except for support in historical migrations. * **fields.W907**: ``django.contrib.postgres.fields.CITextField`` is deprecated. Support for it (except in historical migrations) will be removed - in Django 5.1. + in Django 5.1. *This check appeared in Django 4.2 and 5.0*. +* **fields.E907**: ``django.contrib.postgres.fields.CITextField`` is removed + except for support for historical migrations. File fields ~~~~~~~~~~~ diff --git a/docs/ref/contrib/postgres/fields.txt b/docs/ref/contrib/postgres/fields.txt index 0348bb235f13..ec767b50e91a 100644 --- a/docs/ref/contrib/postgres/fields.txt +++ b/docs/ref/contrib/postgres/fields.txt @@ -277,71 +277,6 @@ transform do not change. For example: at the database level and cannot be supported in a logical, consistent fashion by Django. -``CIText`` fields -================= - -.. class:: CIText(**options) - - .. deprecated:: 4.2 - - A mixin to create case-insensitive text fields backed by the citext_ type. - Read about `the performance considerations`_ prior to using it. - - To use ``citext``, use the :class:`.CITextExtension` operation to - :ref:`set up the citext extension ` in - PostgreSQL before the first ``CreateModel`` migration operation. - - If you're using an :class:`~django.contrib.postgres.fields.ArrayField` - of ``CIText`` fields, you must add ``'django.contrib.postgres'`` in your - :setting:`INSTALLED_APPS`, otherwise field values will appear as strings - like ``'{thoughts,django}'``. - - Several fields that use the mixin are provided: - -.. class:: CICharField(**options) - - .. deprecated:: 4.2 - - ``CICharField`` is deprecated in favor of - ``CharField(db_collation="…")`` with a case-insensitive - non-deterministic collation. - -.. class:: CIEmailField(**options) - - .. deprecated:: 4.2 - - ``CIEmailField`` is deprecated in favor of - ``EmailField(db_collation="…")`` with a case-insensitive - non-deterministic collation. - -.. class:: CITextField(**options) - - .. deprecated:: 4.2 - - ``CITextField`` is deprecated in favor of - ``TextField(db_collation="…")`` with a case-insensitive - non-deterministic collation. - - These fields subclass :class:`~django.db.models.CharField`, - :class:`~django.db.models.EmailField`, and - :class:`~django.db.models.TextField`, respectively. - - ``max_length`` won't be enforced in the database since ``citext`` behaves - similar to PostgreSQL's ``text`` type. - - .. _citext: https://www.postgresql.org/docs/current/citext.html - .. _the performance considerations: https://www.postgresql.org/docs/current/citext.html#id-1.11.7.19.9 - -.. admonition:: Case-insensitive collations - - It's preferable to use non-deterministic collations instead of the - ``citext`` extension. You can create them using the - :class:`~django.contrib.postgres.operations.CreateCollation` migration - operation. For more details, see :ref:`manage-postgresql-collations` and - the PostgreSQL documentation about `non-deterministic collations`_. - - .. _non-deterministic collations: https://www.postgresql.org/docs/current/collation.html#COLLATION-NONDETERMINISTIC - ``HStoreField`` =============== diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 9a0cd94f7ee5..d4510d30db60 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -260,3 +260,10 @@ to remove usage of these features. * The ``django.contrib.auth.hashers.SHA1PasswordHasher``, ``django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher``, and ``django.contrib.auth.hashers.UnsaltedMD5PasswordHasher`` are removed. + +* The model ``django.contrib.postgres.fields.CICharField``, + ``django.contrib.postgres.fields.CIEmailField``, and + ``django.contrib.postgres.fields.CITextField`` are removed, except for + support in historical migrations. + +* The ``django.contrib.postgres.fields.CIText`` mixin is removed. diff --git a/tests/backends/postgresql/tests.py b/tests/backends/postgresql/tests.py index 947d51ea1e4b..590dbe6073d6 100644 --- a/tests/backends/postgresql/tests.py +++ b/tests/backends/postgresql/tests.py @@ -368,13 +368,6 @@ def test_lookup_cast(self): for lookup in lookups: with self.subTest(lookup=lookup): self.assertIn("::text", do.lookup_cast(lookup)) - # RemovedInDjango51Warning. - for lookup in lookups: - for field_type in ("CICharField", "CIEmailField", "CITextField"): - with self.subTest(lookup=lookup, field_type=field_type): - self.assertIn( - "::citext", do.lookup_cast(lookup, internal_type=field_type) - ) def test_correct_extraction_psycopg_version(self): from django.db.backends.postgresql.base import Database, psycopg_version diff --git a/tests/invalid_models_tests/test_deprecated_fields.py b/tests/invalid_models_tests/test_deprecated_fields.py index 429e15febc07..65f65e752825 100644 --- a/tests/invalid_models_tests/test_deprecated_fields.py +++ b/tests/invalid_models_tests/test_deprecated_fields.py @@ -104,46 +104,42 @@ class PostgresCIFieldsModel(models.Model): self.assertEqual( PostgresCIFieldsModel.check(), [ - checks.Warning( - "django.contrib.postgres.fields.CICharField is deprecated. Support " - "for it (except in historical migrations) will be removed in " - "Django 5.1.", + checks.Error( + "django.contrib.postgres.fields.CICharField is removed except for " + "support in historical migrations.", hint=( 'Use CharField(db_collation="…") with a case-insensitive ' "non-deterministic collation instead." ), obj=PostgresCIFieldsModel._meta.get_field("ci_char"), - id="fields.W905", + id="fields.E905", ), - checks.Warning( - "django.contrib.postgres.fields.CIEmailField is deprecated. " - "Support for it (except in historical migrations) will be removed " - "in Django 5.1.", + checks.Error( + "django.contrib.postgres.fields.CIEmailField is removed except for " + "support in historical migrations.", hint=( 'Use EmailField(db_collation="…") with a case-insensitive ' "non-deterministic collation instead." ), obj=PostgresCIFieldsModel._meta.get_field("ci_email"), - id="fields.W906", + id="fields.E906", ), - checks.Warning( - "django.contrib.postgres.fields.CITextField is deprecated. Support " - "for it (except in historical migrations) will be removed in " - "Django 5.1.", + checks.Error( + "django.contrib.postgres.fields.CITextField is removed except for " + "support in historical migrations.", hint=( 'Use TextField(db_collation="…") with a case-insensitive ' "non-deterministic collation instead." ), obj=PostgresCIFieldsModel._meta.get_field("ci_text"), - id="fields.W907", + id="fields.E907", ), - checks.Warning( - "Base field for array has warnings:\n" - " django.contrib.postgres.fields.CITextField is deprecated. " - "Support for it (except in historical migrations) will be removed " - "in Django 5.1. (fields.W907)", + checks.Error( + "Base field for array has errors:\n" + " django.contrib.postgres.fields.CITextField is removed except " + "for support in historical migrations. (fields.E907)", obj=PostgresCIFieldsModel._meta.get_field("array_ci_text"), - id="postgres.W004", + id="postgres.E001", ), ], ) diff --git a/tests/postgres_tests/fields.py b/tests/postgres_tests/fields.py index 1565b5ed439e..c2513fca0cb8 100644 --- a/tests/postgres_tests/fields.py +++ b/tests/postgres_tests/fields.py @@ -7,9 +7,6 @@ from django.db import models try: - from django.contrib.postgres.fields import CICharField # RemovedInDjango51Warning. - from django.contrib.postgres.fields import CIEmailField # RemovedInDjango51Warning. - from django.contrib.postgres.fields import CITextField # RemovedInDjango51Warning. from django.contrib.postgres.fields import ( ArrayField, BigIntegerRangeField, @@ -47,9 +44,6 @@ def deconstruct(self): ArrayField = DummyArrayField BigIntegerRangeField = models.Field - CICharField = models.Field # RemovedInDjango51Warning. - CIEmailField = models.Field # RemovedInDjango51Warning. - CITextField = models.Field # RemovedInDjango51Warning. DateRangeField = models.Field DateTimeRangeField = DummyContinuousRangeField DecimalRangeField = DummyContinuousRangeField diff --git a/tests/postgres_tests/migrations/0002_create_test_models.py b/tests/postgres_tests/migrations/0002_create_test_models.py index 011a0d729b89..5538b436ad3f 100644 --- a/tests/postgres_tests/migrations/0002_create_test_models.py +++ b/tests/postgres_tests/migrations/0002_create_test_models.py @@ -3,9 +3,6 @@ from ..fields import ( ArrayField, BigIntegerRangeField, - CICharField, - CIEmailField, - CITextField, DateRangeField, DateTimeRangeField, DecimalRangeField, @@ -290,23 +287,6 @@ class Migration(migrations.Migration): options=None, bases=None, ), - # RemovedInDjango51Warning. - migrations.CreateModel( - name="CITestModel", - fields=[ - ( - "name", - CICharField(primary_key=True, serialize=False, max_length=255), - ), - ("email", CIEmailField()), - ("description", CITextField()), - ("array_field", ArrayField(CITextField(), null=True)), - ], - options={ - "required_db_vendor": "postgresql", - }, - bases=None, - ), migrations.CreateModel( name="Line", fields=[ diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py index 05f2732fb8b5..a97894e32778 100644 --- a/tests/postgres_tests/models.py +++ b/tests/postgres_tests/models.py @@ -3,9 +3,6 @@ from .fields import ( ArrayField, BigIntegerRangeField, - CICharField, - CIEmailField, - CITextField, DateRangeField, DateTimeRangeField, DecimalRangeField, @@ -119,14 +116,6 @@ class Character(models.Model): name = models.CharField(max_length=255) -# RemovedInDjango51Warning. -class CITestModel(PostgreSQLModel): - name = CICharField(primary_key=True, max_length=255) - email = CIEmailField() - description = CITextField() - array_field = ArrayField(CITextField(), null=True) - - class Line(PostgreSQLModel): scene = models.ForeignKey("Scene", models.CASCADE) character = models.ForeignKey("Character", models.CASCADE) diff --git a/tests/postgres_tests/test_citext.py b/tests/postgres_tests/test_citext.py deleted file mode 100644 index 2abb56b39f8a..000000000000 --- a/tests/postgres_tests/test_citext.py +++ /dev/null @@ -1,91 +0,0 @@ -# RemovedInDjango51Warning. -""" -The citext PostgreSQL extension supports indexing of case-insensitive text -strings and thus eliminates the need for operations such as iexact and other -modifiers to enforce use of an index. -""" -from django.db import IntegrityError -from django.utils.deprecation import RemovedInDjango51Warning - -from . import PostgreSQLTestCase -from .models import CITestModel - - -class CITextTestCase(PostgreSQLTestCase): - case_sensitive_lookups = ("contains", "startswith", "endswith", "regex") - - @classmethod - def setUpTestData(cls): - cls.john = CITestModel.objects.create( - name="JoHn", - email="joHn@johN.com", - description="Average Joe named JoHn", - array_field=["JoE", "jOhn"], - ) - - def test_equal_lowercase(self): - """ - citext removes the need for iexact as the index is case-insensitive. - """ - self.assertEqual( - CITestModel.objects.filter(name=self.john.name.lower()).count(), 1 - ) - self.assertEqual( - CITestModel.objects.filter(email=self.john.email.lower()).count(), 1 - ) - self.assertEqual( - CITestModel.objects.filter( - description=self.john.description.lower() - ).count(), - 1, - ) - - def test_fail_citext_primary_key(self): - """ - Creating an entry for a citext field used as a primary key which - clashes with an existing value isn't allowed. - """ - with self.assertRaises(IntegrityError): - CITestModel.objects.create(name="John") - - def test_array_field(self): - instance = CITestModel.objects.get() - self.assertEqual(instance.array_field, self.john.array_field) - self.assertTrue( - CITestModel.objects.filter(array_field__contains=["joe"]).exists() - ) - - def test_lookups_name_char(self): - for lookup in self.case_sensitive_lookups: - with self.subTest(lookup=lookup): - query = {"name__{}".format(lookup): "john"} - self.assertSequenceEqual( - CITestModel.objects.filter(**query), [self.john] - ) - - def test_lookups_description_text(self): - for lookup, string in zip( - self.case_sensitive_lookups, ("average", "average joe", "john", "Joe.named") - ): - with self.subTest(lookup=lookup, string=string): - query = {"description__{}".format(lookup): string} - self.assertSequenceEqual( - CITestModel.objects.filter(**query), [self.john] - ) - - def test_lookups_email(self): - for lookup, string in zip( - self.case_sensitive_lookups, ("john", "john", "john.com", "john.com") - ): - with self.subTest(lookup=lookup, string=string): - query = {"email__{}".format(lookup): string} - self.assertSequenceEqual( - CITestModel.objects.filter(**query), [self.john] - ) - - def test_citext_deprecated(self): - from django.contrib.postgres.fields import CIText - - msg = "django.contrib.postgres.fields.CIText mixin is deprecated." - with self.assertRaisesMessage(RemovedInDjango51Warning, msg): - CIText() diff --git a/tests/runtests.py b/tests/runtests.py index 2eb7490170ad..5d138ffb0e0f 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -245,13 +245,6 @@ def setup_collect_tests(start_at, start_after, test_labels=None): settings.LOGGING = log_config settings.SILENCED_SYSTEM_CHECKS = [ "fields.W342", # ForeignKey(unique=True) -> OneToOneField - # django.contrib.postgres.fields.CICharField deprecated. - "fields.W905", - "postgres.W004", - # django.contrib.postgres.fields.CIEmailField deprecated. - "fields.W906", - # django.contrib.postgres.fields.CITextField deprecated. - "fields.W907", ] # Load all the ALWAYS_INSTALLED_APPS. From 7433237664e9a942d6584b4e70fd6a5174cf8f39 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Thu, 14 Sep 2023 06:04:12 +0200 Subject: [PATCH 009/748] Refs #33924 -- Removed BaseGeometryWidget.map_height/map_width attributes per deprecation timeline. --- django/contrib/gis/forms/widgets.py | 17 +---------------- .../contrib/gis/static/gis/js/OLMapWidget.js | 5 ----- .../contrib/gis/templates/gis/openlayers.html | 3 +-- docs/ref/contrib/gis/forms-api.txt | 10 ---------- docs/releases/5.1.txt | 3 +++ tests/gis_tests/test_geoforms.py | 19 ------------------- 6 files changed, 5 insertions(+), 52 deletions(-) diff --git a/django/contrib/gis/forms/widgets.py b/django/contrib/gis/forms/widgets.py index 49ca48794b4a..55895ae9f362 100644 --- a/django/contrib/gis/forms/widgets.py +++ b/django/contrib/gis/forms/widgets.py @@ -1,5 +1,4 @@ import logging -import warnings from django.conf import settings from django.contrib.gis import gdal @@ -7,7 +6,6 @@ from django.contrib.gis.geos import GEOSException, GEOSGeometry from django.forms.widgets import Widget from django.utils import translation -from django.utils.deprecation import RemovedInDjango51Warning logger = logging.getLogger("django.contrib.gis") @@ -20,8 +18,6 @@ class BaseGeometryWidget(Widget): geom_type = "GEOMETRY" map_srid = 4326 - map_width = 600 # RemovedInDjango51Warning - map_height = 400 # RemovedInDjango51Warning display_raw = False supports_3d = False @@ -29,19 +25,8 @@ class BaseGeometryWidget(Widget): def __init__(self, attrs=None): self.attrs = {} - for key in ("geom_type", "map_srid", "map_width", "map_height", "display_raw"): + for key in ("geom_type", "map_srid", "display_raw"): self.attrs[key] = getattr(self, key) - if ( - (attrs and ("map_width" in attrs or "map_height" in attrs)) - or self.map_width != 600 - or self.map_height != 400 - ): - warnings.warn( - "The map_height and map_width widget attributes are deprecated. Please " - "use CSS to size map widgets.", - category=RemovedInDjango51Warning, - stacklevel=2, - ) if attrs: self.attrs.update(attrs) diff --git a/django/contrib/gis/static/gis/js/OLMapWidget.js b/django/contrib/gis/static/gis/js/OLMapWidget.js index b750327409b7..f3025f24ac40 100644 --- a/django/contrib/gis/static/gis/js/OLMapWidget.js +++ b/django/contrib/gis/static/gis/js/OLMapWidget.js @@ -62,11 +62,6 @@ class MapWidget { this.options.base_layer = new ol.layer.Tile({source: new ol.source.OSM()}); } - // RemovedInDjango51Warning: when the deprecation ends, remove setting - // width/height (3 lines below). - const mapContainer = document.getElementById(this.options.map_id); - mapContainer.style.width = `${mapContainer.dataset.width}px`; - mapContainer.style.height = `${mapContainer.dataset.height}px`; this.map = this.createMap(); this.featureCollection = new ol.Collection(); this.featureOverlay = new ol.layer.Vector({ diff --git a/django/contrib/gis/templates/gis/openlayers.html b/django/contrib/gis/templates/gis/openlayers.html index 2849813c6efe..f9f7e5fa5196 100644 --- a/django/contrib/gis/templates/gis/openlayers.html +++ b/django/contrib/gis/templates/gis/openlayers.html @@ -1,8 +1,7 @@ {% load i18n l10n %}
- {# RemovedInDjango51Warning: when the deprecation ends, remove data-width and data-height attributes. #} -
+
{% if not disabled %}{% translate "Delete all Features" %}{% endif %} {% if display_raw %}

{% translate "Debugging window (serialized value)" %}

{% endif %}
From 9d52e0720f79ac3cff3d9888a97ac227884a621e Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Sun, 31 Dec 2023 01:32:37 -0600 Subject: [PATCH 284/748] Fixed #35051 -- Prevented runserver from removing non-zero Content-Length for HEAD requests. --- django/core/servers/basehttp.py | 1 + tests/servers/test_basehttp.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index 6afe17cec477..495657d26496 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -134,6 +134,7 @@ def cleanup_headers(self): if ( self.environ["REQUEST_METHOD"] == "HEAD" and "Content-Length" in self.headers + and str(self.headers["Content-Length"]) == "0" ): del self.headers["Content-Length"] # HTTP/1.1 requires support for persistent connections. Send 'close' if diff --git a/tests/servers/test_basehttp.py b/tests/servers/test_basehttp.py index 1e535e933e24..cc4701114a78 100644 --- a/tests/servers/test_basehttp.py +++ b/tests/servers/test_basehttp.py @@ -161,6 +161,45 @@ def makefile(mode, *a, **kw): ) self.assertNotIn(b"Connection: close\r\n", lines) + def test_non_zero_content_length_set_head_request(self): + hello_world_body = b"Hello World" + content_length = len(hello_world_body) + + def test_app(environ, start_response): + """ + A WSGI app that returns a hello world with non-zero Content-Length. + """ + start_response("200 OK", [("Content-length", str(content_length))]) + return [hello_world_body] + + rfile = BytesIO(b"HEAD / HTTP/1.0\r\n") + rfile.seek(0) + + wfile = UnclosableBytesIO() + + def makefile(mode, *a, **kw): + if mode == "rb": + return rfile + elif mode == "wb": + return wfile + + request = Stub(makefile=makefile) + server = Stub(base_environ={}, get_app=lambda: test_app) + + # Prevent logging from appearing in test output. + with self.assertLogs("django.server", "INFO"): + # Instantiating a handler runs the request as side effect. + WSGIRequestHandler(request, "192.168.0.2", server) + + wfile.seek(0) + lines = list(wfile.readlines()) + body = lines[-1] + # The body is not returned in a HEAD response. + self.assertEqual(body, b"\r\n") + # Non-zero Content-Length is not removed. + self.assertEqual(lines[-2], f"Content-length: {content_length}\r\n".encode()) + self.assertNotIn(b"Connection: close\r\n", lines) + class WSGIServerTestCase(SimpleTestCase): request_factory = RequestFactory() From 81ccf92f154c6d9eac3e30bac0aa67574d0ace15 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Sun, 31 Dec 2023 08:07:19 +0000 Subject: [PATCH 285/748] Used JSON_OBJECT database function on PostgreSQL 16+. --- django/db/models/functions/comparison.py | 43 +++++++++++++----------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/django/db/models/functions/comparison.py b/django/db/models/functions/comparison.py index eec2ab752d19..ae41f1da95ea 100644 --- a/django/db/models/functions/comparison.py +++ b/django/db/models/functions/comparison.py @@ -159,35 +159,40 @@ def as_sql(self, compiler, connection, **extra_context): ) return super().as_sql(compiler, connection, **extra_context) - def as_postgresql(self, compiler, connection, **extra_context): - copy = self.copy() - copy.set_source_expressions( - [ - Cast(expression, TextField()) if index % 2 == 0 else expression - for index, expression in enumerate(copy.get_source_expressions()) - ] - ) - return super(JSONObject, copy).as_sql( - compiler, - connection, - function="JSONB_BUILD_OBJECT", - **extra_context, - ) - - def as_oracle(self, compiler, connection, **extra_context): + def as_native(self, compiler, connection, *, returning, **extra_context): class ArgJoiner: def join(self, args): - args = [" VALUE ".join(arg) for arg in zip(args[::2], args[1::2])] - return ", ".join(args) + pairs = zip(args[::2], args[1::2], strict=True) + return ", ".join([" VALUE ".join(pair) for pair in pairs]) return self.as_sql( compiler, connection, arg_joiner=ArgJoiner(), - template="%(function)s(%(expressions)s RETURNING CLOB)", + template=f"%(function)s(%(expressions)s RETURNING {returning})", **extra_context, ) + def as_postgresql(self, compiler, connection, **extra_context): + if not connection.features.is_postgresql_16: + copy = self.copy() + copy.set_source_expressions( + [ + Cast(expression, TextField()) if index % 2 == 0 else expression + for index, expression in enumerate(copy.get_source_expressions()) + ] + ) + return super(JSONObject, copy).as_sql( + compiler, + connection, + function="JSONB_BUILD_OBJECT", + **extra_context, + ) + return self.as_native(compiler, connection, returning="JSONB", **extra_context) + + def as_oracle(self, compiler, connection, **extra_context): + return self.as_native(compiler, connection, returning="CLOB", **extra_context) + class Least(Func): """ From d88ec42bd0a37340c8477a6f20bf26e58bd84735 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Sun, 31 Dec 2023 10:01:31 +0100 Subject: [PATCH 286/748] Used addCleanup() in tests where appropriate. --- tests/app_loading/tests.py | 4 +- tests/asgi/tests.py | 4 +- tests/auth_tests/test_auth_backends.py | 21 ++++----- tests/auth_tests/test_management.py | 6 +-- tests/auth_tests/test_models.py | 4 +- tests/auth_tests/test_remote_user.py | 8 ++-- tests/auth_tests/test_signals.py | 11 ++--- tests/cache/tests.py | 13 ++---- tests/contenttypes_tests/test_models.py | 4 +- tests/contenttypes_tests/test_operations.py | 9 ++-- tests/custom_lookups/tests.py | 4 +- tests/defer_regress/tests.py | 11 +++-- tests/delete_regress/tests.py | 6 +-- tests/file_storage/tests.py | 46 ++++++------------- tests/forms_tests/tests/test_input_formats.py | 12 ++--- tests/gis_tests/utils.py | 11 ++--- tests/handlers/tests.py | 10 ++-- tests/httpwrappers/tests.py | 4 +- tests/i18n/patterns/tests.py | 4 +- tests/i18n/tests.py | 5 +- tests/logging_tests/tests.py | 12 ++--- tests/mail/tests.py | 22 +++------ tests/messages_tests/base.py | 8 ++-- tests/model_fields/test_imagefield.py | 13 ++---- tests/sessions_tests/tests.py | 6 +-- tests/settings_tests/tests.py | 4 +- tests/sites_tests/tests.py | 4 +- tests/staticfiles_tests/cases.py | 9 ++-- tests/staticfiles_tests/test_management.py | 10 ++-- tests/staticfiles_tests/test_storage.py | 22 ++++----- .../template_tests/syntax_tests/i18n/base.py | 4 +- tests/transactions/tests.py | 12 ++--- tests/utils_tests/test_archive.py | 6 +-- tests/utils_tests/test_autoreload.py | 18 +++----- tests/utils_tests/test_dateformat.py | 6 +-- tests/view_tests/tests/test_debug.py | 4 +- tests/wsgi/tests.py | 4 +- 37 files changed, 121 insertions(+), 240 deletions(-) diff --git a/tests/app_loading/tests.py b/tests/app_loading/tests.py index 1499115386ba..d8a837c2f6ea 100644 --- a/tests/app_loading/tests.py +++ b/tests/app_loading/tests.py @@ -8,9 +8,7 @@ class EggLoadingTest(SimpleTestCase): def setUp(self): self.egg_dir = "%s/eggs" % os.path.dirname(__file__) - - def tearDown(self): - apps.clear_cache() + self.addCleanup(apps.clear_cache) def test_egg1(self): """Models module can be loaded from an app in an egg""" diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py index ced24c658e87..3aeade4c05e3 100644 --- a/tests/asgi/tests.py +++ b/tests/asgi/tests.py @@ -32,9 +32,7 @@ class ASGITest(SimpleTestCase): def setUp(self): request_started.disconnect(close_old_connections) - - def tearDown(self): - request_started.connect(close_old_connections) + self.addCleanup(request_started.connect, close_old_connections) async def test_get_asgi_application(self): """ diff --git a/tests/auth_tests/test_auth_backends.py b/tests/auth_tests/test_auth_backends.py index 81406f37e6d3..a7005de8a4d5 100644 --- a/tests/auth_tests/test_auth_backends.py +++ b/tests/auth_tests/test_auth_backends.py @@ -95,19 +95,16 @@ class BaseModelBackendTest: backend = "django.contrib.auth.backends.ModelBackend" def setUp(self): - self.patched_settings = modify_settings( + # The custom_perms test messes with ContentTypes, which will be cached. + # Flush the cache to ensure there are no side effects. + self.addCleanup(ContentType.objects.clear_cache) + patched_settings = modify_settings( AUTHENTICATION_BACKENDS={"append": self.backend}, ) - self.patched_settings.enable() + patched_settings.enable() + self.addCleanup(patched_settings.disable) self.create_users() - def tearDown(self): - self.patched_settings.disable() - # The custom_perms test messes with ContentTypes, which will - # be cached; flush the cache to ensure there are no side effects - # Refs #14975, #14925 - ContentType.objects.clear_cache() - def test_has_perm(self): user = self.UserModel._default_manager.get(pk=self.user.pk) self.assertIs(user.has_perm("auth.test"), False) @@ -615,9 +612,9 @@ def setUpTestData(cls): def setUp(self): self.user_login_failed = [] signals.user_login_failed.connect(self.user_login_failed_listener) - - def tearDown(self): - signals.user_login_failed.disconnect(self.user_login_failed_listener) + self.addCleanup( + signals.user_login_failed.disconnect, self.user_login_failed_listener + ) def user_login_failed_listener(self, sender, credentials, **kwargs): self.user_login_failed.append(credentials) diff --git a/tests/auth_tests/test_management.py b/tests/auth_tests/test_management.py index 872fe75e8aed..0cc56b6760d7 100644 --- a/tests/auth_tests/test_management.py +++ b/tests/auth_tests/test_management.py @@ -163,11 +163,9 @@ def setUpTestData(cls): def setUp(self): self.stdout = StringIO() + self.addCleanup(self.stdout.close) self.stderr = StringIO() - - def tearDown(self): - self.stdout.close() - self.stderr.close() + self.addCleanup(self.stderr.close) @mock.patch.object(getpass, "getpass", return_value="password") def test_get_pass(self, mock_get_pass): diff --git a/tests/auth_tests/test_models.py b/tests/auth_tests/test_models.py index b9a006f96dcd..34f411f2f957 100644 --- a/tests/auth_tests/test_models.py +++ b/tests/auth_tests/test_models.py @@ -519,9 +519,7 @@ def post_save_listener(self, *args, **kwargs): def setUp(self): self.signals_count = 0 post_save.connect(self.post_save_listener, sender=User) - - def tearDown(self): - post_save.disconnect(self.post_save_listener, sender=User) + self.addCleanup(post_save.disconnect, self.post_save_listener, sender=User) def test_create_user(self): User.objects.create_user("JohnDoe") diff --git a/tests/auth_tests/test_remote_user.py b/tests/auth_tests/test_remote_user.py index ea4399a44aae..6066ab96e9cc 100644 --- a/tests/auth_tests/test_remote_user.py +++ b/tests/auth_tests/test_remote_user.py @@ -21,14 +21,12 @@ class RemoteUserTest(TestCase): known_user2 = "knownuser2" def setUp(self): - self.patched_settings = modify_settings( + patched_settings = modify_settings( AUTHENTICATION_BACKENDS={"append": self.backend}, MIDDLEWARE={"append": self.middleware}, ) - self.patched_settings.enable() - - def tearDown(self): - self.patched_settings.disable() + patched_settings.enable() + self.addCleanup(patched_settings.disable) def test_no_remote_user(self): """Users are not created when remote user is not specified.""" diff --git a/tests/auth_tests/test_signals.py b/tests/auth_tests/test_signals.py index b97377e2c94a..c9a61ada0eb8 100644 --- a/tests/auth_tests/test_signals.py +++ b/tests/auth_tests/test_signals.py @@ -30,14 +30,13 @@ def setUp(self): self.logged_out = [] self.login_failed = [] signals.user_logged_in.connect(self.listener_login) + self.addCleanup(signals.user_logged_in.disconnect, self.listener_login) signals.user_logged_out.connect(self.listener_logout) + self.addCleanup(signals.user_logged_out.disconnect, self.listener_logout) signals.user_login_failed.connect(self.listener_login_failed) - - def tearDown(self): - """Disconnect the listeners""" - signals.user_logged_in.disconnect(self.listener_login) - signals.user_logged_out.disconnect(self.listener_logout) - signals.user_login_failed.disconnect(self.listener_login_failed) + self.addCleanup( + signals.user_login_failed.disconnect, self.listener_login_failed + ) def test_login(self): # Only a successful login will trigger the success signal. diff --git a/tests/cache/tests.py b/tests/cache/tests.py index c2a1ebdbb844..e6ebb718f1a6 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -1155,11 +1155,7 @@ def setUp(self): # The super calls needs to happen first for the settings override. super().setUp() self.create_table() - - def tearDown(self): - # The super call needs to happen first because it uses the database. - super().tearDown() - self.drop_table() + self.addCleanup(self.drop_table) def create_table(self): management.call_command("createcachetable", verbosity=0) @@ -2509,12 +2505,9 @@ class CacheMiddlewareTest(SimpleTestCase): def setUp(self): self.default_cache = caches["default"] + self.addCleanup(self.default_cache.clear) self.other_cache = caches["other"] - - def tearDown(self): - self.default_cache.clear() - self.other_cache.clear() - super().tearDown() + self.addCleanup(self.other_cache.clear) def test_constructor(self): """ diff --git a/tests/contenttypes_tests/test_models.py b/tests/contenttypes_tests/test_models.py index 36c14cf56f24..02036de83f00 100644 --- a/tests/contenttypes_tests/test_models.py +++ b/tests/contenttypes_tests/test_models.py @@ -12,9 +12,7 @@ class ContentTypesTests(TestCase): def setUp(self): ContentType.objects.clear_cache() - - def tearDown(self): - ContentType.objects.clear_cache() + self.addCleanup(ContentType.objects.clear_cache) def test_lookup_cache(self): """ diff --git a/tests/contenttypes_tests/test_operations.py b/tests/contenttypes_tests/test_operations.py index a2bff373a47d..d44648d9fe6e 100644 --- a/tests/contenttypes_tests/test_operations.py +++ b/tests/contenttypes_tests/test_operations.py @@ -29,11 +29,10 @@ def setUp(self): models.signals.post_migrate.connect( self.assertOperationsInjected, sender=app_config ) - - def tearDown(self): - app_config = apps.get_app_config("contenttypes_tests") - models.signals.post_migrate.disconnect( - self.assertOperationsInjected, sender=app_config + self.addCleanup( + models.signals.post_migrate.disconnect, + self.assertOperationsInjected, + sender=app_config, ) def assertOperationsInjected(self, plan, **kwargs): diff --git a/tests/custom_lookups/tests.py b/tests/custom_lookups/tests.py index a636977b67e3..8fc31d4e6bca 100644 --- a/tests/custom_lookups/tests.py +++ b/tests/custom_lookups/tests.py @@ -460,9 +460,7 @@ def setUpTestData(cls): def setUp(self): models.DateField.register_lookup(YearTransform) - - def tearDown(self): - models.DateField._unregister_lookup(YearTransform) + self.addCleanup(models.DateField._unregister_lookup, YearTransform) @unittest.skipUnless( connection.vendor == "postgresql", "PostgreSQL specific SQL used" diff --git a/tests/defer_regress/tests.py b/tests/defer_regress/tests.py index 3dfe96ddb3a3..10100e348db9 100644 --- a/tests/defer_regress/tests.py +++ b/tests/defer_regress/tests.py @@ -322,12 +322,13 @@ def setUp(self): self.post_delete_senders = [] for sender in self.senders: models.signals.pre_delete.connect(self.pre_delete_receiver, sender) + self.addCleanup( + models.signals.pre_delete.disconnect, self.pre_delete_receiver, sender + ) models.signals.post_delete.connect(self.post_delete_receiver, sender) - - def tearDown(self): - for sender in self.senders: - models.signals.pre_delete.disconnect(self.pre_delete_receiver, sender) - models.signals.post_delete.disconnect(self.post_delete_receiver, sender) + self.addCleanup( + models.signals.post_delete.disconnect, self.post_delete_receiver, sender + ) def pre_delete_receiver(self, sender, **kwargs): self.pre_delete_senders.append(sender) diff --git a/tests/delete_regress/tests.py b/tests/delete_regress/tests.py index 71e3bcb405f7..89f4d5ddd89a 100644 --- a/tests/delete_regress/tests.py +++ b/tests/delete_regress/tests.py @@ -51,11 +51,9 @@ def setUp(self): # Create a second connection to the default database self.conn2 = connection.copy() self.conn2.set_autocommit(False) - - def tearDown(self): # Close down the second connection. - self.conn2.rollback() - self.conn2.close() + self.addCleanup(self.conn2.close) + self.addCleanup(self.conn2.rollback) def test_concurrent_delete(self): """Concurrent deletes don't collide and lock the database (#9479).""" diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py index 8c47e437429f..637de0a3c9fd 100644 --- a/tests/file_storage/tests.py +++ b/tests/file_storage/tests.py @@ -71,16 +71,10 @@ class FileStorageTests(SimpleTestCase): def setUp(self): self.temp_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.temp_dir) self.storage = self.storage_class( location=self.temp_dir, base_url="/test_media_url/" ) - # Set up a second temporary directory which is ensured to have a mixed - # case name. - self.temp_dir2 = tempfile.mkdtemp(suffix="aBc") - - def tearDown(self): - shutil.rmtree(self.temp_dir) - shutil.rmtree(self.temp_dir2) def test_empty_location(self): """ @@ -414,14 +408,16 @@ def test_file_storage_preserves_filename_case(self): """The storage backend should preserve case of filenames.""" # Create a storage backend associated with the mixed case name # directory. - other_temp_storage = self.storage_class(location=self.temp_dir2) + temp_dir2 = tempfile.mkdtemp(suffix="aBc") + self.addCleanup(shutil.rmtree, temp_dir2) + other_temp_storage = self.storage_class(location=temp_dir2) # Ask that storage backend to store a file with a mixed case filename. mixed_case = "CaSe_SeNsItIvE" file = other_temp_storage.open(mixed_case, "w") file.write("storage contents") file.close() self.assertEqual( - os.path.join(self.temp_dir2, mixed_case), + os.path.join(temp_dir2, mixed_case), other_temp_storage.path(mixed_case), ) other_temp_storage.delete(mixed_case) @@ -917,9 +913,7 @@ def setUp(self): self.temp_storage_location = tempfile.mkdtemp( suffix="filefield_callable_storage" ) - - def tearDown(self): - shutil.rmtree(self.temp_storage_location) + self.addCleanup(shutil.rmtree, self.temp_storage_location) def test_callable_base_class_error_raises(self): class NotStorage: @@ -993,12 +987,10 @@ def chunks(self): class FileSaveRaceConditionTest(SimpleTestCase): def setUp(self): self.storage_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.storage_dir) self.storage = FileSystemStorage(self.storage_dir) self.thread = threading.Thread(target=self.save_file, args=["conflict"]) - def tearDown(self): - shutil.rmtree(self.storage_dir) - def save_file(self, name): name = self.storage.save(name, SlowFile(b"Data")) @@ -1017,12 +1009,10 @@ def test_race_condition(self): class FileStoragePermissions(unittest.TestCase): def setUp(self): self.umask = 0o027 - self.old_umask = os.umask(self.umask) + old_umask = os.umask(self.umask) + self.addCleanup(os.umask, old_umask) self.storage_dir = tempfile.mkdtemp() - - def tearDown(self): - shutil.rmtree(self.storage_dir) - os.umask(self.old_umask) + self.addCleanup(shutil.rmtree, self.storage_dir) @override_settings(FILE_UPLOAD_PERMISSIONS=0o654) def test_file_upload_permissions(self): @@ -1059,11 +1049,9 @@ def test_file_upload_directory_default_permissions(self): class FileStoragePathParsing(SimpleTestCase): def setUp(self): self.storage_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.storage_dir) self.storage = FileSystemStorage(self.storage_dir) - def tearDown(self): - shutil.rmtree(self.storage_dir) - def test_directory_with_dot(self): """Regression test for #9610. @@ -1095,11 +1083,9 @@ def test_first_character_dot(self): class ContentFileStorageTestCase(unittest.TestCase): def setUp(self): - self.storage_dir = tempfile.mkdtemp() - self.storage = FileSystemStorage(self.storage_dir) - - def tearDown(self): - shutil.rmtree(self.storage_dir) + storage_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, storage_dir) + self.storage = FileSystemStorage(storage_dir) def test_content_saving(self): """ @@ -1120,11 +1106,9 @@ class FileLikeObjectTestCase(LiveServerTestCase): def setUp(self): self.temp_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.temp_dir) self.storage = FileSystemStorage(location=self.temp_dir) - def tearDown(self): - shutil.rmtree(self.temp_dir) - def test_urllib_request_urlopen(self): """ Test the File storage API with a file-like object coming from diff --git a/tests/forms_tests/tests/test_input_formats.py b/tests/forms_tests/tests/test_input_formats.py index 1923319759de..7a0dfca8a70d 100644 --- a/tests/forms_tests/tests/test_input_formats.py +++ b/tests/forms_tests/tests/test_input_formats.py @@ -12,9 +12,7 @@ def setUp(self): # nl/formats.py has customized TIME_INPUT_FORMATS: # ['%H:%M:%S', '%H.%M:%S', '%H.%M', '%H:%M'] activate("nl") - - def tearDown(self): - deactivate() + self.addCleanup(deactivate) def test_timeField(self): "TimeFields can parse dates in the default format" @@ -323,9 +321,7 @@ def test_localized_timeField_with_inputformat(self): class LocalizedDateTests(SimpleTestCase): def setUp(self): activate("de") - - def tearDown(self): - deactivate() + self.addCleanup(deactivate) def test_dateField(self): "DateFields can parse dates in the default format" @@ -637,9 +633,7 @@ def test_localized_dateField_with_inputformat(self): class LocalizedDateTimeTests(SimpleTestCase): def setUp(self): activate("de") - - def tearDown(self): - deactivate() + self.addCleanup(deactivate) def test_dateTimeField(self): "DateTimeFields can parse dates in the default format" diff --git a/tests/gis_tests/utils.py b/tests/gis_tests/utils.py index 2fbfd438ebf3..2431ba4cec41 100644 --- a/tests/gis_tests/utils.py +++ b/tests/gis_tests/utils.py @@ -64,12 +64,7 @@ def __getattribute__(self, name): vendor_impl = "as_" + connection.vendor __getattribute__original = Func.__getattribute__ - self.func_patcher = mock.patch.object( - Func, "__getattribute__", __getattribute__ - ) - self.func_patcher.start() + func_patcher = mock.patch.object(Func, "__getattribute__", __getattribute__) + func_patcher.start() + self.addCleanup(func_patcher.stop) super().setUp() - - def tearDown(self): - super().tearDown() - self.func_patcher.stop() diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py index ab3837d25e09..76d99e25045f 100644 --- a/tests/handlers/tests.py +++ b/tests/handlers/tests.py @@ -16,9 +16,7 @@ class HandlerTests(SimpleTestCase): def setUp(self): request_started.disconnect(close_old_connections) - - def tearDown(self): - request_started.connect(close_old_connections) + self.addCleanup(request_started.connect, close_old_connections) def test_middleware_initialized(self): handler = WSGIHandler() @@ -150,11 +148,9 @@ def setUp(self): self.signals = [] self.signaled_environ = None request_started.connect(self.register_started) + self.addCleanup(request_started.disconnect, self.register_started) request_finished.connect(self.register_finished) - - def tearDown(self): - request_started.disconnect(self.register_started) - request_finished.disconnect(self.register_finished) + self.addCleanup(request_finished.disconnect, self.register_finished) def register_started(self, **kwargs): self.signals.append("started") diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index 0a41ea5ec630..cce8402d3fc0 100644 --- a/tests/httpwrappers/tests.py +++ b/tests/httpwrappers/tests.py @@ -762,9 +762,7 @@ def setUp(self): # Disable the request_finished signal during this test # to avoid interfering with the database connection. request_finished.disconnect(close_old_connections) - - def tearDown(self): - request_finished.connect(close_old_connections) + self.addCleanup(request_finished.connect, close_old_connections) def test_response(self): filename = os.path.join(os.path.dirname(__file__), "abc.txt") diff --git a/tests/i18n/patterns/tests.py b/tests/i18n/patterns/tests.py index df55a1ee130d..e2fee904b149 100644 --- a/tests/i18n/patterns/tests.py +++ b/tests/i18n/patterns/tests.py @@ -52,10 +52,8 @@ class URLTestCaseBase(SimpleTestCase): def setUp(self): # Make sure the cache is empty before we are doing our tests. clear_url_caches() - - def tearDown(self): # Make sure we will leave an empty cache for other testcases. - clear_url_caches() + self.addCleanup(clear_url_caches) class URLPrefixTests(URLTestCaseBase): diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index d44ddb9f8345..355505a10d14 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -1756,10 +1756,7 @@ class ResolutionOrderI18NTests(SimpleTestCase): def setUp(self): super().setUp() activate("de") - - def tearDown(self): - deactivate() - super().tearDown() + self.addCleanup(deactivate) def assertGettext(self, msgid, msgstr): result = gettext(msgid) diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py index c73a3acd6d72..20d2852fde00 100644 --- a/tests/logging_tests/tests.py +++ b/tests/logging_tests/tests.py @@ -579,20 +579,18 @@ def setUp(self): [formatter_simple] format=%(message)s """ - self.temp_file = NamedTemporaryFile() - self.temp_file.write(logging_conf.encode()) - self.temp_file.flush() + temp_file = NamedTemporaryFile() + temp_file.write(logging_conf.encode()) + temp_file.flush() + self.addCleanup(temp_file.close) self.write_settings( "settings.py", sdict={ "LOGGING_CONFIG": '"logging.config.fileConfig"', - "LOGGING": 'r"%s"' % self.temp_file.name, + "LOGGING": 'r"%s"' % temp_file.name, }, ) - def tearDown(self): - self.temp_file.close() - def test_custom_logging(self): out, err = self.run_manage(["check"]) self.assertNoOutput(err) diff --git a/tests/mail/tests.py b/tests/mail/tests.py index 6f92194d1b67..73eceafa466a 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -1173,11 +1173,9 @@ class BaseEmailBackendTests(HeadersCheckMixin): email_backend = None def setUp(self): - self.settings_override = override_settings(EMAIL_BACKEND=self.email_backend) - self.settings_override.enable() - - def tearDown(self): - self.settings_override.disable() + settings_override = override_settings(EMAIL_BACKEND=self.email_backend) + settings_override.enable() + self.addCleanup(settings_override.disable) def assertStartsWith(self, first, second): if not first.startswith(second): @@ -1575,12 +1573,9 @@ def setUp(self): super().setUp() self.tmp_dir = self.mkdtemp() self.addCleanup(shutil.rmtree, self.tmp_dir) - self._settings_override = override_settings(EMAIL_FILE_PATH=self.tmp_dir) - self._settings_override.enable() - - def tearDown(self): - self._settings_override.disable() - super().tearDown() + _settings_override = override_settings(EMAIL_FILE_PATH=self.tmp_dir) + _settings_override.enable() + self.addCleanup(_settings_override.disable) def mkdtemp(self): return tempfile.mkdtemp() @@ -1754,10 +1749,7 @@ class SMTPBackendTests(BaseEmailBackendTests, SMTPBackendTestsBase): def setUp(self): super().setUp() self.smtp_handler.flush_mailbox() - - def tearDown(self): - self.smtp_handler.flush_mailbox() - super().tearDown() + self.addCleanup(self.smtp_handler.flush_mailbox) def flush_mailbox(self): self.smtp_handler.flush_mailbox() diff --git a/tests/messages_tests/base.py b/tests/messages_tests/base.py index 6fe8892ac1aa..34582b546255 100644 --- a/tests/messages_tests/base.py +++ b/tests/messages_tests/base.py @@ -32,7 +32,7 @@ class BaseTests: } def setUp(self): - self.settings_override = override_settings( + settings_override = override_settings( TEMPLATES=[ { "BACKEND": "django.template.backends.django.DjangoTemplates", @@ -52,10 +52,8 @@ def setUp(self): % (self.storage_class.__module__, self.storage_class.__name__), SESSION_SERIALIZER="django.contrib.sessions.serializers.JSONSerializer", ) - self.settings_override.enable() - - def tearDown(self): - self.settings_override.disable() + settings_override.enable() + self.addCleanup(settings_override.disable) def get_request(self): return HttpRequest() diff --git a/tests/model_fields/test_imagefield.py b/tests/model_fields/test_imagefield.py index 8bbfee30f254..8c93ed1bdbeb 100644 --- a/tests/model_fields/test_imagefield.py +++ b/tests/model_fields/test_imagefield.py @@ -55,20 +55,13 @@ def setUp(self): if os.path.exists(temp_storage_dir): shutil.rmtree(temp_storage_dir) os.mkdir(temp_storage_dir) - + self.addCleanup(shutil.rmtree, temp_storage_dir) file_path1 = os.path.join(os.path.dirname(__file__), "4x8.png") self.file1 = self.File(open(file_path1, "rb"), name="4x8.png") - + self.addCleanup(self.file1.close) file_path2 = os.path.join(os.path.dirname(__file__), "8x4.png") self.file2 = self.File(open(file_path2, "rb"), name="8x4.png") - - def tearDown(self): - """ - Removes temp directory and all its contents. - """ - self.file1.close() - self.file2.close() - shutil.rmtree(temp_storage_dir) + self.addCleanup(self.file2.close) def check_dimensions(self, instance, width, height, field_name="mugshot"): """ diff --git a/tests/sessions_tests/tests.py b/tests/sessions_tests/tests.py index 5a80a7b211df..7e0677d08d98 100644 --- a/tests/sessions_tests/tests.py +++ b/tests/sessions_tests/tests.py @@ -49,12 +49,10 @@ class SessionTestsMixin: def setUp(self): self.session = self.backend() - - def tearDown(self): # NB: be careful to delete any sessions created; stale sessions fill up # the /tmp (with some backends) and eventually overwhelm it after lots # of runs (think buildbots) - self.session.delete() + self.addCleanup(self.session.delete) def test_new_session(self): self.assertIs(self.session.modified, False) @@ -532,6 +530,7 @@ def setUp(self): # Do file session tests in an isolated directory, and kill it after we're done. self.original_session_file_path = settings.SESSION_FILE_PATH self.temp_session_store = settings.SESSION_FILE_PATH = self.mkdtemp() + self.addCleanup(shutil.rmtree, self.temp_session_store) # Reset the file session backend's internal caches if hasattr(self.backend, "_storage_path"): del self.backend._storage_path @@ -540,7 +539,6 @@ def setUp(self): def tearDown(self): super().tearDown() settings.SESSION_FILE_PATH = self.original_session_file_path - shutil.rmtree(self.temp_session_store) def mkdtemp(self): return tempfile.mkdtemp() diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index b2044878c98f..4fc35689d615 100644 --- a/tests/settings_tests/tests.py +++ b/tests/settings_tests/tests.py @@ -156,9 +156,7 @@ class SettingsTests(SimpleTestCase): def setUp(self): self.testvalue = None signals.setting_changed.connect(self.signal_callback) - - def tearDown(self): - signals.setting_changed.disconnect(self.signal_callback) + self.addCleanup(signals.setting_changed.disconnect, self.signal_callback) def signal_callback(self, sender, setting, value, **kwargs): if setting == "TEST": diff --git a/tests/sites_tests/tests.py b/tests/sites_tests/tests.py index f0ac9dc2ece6..4f5b07ee8f2f 100644 --- a/tests/sites_tests/tests.py +++ b/tests/sites_tests/tests.py @@ -27,9 +27,7 @@ def setUpTestData(cls): def setUp(self): Site.objects.clear_cache() - - def tearDown(self): - Site.objects.clear_cache() + self.addCleanup(Site.objects.clear_cache) def test_site_manager(self): # Make sure that get_current() does not return a deleted Site object. diff --git a/tests/staticfiles_tests/cases.py b/tests/staticfiles_tests/cases.py index 5e8b150b331c..a4816bc09a38 100644 --- a/tests/staticfiles_tests/cases.py +++ b/tests/staticfiles_tests/cases.py @@ -71,16 +71,13 @@ def setUp(self): temp_dir = self.mkdtemp() # Override the STATIC_ROOT for all tests from setUp to tearDown # rather than as a context manager - self.patched_settings = self.settings(STATIC_ROOT=temp_dir) - self.patched_settings.enable() + patched_settings = self.settings(STATIC_ROOT=temp_dir) + patched_settings.enable() if self.run_collectstatic_in_setUp: self.run_collectstatic() # Same comment as in runtests.teardown. self.addCleanup(shutil.rmtree, temp_dir) - - def tearDown(self): - self.patched_settings.disable() - super().tearDown() + self.addCleanup(patched_settings.disable) def mkdtemp(self): return tempfile.mkdtemp() diff --git a/tests/staticfiles_tests/test_management.py b/tests/staticfiles_tests/test_management.py index 46e6f3e76486..c0d381738393 100644 --- a/tests/staticfiles_tests/test_management.py +++ b/tests/staticfiles_tests/test_management.py @@ -468,18 +468,14 @@ def setUp(self): os.utime(self.testfile_path, (self.orig_atime - 1, self.orig_mtime - 1)) - self.settings_with_test_app = self.modify_settings( + settings_with_test_app = self.modify_settings( INSTALLED_APPS={"prepend": "staticfiles_test_app"}, ) with extend_sys_path(self.temp_dir): - self.settings_with_test_app.enable() - + settings_with_test_app.enable() + self.addCleanup(settings_with_test_app.disable) super().setUp() - def tearDown(self): - super().tearDown() - self.settings_with_test_app.disable() - def test_ordering_override(self): """ Test if collectstatic takes files in proper order diff --git a/tests/staticfiles_tests/test_storage.py b/tests/staticfiles_tests/test_storage.py index ea2293ccdee6..1e537dfe549a 100644 --- a/tests/staticfiles_tests/test_storage.py +++ b/tests/staticfiles_tests/test_storage.py @@ -417,16 +417,15 @@ def setUp(self): with open(self._clear_filename, "w") as f: f.write("to be deleted in one test") - self.patched_settings = self.settings( + patched_settings = self.settings( STATICFILES_DIRS=settings.STATICFILES_DIRS + [temp_dir], ) - self.patched_settings.enable() + patched_settings.enable() + self.addCleanup(patched_settings.disable) self.addCleanup(shutil.rmtree, temp_dir) self._manifest_strict = storage.staticfiles_storage.manifest_strict def tearDown(self): - self.patched_settings.disable() - if os.path.exists(self._clear_filename): os.unlink(self._clear_filename) @@ -702,13 +701,13 @@ def __init__(self, *args, manifest_storage=None, **kwargs): class TestCustomManifestStorage(SimpleTestCase): def setUp(self): - self.manifest_path = Path(tempfile.mkdtemp()) - self.addCleanup(shutil.rmtree, self.manifest_path) + manifest_path = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, manifest_path) self.staticfiles_storage = CustomManifestStorage( - manifest_location=self.manifest_path, + manifest_location=manifest_path, ) - self.manifest_file = self.manifest_path / self.staticfiles_storage.manifest_name + self.manifest_file = manifest_path / self.staticfiles_storage.manifest_name # Manifest without paths. self.manifest = {"version": self.staticfiles_storage.manifest_version} with self.manifest_file.open("w") as manifest_file: @@ -762,13 +761,10 @@ class TestStaticFilePermissions(CollectionTestCase): def setUp(self): self.umask = 0o027 - self.old_umask = os.umask(self.umask) + old_umask = os.umask(self.umask) + self.addCleanup(os.umask, old_umask) super().setUp() - def tearDown(self): - os.umask(self.old_umask) - super().tearDown() - # Don't run collectstatic command in this test class. def run_collectstatic(self, **kwargs): pass diff --git a/tests/template_tests/syntax_tests/i18n/base.py b/tests/template_tests/syntax_tests/i18n/base.py index 6197179ee425..ff21405e5e85 100644 --- a/tests/template_tests/syntax_tests/i18n/base.py +++ b/tests/template_tests/syntax_tests/i18n/base.py @@ -19,6 +19,4 @@ class MultipleLocaleActivationTestCase(SimpleTestCase): def setUp(self): self._old_language = get_language() - - def tearDown(self): - activate(self._old_language) + self.addCleanup(activate, self._old_language) diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index 2419eb47f2e7..9fe8c58593bb 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -259,12 +259,10 @@ class AtomicWithoutAutocommitTests(AtomicTests): def setUp(self): transaction.set_autocommit(False) - - def tearDown(self): + self.addCleanup(transaction.set_autocommit, True) # The tests access the database after exercising 'atomic', initiating # a transaction ; a rollback is required before restoring autocommit. - transaction.rollback() - transaction.set_autocommit(True) + self.addCleanup(transaction.rollback) @skipUnlessDBFeature("uses_savepoints") @@ -512,10 +510,8 @@ class NonAutocommitTests(TransactionTestCase): def setUp(self): transaction.set_autocommit(False) - - def tearDown(self): - transaction.rollback() - transaction.set_autocommit(True) + self.addCleanup(transaction.set_autocommit, True) + self.addCleanup(transaction.rollback) def test_orm_query_after_error_and_rollback(self): """ diff --git a/tests/utils_tests/test_archive.py b/tests/utils_tests/test_archive.py index 8cd107063f9c..89a45bc072ad 100644 --- a/tests/utils_tests/test_archive.py +++ b/tests/utils_tests/test_archive.py @@ -26,11 +26,9 @@ class TestArchive(unittest.TestCase): def setUp(self): self.testdir = os.path.join(os.path.dirname(__file__), "archives") - self.old_cwd = os.getcwd() + old_cwd = os.getcwd() os.chdir(self.testdir) - - def tearDown(self): - os.chdir(self.old_cwd) + self.addCleanup(os.chdir, old_cwd) def test_extract_function(self): with os.scandir(self.testdir) as entries: diff --git a/tests/utils_tests/test_autoreload.py b/tests/utils_tests/test_autoreload.py index fd3350649905..83f3e3898faf 100644 --- a/tests/utils_tests/test_autoreload.py +++ b/tests/utils_tests/test_autoreload.py @@ -315,14 +315,12 @@ def test_common_roots(self): class TestSysPathDirectories(SimpleTestCase): def setUp(self): - self._directory = tempfile.TemporaryDirectory() - self.directory = Path(self._directory.name).resolve(strict=True).absolute() + _directory = tempfile.TemporaryDirectory() + self.addCleanup(_directory.cleanup) + self.directory = Path(_directory.name).resolve(strict=True).absolute() self.file = self.directory / "test" self.file.touch() - def tearDown(self): - self._directory.cleanup() - def test_sys_paths_with_directories(self): with extend_sys_path(str(self.file)): paths = list(autoreload.sys_path_directories()) @@ -542,15 +540,13 @@ class ReloaderTests(SimpleTestCase): RELOADER_CLS = None def setUp(self): - self._tempdir = tempfile.TemporaryDirectory() - self.tempdir = Path(self._tempdir.name).resolve(strict=True).absolute() + _tempdir = tempfile.TemporaryDirectory() + self.tempdir = Path(_tempdir.name).resolve(strict=True).absolute() self.existing_file = self.ensure_file(self.tempdir / "test.py") self.nonexistent_file = (self.tempdir / "does_not_exist.py").absolute() self.reloader = self.RELOADER_CLS() - - def tearDown(self): - self._tempdir.cleanup() - self.reloader.stop() + self.addCleanup(self.reloader.stop) + self.addCleanup(_tempdir.cleanup) def ensure_file(self, path): path.parent.mkdir(exist_ok=True, parents=True) diff --git a/tests/utils_tests/test_dateformat.py b/tests/utils_tests/test_dateformat.py index dce678e1720e..33e2c5733343 100644 --- a/tests/utils_tests/test_dateformat.py +++ b/tests/utils_tests/test_dateformat.py @@ -10,11 +10,9 @@ @override_settings(TIME_ZONE="Europe/Copenhagen") class DateFormatTests(SimpleTestCase): def setUp(self): - self._orig_lang = translation.get_language() + _orig_lang = translation.get_language() translation.activate("en-us") - - def tearDown(self): - translation.activate(self._orig_lang) + self.addCleanup(translation.activate, _orig_lang) def test_date(self): d = date(2009, 5, 16) diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index 65f9db89bfe6..54c5da056e1e 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -1904,9 +1904,7 @@ class CustomExceptionReporterFilter(SafeExceptionReporterFilter): class CustomExceptionReporterFilterTests(SimpleTestCase): def setUp(self): get_default_exception_reporter_filter.cache_clear() - - def tearDown(self): - get_default_exception_reporter_filter.cache_clear() + self.addCleanup(get_default_exception_reporter_filter.cache_clear) def test_setting_allows_custom_subclass(self): self.assertIsInstance( diff --git a/tests/wsgi/tests.py b/tests/wsgi/tests.py index b1b5a7d00ef1..39d72d76971c 100644 --- a/tests/wsgi/tests.py +++ b/tests/wsgi/tests.py @@ -14,9 +14,7 @@ class WSGITest(SimpleTestCase): def setUp(self): request_started.disconnect(close_old_connections) - - def tearDown(self): - request_started.connect(close_old_connections) + self.addCleanup(request_started.connect, close_old_connections) def test_get_wsgi_application(self): """ From 39a00f39c55b4dbb6699fbb885b989990a7b5c39 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Tue, 12 Dec 2023 16:16:57 +0000 Subject: [PATCH 287/748] Added note about SELECT index in GROUP BY on Oracle 23c. As this isn't enabled by default and would be unsafe to enforce, just add a comment to note that this has to stay disabled. https://docs.oracle.com/en/database/oracle/oracle-database/23/nfcoa/application-development.html#GUID-EDDF041F-C10D-4334-838A-706227D7BFE0 --- django/db/backends/oracle/features.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py index f7937f24e359..b3531a7cc717 100644 --- a/django/db/backends/oracle/features.py +++ b/django/db/backends/oracle/features.py @@ -8,6 +8,9 @@ class DatabaseFeatures(BaseDatabaseFeatures): # Oracle crashes with "ORA-00932: inconsistent datatypes: expected - got # BLOB" when grouping by LOBs (#24096). allows_group_by_lob = False + # Although GROUP BY select index is supported by Oracle 23c+, it requires + # GROUP_BY_POSITION_ENABLED to be enabled to avoid backward compatibility + # issues. Introspection of this settings is not straightforward. allows_group_by_select_index = False interprets_empty_strings_as_nulls = True has_select_for_update = True From a816efe238eaeb88fd0a53b124c6337bfa60b5a6 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Tue, 12 Dec 2023 17:21:21 +0000 Subject: [PATCH 288/748] Supported native aggregation over INTERVALs on Oracle 23c. https://docs.oracle.com/en/database/oracle/oracle-database/23/nfcoa/application-development.html#GUID-CE5F8EED-934D-458D-B81C-6C8D617F31A2 --- django/db/backends/oracle/features.py | 4 ++++ django/db/models/functions/mixins.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py index b3531a7cc717..05632bb10e51 100644 --- a/django/db/backends/oracle/features.py +++ b/django/db/backends/oracle/features.py @@ -179,3 +179,7 @@ def supports_frame_exclusion(self): @cached_property def supports_boolean_expr_in_select_clause(self): return self.connection.oracle_version >= (23,) + + @cached_property + def supports_aggregation_over_interval_types(self): + return self.connection.oracle_version >= (23,) diff --git a/django/db/models/functions/mixins.py b/django/db/models/functions/mixins.py index caf20e131d87..661eee1c134c 100644 --- a/django/db/models/functions/mixins.py +++ b/django/db/models/functions/mixins.py @@ -31,7 +31,10 @@ def as_mysql(self, compiler, connection, **extra_context): return sql, params def as_oracle(self, compiler, connection, **extra_context): - if self.output_field.get_internal_type() == "DurationField": + if ( + self.output_field.get_internal_type() == "DurationField" + and not connection.features.supports_aggregation_over_interval_types + ): expression = self.get_source_expressions()[0] options = self._get_repr_options() from django.db.backends.oracle.functions import ( From c72001644fa794b82fa88a7d2ecc20197b01b6f2 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Tue, 12 Dec 2023 16:07:31 +0000 Subject: [PATCH 289/748] Updated DatabaseFeatures.bare_select_suffix on Oracle 23c. https://docs.oracle.com/en/database/oracle/oracle-database/23/nfcoa/application-development.html#GUID-4EB70EB9-4EE3-4FE2-99C4-86F7AAC60F12 --- django/db/backends/oracle/features.py | 8 ++++++-- django/db/backends/oracle/operations.py | 7 +++++-- django/db/models/functions/text.py | 5 +++-- tests/backends/oracle/tests.py | 3 ++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py index 05632bb10e51..002e03e2b534 100644 --- a/django/db/backends/oracle/features.py +++ b/django/db/backends/oracle/features.py @@ -37,7 +37,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): requires_literal_defaults = True supports_default_keyword_in_bulk_insert = False closed_cursor_error_class = InterfaceError - bare_select_suffix = " FROM DUAL" # Select for update with limit can be achieved on Oracle, but not with the # current backend. supports_select_for_update_with_limit = False @@ -159,9 +158,10 @@ def test_collations(self): @cached_property def supports_collation_on_charfield(self): + sql = "SELECT CAST('a' AS VARCHAR2(4001))" + self.bare_select_suffix with self.connection.cursor() as cursor: try: - cursor.execute("SELECT CAST('a' AS VARCHAR2(4001)) FROM dual") + cursor.execute(sql) except DatabaseError as e: if e.args[0].code == 910: return False @@ -183,3 +183,7 @@ def supports_boolean_expr_in_select_clause(self): @cached_property def supports_aggregation_over_interval_types(self): return self.connection.oracle_version >= (23,) + + @cached_property + def bare_select_suffix(self): + return "" if self.connection.oracle_version >= (23,) else " FROM DUAL" diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index 11043a9b51ba..9e8172b80a10 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -54,7 +54,7 @@ class DatabaseOperations(BaseDatabaseOperations): SELECT NVL(last_number - cache_size, 0) INTO seq_value FROM user_sequences WHERE sequence_name = seq_name; WHILE table_value > seq_value LOOP - EXECUTE IMMEDIATE 'SELECT "'||seq_name||'".nextval FROM DUAL' + EXECUTE IMMEDIATE 'SELECT "'||seq_name||'".nextval%(suffix)s' INTO seq_value; END LOOP; END; @@ -527,6 +527,7 @@ def sequence_reset_by_name_sql(self, style, sequences): "column": column, "table_name": strip_quotes(table), "column_name": strip_quotes(column), + "suffix": self.connection.features.bare_select_suffix, } sql.append(query) return sql @@ -550,6 +551,7 @@ def sequence_reset_sql(self, style, model_list): "column": column, "table_name": strip_quotes(table), "column_name": strip_quotes(column), + "suffix": self.connection.features.bare_select_suffix, } ) # Only one AutoField is allowed per model, so don't @@ -683,7 +685,8 @@ def bulk_insert_sql(self, fields, placeholder_rows): if not query: placeholder = "%s col_%s" % (placeholder, i) select.append(placeholder) - query.append("SELECT %s FROM DUAL" % ", ".join(select)) + suffix = self.connection.features.bare_select_suffix + query.append(f"SELECT %s{suffix}" % ", ".join(select)) # Bulk insert to tables with Oracle identity columns causes Oracle to # add sequence.nextval to it. Sequence.nextval cannot be used with the # UNION operator. To prevent incorrect SQL, move UNION to a subquery. diff --git a/django/db/models/functions/text.py b/django/db/models/functions/text.py index 500fbea19403..392061880c72 100644 --- a/django/db/models/functions/text.py +++ b/django/db/models/functions/text.py @@ -261,13 +261,14 @@ class Reverse(Transform): def as_oracle(self, compiler, connection, **extra_context): # REVERSE in Oracle is undocumented and doesn't support multi-byte # strings. Use a special subquery instead. + suffix = connection.features.bare_select_suffix sql, params = super().as_sql( compiler, connection, template=( "(SELECT LISTAGG(s) WITHIN GROUP (ORDER BY n DESC) FROM " - "(SELECT LEVEL n, SUBSTR(%(expressions)s, LEVEL, 1) s " - "FROM DUAL CONNECT BY LEVEL <= LENGTH(%(expressions)s)) " + f"(SELECT LEVEL n, SUBSTR(%(expressions)s, LEVEL, 1) s{suffix} " + "CONNECT BY LEVEL <= LENGTH(%(expressions)s)) " "GROUP BY %(expressions)s)" ), **extra_context, diff --git a/tests/backends/oracle/tests.py b/tests/backends/oracle/tests.py index 1ad98a88cf21..a4aa26cd2edd 100644 --- a/tests/backends/oracle/tests.py +++ b/tests/backends/oracle/tests.py @@ -43,8 +43,9 @@ def test_order_of_nls_parameters(self): An 'almost right' datetime works with configured NLS parameters (#18465). """ + suffix = connection.features.bare_select_suffix with connection.cursor() as cursor: - query = "select 1 from dual where '1936-12-29 00:00' < sysdate" + query = f"SELECT 1{suffix} WHERE '1936-12-29 00:00' < SYSDATE" # The query succeeds without errors - pre #18465 this # wasn't the case. cursor.execute(query) From 8fcd7b01eec85a509762dd8dbb3a27b7ab521e94 Mon Sep 17 00:00:00 2001 From: Zowie Beha <113861530+1zzowiebeha@users.noreply.github.com> Date: Sat, 30 Dec 2023 16:54:34 -0500 Subject: [PATCH 290/748] Fixed #35072 -- Corrected Field.choices description in models topic. --- docs/topics/db/models.txt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index cc6c1f5298d0..b419185bbcf1 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -157,9 +157,12 @@ ones: `, the field will be required. :attr:`~Field.choices` - A :term:`sequence` of 2-tuples to use as choices for this field. If this - is given, the default form widget will be a select box instead of the - standard text field and will limit choices to the choices given. + A :term:`sequence` of 2-value tuples, a :term:`mapping`, an + :ref:`enumeration type `, or a callable (that + expects no arguments and returns any of the previous formats), to use as + choices for this field. If this is given, the default form widget will be a + select box instead of the standard text field and will limit choices to the + choices given. A choices list looks like this:: @@ -216,6 +219,10 @@ ones: Further examples are available in the :ref:`model field reference `. + .. versionchanged:: 5.0 + + Support for mappings and callables was added. + :attr:`~Field.default` The default value for the field. This can be a value or a callable object. If callable it will be called every time a new object is From e29d1870dd2b44f1b12c4ddf29b3fd24a903f7fd Mon Sep 17 00:00:00 2001 From: Michael <6431441+expert-m@users.noreply.github.com> Date: Tue, 2 Jan 2024 07:30:16 +0300 Subject: [PATCH 291/748] Improved variable names in QuerySet.delete(). --- django/db/models/query.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index de00bba8d75a..61d400200056 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1183,11 +1183,11 @@ def delete(self): collector = Collector(using=del_query.db, origin=self) collector.collect(del_query) - deleted, _rows_count = collector.delete() + num_deleted, num_deleted_per_model = collector.delete() # Clear the result cache, in case this QuerySet gets reused. self._result_cache = None - return deleted, _rows_count + return num_deleted, num_deleted_per_model delete.alters_data = True delete.queryset_only = True From 3915d4c70d0d7673abe675525b58117a5099afd3 Mon Sep 17 00:00:00 2001 From: Salvo Polizzi Date: Sun, 31 Dec 2023 10:07:13 +0100 Subject: [PATCH 292/748] Fixed #35060 -- Deprecated passing positional arguments to Model.save()/asave(). --- django/contrib/auth/base_user.py | 3 ++ django/db/models/base.py | 63 +++++++++++++++++++++++++++++++- docs/internals/deprecation.txt | 3 ++ docs/ref/models/instances.txt | 12 ++++-- docs/releases/5.1.txt | 3 ++ docs/topics/db/models.txt | 40 +++++++++----------- tests/basic/tests.py | 46 +++++++++++++++++++++++ tests/model_forms/models.py | 2 +- 8 files changed, 142 insertions(+), 30 deletions(-) diff --git a/django/contrib/auth/base_user.py b/django/contrib/auth/base_user.py index aa8e9f8a849a..ccfe19fcc1ab 100644 --- a/django/contrib/auth/base_user.py +++ b/django/contrib/auth/base_user.py @@ -54,6 +54,9 @@ class Meta: def __str__(self): return self.get_username() + # RemovedInDjango60Warning: When the deprecation ends, replace with: + # def save(self, **kwargs): + # super().save(**kwargs) def save(self, *args, **kwargs): super().save(*args, **kwargs) if self._password is not None: diff --git a/django/db/models/base.py b/django/db/models/base.py index 4a9150bf37fa..46ac762ccd46 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -49,6 +49,7 @@ pre_save, ) from django.db.models.utils import AltersData, make_model_tuple +from django.utils.deprecation import RemovedInDjango60Warning from django.utils.encoding import force_str from django.utils.hashable import make_hashable from django.utils.text import capfirst, get_text_list @@ -764,8 +765,17 @@ def serializable_value(self, field_name): return getattr(self, field_name) return getattr(self, field.attname) + # RemovedInDjango60Warning: When the deprecation ends, replace with: + # def save( + # self, *, force_insert=False, force_update=False, using=None, update_fields=None, + # ): def save( - self, force_insert=False, force_update=False, using=None, update_fields=None + self, + *args, + force_insert=False, + force_update=False, + using=None, + update_fields=None, ): """ Save the current instance. Override this in a subclass if you want to @@ -775,6 +785,26 @@ def save( that the "save" must be an SQL insert or update (or equivalent for non-SQL backends), respectively. Normally, they should not be set. """ + # RemovedInDjango60Warning. + if args: + warnings.warn( + "Passing positional arguments to save() is deprecated", + RemovedInDjango60Warning, + stacklevel=2, + ) + for arg, attr in zip( + args, ["force_insert", "force_update", "using", "update_fields"] + ): + if arg: + if attr == "force_insert": + force_insert = arg + elif attr == "force_update": + force_update = arg + elif attr == "using": + using = arg + else: + update_fields = arg + self._prepare_related_fields_for_save(operation_name="save") using = using or router.db_for_write(self.__class__, instance=self) @@ -828,9 +858,38 @@ def save( save.alters_data = True + # RemovedInDjango60Warning: When the deprecation ends, replace with: + # async def asave( + # self, *, force_insert=False, force_update=False, using=None, update_fields=None, + # ): async def asave( - self, force_insert=False, force_update=False, using=None, update_fields=None + self, + *args, + force_insert=False, + force_update=False, + using=None, + update_fields=None, ): + # RemovedInDjango60Warning. + if args: + warnings.warn( + "Passing positional arguments to asave() is deprecated", + RemovedInDjango60Warning, + stacklevel=2, + ) + for arg, attr in zip( + args, ["force_insert", "force_update", "using", "update_fields"] + ): + if arg: + if attr == "force_insert": + force_insert = arg + elif attr == "force_update": + force_update = arg + elif attr == "using": + using = arg + else: + update_fields = arg + return await sync_to_async(self.save)( force_insert=force_insert, force_update=force_update, diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index a1b00c364a33..07e0f46856a2 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -68,6 +68,9 @@ details on these changes. * The ``django.contrib.gis.geoip2.GeoIP2.open()`` method will be removed. +* Support for passing positional arguments to ``Model.save()`` and + ``Model.asave()`` will be removed. + .. _deprecation-removed-in-5.1: 5.1 diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 81f9bfb43324..45af7f244fc1 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -116,7 +116,7 @@ are loaded from the database:: return instance - def save(self, *args, **kwargs): + def save(self, **kwargs): # Check how the current values differ from ._loaded_values. For example, # prevent changing the creator_id of the model. (This example doesn't # support cases where 'creator_id' is deferred). @@ -124,7 +124,7 @@ are loaded from the database:: self.creator_id != self._loaded_values["creator_id"] ): raise ValueError("Updating the value of creator isn't allowed") - super().save(*args, **kwargs) + super().save(**kwargs) The example above shows a full ``from_db()`` implementation to clarify how that is done. In this case it would be possible to use a ``super()`` call in the @@ -410,8 +410,8 @@ Saving objects To save an object back to the database, call ``save()``: -.. method:: Model.save(force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None) -.. method:: Model.asave(force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None) +.. method:: Model.save(*, force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None) +.. method:: Model.asave(*, force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None) *Asynchronous version*: ``asave()`` @@ -424,6 +424,10 @@ method. See :ref:`overriding-model-methods` for more details. The model save process also has some subtleties; see the sections below. +.. deprecated:: 5.1 + + Support for positional arguments is deprecated. + Auto-incrementing primary keys ------------------------------ diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index b825e9be4f2a..539ff566a351 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -331,6 +331,9 @@ Miscellaneous * The ``django.contrib.gis.geoip2.GeoIP2.open()`` method is deprecated. Use the :class:`~django.contrib.gis.geoip2.GeoIP2` constructor instead. +* Passing positional arguments to :meth:`.Model.save` and :meth:`.Model.asave` + is deprecated in favor of keyword-only arguments. + Features removed in 5.1 ======================= diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index b419185bbcf1..244e9bbb16d1 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -868,9 +868,9 @@ to happen whenever you save an object. For example (see name = models.CharField(max_length=100) tagline = models.TextField() - def save(self, *args, **kwargs): + def save(self, **kwargs): do_something() - super().save(*args, **kwargs) # Call the "real" save() method. + super().save(**kwargs) # Call the "real" save() method. do_something_else() You can also prevent saving:: @@ -882,24 +882,23 @@ You can also prevent saving:: name = models.CharField(max_length=100) tagline = models.TextField() - def save(self, *args, **kwargs): + def save(self, **kwargs): if self.name == "Yoko Ono's blog": return # Yoko shall never have her own blog! else: - super().save(*args, **kwargs) # Call the "real" save() method. + super().save(**kwargs) # Call the "real" save() method. It's important to remember to call the superclass method -- that's -that ``super().save(*args, **kwargs)`` business -- to ensure -that the object still gets saved into the database. If you forget to -call the superclass method, the default behavior won't happen and the -database won't get touched. +that ``super().save(**kwargs)`` business -- to ensure that the object still +gets saved into the database. If you forget to call the superclass method, the +default behavior won't happen and the database won't get touched. It's also important that you pass through the arguments that can be -passed to the model method -- that's what the ``*args, **kwargs`` bit -does. Django will, from time to time, extend the capabilities of -built-in model methods, adding new arguments. If you use ``*args, -**kwargs`` in your method definitions, you are guaranteed that your -code will automatically support those arguments when they are added. +passed to the model method -- that's what the ``**kwargs`` bit does. Django +will, from time to time, extend the capabilities of built-in model methods, +adding new keyword arguments. If you use ``**kwargs`` in your method +definitions, you are guaranteed that your code will automatically support those +arguments when they are added. If you wish to update a field value in the :meth:`~Model.save` method, you may also want to have this field added to the ``update_fields`` keyword argument. @@ -914,18 +913,13 @@ example:: name = models.CharField(max_length=100) slug = models.TextField() - def save( - self, force_insert=False, force_update=False, using=None, update_fields=None - ): + def save(self, **kwargs): self.slug = slugify(self.name) - if update_fields is not None and "name" in update_fields: + if ( + update_fields := kwargs.get("update_fields") + ) is not None and "name" in update_fields: update_fields = {"slug"}.union(update_fields) - super().save( - force_insert=force_insert, - force_update=force_update, - using=using, - update_fields=update_fields, - ) + super().save(**kwargs) See :ref:`ref-models-update-fields` for more details. diff --git a/tests/basic/tests.py b/tests/basic/tests.py index ad82cffe8c21..990549edfc4b 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -13,6 +13,8 @@ TransactionTestCase, skipUnlessDBFeature, ) +from django.test.utils import ignore_warnings +from django.utils.deprecation import RemovedInDjango60Warning from django.utils.translation import gettext_lazy from .models import ( @@ -187,6 +189,50 @@ def test_save_parent_primary_with_default(self): with self.assertNumQueries(2): ChildPrimaryKeyWithDefault().save() + def test_save_deprecation(self): + a = Article(headline="original", pub_date=datetime(2014, 5, 16)) + msg = "Passing positional arguments to save() is deprecated" + with self.assertWarnsMessage(RemovedInDjango60Warning, msg): + a.save(False, False, None, None) + self.assertEqual(Article.objects.count(), 1) + + async def test_asave_deprecation(self): + a = Article(headline="original", pub_date=datetime(2014, 5, 16)) + msg = "Passing positional arguments to asave() is deprecated" + with self.assertWarnsMessage(RemovedInDjango60Warning, msg): + await a.asave(False, False, None, None) + self.assertEqual(await Article.objects.acount(), 1) + + @ignore_warnings(category=RemovedInDjango60Warning) + def test_save_positional_arguments(self): + a = Article.objects.create(headline="original", pub_date=datetime(2014, 5, 16)) + a.headline = "changed" + + a.save(False, False, None, ["pub_date"]) + a.refresh_from_db() + self.assertEqual(a.headline, "original") + + a.headline = "changed" + a.save(False, False, None, ["pub_date", "headline"]) + a.refresh_from_db() + self.assertEqual(a.headline, "changed") + + @ignore_warnings(category=RemovedInDjango60Warning) + async def test_asave_positional_arguments(self): + a = await Article.objects.acreate( + headline="original", pub_date=datetime(2014, 5, 16) + ) + a.headline = "changed" + + await a.asave(False, False, None, ["pub_date"]) + await a.arefresh_from_db() + self.assertEqual(a.headline, "original") + + a.headline = "changed" + await a.asave(False, False, None, ["pub_date", "headline"]) + await a.arefresh_from_db() + self.assertEqual(a.headline, "changed") + class ModelTest(TestCase): def test_objects_attribute_is_only_available_on_the_class_itself(self): diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py index b6da15f48ac8..c28461d8627f 100644 --- a/tests/model_forms/models.py +++ b/tests/model_forms/models.py @@ -463,7 +463,7 @@ def __init__(self, *args, **kwargs): self._savecount = 0 def save(self, force_insert=False, force_update=False): - super().save(force_insert, force_update) + super().save(force_insert=force_insert, force_update=force_update) self._savecount += 1 From f82a2c3b3d553f36661cfdce5261bffb669d68a9 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Tue, 2 Jan 2024 09:57:41 +0100 Subject: [PATCH 293/748] Added release date for 5.0.1 and 4.2.9. --- docs/releases/4.2.9.txt | 2 +- docs/releases/5.0.1.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/releases/4.2.9.txt b/docs/releases/4.2.9.txt index 5202fbb6e77e..9a1e7dc19857 100644 --- a/docs/releases/4.2.9.txt +++ b/docs/releases/4.2.9.txt @@ -2,7 +2,7 @@ Django 4.2.9 release notes ========================== -*Expected January 2, 2024* +*January 2, 2024* Django 4.2.9 fixes a bug in 4.2.8. diff --git a/docs/releases/5.0.1.txt b/docs/releases/5.0.1.txt index 4344b0d1818d..8d2bd6a18732 100644 --- a/docs/releases/5.0.1.txt +++ b/docs/releases/5.0.1.txt @@ -2,7 +2,7 @@ Django 5.0.1 release notes ========================== -*Expected January 2, 2024* +*January 2, 2024* Django 5.0.1 fixes several bugs in 5.0. From f412add786dfc18424eee6281ec8cc97220b04fc Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Tue, 2 Jan 2024 10:28:34 +0100 Subject: [PATCH 294/748] Added stub release notes for 5.0.2. --- docs/releases/5.0.2.txt | 12 ++++++++++++ docs/releases/index.txt | 1 + 2 files changed, 13 insertions(+) create mode 100644 docs/releases/5.0.2.txt diff --git a/docs/releases/5.0.2.txt b/docs/releases/5.0.2.txt new file mode 100644 index 000000000000..4ba818dcb37a --- /dev/null +++ b/docs/releases/5.0.2.txt @@ -0,0 +1,12 @@ +========================== +Django 5.0.2 release notes +========================== + +*Expected February 5, 2024* + +Django 5.0.2 fixes several bugs in 5.0.1. + +Bugfixes +======== + +* ... diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 3233dbf5014b..8a70de5869fc 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -32,6 +32,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 5.0.2 5.0.1 5.0 From 45f778eded9dff59cfdd4dce8720daf87a08cfac Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Sat, 29 May 2021 00:53:18 +0100 Subject: [PATCH 295/748] Fixed #35075 -- Added deduplicate_items parameter to BTreeIndex. --- django/contrib/postgres/indexes.py | 9 +++++++- docs/ref/contrib/postgres/indexes.txt | 11 ++++++++- docs/releases/5.1.txt | 3 ++- docs/spelling_wordlist | 1 + tests/postgres_tests/test_indexes.py | 32 ++++++++++++++++++++++----- 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/django/contrib/postgres/indexes.py b/django/contrib/postgres/indexes.py index 363a9431ae1d..cc944ed33558 100644 --- a/django/contrib/postgres/indexes.py +++ b/django/contrib/postgres/indexes.py @@ -117,20 +117,27 @@ def get_with_params(self): class BTreeIndex(PostgresIndex): suffix = "btree" - def __init__(self, *expressions, fillfactor=None, **kwargs): + def __init__(self, *expressions, fillfactor=None, deduplicate_items=None, **kwargs): self.fillfactor = fillfactor + self.deduplicate_items = deduplicate_items super().__init__(*expressions, **kwargs) def deconstruct(self): path, args, kwargs = super().deconstruct() if self.fillfactor is not None: kwargs["fillfactor"] = self.fillfactor + if self.deduplicate_items is not None: + kwargs["deduplicate_items"] = self.deduplicate_items return path, args, kwargs def get_with_params(self): with_params = [] if self.fillfactor is not None: with_params.append("fillfactor = %d" % self.fillfactor) + if self.deduplicate_items is not None: + with_params.append( + "deduplicate_items = %s" % ("on" if self.deduplicate_items else "off") + ) return with_params diff --git a/docs/ref/contrib/postgres/indexes.txt b/docs/ref/contrib/postgres/indexes.txt index 5dfbef5c4c6e..73ef195309bb 100644 --- a/docs/ref/contrib/postgres/indexes.txt +++ b/docs/ref/contrib/postgres/indexes.txt @@ -46,14 +46,23 @@ available from the ``django.contrib.postgres.indexes`` module. ``BTreeIndex`` ============== -.. class:: BTreeIndex(*expressions, fillfactor=None, **options) +.. class:: BTreeIndex(*expressions, fillfactor=None, deduplicate_items=None, **options) Creates a B-Tree index. Provide an integer value from 10 to 100 to the fillfactor_ parameter to tune how packed the index pages will be. PostgreSQL's default is 90. + Provide a boolean value to the deduplicate_items_ parameter to control + whether deduplication is enabled. PostgreSQL enables deduplication by + default. + + .. versionchanged:: 5.1 + + The ``deduplicate_items`` parameter was added. + .. _fillfactor: https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-STORAGE-PARAMETERS + .. _deduplicate_items: https://www.postgresql.org/docs/current/btree-implementation.html#BTREE-DEDUPLICATION ``GinIndex`` ============ diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 539ff566a351..f949b31ad25b 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -63,7 +63,8 @@ Minor features :mod:`django.contrib.postgres` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* :class:`~django.contrib.postgres.indexes.BTreeIndex` now supports the + ``deduplicate_items`` parameter. :mod:`django.contrib.redirects` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index c217719a8a29..5828b24253bb 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -119,6 +119,7 @@ datetimes declaratively decrementing deduplicates +deduplication deepcopy deferrable deprecations diff --git a/tests/postgres_tests/test_indexes.py b/tests/postgres_tests/test_indexes.py index d063ac64a2d1..8a7ee39a76ea 100644 --- a/tests/postgres_tests/test_indexes.py +++ b/tests/postgres_tests/test_indexes.py @@ -143,12 +143,29 @@ def test_suffix(self): self.assertEqual(BTreeIndex.suffix, "btree") def test_deconstruction(self): - index = BTreeIndex(fields=["title"], name="test_title_btree", fillfactor=80) + index = BTreeIndex(fields=["title"], name="test_title_btree") + path, args, kwargs = index.deconstruct() + self.assertEqual(path, "django.contrib.postgres.indexes.BTreeIndex") + self.assertEqual(args, ()) + self.assertEqual(kwargs, {"fields": ["title"], "name": "test_title_btree"}) + + index = BTreeIndex( + fields=["title"], + name="test_title_btree", + fillfactor=80, + deduplicate_items=False, + ) path, args, kwargs = index.deconstruct() self.assertEqual(path, "django.contrib.postgres.indexes.BTreeIndex") self.assertEqual(args, ()) self.assertEqual( - kwargs, {"fields": ["title"], "name": "test_title_btree", "fillfactor": 80} + kwargs, + { + "fields": ["title"], + "name": "test_title_btree", + "fillfactor": 80, + "deduplicate_items": False, + }, ) @@ -455,13 +472,18 @@ def test_btree_index(self): ) def test_btree_parameters(self): - index_name = "integer_array_btree_fillfactor" - index = BTreeIndex(fields=["field"], name=index_name, fillfactor=80) + index_name = "integer_array_btree_parameters" + index = BTreeIndex( + fields=["field"], name=index_name, fillfactor=80, deduplicate_items=False + ) with connection.schema_editor() as editor: editor.add_index(CharFieldModel, index) constraints = self.get_constraints(CharFieldModel._meta.db_table) self.assertEqual(constraints[index_name]["type"], BTreeIndex.suffix) - self.assertEqual(constraints[index_name]["options"], ["fillfactor=80"]) + self.assertEqual( + constraints[index_name]["options"], + ["fillfactor=80", "deduplicate_items=off"], + ) with connection.schema_editor() as editor: editor.remove_index(CharFieldModel, index) self.assertNotIn( From 8fb0be3500cc7519a56985b1b6f415d75ac6fedb Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Sat, 23 Dec 2023 23:05:05 +0100 Subject: [PATCH 296/748] Fixed #33277 -- Disallowed database connections in threads in SimpleTestCase. --- django/test/testcases.py | 31 +++++++++++++++++++++++++++++++ docs/releases/5.1.txt | 3 +++ tests/test_utils/tests.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/django/test/testcases.py b/django/test/testcases.py index b5d426f75fe7..7382f7f0f096 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -10,6 +10,7 @@ from copy import copy, deepcopy from difflib import get_close_matches from functools import wraps +from unittest import mock from unittest.suite import _DebugResult from unittest.util import safe_repr from urllib.parse import ( @@ -37,6 +38,7 @@ from django.core.servers.basehttp import ThreadedWSGIServer, WSGIRequestHandler from django.core.signals import setting_changed from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction +from django.db.backends.base.base import NO_DB_ALIAS, BaseDatabaseWrapper from django.forms.fields import CharField from django.http import QueryDict from django.http.request import split_domain_port, validate_host @@ -255,6 +257,13 @@ def _add_databases_failures(cls): } method = getattr(connection, name) setattr(connection, name, _DatabaseFailure(method, message)) + cls.enterClassContext( + mock.patch.object( + BaseDatabaseWrapper, + "ensure_connection", + new=cls.ensure_connection_patch_method(), + ) + ) @classmethod def _remove_databases_failures(cls): @@ -266,6 +275,28 @@ def _remove_databases_failures(cls): method = getattr(connection, name) setattr(connection, name, method.wrapped) + @classmethod + def ensure_connection_patch_method(cls): + real_ensure_connection = BaseDatabaseWrapper.ensure_connection + + def patched_ensure_connection(self, *args, **kwargs): + if ( + self.connection is None + and self.alias not in cls.databases + and self.alias != NO_DB_ALIAS + ): + # Connection has not yet been established, but the alias is not allowed. + message = cls._disallowed_database_msg % { + "test": f"{cls.__module__}.{cls.__qualname__}", + "alias": self.alias, + "operation": "threaded connections", + } + return _DatabaseFailure(self.ensure_connection, message)() + + real_ensure_connection(self, *args, **kwargs) + + return patched_ensure_connection + def __call__(self, result=None): """ Wrapper around default __call__ method to perform common Django test diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index f949b31ad25b..544b1f5d0855 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -250,6 +250,9 @@ Tests * The new :meth:`.SimpleTestCase.assertNotInHTML` assertion allows testing that an HTML fragment is not contained in the given HTML haystack. +* In order to enforce test isolation, database connections inside threads are + no longer allowed in :class:`~django.test.SimpleTestCase`. + URLs ~~~~ diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index ce78ffc0084b..65a782bf87b6 100644 --- a/tests/test_utils/tests.py +++ b/tests/test_utils/tests.py @@ -1,5 +1,6 @@ import os import sys +import threading import unittest import warnings from io import StringIO @@ -2093,6 +2094,29 @@ def test_disallowed_database_chunked_cursor_queries(self): with self.assertRaisesMessage(DatabaseOperationForbidden, expected_message): next(Car.objects.iterator()) + def test_disallowed_thread_database_connection(self): + expected_message = ( + "Database threaded connections to 'default' are not allowed in " + "SimpleTestCase subclasses. Either subclass TestCase or TransactionTestCase" + " to ensure proper test isolation or add 'default' to " + "test_utils.tests.DisallowedDatabaseQueriesTests.databases to " + "silence this failure." + ) + + exceptions = [] + + def thread_func(): + try: + Car.objects.first() + except DatabaseOperationForbidden as e: + exceptions.append(e) + + t = threading.Thread(target=thread_func) + t.start() + t.join() + self.assertEqual(len(exceptions), 1) + self.assertEqual(exceptions[0].args[0], expected_message) + class AllowedDatabaseQueriesTests(SimpleTestCase): databases = {"default"} @@ -2103,6 +2127,14 @@ def test_allowed_database_queries(self): def test_allowed_database_chunked_cursor_queries(self): next(Car.objects.iterator(), None) + def test_allowed_threaded_database_queries(self): + def thread_func(): + next(Car.objects.iterator(), None) + + t = threading.Thread(target=thread_func) + t.start() + t.join() + class DatabaseAliasTests(SimpleTestCase): def setUp(self): From c65f49d3cb8709f2f694f78b4849bc7693e90416 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 3 Jan 2024 21:17:57 +0000 Subject: [PATCH 297/748] Refs #33690 -- Updated tutorial for admin dark mode toggle. --- docs/intro/tutorial07.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/intro/tutorial07.txt b/docs/intro/tutorial07.txt index f53d0b524c0d..9cdadbe28692 100644 --- a/docs/intro/tutorial07.txt +++ b/docs/intro/tutorial07.txt @@ -368,6 +368,9 @@ a section of code like: {% block branding %} + {% if user.is_anonymous %} + {% include "admin/color_theme_toggle.html" %} + {% endif %} {% endblock %} We use this approach to teach you how to override templates. In an actual From d89a465e62ad876cc7f1332d1712700cb81f3995 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 4 Jan 2024 04:35:46 +0000 Subject: [PATCH 298/748] Refs #34140 -- Fixed blacken-docs pre-commit configuration. Missed in 6015bab80e28aef2669f6fac53423aa65f70cb08. The default blacken-docs hook definition does not apply to .txt files, which the Django documentation uses. This commit overrides that definition to point blacken-docs at the appropriate files. --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6304b41cc9c2..d42a0dd21266 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,6 +10,7 @@ repos: - id: blacken-docs additional_dependencies: - black==23.10.0 + files: 'docs/.*\.txt$' - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: From 0c5456ef37a22e2ce0d31e7ebf99d9fa04a5b9c4 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Thu, 4 Jan 2024 05:55:29 +0100 Subject: [PATCH 299/748] Used enterClassContext() where appropriate. --- django/test/testcases.py | 14 ++---- tests/auth_tests/test_auth_backends.py | 12 +++-- tests/auth_tests/test_remote_user.py | 14 +++--- tests/forms_tests/tests/test_input_formats.py | 22 +++++---- tests/mail/tests.py | 8 ++-- tests/messages_tests/base.py | 47 ++++++++++--------- tests/staticfiles_tests/test_liveserver.py | 5 +- tests/utils_tests/test_dateformat.py | 8 ++-- 8 files changed, 65 insertions(+), 65 deletions(-) diff --git a/django/test/testcases.py b/django/test/testcases.py index 7382f7f0f096..ce681f287ce0 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -211,13 +211,9 @@ class SimpleTestCase(unittest.TestCase): def setUpClass(cls): super().setUpClass() if cls._overridden_settings: - cls._cls_overridden_context = override_settings(**cls._overridden_settings) - cls._cls_overridden_context.enable() - cls.addClassCleanup(cls._cls_overridden_context.disable) + cls.enterClassContext(override_settings(**cls._overridden_settings)) if cls._modified_settings: - cls._cls_modified_context = modify_settings(cls._modified_settings) - cls._cls_modified_context.enable() - cls.addClassCleanup(cls._cls_modified_context.disable) + cls.enterClassContext(modify_settings(cls._modified_settings)) cls._add_databases_failures() cls.addClassCleanup(cls._remove_databases_failures) @@ -1732,11 +1728,9 @@ def _make_connections_override(cls): @classmethod def setUpClass(cls): super().setUpClass() - cls._live_server_modified_settings = modify_settings( - ALLOWED_HOSTS={"append": cls.allowed_host}, + cls.enterClassContext( + modify_settings(ALLOWED_HOSTS={"append": cls.allowed_host}) ) - cls._live_server_modified_settings.enable() - cls.addClassCleanup(cls._live_server_modified_settings.disable) cls._start_server_thread() @classmethod diff --git a/tests/auth_tests/test_auth_backends.py b/tests/auth_tests/test_auth_backends.py index a7005de8a4d5..3b4f40e6e0f9 100644 --- a/tests/auth_tests/test_auth_backends.py +++ b/tests/auth_tests/test_auth_backends.py @@ -94,15 +94,17 @@ class BaseModelBackendTest: backend = "django.contrib.auth.backends.ModelBackend" + @classmethod + def setUpClass(cls): + cls.enterClassContext( + modify_settings(AUTHENTICATION_BACKENDS={"append": cls.backend}) + ) + super().setUpClass() + def setUp(self): # The custom_perms test messes with ContentTypes, which will be cached. # Flush the cache to ensure there are no side effects. self.addCleanup(ContentType.objects.clear_cache) - patched_settings = modify_settings( - AUTHENTICATION_BACKENDS={"append": self.backend}, - ) - patched_settings.enable() - self.addCleanup(patched_settings.disable) self.create_users() def test_has_perm(self): diff --git a/tests/auth_tests/test_remote_user.py b/tests/auth_tests/test_remote_user.py index 6066ab96e9cc..d3cf4b9da530 100644 --- a/tests/auth_tests/test_remote_user.py +++ b/tests/auth_tests/test_remote_user.py @@ -20,13 +20,15 @@ class RemoteUserTest(TestCase): known_user = "knownuser" known_user2 = "knownuser2" - def setUp(self): - patched_settings = modify_settings( - AUTHENTICATION_BACKENDS={"append": self.backend}, - MIDDLEWARE={"append": self.middleware}, + @classmethod + def setUpClass(cls): + cls.enterClassContext( + modify_settings( + AUTHENTICATION_BACKENDS={"append": cls.backend}, + MIDDLEWARE={"append": cls.middleware}, + ) ) - patched_settings.enable() - self.addCleanup(patched_settings.disable) + super().setUpClass() def test_no_remote_user(self): """Users are not created when remote user is not specified.""" diff --git a/tests/forms_tests/tests/test_input_formats.py b/tests/forms_tests/tests/test_input_formats.py index 7a0dfca8a70d..c5023d8d103d 100644 --- a/tests/forms_tests/tests/test_input_formats.py +++ b/tests/forms_tests/tests/test_input_formats.py @@ -4,15 +4,15 @@ from django.core.exceptions import ValidationError from django.test import SimpleTestCase, override_settings from django.utils import translation -from django.utils.translation import activate, deactivate class LocalizedTimeTests(SimpleTestCase): - def setUp(self): + @classmethod + def setUpClass(cls): # nl/formats.py has customized TIME_INPUT_FORMATS: # ['%H:%M:%S', '%H.%M:%S', '%H.%M', '%H:%M'] - activate("nl") - self.addCleanup(deactivate) + cls.enterClassContext(translation.override("nl")) + super().setUpClass() def test_timeField(self): "TimeFields can parse dates in the default format" @@ -319,9 +319,10 @@ def test_localized_timeField_with_inputformat(self): class LocalizedDateTests(SimpleTestCase): - def setUp(self): - activate("de") - self.addCleanup(deactivate) + @classmethod + def setUpClass(cls): + cls.enterClassContext(translation.override("de")) + super().setUpClass() def test_dateField(self): "DateFields can parse dates in the default format" @@ -631,9 +632,10 @@ def test_localized_dateField_with_inputformat(self): class LocalizedDateTimeTests(SimpleTestCase): - def setUp(self): - activate("de") - self.addCleanup(deactivate) + @classmethod + def setUpClass(cls): + cls.enterClassContext(translation.override("de")) + super().setUpClass() def test_dateTimeField(self): "DateTimeFields can parse dates in the default format" diff --git a/tests/mail/tests.py b/tests/mail/tests.py index 73eceafa466a..cd981b6e9ec1 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -1172,10 +1172,10 @@ def test_8bit_non_latin(self): class BaseEmailBackendTests(HeadersCheckMixin): email_backend = None - def setUp(self): - settings_override = override_settings(EMAIL_BACKEND=self.email_backend) - settings_override.enable() - self.addCleanup(settings_override.disable) + @classmethod + def setUpClass(cls): + cls.enterClassContext(override_settings(EMAIL_BACKEND=cls.email_backend)) + super().setUpClass() def assertStartsWith(self, first, second): if not first.startswith(second): diff --git a/tests/messages_tests/base.py b/tests/messages_tests/base.py index 34582b546255..ce4b2acac880 100644 --- a/tests/messages_tests/base.py +++ b/tests/messages_tests/base.py @@ -31,29 +31,32 @@ class BaseTests: "error": constants.ERROR, } - def setUp(self): - settings_override = override_settings( - TEMPLATES=[ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": ( - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ), - }, - } - ], - ROOT_URLCONF="messages_tests.urls", - MESSAGE_TAGS={}, - MESSAGE_STORAGE="%s.%s" - % (self.storage_class.__module__, self.storage_class.__name__), - SESSION_SERIALIZER="django.contrib.sessions.serializers.JSONSerializer", + @classmethod + def setUpClass(cls): + cls.enterClassContext( + override_settings( + TEMPLATES=[ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": ( + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ), + }, + } + ], + ROOT_URLCONF="messages_tests.urls", + MESSAGE_TAGS={}, + MESSAGE_STORAGE=( + f"{cls.storage_class.__module__}.{cls.storage_class.__name__}" + ), + SESSION_SERIALIZER="django.contrib.sessions.serializers.JSONSerializer", + ) ) - settings_override.enable() - self.addCleanup(settings_override.disable) + super().setUpClass() def get_request(self): return HttpRequest() diff --git a/tests/staticfiles_tests/test_liveserver.py b/tests/staticfiles_tests/test_liveserver.py index 714ebd1a8a58..0575d684c5f9 100644 --- a/tests/staticfiles_tests/test_liveserver.py +++ b/tests/staticfiles_tests/test_liveserver.py @@ -25,10 +25,7 @@ class LiveServerBase(StaticLiveServerTestCase): @classmethod def setUpClass(cls): - # Override settings - cls.settings_override = override_settings(**TEST_SETTINGS) - cls.settings_override.enable() - cls.addClassCleanup(cls.settings_override.disable) + cls.enterClassContext(override_settings(**TEST_SETTINGS)) super().setUpClass() diff --git a/tests/utils_tests/test_dateformat.py b/tests/utils_tests/test_dateformat.py index 33e2c5733343..dc330a710b35 100644 --- a/tests/utils_tests/test_dateformat.py +++ b/tests/utils_tests/test_dateformat.py @@ -9,10 +9,10 @@ @override_settings(TIME_ZONE="Europe/Copenhagen") class DateFormatTests(SimpleTestCase): - def setUp(self): - _orig_lang = translation.get_language() - translation.activate("en-us") - self.addCleanup(translation.activate, _orig_lang) + @classmethod + def setUpClass(cls): + cls.enterClassContext(translation.override("en-us")) + super().setUpClass() def test_date(self): d = date(2009, 5, 16) From 05f124348e72c1dcf1f6e5de72ffc1f67ad9aa77 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 3 Jan 2024 21:21:10 +0000 Subject: [PATCH 300/748] Fixed #35084 -- Recommended 'django_' prefix for reusable app modules. --- docs/intro/reusable-apps.txt | 79 +++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/docs/intro/reusable-apps.txt b/docs/intro/reusable-apps.txt index 19d9063edd2c..f40daf4dfbdf 100644 --- a/docs/intro/reusable-apps.txt +++ b/docs/intro/reusable-apps.txt @@ -121,16 +121,17 @@ Python *packaging* refers to preparing your app in a specific format that can be easily installed and used. Django itself is packaged very much like this. For a small app like polls, this process isn't too difficult. -#. First, create a parent directory for ``polls``, outside of your Django +#. First, create a parent directory for the package, outside of your Django project. Call this directory ``django-polls``. .. admonition:: Choosing a name for your app - When choosing a name for your package, check resources like PyPI to avoid - naming conflicts with existing packages. It's often useful to prepend - ``django-`` to your module name when creating a package to distribute. - This helps others looking for Django apps identify your app as Django - specific. + When choosing a name for your package, check PyPI to avoid naming + conflicts with existing packages. We recommend using a ``django-`` + prefix for package names, to identify your package as specific to + Django, and a corresponding ``django_`` prefix for your module name. For + example, the ``django-ratelimit`` package contains the + ``django_ratelimit`` module. Application labels (that is, the final part of the dotted path to application packages) *must* be unique in :setting:`INSTALLED_APPS`. @@ -138,19 +139,35 @@ this. For a small app like polls, this process isn't too difficult. `, for example ``auth``, ``admin``, or ``messages``. -#. Move the ``polls`` directory into the ``django-polls`` directory. +#. Move the ``polls`` directory into ``django-polls`` directory, and rename it + to ``django_polls``. + +#. Edit ``django_polls/apps.py`` so that :attr:`~.AppConfig.name` refers to the + new module name and add :attr:`~.AppConfig.label` to give a short name for + the app: + + .. code-block:: python + :caption: ``django-polls/django_polls/apps.py`` + + from django.apps import AppConfig + + + class PollsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "django_polls" + label = "polls" #. Create a file ``django-polls/README.rst`` with the following contents: .. code-block:: rst :caption: ``django-polls/README.rst`` - ===== - Polls - ===== + ============ + django-polls + ============ - Polls is a Django app to conduct web-based polls. For each question, - visitors can choose between a fixed number of answers. + django-polls is a Django app to conduct web-based polls. For each + question, visitors can choose between a fixed number of answers. Detailed documentation is in the "docs" directory. @@ -161,19 +178,18 @@ this. For a small app like polls, this process isn't too difficult. INSTALLED_APPS = [ ..., - "polls", + "django_polls", ] 2. Include the polls URLconf in your project urls.py like this:: - path("polls/", include("polls.urls")), + path("polls/", include("django_polls.urls")), - 3. Run ``python manage.py migrate`` to create the polls models. + 3. Run ``python manage.py migrate`` to create the models. - 4. Start the development server and visit http://127.0.0.1:8000/admin/ - to create a poll (you'll need the Admin app enabled). + 4. Start the development server and visit the admin to create a poll. - 5. Visit http://127.0.0.1:8000/polls/ to participate in the poll. + 5. Visit the ``/polls/`` URL to participate in the poll. #. Create a ``django-polls/LICENSE`` file. Choosing a license is beyond the scope of this tutorial, but suffice it to say that code released publicly @@ -251,8 +267,8 @@ this. For a small app like polls, this process isn't too difficult. include LICENSE include README.rst - recursive-include polls/static * - recursive-include polls/templates * + recursive-include django_polls/static * + recursive-include django_polls/templates * #. It's optional, but recommended, to include detailed documentation with your app. Create an empty directory ``django-polls/docs`` for future @@ -266,8 +282,8 @@ this. For a small app like polls, this process isn't too difficult. you add some files to it. Many Django apps also provide their documentation online through sites like `readthedocs.org `_. -#. Try building your package with ``python setup.py sdist`` (run from inside - ``django-polls``). This creates a directory called ``dist`` and builds your +#. Try building your package by running ``python setup.py sdist`` inside + ``django-polls``. This creates a directory called ``dist`` and builds your new package, ``django-polls-0.1.tar.gz``. For more information on packaging, see Python's `Tutorial on Packaging and @@ -299,14 +315,21 @@ working. We'll now fix this by installing our new ``django-polls`` package. python -m pip install --user django-polls/dist/django-polls-0.1.tar.gz -#. With luck, your Django project should now work correctly again. Run the - server again to confirm this. +#. Update ``mysite/settings.py`` to point to the new module name:: -#. To uninstall the package, use pip: + INSTALLED_APPS = [ + "django_polls.apps.PollsConfig", + ..., + ] - .. code-block:: shell +#. Update ``mysite/urls.py`` to point to the new module name:: + + urlpatterns = [ + path("polls/", include("django_polls.urls")), + ..., + ] - python -m pip uninstall django-polls +#. Run the development server to confirm the project continues to work. Publishing your app =================== @@ -326,7 +349,7 @@ the world! If this wasn't just an example, you could now: Installing Python packages with a virtual environment ===================================================== -Earlier, we installed the polls app as a user library. This has some +Earlier, we installed ``django-polls`` as a user library. This has some disadvantages: * Modifying the user libraries can affect other Python software on your system. From edcf8532ffda006bc125d9c93fca59f9037f490f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 5 Jan 2024 06:03:19 +0100 Subject: [PATCH 301/748] Removed obsolete rpm-related install code. --- scripts/rpm-install.sh | 28 ---------------------------- setup.cfg | 4 ---- 2 files changed, 32 deletions(-) delete mode 100644 scripts/rpm-install.sh diff --git a/scripts/rpm-install.sh b/scripts/rpm-install.sh deleted file mode 100644 index 89cf4dd04954..000000000000 --- a/scripts/rpm-install.sh +++ /dev/null @@ -1,28 +0,0 @@ -#! /bin/sh -# -# This file becomes the install section of the generated spec file. -# - -# This is what dist.py normally does. -%{__python} setup.py install --root=${RPM_BUILD_ROOT} --record="INSTALLED_FILES" - -# Sort the filelist so that directories appear before files. This avoids -# duplicate filename problems on some systems. -touch DIRS -for i in `cat INSTALLED_FILES`; do - if [ -f ${RPM_BUILD_ROOT}/$i ]; then - echo $i >>FILES - fi - if [ -d ${RPM_BUILD_ROOT}/$i ]; then - echo %dir $i >>DIRS - fi -done - -# Make sure we match foo.pyo and foo.pyc along with foo.py (but only once each) -sed -e "/\.py[co]$/d" -e "s/\.py$/.py*/" DIRS FILES >INSTALLED_FILES - -mkdir -p ${RPM_BUILD_ROOT}/%{_mandir}/man1/ -cp docs/man/* ${RPM_BUILD_ROOT}/%{_mandir}/man1/ -cat << EOF >> INSTALLED_FILES -%doc %{_mandir}/man1/*" -EOF diff --git a/setup.cfg b/setup.cfg index c6f48bab1388..2f92092aa907 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,10 +50,6 @@ console_scripts = argon2 = argon2-cffi >= 19.1.0 bcrypt = bcrypt -[bdist_rpm] -doc_files = docs extras AUTHORS INSTALL LICENSE README.rst -install_script = scripts/rpm-install.sh - [flake8] exclude = build,.git,.tox,./tests/.env extend-ignore = E203 From 7dd19a367e58752bc0b138f5b1a9a3aa35581965 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 5 Jan 2024 08:15:45 +0100 Subject: [PATCH 302/748] Moved isort config from setup.cfg to pyproject.toml. --- pyproject.toml | 5 +++++ setup.cfg | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2c776030cea2..f8632ac3cea4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,3 +5,8 @@ build-backend = 'setuptools.build_meta' [tool.black] target-version = ['py310'] force-exclude = 'tests/test_runner_apps/tagged/tests_syntax_error.py' + +[tool.isort] +profile = 'black' +default_section = 'THIRDPARTY' +known_first_party = 'django' diff --git a/setup.cfg b/setup.cfg index 2f92092aa907..29814e54e6b6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,8 +59,3 @@ per-file-ignores = django/core/cache/backends/base.py:W601 django/core/cache/backends/redis.py:W601 tests/cache/tests.py:W601 - -[isort] -profile = black -default_section = THIRDPARTY -known_first_party = django From 9b056aa5afbd1f037189f5b9250ef68e87a93e19 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 5 Jan 2024 08:23:31 +0100 Subject: [PATCH 303/748] Bumped versions in pre-commit and npm configurations. --- .pre-commit-config.yaml | 10 +++++----- package.json | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d42a0dd21266..1c3571406fda 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.10.0 + rev: 23.12.1 hooks: - id: black exclude: \.py-tpl$ @@ -9,17 +9,17 @@ repos: hooks: - id: blacken-docs additional_dependencies: - - black==23.10.0 + - black==23.12.1 files: 'docs/.*\.txt$' - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + rev: 7.0.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-eslint - rev: v8.52.0 + rev: v8.56.0 hooks: - id: eslint diff --git a/package.json b/package.json index e9b3982dc68d..02589973403b 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "npm": ">=1.3.0" }, "devDependencies": { - "eslint": "^8.52.0", - "puppeteer": "^21.4.0", + "eslint": "^8.56.0", + "puppeteer": "^21.7.0", "grunt": "^1.6.1", "grunt-cli": "^1.4.3", "grunt-contrib-qunit": "^8.0.1", From 45f59d0eab56629f88826117e911af05bd55d2c3 Mon Sep 17 00:00:00 2001 From: David Smith <39445562+smithdc1@users.noreply.github.com> Date: Thu, 4 Jan 2024 20:50:14 +0000 Subject: [PATCH 304/748] Fixed #35086 -- Added support for BoundedCircle on Spatialite 5.1+. Spatialite 5.1 added support for BoundingCircle (GEOSMinimumBoundingCircle). GEOS 3.7 is required which is lower than Django's currently supported minmum of 3.8. https://groups.google.com/g/spatialite-users/c/hAJ2SgitN4M https://www.gaia-gis.it/gaia-sins/spatialite-sql-5.1.0.html --- django/contrib/gis/db/backends/spatialite/operations.py | 5 ++++- django/contrib/gis/db/models/functions.py | 7 +++++++ docs/ref/contrib/gis/db-api.txt | 2 +- docs/ref/contrib/gis/functions.txt | 7 ++++++- docs/releases/5.1.txt | 3 ++- tests/gis_tests/geoapp/test_functions.py | 7 ++++++- 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 0b8b26ab6f8b..8a3d84b5de62 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -66,6 +66,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): function_names = { "AsWKB": "St_AsBinary", + "BoundingCircle": "GEOSMinimumBoundingCircle", "ForcePolygonCW": "ST_ForceLHR", "FromWKB": "ST_GeomFromWKB", "FromWKT": "ST_GeomFromText", @@ -80,9 +81,11 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): @cached_property def unsupported_functions(self): - unsupported = {"BoundingCircle", "GeometryDistance", "IsEmpty", "MemSize"} + unsupported = {"GeometryDistance", "IsEmpty", "MemSize"} if not self.geom_lib_version(): unsupported |= {"Azimuth", "GeoHash", "MakeValid"} + if self.spatial_version < (5, 1): + unsupported |= {"BoundingCircle"} return unsupported @cached_property diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index bd02e3717d85..419b64c5e8e6 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/django/contrib/gis/db/models/functions.py @@ -274,6 +274,13 @@ def as_oracle(self, compiler, connection, **extra_context): compiler, connection, **extra_context ) + def as_sqlite(self, compiler, connection, **extra_context): + clone = self.copy() + clone.set_source_expressions([self.get_source_expressions()[0]]) + return super(BoundingCircle, clone).as_sqlite( + compiler, connection, **extra_context + ) + class Centroid(OracleToleranceMixin, GeomOutputGeoFunc): arity = 1 diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index df1d3847e60c..f2dd1c7bf433 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -397,7 +397,7 @@ Function PostGIS Oracle MariaDB MySQL :class:`AsWKB` X X X X X :class:`AsWKT` X X X X X :class:`Azimuth` X X (LWGEOM/RTTOPO) -:class:`BoundingCircle` X X +:class:`BoundingCircle` X X X (≥ 5.1) :class:`Centroid` X X X X X :class:`ClosestPoint` X X :class:`Difference` X X X X X diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index 36ef651cfe72..f55d314b3f85 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/docs/ref/contrib/gis/functions.txt @@ -230,13 +230,18 @@ south = ``π``; west = ``3π/2``. *Availability*: `PostGIS `__, `Oracle `_ +SDO_GEOM-reference.html#GUID-82A61626-BB64-4793-B53D-A0DBEC91831A>`_, +SpatiaLite 5.1+ Accepts a single geographic field or expression and returns the smallest circle polygon that can fully contain the geometry. The ``num_seg`` parameter is used only on PostGIS. +.. versionchanged:: 5.1 + + SpatiaLite 5.1+ support was added. + ``Centroid`` ============ diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 544b1f5d0855..03ca07f298d2 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -53,7 +53,8 @@ Minor features :mod:`django.contrib.gis` ~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* :class:`~django.contrib.gis.db.models.functions.BoundingCircle` is now + supported on SpatiaLite 5.1+. :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py index 047c7f903602..9edadc48c21e 100644 --- a/tests/gis_tests/geoapp/test_functions.py +++ b/tests/gis_tests/geoapp/test_functions.py @@ -258,7 +258,12 @@ def circle_num_points(num_seg): # num_seg is the number of segments per quarter circle. return (4 * num_seg) + 1 - expected_areas = (169, 136) if connection.ops.postgis else (171, 126) + if connection.ops.postgis: + expected_areas = (169, 136) + elif connection.ops.spatialite: + expected_areas = (168, 135) + else: # Oracle. + expected_areas = (171, 126) qs = Country.objects.annotate( circle=functions.BoundingCircle("mpoly") ).order_by("name") From 5c043286e2fde48a6230cbd2d91d5bdd3c49a418 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 5 Jan 2024 16:50:48 +0100 Subject: [PATCH 305/748] Simplified dropping spatial indexes on MySQL and Oracle. --- django/contrib/gis/db/backends/mysql/schema.py | 8 ++------ django/contrib/gis/db/backends/oracle/schema.py | 11 ++--------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/django/contrib/gis/db/backends/mysql/schema.py b/django/contrib/gis/db/backends/mysql/schema.py index 1c9fe0e56585..27f6df174a8d 100644 --- a/django/contrib/gis/db/backends/mysql/schema.py +++ b/django/contrib/gis/db/backends/mysql/schema.py @@ -9,7 +9,6 @@ class MySQLGISSchemaEditor(DatabaseSchemaEditor): sql_add_spatial_index = "CREATE SPATIAL INDEX %(index)s ON %(table)s(%(column)s)" - sql_drop_spatial_index = "DROP INDEX %(index)s ON %(table)s" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -56,11 +55,8 @@ def add_field(self, model, field): def remove_field(self, model, field): if isinstance(field, GeometryField) and field.spatial_index: - qn = self.connection.ops.quote_name - sql = self.sql_drop_spatial_index % { - "index": qn(self._create_spatial_index_name(model, field)), - "table": qn(model._meta.db_table), - } + index_name = self._create_spatial_index_name(model, field) + sql = self._delete_index_sql(model, index_name) try: self.execute(sql) except OperationalError: diff --git a/django/contrib/gis/db/backends/oracle/schema.py b/django/contrib/gis/db/backends/oracle/schema.py index c9192d03fa02..fd542531ebae 100644 --- a/django/contrib/gis/db/backends/oracle/schema.py +++ b/django/contrib/gis/db/backends/oracle/schema.py @@ -20,7 +20,6 @@ class OracleGISSchemaEditor(DatabaseSchemaEditor): "CREATE INDEX %(index)s ON %(table)s(%(column)s) " "INDEXTYPE IS MDSYS.SPATIAL_INDEX" ) - sql_drop_spatial_index = "DROP INDEX %(index)s" sql_clear_geometry_table_metadata = ( "DELETE FROM USER_SDO_GEOM_METADATA WHERE TABLE_NAME = %(table)s" ) @@ -98,14 +97,8 @@ def remove_field(self, model, field): } ) if field.spatial_index: - self.execute( - self.sql_drop_spatial_index - % { - "index": self.quote_name( - self._create_spatial_index_name(model, field) - ), - } - ) + index_name = self._create_spatial_index_name(model, field) + self.execute(self._delete_index_sql(model, index_name)) super().remove_field(model, field) def run_geometry_sql(self): From 53fc6ac64976a7693d2272050a03b71e56b16578 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Sat, 6 Jan 2024 14:07:49 +0000 Subject: [PATCH 306/748] Fixed #35088 -- Added support for Collect on MySQL 8.0.24+. --- .../gis/db/backends/mysql/operations.py | 27 ++++++++++++++----- docs/ref/contrib/gis/db-api.txt | 18 ++++++------- docs/ref/contrib/gis/geoquerysets.txt | 6 ++++- docs/releases/5.1.txt | 3 +++ 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index 886db605cd50..1004cfb56484 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -31,6 +31,11 @@ def select(self): def from_text(self): return self.geom_func_prefix + "GeomFromText" + @cached_property + def collect(self): + if self.connection.features.supports_collect_aggr: + return self.geom_func_prefix + "Collect" + @cached_property def gis_operators(self): operators = { @@ -54,13 +59,18 @@ def gis_operators(self): operators["relate"] = SpatialOperator(func="ST_Relate") return operators - disallowed_aggregates = ( - models.Collect, - models.Extent, - models.Extent3D, - models.MakeLine, - models.Union, - ) + @cached_property + def disallowed_aggregates(self): + disallowed_aggregates = [ + models.Extent, + models.Extent3D, + models.MakeLine, + models.Union, + ] + is_mariadb = self.connection.mysql_is_mariadb + if is_mariadb or self.connection.mysql_version < (8, 0, 24): + disallowed_aggregates.insert(0, models.Collect) + return tuple(disallowed_aggregates) function_names = { "FromWKB": "ST_GeomFromWKB", @@ -128,3 +138,6 @@ def converter(value, expression, connection): return geom return converter + + def spatial_aggregate_name(self, agg_name): + return getattr(self, agg_name.lower()) diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index f2dd1c7bf433..bce6f2efcca4 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -431,20 +431,20 @@ Aggregate Functions ------------------- The following table provides a summary of what GIS-specific aggregate functions -are available on each spatial backend. Please note that MySQL does not +are available on each spatial backend. Please note that MariaDB does not support any of these aggregates, and is thus excluded from the table. .. currentmodule:: django.contrib.gis.db.models -======================= ======= ====== ========== -Aggregate PostGIS Oracle SpatiaLite -======================= ======= ====== ========== -:class:`Collect` X X -:class:`Extent` X X X +======================= ======= ====== ============ ========== +Aggregate PostGIS Oracle MySQL SpatiaLite +======================= ======= ====== ============ ========== +:class:`Collect` X X (≥ 8.0.24) X +:class:`Extent` X X X :class:`Extent3D` X -:class:`MakeLine` X X -:class:`Union` X X X -======================= ======= ====== ========== +:class:`MakeLine` X X +:class:`Union` X X X +======================= ======= ====== ============ ========== .. rubric:: Footnotes .. [#fnwkt] *See* Open Geospatial Consortium, Inc., `OpenGIS Simple Feature Specification For SQL `_, Document 99-049 (May 5, 1999), at Ch. 3.2.5, p. 3-11 (SQL Textual Representation of Geometry). diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index 99b8638a65b3..c0dd8d71c822 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -870,7 +870,7 @@ Example: .. class:: Collect(geo_field, filter=None) -*Availability*: `PostGIS `__, +*Availability*: `PostGIS `__, MySQL, SpatiaLite Returns a ``GEOMETRYCOLLECTION`` or a ``MULTI`` geometry object from the geometry @@ -883,6 +883,10 @@ caring about dissolving boundaries. Support for using the ``filter`` argument was added. +.. versionchanged:: 5.1 + + MySQL 8.0.24+ support was added. + ``Extent`` ~~~~~~~~~~ diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 03ca07f298d2..e84a27a0ec42 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -56,6 +56,9 @@ Minor features * :class:`~django.contrib.gis.db.models.functions.BoundingCircle` is now supported on SpatiaLite 5.1+. +* :class:`~django.contrib.gis.db.models.Collect` is now supported on MySQL + 8.0.24+. + :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From cc56c22a24ef717cc3111e92ca146136fa518d55 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Sun, 7 Jan 2024 07:15:40 +0000 Subject: [PATCH 307/748] Fixed #35091 -- Allowed GeoIP2 querying using IPv4Address/IPv6Address. --- django/contrib/gis/geoip2.py | 7 ++++--- docs/ref/contrib/gis/geoip2.txt | 9 +++++---- docs/releases/5.1.txt | 3 +++ tests/gis_tests/test_geoip2.py | 31 +++++++++++++++++++++---------- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/django/contrib/gis/geoip2.py b/django/contrib/gis/geoip2.py index 5f49954209df..d0f3bb9fb344 100644 --- a/django/contrib/gis/geoip2.py +++ b/django/contrib/gis/geoip2.py @@ -12,6 +12,7 @@ directory corresponding to settings.GEOIP_PATH. """ +import ipaddress import socket import warnings @@ -172,10 +173,10 @@ def __repr__(self): def _check_query(self, query, city=False, city_or_country=False): "Check the query and database availability." - # Making sure a string was passed in for the query. - if not isinstance(query, str): + if not isinstance(query, (str, ipaddress.IPv4Address, ipaddress.IPv6Address)): raise TypeError( - "GeoIP query must be a string, not type %s" % type(query).__name__ + "GeoIP query must be a string or instance of IPv4Address or " + "IPv6Address, not type %s" % type(query).__name__, ) # Extra checks for the existence of country and city databases. diff --git a/docs/ref/contrib/gis/geoip2.txt b/docs/ref/contrib/gis/geoip2.txt index aca31bf78b00..1d27e3965766 100644 --- a/docs/ref/contrib/gis/geoip2.txt +++ b/docs/ref/contrib/gis/geoip2.txt @@ -107,10 +107,11 @@ and given cache setting. Querying -------- -All the following querying routines may take either a string IP address -or a fully qualified domain name (FQDN). For example, both -``'205.186.163.125'`` and ``'djangoproject.com'`` would be valid query -parameters. +All the following querying routines may take an instance of +:class:`~ipaddress.IPv4Address` or :class:`~ipaddress.IPv6Address`, a string IP +address, or a fully qualified domain name (FQDN). For example, +``IPv4Address("205.186.163.125")``, ``"205.186.163.125"``, and +``"djangoproject.com"`` would all be valid query parameters. .. method:: GeoIP2.city(query) diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index e84a27a0ec42..d458811471a6 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -59,6 +59,9 @@ Minor features * :class:`~django.contrib.gis.db.models.Collect` is now supported on MySQL 8.0.24+. +* :class:`~django.contrib.gis.geoip2.GeoIP2` now allows querying using + :class:`ipaddress.IPv4Address` or :class:`ipaddress.IPv6Address` objects. + :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/test_geoip2.py b/tests/gis_tests/test_geoip2.py index 9cd5ffbdfe70..412728b3f432 100644 --- a/tests/gis_tests/test_geoip2.py +++ b/tests/gis_tests/test_geoip2.py @@ -1,3 +1,4 @@ +import ipaddress import itertools import pathlib from unittest import mock, skipUnless @@ -25,15 +26,20 @@ def build_geoip_path(*parts): ) class GeoLite2Test(SimpleTestCase): fqdn = "sky.uk" - ipv4 = "2.125.160.216" - ipv6 = "::ffff:027d:a0d8" + ipv4_str = "2.125.160.216" + ipv6_str = "::ffff:027d:a0d8" + ipv4_addr = ipaddress.ip_address(ipv4_str) + ipv6_addr = ipaddress.ip_address(ipv6_str) + query_values = (fqdn, ipv4_str, ipv6_str, ipv4_addr, ipv6_addr) @classmethod def setUpClass(cls): # Avoid referencing __file__ at module level. cls.enterClassContext(override_settings(GEOIP_PATH=build_geoip_path())) # Always mock host lookup to avoid test breakage if DNS changes. - cls.enterClassContext(mock.patch("socket.gethostbyname", return_value=cls.ipv4)) + cls.enterClassContext( + mock.patch("socket.gethostbyname", return_value=cls.ipv4_str) + ) super().setUpClass() @@ -86,7 +92,10 @@ def test_bad_query(self): functions += (g.country, g.country_code, g.country_name) values = (123, 123.45, b"", (), [], {}, set(), frozenset(), GeoIP2) - msg = "GeoIP query must be a string, not type" + msg = ( + "GeoIP query must be a string or instance of IPv4Address or IPv6Address, " + "not type" + ) for function, value in itertools.product(functions, values): with self.subTest(function=function.__qualname__, type=type(value)): with self.assertRaisesMessage(TypeError, msg): @@ -94,7 +103,7 @@ def test_bad_query(self): def test_country(self): g = GeoIP2(city="") - for query in (self.fqdn, self.ipv4, self.ipv6): + for query in self.query_values: with self.subTest(query=query): self.assertEqual( g.country(query), @@ -108,7 +117,7 @@ def test_country(self): def test_city(self): g = GeoIP2(country="") - for query in (self.fqdn, self.ipv4, self.ipv6): + for query in self.query_values: with self.subTest(query=query): self.assertEqual( g.city(query), @@ -188,15 +197,17 @@ def test_repr(self): def test_check_query(self): g = GeoIP2() - self.assertEqual(g._check_query(self.ipv4), self.ipv4) - self.assertEqual(g._check_query(self.ipv6), self.ipv6) - self.assertEqual(g._check_query(self.fqdn), self.ipv4) + self.assertEqual(g._check_query(self.fqdn), self.ipv4_str) + self.assertEqual(g._check_query(self.ipv4_str), self.ipv4_str) + self.assertEqual(g._check_query(self.ipv6_str), self.ipv6_str) + self.assertEqual(g._check_query(self.ipv4_addr), self.ipv4_addr) + self.assertEqual(g._check_query(self.ipv6_addr), self.ipv6_addr) def test_coords_deprecation_warning(self): g = GeoIP2() msg = "GeoIP2.coords() is deprecated. Use GeoIP2.lon_lat() instead." with self.assertWarnsMessage(RemovedInDjango60Warning, msg): - e1, e2 = g.coords(self.ipv4) + e1, e2 = g.coords(self.ipv4_str) self.assertIsInstance(e1, float) self.assertIsInstance(e2, float) From a9094ec1f43dca7f2a649327afcd5e6226b4959c Mon Sep 17 00:00:00 2001 From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> Date: Fri, 5 Jan 2024 14:08:25 +0100 Subject: [PATCH 308/748] Fixed #35087 -- Reallowed filtering against foreign keys not listed in ModelAdmin.list_filters. Regression in f80669d2f5a5f1db9e9b73ca893fefba34f955e7. --- django/contrib/admin/options.py | 5 +++-- docs/releases/5.0.2.txt | 4 +++- tests/modeladmin/tests.py | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index af69f4cb3b56..e3703f586683 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -467,7 +467,8 @@ def lookup_allowed(self, lookup, value, request=None): relation_parts = [] prev_field = None - for part in lookup.split(LOOKUP_SEP): + parts = lookup.split(LOOKUP_SEP) + for part in parts: try: field = model._meta.get_field(part) except FieldDoesNotExist: @@ -491,7 +492,7 @@ def lookup_allowed(self, lookup, value, request=None): prev_field = field model = field.path_infos[-1].to_opts.model - if not relation_parts: + if not relation_parts or len(parts) == 1: # Either a local field filter, or no fields at all. return True valid_lookups = {self.date_hierarchy} diff --git a/docs/releases/5.0.2.txt b/docs/releases/5.0.2.txt index 4ba818dcb37a..facfed26f509 100644 --- a/docs/releases/5.0.2.txt +++ b/docs/releases/5.0.2.txt @@ -9,4 +9,6 @@ Django 5.0.2 fixes several bugs in 5.0.1. Bugfixes ======== -* ... +* Reallowed, following a regression in Django 5.0.1, filtering against local + foreign keys not included in :attr:`.ModelAdmin.list_filter` + (:ticket:`35087`). diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index e0c4d6e7271f..fad2dfaa1cde 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -162,6 +162,20 @@ class EmployeeProfileAdmin(ModelAdmin): True, ) + @isolate_apps("modeladmin") + def test_lookup_allowed_for_local_fk_fields(self): + class Country(models.Model): + pass + + class Place(models.Model): + country = models.ForeignKey(Country, models.CASCADE) + + class PlaceAdmin(ModelAdmin): + pass + + ma = PlaceAdmin(Place, self.site) + self.assertIs(ma.lookup_allowed("country", "1", request), True) + @isolate_apps("modeladmin") def test_lookup_allowed_non_autofield_primary_key(self): class Country(models.Model): From 415982be105380af5116692a2f6e91c8092803fb Mon Sep 17 00:00:00 2001 From: syed waheed Date: Fri, 5 Jan 2024 22:53:35 +0530 Subject: [PATCH 309/748] Fixed #33481 -- Clarified remove_stale_contenttypes data loss warning. --- .../management/commands/remove_stale_contenttypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py b/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py index 51de847dbf64..950e615f0c86 100644 --- a/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py +++ b/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py @@ -82,7 +82,7 @@ def handle(self, **options): "are:\n\n" f"{content_type_display}\n\n" "This list doesn't include any cascade deletions to data " - "outside of Django's\n" + "outside of Django\n" "models (uncommon).\n\n" "Are you sure you want to delete these content types?\n" "If you're unsure, answer 'no'." From 1b0a8991aec7079405be6b4be5fb17d69e1f4ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Lazarevi=C4=87?= Date: Mon, 8 Jan 2024 13:41:42 +0700 Subject: [PATCH 310/748] Refs #28404 -- Split test_null_display_for_field() test. --- tests/admin_utils/tests.py | 52 +++++++++++++++----------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/tests/admin_utils/tests.py b/tests/admin_utils/tests.py index 0adee38afa09..582ed23b4d65 100644 --- a/tests/admin_utils/tests.py +++ b/tests/admin_utils/tests.py @@ -149,43 +149,33 @@ def simple_function(obj): self.assertEqual(value, resolved_value) - def test_null_display_for_field(self): - """ - Regression test for #12550: display_for_field should handle None - value. - """ - display_value = display_for_field(None, models.CharField(), self.empty_value) - self.assertEqual(display_value, self.empty_value) - - display_value = display_for_field( - None, models.CharField(choices=((None, "test_none"),)), self.empty_value - ) + def test_empty_value_display_for_field(self): + tests = [ + models.CharField(), + models.DateField(), + models.DecimalField(), + models.FloatField(), + models.JSONField(), + models.TimeField(), + ] + for model_field in tests: + with self.subTest(model_field=model_field): + display_value = display_for_field(None, model_field, self.empty_value) + self.assertEqual(display_value, self.empty_value) + + def test_empty_value_display_choices(self): + model_field = models.CharField(choices=((None, "test_none"),)) + display_value = display_for_field(None, model_field, self.empty_value) self.assertEqual(display_value, "test_none") - display_value = display_for_field(None, models.DateField(), self.empty_value) - self.assertEqual(display_value, self.empty_value) - - display_value = display_for_field(None, models.TimeField(), self.empty_value) - self.assertEqual(display_value, self.empty_value) - - display_value = display_for_field( - None, models.BooleanField(null=True), self.empty_value - ) + def test_empty_value_display_booleanfield(self): + model_field = models.BooleanField(null=True) + display_value = display_for_field(None, model_field, self.empty_value) expected = ( - 'None' - % settings.STATIC_URL + f'None' ) self.assertHTMLEqual(display_value, expected) - display_value = display_for_field(None, models.DecimalField(), self.empty_value) - self.assertEqual(display_value, self.empty_value) - - display_value = display_for_field(None, models.FloatField(), self.empty_value) - self.assertEqual(display_value, self.empty_value) - - display_value = display_for_field(None, models.JSONField(), self.empty_value) - self.assertEqual(display_value, self.empty_value) - def test_json_display_for_field(self): tests = [ ({"a": {"b": "c"}}, '{"a": {"b": "c"}}'), From 6dae40839ba828b704eed1f0749c48a21bd88d78 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Tue, 9 Jan 2024 14:18:42 +0100 Subject: [PATCH 311/748] Fixed #35096 -- Corrected alignment for error lists in admin "wide" forms. Regression in be06c39abe80ca650e37810d16d15ff60e8c9727 (LTR) and b34a4771a3d4cd7829a1f38a0f6a7a0da519a724 (RTL). --- django/contrib/admin/static/admin/css/forms.css | 1 + django/contrib/admin/static/admin/css/rtl.css | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css index 5ba9d1ff1bf0..1d9fa9858eec 100644 --- a/django/contrib/admin/static/admin/css/forms.css +++ b/django/contrib/admin/static/admin/css/forms.css @@ -195,6 +195,7 @@ fieldset .fieldBox { } form .wide p.help, +form .wide ul.errorlist, form .wide div.help { padding-left: 50px; } diff --git a/django/contrib/admin/static/admin/css/rtl.css b/django/contrib/admin/static/admin/css/rtl.css index 58ba97710b27..bf636720fdda 100644 --- a/django/contrib/admin/static/admin/css/rtl.css +++ b/django/contrib/admin/static/admin/css/rtl.css @@ -165,7 +165,9 @@ form .aligned p.time div.help.timezonewarning { padding-right: 0; } -form .wide p.help, form .wide div.help { +form .wide p.help, +form .wide ul.errorlist, +form .wide div.help { padding-left: 0; padding-right: 50px; } From ecd3071dac9bc32028849b1563dc30e49744950e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 9 Jan 2024 12:08:03 -0500 Subject: [PATCH 312/748] Fixed #35097 -- Tested parse_datetime() with bare date. Regression test for behavior change in f35ab74752adb37138112657c1bc8b91f50e799b. --- tests/utils_tests/test_dateparse.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/utils_tests/test_dateparse.py b/tests/utils_tests/test_dateparse.py index 17d532a09f6d..12611e12c240 100644 --- a/tests/utils_tests/test_dateparse.py +++ b/tests/utils_tests/test_dateparse.py @@ -46,6 +46,7 @@ def test_parse_time(self): def test_parse_datetime(self): valid_inputs = ( + ("2012-04-23", datetime(2012, 4, 23)), ("2012-04-23T09:15:00", datetime(2012, 4, 23, 9, 15)), ("2012-4-9 4:8:16", datetime(2012, 4, 9, 4, 8, 16)), ( From ec7651586d2d94e1ccd8f905c6a3776ad936b62d Mon Sep 17 00:00:00 2001 From: evananyonga Date: Fri, 5 Jan 2024 12:17:37 +0300 Subject: [PATCH 313/748] Made management command examples more consistent in docs. --- docs/topics/testing/tools.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 068e452ad0b2..cd05db2cf84f 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -2187,8 +2187,8 @@ redirected into a ``StringIO`` instance:: class ClosepollTest(TestCase): def test_command_output(self): out = StringIO() - call_command("closepoll", stdout=out) - self.assertIn("Expected output", out.getvalue()) + call_command("closepoll", poll_ids=[1], stdout=out) + self.assertIn('Successfully closed poll "1"', out.getvalue()) .. _skipping-tests: From 9b02ad91ead3db75036be981bab2083aebc993a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Lazarevi=C4=87?= Date: Mon, 8 Jan 2024 15:47:09 +0700 Subject: [PATCH 314/748] Fixed #28404 -- Made displaying values in admin respect Field's empty_values. --- AUTHORS | 1 + django/contrib/admin/utils.py | 2 +- tests/admin_changelist/tests.py | 34 +++++++++++++++++++++++++++------ tests/admin_utils/tests.py | 9 ++++++--- tests/admin_widgets/tests.py | 2 +- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/AUTHORS b/AUTHORS index f63f712f3a41..8319353cf551 100644 --- a/AUTHORS +++ b/AUTHORS @@ -47,6 +47,7 @@ answer newbie questions, and generally made Django that much better: Aleksandra Sendecka Aleksi Häkli Alex Dutton + Alexander Lazarević Alexander Myodov Alexandr Tatarinov Alex Aktsipetrov diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py index d6c54dd440d1..97a09143ade0 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admin/utils.py @@ -433,7 +433,7 @@ def display_for_field(value, field, empty_value_display): # general null test. elif isinstance(field, models.BooleanField): return _boolean_icon(value) - elif value is None: + elif value in field.empty_values: return empty_value_display elif isinstance(field, models.DateTimeField): return formats.localize(timezone.template_localtime(value)) diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index 4870d9bbe974..b4739b572dc3 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -74,15 +74,15 @@ ) -def build_tbody_html(obj, href, extra_fields): +def build_tbody_html(obj, href, field_name, extra_fields): return ( "" '' '' - 'name' + '{}' "{}" - ).format(obj.pk, str(obj), href, extra_fields) + ).format(obj.pk, str(obj), href, field_name, extra_fields) @override_settings(ROOT_URLCONF="admin_changelist.urls") @@ -245,7 +245,7 @@ def test_result_list_empty_changelist_value(self): table_output = template.render(context) link = reverse("admin:admin_changelist_child_change", args=(new_child.id,)) row_html = build_tbody_html( - new_child, link, '-' + new_child, link, "name", '-' ) self.assertNotEqual( table_output.find(row_html), @@ -253,6 +253,24 @@ def test_result_list_empty_changelist_value(self): "Failed to find expected row element: %s" % table_output, ) + def test_result_list_empty_changelist_value_blank_string(self): + new_child = Child.objects.create(name="", parent=None) + request = self.factory.get("/child/") + request.user = self.superuser + m = ChildAdmin(Child, custom_site) + cl = m.get_changelist_instance(request) + cl.formset = None + template = Template( + "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}" + ) + context = Context({"cl": cl, "opts": Child._meta}) + table_output = template.render(context) + link = reverse("admin:admin_changelist_child_change", args=(new_child.id,)) + row_html = build_tbody_html( + new_child, link, "-", '-' + ) + self.assertInHTML(row_html, table_output) + def test_result_list_set_empty_value_display_on_admin_site(self): """ Empty value display can be set on AdminSite. @@ -272,7 +290,7 @@ def test_result_list_set_empty_value_display_on_admin_site(self): table_output = template.render(context) link = reverse("admin:admin_changelist_child_change", args=(new_child.id,)) row_html = build_tbody_html( - new_child, link, '???' + new_child, link, "name", '???' ) self.assertNotEqual( table_output.find(row_html), @@ -299,6 +317,7 @@ def test_result_list_set_empty_value_display_in_model_admin(self): row_html = build_tbody_html( new_child, link, + "name", '&dagger;' '-empty-', ) @@ -327,7 +346,10 @@ def test_result_list_html(self): table_output = template.render(context) link = reverse("admin:admin_changelist_child_change", args=(new_child.id,)) row_html = build_tbody_html( - new_child, link, '%s' % new_parent + new_child, + link, + "name", + '%s' % new_parent, ) self.assertNotEqual( table_output.find(row_html), diff --git a/tests/admin_utils/tests.py b/tests/admin_utils/tests.py index 582ed23b4d65..393770bd2d09 100644 --- a/tests/admin_utils/tests.py +++ b/tests/admin_utils/tests.py @@ -159,9 +159,12 @@ def test_empty_value_display_for_field(self): models.TimeField(), ] for model_field in tests: - with self.subTest(model_field=model_field): - display_value = display_for_field(None, model_field, self.empty_value) - self.assertEqual(display_value, self.empty_value) + for value in model_field.empty_values: + with self.subTest(model_field=model_field, empty_value=value): + display_value = display_for_field( + value, model_field, self.empty_value + ) + self.assertEqual(display_value, self.empty_value) def test_empty_value_display_choices(self): model_field = models.CharField(choices=((None, "test_none"),)) diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index d497599435f2..50c26095ff21 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -641,7 +641,7 @@ def test_readonly_fields(self): response = self.client.get(reverse("admin:admin_widgets_album_add")) self.assertContains( response, - '
', + '
-
', html=True, ) From f50184a84b885f4474d5030af968195219a360b9 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Wed, 10 Jan 2024 11:09:44 +0000 Subject: [PATCH 315/748] Fixed #35092 -- Exposed extra fields for GeoIP2.country() and GeoIP2.city() responses. --- django/contrib/gis/geoip2.py | 12 ++++++++++-- docs/ref/contrib/gis/geoip2.txt | 34 ++++++++++++++++++++------------- docs/releases/5.1.txt | 8 ++++++++ tests/gis_tests/test_geoip2.py | 15 +++++++++++++-- 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/django/contrib/gis/geoip2.py b/django/contrib/gis/geoip2.py index d0f3bb9fb344..d59d00283d92 100644 --- a/django/contrib/gis/geoip2.py +++ b/django/contrib/gis/geoip2.py @@ -203,18 +203,23 @@ def city(self, query): response = self._city.city(enc_query) region = response.subdivisions[0] if response.subdivisions else None return { + "accuracy_radius": response.location.accuracy_radius, "city": response.city.name, "continent_code": response.continent.code, "continent_name": response.continent.name, "country_code": response.country.iso_code, "country_name": response.country.name, - "dma_code": response.location.metro_code, "is_in_european_union": response.country.is_in_european_union, "latitude": response.location.latitude, "longitude": response.location.longitude, + "metro_code": response.location.metro_code, "postal_code": response.postal.code, - "region": region.iso_code if region else None, + "region_code": region.iso_code if region else None, + "region_name": region.name if region else None, "time_zone": response.location.time_zone, + # Kept for backward compatibility. + "dma_code": response.location.metro_code, + "region": region.iso_code if region else None, } def country_code(self, query): @@ -235,8 +240,11 @@ def country(self, query): enc_query = self._check_query(query, city_or_country=True) response = self._country_or_city(enc_query) return { + "continent_code": response.continent.code, + "continent_name": response.continent.name, "country_code": response.country.iso_code, "country_name": response.country.name, + "is_in_european_union": response.country.is_in_european_union, } def coords(self, query, ordering=("longitude", "latitude")): diff --git a/docs/ref/contrib/gis/geoip2.txt b/docs/ref/contrib/gis/geoip2.txt index 1d27e3965766..2be6ea516c0c 100644 --- a/docs/ref/contrib/gis/geoip2.txt +++ b/docs/ref/contrib/gis/geoip2.txt @@ -33,20 +33,28 @@ Here is an example of its usage: >>> from django.contrib.gis.geoip2 import GeoIP2 >>> g = GeoIP2() >>> g.country("google.com") - {'country_code': 'US', 'country_name': 'United States'} + {'continent_code': 'NA', + 'continent_name': 'North America', + 'country_code': 'US', + 'country_name': 'United States', + 'is_in_european_union': False} >>> g.city("72.14.207.99") - {'city': 'Mountain View', - 'continent_code': 'NA', - 'continent_name': 'North America', - 'country_code': 'US', - 'country_name': 'United States', - 'dma_code': 807, - 'is_in_european_union': False, - 'latitude': 37.419200897216797, - 'longitude': -122.05740356445312, - 'postal_code': '94043', - 'region': 'CA', - 'time_zone': 'America/Los_Angeles'} + {'accuracy_radius': 1000, + 'city': 'Mountain View', + 'continent_code': 'NA', + 'continent_name': 'North America', + 'country_code': 'US', + 'country_name': 'United States', + 'is_in_european_union': False, + 'latitude': 37.419200897216797, + 'longitude': -122.05740356445312, + 'metro_code': 807, + 'postal_code': '94043', + 'region_code': 'CA', + 'region_name': 'California', + 'time_zone': 'America/Los_Angeles', + 'dma_code': 807, + 'region': 'CA'} >>> g.lat_lon("salon.com") (39.0437, -77.4875) >>> g.lon_lat("uh.edu") diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index d458811471a6..547a38d0b43c 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -62,6 +62,14 @@ Minor features * :class:`~django.contrib.gis.geoip2.GeoIP2` now allows querying using :class:`ipaddress.IPv4Address` or :class:`ipaddress.IPv6Address` objects. +* :meth:`.GeoIP2.country` now exposes the ``continent_code``, + ``continent_name``, and ``is_in_european_union`` values. + +* :meth:`.GeoIP2.city` now exposes the ``accuracy_radius`` and ``region_name`` + values. In addition the ``dma_code`` and ``region`` values are now exposed as + ``metro_code`` and ``region_code``, but the previous keys are also retained + for backward compatibility. + :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/test_geoip2.py b/tests/gis_tests/test_geoip2.py index 412728b3f432..f27c7cb86166 100644 --- a/tests/gis_tests/test_geoip2.py +++ b/tests/gis_tests/test_geoip2.py @@ -108,8 +108,11 @@ def test_country(self): self.assertEqual( g.country(query), { + "continent_code": "EU", + "continent_name": "Europe", "country_code": "GB", "country_name": "United Kingdom", + "is_in_european_union": False, }, ) self.assertEqual(g.country_code(query), "GB") @@ -122,18 +125,23 @@ def test_city(self): self.assertEqual( g.city(query), { + "accuracy_radius": 100, "city": "Boxford", "continent_code": "EU", "continent_name": "Europe", "country_code": "GB", "country_name": "United Kingdom", - "dma_code": None, "is_in_european_union": False, "latitude": 51.75, "longitude": -1.25, + "metro_code": None, "postal_code": "OX1", - "region": "ENG", + "region_code": "ENG", + "region_name": "England", "time_zone": "Europe/London", + # Kept for backward compatibility. + "dma_code": None, + "region": "ENG", }, ) @@ -148,8 +156,11 @@ def test_city(self): self.assertEqual( g.country(query), { + "continent_code": "EU", + "continent_name": "Europe", "country_code": "GB", "country_name": "United Kingdom", + "is_in_european_union": False, }, ) self.assertEqual(g.country_code(query), "GB") From 8d2c16252e78a0fae2356ead3a7b3f879a653046 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Wed, 10 Jan 2024 21:00:42 +0100 Subject: [PATCH 316/748] Fixed #34769 -- Fixed key transforms on Oracle 21c+. Oracle 21c introduced support for primivites in JSON fields that caused changes in handling them by JSON_QUERY/JSON_VALUE functions. --- django/db/models/fields/json.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/django/db/models/fields/json.py b/django/db/models/fields/json.py index 7b9b2ae6b26a..571e6e79f345 100644 --- a/django/db/models/fields/json.py +++ b/django/db/models/fields/json.py @@ -352,10 +352,13 @@ def as_mysql(self, compiler, connection): def as_oracle(self, compiler, connection): lhs, params, key_transforms = self.preprocess_lhs(compiler, connection) json_path = compile_json_path(key_transforms) - return ( - "COALESCE(JSON_QUERY(%s, '%s'), JSON_VALUE(%s, '%s'))" - % ((lhs, json_path) * 2) - ), tuple(params) * 2 + if connection.features.supports_primitives_in_json_field: + sql = ( + "COALESCE(JSON_VALUE(%s, '%s'), JSON_QUERY(%s, '%s' DISALLOW SCALARS))" + ) + else: + sql = "COALESCE(JSON_QUERY(%s, '%s'), JSON_VALUE(%s, '%s'))" + return sql % ((lhs, json_path) * 2), tuple(params) * 2 def as_postgresql(self, compiler, connection): lhs, params, key_transforms = self.preprocess_lhs(compiler, connection) From 40b5b1596f7505416bd30d5d7582b5a9004ea7d5 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Tue, 6 Apr 2021 09:52:09 +0100 Subject: [PATCH 317/748] Fixed #35100 -- Reworked GeoIP2 database initialization. --- django/contrib/gis/geoip2.py | 126 ++++++++++----------------------- docs/releases/5.1.txt | 7 ++ tests/gis_tests/test_geoip2.py | 73 ++++++++----------- 3 files changed, 75 insertions(+), 131 deletions(-) diff --git a/django/contrib/gis/geoip2.py b/django/contrib/gis/geoip2.py index d59d00283d92..f5058c1c05dc 100644 --- a/django/contrib/gis/geoip2.py +++ b/django/contrib/gis/geoip2.py @@ -21,6 +21,7 @@ from django.core.validators import validate_ipv46_address from django.utils._os import to_path from django.utils.deprecation import RemovedInDjango60Warning +from django.utils.functional import cached_property __all__ = ["HAS_GEOIP2"] @@ -53,13 +54,8 @@ class GeoIP2: (MODE_AUTO, MODE_MMAP_EXT, MODE_MMAP, MODE_FILE, MODE_MEMORY) ) - # Paths to the city & country binary databases. - _city_file = "" - _country_file = "" - - # Initially, pointers to GeoIP file references are NULL. - _city = None - _country = None + _path = None + _reader = None def __init__(self, path=None, cache=0, country=None, city=None): """ @@ -84,114 +80,69 @@ def __init__(self, path=None, cache=0, country=None, city=None): * city: The name of the GeoIP city data file. Defaults to 'GeoLite2-City.mmdb'; overrides the GEOIP_CITY setting. """ - # Checking the given cache option. if cache not in self.cache_options: raise GeoIP2Exception("Invalid GeoIP caching option: %s" % cache) - # Getting the GeoIP data path. path = path or getattr(settings, "GEOIP_PATH", None) + city = city or getattr(settings, "GEOIP_CITY", "GeoLite2-City.mmdb") + country = country or getattr(settings, "GEOIP_COUNTRY", "GeoLite2-Country.mmdb") + if not path: raise GeoIP2Exception( "GeoIP path must be provided via parameter or the GEOIP_PATH setting." ) path = to_path(path) - if path.is_dir(): - # Constructing the GeoIP database filenames using the settings - # dictionary. If the database files for the GeoLite country - # and/or city datasets exist, then try to open them. - country_db = path / ( - country or getattr(settings, "GEOIP_COUNTRY", "GeoLite2-Country.mmdb") - ) - if country_db.is_file(): - self._country = geoip2.database.Reader(str(country_db), mode=cache) - self._country_file = country_db - city_db = path / ( - city or getattr(settings, "GEOIP_CITY", "GeoLite2-City.mmdb") - ) - if city_db.is_file(): - self._city = geoip2.database.Reader(str(city_db), mode=cache) - self._city_file = city_db - if not self._reader: - raise GeoIP2Exception("Could not load a database from %s." % path) - elif path.is_file(): - # Otherwise, some detective work will be needed to figure out - # whether the given database path is for the GeoIP country or city - # databases. - reader = geoip2.database.Reader(str(path), mode=cache) - db_type = reader.metadata().database_type - - if "City" in db_type: - # GeoLite City database detected. - self._city = reader - self._city_file = path - elif "Country" in db_type: - # GeoIP Country database detected. - self._country = reader - self._country_file = path - else: - raise GeoIP2Exception( - "Unable to recognize database edition: %s" % db_type - ) + # Try the path first in case it is the full path to a database. + for path in (path, path / city, path / country): + if path.is_file(): + self._path = path + self._reader = geoip2.database.Reader(path, mode=cache) + break else: - raise GeoIP2Exception("GeoIP path must be a valid file or directory.") - - @property - def _reader(self): - return self._country or self._city + raise GeoIP2Exception( + "Path must be a valid database or directory containing databases." + ) - @property - def _country_or_city(self): - if self._country: - return self._country.country - else: - return self._city.city + database_type = self._metadata.database_type + if not database_type.endswith(("City", "Country")): + raise GeoIP2Exception(f"Unable to handle database edition: {database_type}") def __del__(self): # Cleanup any GeoIP file handles lying around. - if self._city: - self._city.close() - if self._country: - self._country.close() + if self._reader: + self._reader.close() def __repr__(self): - meta = self._reader.metadata() - version = "[v%s.%s]" % ( - meta.binary_format_major_version, - meta.binary_format_minor_version, - ) - return ( - '<%(cls)s %(version)s _country_file="%(country)s", _city_file="%(city)s">' - % { - "cls": self.__class__.__name__, - "version": version, - "country": self._country_file, - "city": self._city_file, - } - ) + m = self._metadata + version = f"v{m.binary_format_major_version}.{m.binary_format_minor_version}" + return f"<{self.__class__.__name__} [{version}] _path='{self._path}'>" + + @cached_property + def _metadata(self): + return self._reader.metadata() - def _check_query(self, query, city=False, city_or_country=False): - "Check the query and database availability." + def _query(self, query, *, require_city=False): if not isinstance(query, (str, ipaddress.IPv4Address, ipaddress.IPv6Address)): raise TypeError( "GeoIP query must be a string or instance of IPv4Address or " "IPv6Address, not type %s" % type(query).__name__, ) - # Extra checks for the existence of country and city databases. - if city_or_country and not (self._country or self._city): - raise GeoIP2Exception("Invalid GeoIP country and city data files.") - elif city and not self._city: - raise GeoIP2Exception("Invalid GeoIP city data file: %s" % self._city_file) + is_city = self._metadata.database_type.endswith("City") + + if require_city and not is_city: + raise GeoIP2Exception(f"Invalid GeoIP city data file: {self._path}") - # Return the query string back to the caller. GeoIP2 only takes IP addresses. try: validate_ipv46_address(query) except ValidationError: + # GeoIP2 only takes IP addresses, so try to resolve a hostname. query = socket.gethostbyname(query) - return query + function = self._reader.city if is_city else self._reader.country + return function(query) def city(self, query): """ @@ -199,8 +150,7 @@ def city(self, query): Fully Qualified Domain Name (FQDN). Some information in the dictionary may be undefined (None). """ - enc_query = self._check_query(query, city=True) - response = self._city.city(enc_query) + response = self._query(query, require_city=True) region = response.subdivisions[0] if response.subdivisions else None return { "accuracy_radius": response.location.accuracy_radius, @@ -236,9 +186,7 @@ def country(self, query): IP address or a Fully Qualified Domain Name (FQDN). For example, both '24.124.1.80' and 'djangoproject.com' are valid parameters. """ - # Returning the country code and name - enc_query = self._check_query(query, city_or_country=True) - response = self._country_or_city(enc_query) + response = self._query(query, require_city=False) return { "continent_code": response.continent.code, "continent_name": response.continent.name, diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 547a38d0b43c..2f672e3dce0c 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -305,6 +305,13 @@ backends. * Support for GDAL 2.4 is removed. +* :class:`~django.contrib.gis.geoip2.GeoIP2` no longer opens both city and + country databases when a directory path is provided, preferring the city + database, if it is available. The country database is a subset of the city + database and both are not typically needed. If you require use of the country + database when in the same directory as the city database, explicitly pass the + country database path to the constructor. + Dropped support for MariaDB 10.4 -------------------------------- diff --git a/tests/gis_tests/test_geoip2.py b/tests/gis_tests/test_geoip2.py index f27c7cb86166..12837725c9a4 100644 --- a/tests/gis_tests/test_geoip2.py +++ b/tests/gis_tests/test_geoip2.py @@ -50,17 +50,11 @@ def test_init(self): g2 = GeoIP2(settings.GEOIP_PATH, GeoIP2.MODE_AUTO) # Path provided as a string. g3 = GeoIP2(str(settings.GEOIP_PATH)) - for g in (g1, g2, g3): - self.assertTrue(g._country) - self.assertTrue(g._city) - # Only passing in the location of one database. g4 = GeoIP2(settings.GEOIP_PATH / settings.GEOIP_CITY, country="") - self.assertTrue(g4._city) - self.assertIsNone(g4._country) g5 = GeoIP2(settings.GEOIP_PATH / settings.GEOIP_COUNTRY, city="") - self.assertTrue(g5._country) - self.assertIsNone(g5._city) + for g in (g1, g2, g3, g4, g5): + self.assertTrue(g._reader) # Improper parameters. bad_params = (23, "foo", 15.23) @@ -76,7 +70,7 @@ def test_init(self): def test_no_database_file(self): invalid_path = pathlib.Path(__file__).parent.joinpath("data/invalid").resolve() - msg = f"Could not load a database from {invalid_path}." + msg = "Path must be a valid database or directory containing databases." with self.assertRaisesMessage(GeoIP2Exception, msg): GeoIP2(invalid_path) @@ -103,6 +97,25 @@ def test_bad_query(self): def test_country(self): g = GeoIP2(city="") + self.assertIs(g._metadata.database_type.endswith("Country"), True) + for query in self.query_values: + with self.subTest(query=query): + self.assertEqual( + g.country(query), + { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "GB", + "country_name": "United Kingdom", + "is_in_european_union": False, + }, + ) + self.assertEqual(g.country_code(query), "GB") + self.assertEqual(g.country_name(query), "United Kingdom") + + def test_country_using_city_database(self): + g = GeoIP2(country="") + self.assertIs(g._metadata.database_type.endswith("City"), True) for query in self.query_values: with self.subTest(query=query): self.assertEqual( @@ -120,6 +133,7 @@ def test_country(self): def test_city(self): g = GeoIP2(country="") + self.assertIs(g._metadata.database_type.endswith("City"), True) for query in self.query_values: with self.subTest(query=query): self.assertEqual( @@ -179,40 +193,16 @@ def test_not_found(self): def test_del(self): g = GeoIP2() - city = g._city - country = g._country - self.assertIs(city._db_reader.closed, False) - self.assertIs(country._db_reader.closed, False) + reader = g._reader + self.assertIs(reader._db_reader.closed, False) del g - self.assertIs(city._db_reader.closed, True) - self.assertIs(country._db_reader.closed, True) + self.assertIs(reader._db_reader.closed, True) def test_repr(self): g = GeoIP2() - meta = g._reader.metadata() - version = "%s.%s" % ( - meta.binary_format_major_version, - meta.binary_format_minor_version, - ) - country_path = g._country_file - city_path = g._city_file - expected = ( - '' - % { - "version": version, - "country": country_path, - "city": city_path, - } - ) - self.assertEqual(repr(g), expected) - - def test_check_query(self): - g = GeoIP2() - self.assertEqual(g._check_query(self.fqdn), self.ipv4_str) - self.assertEqual(g._check_query(self.ipv4_str), self.ipv4_str) - self.assertEqual(g._check_query(self.ipv6_str), self.ipv6_str) - self.assertEqual(g._check_query(self.ipv4_addr), self.ipv4_addr) - self.assertEqual(g._check_query(self.ipv6_addr), self.ipv6_addr) + m = g._metadata + version = f"{m.binary_format_major_version}.{m.binary_format_minor_version}" + self.assertEqual(repr(g), f"") def test_coords_deprecation_warning(self): g = GeoIP2() @@ -226,8 +216,7 @@ def test_open_deprecation_warning(self): msg = "GeoIP2.open() is deprecated. Use GeoIP2() instead." with self.assertWarnsMessage(RemovedInDjango60Warning, msg): g = GeoIP2.open(settings.GEOIP_PATH, GeoIP2.MODE_AUTO) - self.assertTrue(g._country) - self.assertTrue(g._city) + self.assertTrue(g._reader) @skipUnless(HAS_GEOIP2, "GeoIP2 is required.") @@ -248,7 +237,7 @@ def test_missing_path(self): GeoIP2() def test_unsupported_database(self): - msg = "Unable to recognize database edition: GeoLite2-ASN" + msg = "Unable to handle database edition: GeoLite2-ASN" with self.settings(GEOIP_PATH=build_geoip_path("GeoLite2-ASN-Test.mmdb")): with self.assertRaisesMessage(GeoIP2Exception, msg): GeoIP2() From 4787972c941b0d090cf083e84a98c1791bb2ae4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Lazarevi=C4=87?= Date: Thu, 11 Jan 2024 11:44:01 +0700 Subject: [PATCH 318/748] Refs #28404 -- Made displaying property values in admin respect non-None empty values. --- django/contrib/admin/utils.py | 3 ++- tests/admin_utils/tests.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py index 97a09143ade0..0bcf99ae85f0 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admin/utils.py @@ -6,6 +6,7 @@ from operator import or_ from django.core.exceptions import FieldDoesNotExist +from django.core.validators import EMPTY_VALUES from django.db import models, router from django.db.models.constants import LOOKUP_SEP from django.db.models.deletion import Collector @@ -459,7 +460,7 @@ def display_for_value(value, empty_value_display, boolean=False): if boolean: return _boolean_icon(value) - elif value is None: + elif value in EMPTY_VALUES: return empty_value_display elif isinstance(value, bool): return str(value) diff --git a/tests/admin_utils/tests.py b/tests/admin_utils/tests.py index 393770bd2d09..067b47198d21 100644 --- a/tests/admin_utils/tests.py +++ b/tests/admin_utils/tests.py @@ -17,6 +17,7 @@ lookup_field, quote, ) +from django.core.validators import EMPTY_VALUES from django.db import DEFAULT_DB_ALIAS, models from django.test import SimpleTestCase, TestCase, override_settings from django.utils.formats import localize @@ -249,6 +250,12 @@ def test_list_display_for_value_boolean(self): self.assertEqual(display_for_value(True, ""), "True") self.assertEqual(display_for_value(False, ""), "False") + def test_list_display_for_value_empty(self): + for value in EMPTY_VALUES: + with self.subTest(empty_value=value): + display_value = display_for_value(value, self.empty_value) + self.assertEqual(display_value, self.empty_value) + def test_label_for_field(self): """ Tests for label_for_field From 4eb4ab4122b2b974911d4b1f935728cf35e4208c Mon Sep 17 00:00:00 2001 From: Mark Walker Date: Fri, 1 Dec 2023 00:43:56 +0000 Subject: [PATCH 319/748] Reorganized the Contributing to Django docs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This work follows a comprehensive review conducted during the DjangoCon US 2023 sprints. Changes include: - Updated the title of the main page for better alignment with the content. - Removed emojis to enhance accessibility and avoid cultural specificity. - Improved the layout and navigation of contributing documentation. - Unified sections for communication channels and community links. - Grouped resources according to the Diátaxis systematic approach. Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> Co-authored-by: Daniele Procida --- docs/index.txt | 2 +- docs/internals/contributing/index.txt | 144 +++++++++++------- .../contributing/writing-code/index.txt | 62 ++++---- .../contributing/writing-code/javascript.txt | 6 +- 4 files changed, 127 insertions(+), 87 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index 9a5585c29e3a..00d62f9f11a4 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -325,7 +325,7 @@ Learn about the development process for the Django project itself and about how you can contribute: * **Community:** - :doc:`How to get involved ` | + :doc:`Contributing to Django ` | :doc:`The release process ` | :doc:`Team organization ` | :doc:`The Django source code repository ` | diff --git a/docs/internals/contributing/index.txt b/docs/internals/contributing/index.txt index c96250613ed6..6e3fd948ee26 100644 --- a/docs/internals/contributing/index.txt +++ b/docs/internals/contributing/index.txt @@ -6,63 +6,20 @@ Django is a community that lives on its volunteers. As it keeps growing, we always need more people to help others. You can contribute in many ways, either on the framework itself or in the wider ecosystem. -Work on the Django framework -============================ - -The work on Django itself falls into three major areas: - -Writing code 💻 ---------------- - -Fix a bug, or add a new feature. You can make a pull request and see **your -code** in the next version of Django! - -Start from the :doc:`writing-code/index` docs. - -Writing documentation ✍️ ------------------------- - -Django's documentation is one of its key strengths. It's informative and -thorough. You can help to improve the documentation and keep it relevant as the -framework evolves. - -See :doc:`writing-documentation` for more. - -Localizing Django 🗺️ --------------------- - -Django is translated into over 100 languages - There's even some translation -for Klingon?! The i18n team is always looking for translators to help maintain -and increase language reach. - -See :doc:`localizing` to help translate Django. - -Contributing guide 📖 -===================== - -If you think working *with* Django is fun, wait until you start working *on* -it. Really, **ANYONE** can do something to help make Django better and greater! +Communication channels +====================== -This contributing guide contains everything you need to know to help build the -Django web framework. Browse the following sections to find out how: +We're passionate about helping Django users make the jump to contributing +members of the community. Communication is key - working on Django is being +part of a conversation. Join it, to become familiar with what we're doing and +how we talk about it. You'll be able to form relationships with more +experienced contributors who are there to help guide you towards success. -.. toctree:: - :maxdepth: 2 +Join the Django community +------------------------- - new-contributors - bugs-and-features - triaging-tickets - writing-code/index - writing-documentation - localizing - committing-code - -Join the Django community ❤️ -============================ - -We're passionate about helping Django users make the jump to contributing -members of the community. There are several other ways you can help the -Django community and others to maintain a great ecosystem to work in: +There are several ways you can help the Django community and others to maintain +a great ecosystem to work in: * Join the `Django forum`_. This forum is a place for discussing the Django framework and applications and projects that use it. This is also a good @@ -87,11 +44,86 @@ Django community and others to maintain a great ecosystem to work in: ecosystem of pluggable applications is a big strength of Django, help us build it! -We're looking forward to working with you. Welcome aboard! ⛵️ - .. _posting guidelines: https://code.djangoproject.com/wiki/UsingTheMailingList .. _#django IRC channel: https://web.libera.chat/#django +.. _#django-dev IRC channel: https://web.libera.chat/#django-dev .. _community page: https://www.djangoproject.com/community/ .. _Django Discord server: https://discord.gg/xcRH6mN4fa .. _Django forum: https://forum.djangoproject.com/ .. _register it here: https://www.djangoproject.com/community/add/blogs/ + +Getting started +=============== + +Django encourages and welcomes new contributors, and makes an effort to help +them become experienced, confident contributors to Open Source Software (OSS). +Our documentation contains guidance for first-time contributors, including: + +.. toctree:: + :maxdepth: 1 + + An overview of the contributing process and what's involved. + +Work on the Django framework +============================ + +If you enjoy working *with* Django, wait until you start working *on* it. +Really, **anyone** can do something to improve Django, which will improve the +experience of lots of people! + +The work on Django itself falls into three major areas: + +Contributing code +----------------- + +Fix a bug, or add a new feature. You can make a pull request and see **your +code** in the next version of Django. + +.. toctree:: + :maxdepth: 2 + + writing-code/index + +Contributing documentation +-------------------------- + +Django's documentation is one of its key strengths. It's informative and +thorough. You can help to improve the documentation and keep it relevant as the +framework evolves. + +.. toctree:: + :maxdepth: 2 + + writing-documentation + +Localizing Django +----------------- + +Django is translated into over 100 languages - There's even some translation +for Klingon?! The i18n team is always looking for translators to help maintain +and increase language reach. + +.. toctree:: + :maxdepth: 2 + + localizing + +Other ways of contributing +========================== + +Explore additional avenues of contributing to Django beyond coding. Django's +`ticket tracker`_ is the central hub for managing issues, improvements, and +contributions to Django. It's a valuable resource where you can report bugs you +encounter or assist in triaging existing tickets to ensure a smooth development +workflow. Explore the ways you can make a difference below, and join us in +making Django better for everyone. + +.. toctree:: + :maxdepth: 2 + + bugs-and-features + triaging-tickets + +.. _ticket tracker: https://code.djangoproject.com/ + +We're looking forward to working with you. Welcome aboard! diff --git a/docs/internals/contributing/writing-code/index.txt b/docs/internals/contributing/writing-code/index.txt index 9402c26808f1..72cc26452460 100644 --- a/docs/internals/contributing/writing-code/index.txt +++ b/docs/internals/contributing/writing-code/index.txt @@ -1,43 +1,51 @@ -============ -Writing code -============ +================= +Contributing code +================= -So you'd like to write some code to improve Django? Awesome! There are several -ways you can help Django's development: +So you'd like to write some code, documentation or tests to improve Django? +There are several ways you can help Django's development. -* :doc:`Report bugs <../bugs-and-features>` in our `ticket tracker`_. +Tutorials +========= -* Join the |django-developers| mailing list and share your ideas for how to - improve Django. We're always open to suggestions. You can also interact on - the `Django forum`_ and the `#django-dev IRC channel`_. +The Django tutorial contains a whole section that walks you step-by-step +through the contributing code process. -* :doc:`Submit patches ` for new and/or fixed behavior. If - you're looking for a way to get started contributing to Django read the - :doc:`/intro/contributing` tutorial and have a look at the `easy pickings`_ - tickets. The :ref:`patch-review-checklist` will also be helpful. +.. toctree:: + :maxdepth: 1 + + /intro/contributing -* :doc:`Improve the documentation <../writing-documentation>` or :doc:`write - unit tests `. +How-to guides +============= -* :doc:`Triage tickets and review patches <../triaging-tickets>` created by - other users. +If you already have some familiarity with the processes and principles, +our documentation also contains useful guidance on specific topics: -* Read the :doc:`../new-contributors` to help you get orientated in the - development process. +.. toctree:: + :maxdepth: 1 -Browse the following sections to find out how to give your code patches the -best chances to be included in Django core: + How to submit a patch to Django for new and/or fixed behavior + How to write and run tests + How to run Django's unit tests + How to work with Git and GitHub + +Related topics +============== + +It's important to understand how we work and the conventions we adopt. .. toctree:: :maxdepth: 1 coding-style - unit-tests - submitting-patches - working-with-git javascript + ../committing-code + +We maintain a curated list of small issues suited to first-time or less +experienced contributors, using the "easy pickings" filter. These are strongly +recommended for those contributors looking to make a contribution. + +* Browse `easy pickings`_ tickets. -.. _ticket tracker: https://code.djangoproject.com/ .. _easy pickings: https://code.djangoproject.com/query?status=!closed&easy=1 -.. _#django-dev IRC channel: https://web.libera.chat/#django-dev -.. _Django forum: https://forum.djangoproject.com/ diff --git a/docs/internals/contributing/writing-code/javascript.txt b/docs/internals/contributing/writing-code/javascript.txt index 657cc66ded35..25165206f07c 100644 --- a/docs/internals/contributing/writing-code/javascript.txt +++ b/docs/internals/contributing/writing-code/javascript.txt @@ -1,6 +1,6 @@ -========== -JavaScript -========== +=============== +JavaScript code +=============== While most of Django core is Python, the ``admin`` and ``gis`` contrib apps contain JavaScript code. From 6e520d953773d25a3d3484db67feed446aca0bc1 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Sat, 30 Dec 2023 12:15:13 +0100 Subject: [PATCH 320/748] Avoided nested transactions in SkippingClassTestCase. --- tests/test_utils/tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index 65a782bf87b6..e001e119ee7a 100644 --- a/tests/test_utils/tests.py +++ b/tests/test_utils/tests.py @@ -157,7 +157,9 @@ def test_foo(self): ) -class SkippingClassTestCase(TestCase): +class SkippingClassTestCase(TransactionTestCase): + available_apps = [] + def test_skip_class_unless_db_feature(self): @skipUnlessDBFeature("__class__") class NotSkippedTests(TestCase): From 02eaee12095eebb3d07d02e7b0bdc3f64785d379 Mon Sep 17 00:00:00 2001 From: nessita <124304+nessita@users.noreply.github.com> Date: Fri, 12 Jan 2024 17:27:55 -0300 Subject: [PATCH 321/748] Added test ensuring that validate_password is used in AdminPasswordChangeForm. Co-authored-by: Fabian Braun --- tests/auth_tests/test_forms.py | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py index 81c56a428ea2..14d71572daaa 100644 --- a/tests/auth_tests/test_forms.py +++ b/tests/auth_tests/test_forms.py @@ -1324,6 +1324,42 @@ def test_success(self, password_changed): self.assertEqual(password_changed.call_count, 1) self.assertEqual(form.changed_data, ["password"]) + @override_settings( + AUTH_PASSWORD_VALIDATORS=[ + { + "NAME": ( + "django.contrib.auth.password_validation." + "UserAttributeSimilarityValidator" + ) + }, + { + "NAME": ( + "django.contrib.auth.password_validation.MinimumLengthValidator" + ), + "OPTIONS": { + "min_length": 12, + }, + }, + ] + ) + def test_validates_password(self): + user = User.objects.get(username="testclient") + data = { + "password1": "testclient", + "password2": "testclient", + } + form = AdminPasswordChangeForm(user, data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form["password2"].errors), 2) + self.assertIn( + "The password is too similar to the username.", + form["password2"].errors, + ) + self.assertIn( + "This password is too short. It must contain at least 12 characters.", + form["password2"].errors, + ) + def test_password_whitespace_not_stripped(self): user = User.objects.get(username="testclient") data = { From 92d6cff6a2fee7a3f9244081b84fd82c50cc71aa Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Fri, 15 Dec 2023 18:30:35 -0500 Subject: [PATCH 322/748] Fixed #35028 -- Disabled server-side bindings for named cursors on psycopg >= 3. While we provide a `cursor_factory` based on the value of the `server_side_bindings` option to `psycopg.Connection` it is ignored by the `cursor` method when `name` is specified for `QuerySet.iterator()` usage and it causes the usage of `psycopg.ServerCursor` which performs server-side bindings. Since the ORM doesn't generates SQL that is suitable for server-side bindings when dealing with parametrized expressions a specialized cursor must be used to allow server-side cursors to be used with client-side bindings. Thanks Richard Ebeling for the report. Thanks Florian Apolloner and Daniele Varrazzo for reviews. --- django/db/backends/postgresql/base.py | 42 ++++++++++++++-- .../postgresql/test_server_side_cursors.py | 50 ++++++++++++++++++- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index d92ad587102c..cba89e0cc798 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -321,11 +321,26 @@ def init_connection_state(self): @async_unsafe def create_cursor(self, name=None): if name: - # In autocommit mode, the cursor will be used outside of a - # transaction, hence use a holdable cursor. - cursor = self.connection.cursor( - name, scrollable=False, withhold=self.connection.autocommit - ) + if is_psycopg3 and ( + self.settings_dict.get("OPTIONS", {}).get("server_side_binding") + is not True + ): + # psycopg >= 3 forces the usage of server-side bindings for + # named cursors so a specialized class that implements + # server-side cursors while performing client-side bindings + # must be used if `server_side_binding` is disabled (default). + cursor = ServerSideCursor( + self.connection, + name=name, + scrollable=False, + withhold=self.connection.autocommit, + ) + else: + # In autocommit mode, the cursor will be used outside of a + # transaction, hence use a holdable cursor. + cursor = self.connection.cursor( + name, scrollable=False, withhold=self.connection.autocommit + ) else: cursor = self.connection.cursor() @@ -469,6 +484,23 @@ class ServerBindingCursor(CursorMixin, Database.Cursor): class Cursor(CursorMixin, Database.ClientCursor): pass + class ServerSideCursor( + CursorMixin, Database.client_cursor.ClientCursorMixin, Database.ServerCursor + ): + """ + psycopg >= 3 forces the usage of server-side bindings when using named + cursors but the ORM doesn't yet support the systematic generation of + prepareable SQL (#20516). + + ClientCursorMixin forces the usage of client-side bindings while + ServerCursor implements the logic required to declare and scroll + through named cursors. + + Mixing ClientCursorMixin in wouldn't be necessary if Cursor allowed to + specify how parameters should be bound instead, which ServerCursor + would inherit, but that's not the case. + """ + class CursorDebugWrapper(BaseCursorDebugWrapper): def copy(self, statement): with self.debug_sql(statement): diff --git a/tests/backends/postgresql/test_server_side_cursors.py b/tests/backends/postgresql/test_server_side_cursors.py index 694421b5cb3b..9a6457cce616 100644 --- a/tests/backends/postgresql/test_server_side_cursors.py +++ b/tests/backends/postgresql/test_server_side_cursors.py @@ -4,12 +4,18 @@ from contextlib import contextmanager from django.db import connection, models +from django.db.utils import ProgrammingError from django.test import TestCase from django.test.utils import garbage_collect from django.utils.version import PYPY from ..models import Person +try: + from django.db.backends.postgresql.psycopg_any import is_psycopg3 +except ImportError: + is_psycopg3 = False + @unittest.skipUnless(connection.vendor == "postgresql", "PostgreSQL tests") class ServerSideCursorsPostgres(TestCase): @@ -20,8 +26,8 @@ class ServerSideCursorsPostgres(TestCase): @classmethod def setUpTestData(cls): - Person.objects.create(first_name="a", last_name="a") - Person.objects.create(first_name="b", last_name="b") + cls.p0 = Person.objects.create(first_name="a", last_name="a") + cls.p1 = Person.objects.create(first_name="b", last_name="b") def inspect_cursors(self): with connection.cursor() as cursor: @@ -108,3 +114,43 @@ def test_server_side_cursors_setting(self): # collection breaks the transaction wrapping the test. with self.override_db_setting(DISABLE_SERVER_SIDE_CURSORS=True): self.assertNotUsesCursor(Person.objects.iterator()) + + @unittest.skipUnless( + is_psycopg3, "The server_side_binding option is only effective on psycopg >= 3." + ) + def test_server_side_binding(self): + """ + The ORM still generates SQL that is not suitable for usage as prepared + statements but psycopg >= 3 defaults to using server-side bindings for + server-side cursors which requires some specialized logic when the + `server_side_binding` setting is disabled (default). + """ + + def perform_query(): + # Generates SQL that is known to be problematic from a server-side + # binding perspective as the parametrized ORDER BY clause doesn't + # use the same binding parameter as the SELECT clause. + qs = ( + Person.objects.order_by( + models.functions.Coalesce("first_name", models.Value("")) + ) + .distinct() + .iterator() + ) + self.assertSequenceEqual(list(qs), [self.p0, self.p1]) + + with self.override_db_setting(OPTIONS={}): + perform_query() + + with self.override_db_setting(OPTIONS={"server_side_binding": False}): + perform_query() + + with self.override_db_setting(OPTIONS={"server_side_binding": True}): + # This assertion could start failing the moment the ORM generates + # SQL suitable for usage as prepared statements (#20516) or if + # psycopg >= 3 adapts psycopg.Connection(cursor_factory) machinery + # to allow client-side bindings for named cursors. In the first + # case this whole test could be removed, in the second one it would + # most likely need to be adapted. + with self.assertRaises(ProgrammingError): + perform_query() From d074c7530b812a7b78c9a201c8a70f281ed3a36e Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sat, 13 Jan 2024 14:21:36 -0500 Subject: [PATCH 323/748] Refs #35102 -- Optimized Expression.identity used for equality and hashing. inspect.signature() is quite slow and produces the same object for each instance of the same class as they share their __init__ method which makes it a prime candidate for caching. Thanks Anthony Shaw for the report. --- django/db/models/expressions.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index c20de5995a34..9ee2d65c8af6 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -14,7 +14,7 @@ from django.db.models.constants import LOOKUP_SEP from django.db.models.query_utils import Q from django.utils.deconstruct import deconstructible -from django.utils.functional import cached_property +from django.utils.functional import cached_property, classproperty from django.utils.hashable import make_hashable @@ -485,13 +485,18 @@ def select_format(self, compiler, sql, params): class Expression(BaseExpression, Combinable): """An expression that can be combined with other expressions.""" + @classproperty + @functools.lru_cache(maxsize=128) + def _constructor_signature(cls): + return inspect.signature(cls.__init__) + @cached_property def identity(self): - constructor_signature = inspect.signature(self.__init__) args, kwargs = self._constructor_args - signature = constructor_signature.bind_partial(*args, **kwargs) + signature = self._constructor_signature.bind_partial(self, *args, **kwargs) signature.apply_defaults() - arguments = signature.arguments.items() + arguments = iter(signature.arguments.items()) + next(arguments) identity = [self.__class__] for arg, value in arguments: if isinstance(value, fields.Field): From f3d10546a850df4fe3796f972d5b7e16adf52f54 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Sat, 13 Jan 2024 14:33:20 -0500 Subject: [PATCH 324/748] Refs #35102 -- Optimized replace_expressions()/relabelling aliases by adding early return. This avoids costly hashing. Thanks Anthony Shaw for the report. Co-Authored-By: Simon Charette --- django/db/models/expressions.py | 5 ++++- django/db/models/sql/query.py | 2 ++ django/db/models/sql/where.py | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 9ee2d65c8af6..f25ad1af12cc 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -402,10 +402,13 @@ def relabeled_clone(self, change_map): return clone def replace_expressions(self, replacements): + if not replacements: + return self if replacement := replacements.get(self): return replacement + if not (source_expressions := self.get_source_expressions()): + return self clone = self.copy() - source_expressions = clone.get_source_expressions() clone.set_source_expressions( [ expr.replace_expressions(replacements) if expr else None diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index a79d66eb21e9..ce4fafb1e2bc 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -972,6 +972,8 @@ def change_aliases(self, change_map): relabelling any references to them in select columns and the where clause. """ + if not change_map: + return self # If keys and values of change_map were to intersect, an alias might be # updated twice (e.g. T4 -> T5, T5 -> T6, so also T4 -> T6) depending # on their order in change_map. diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 2f23a2932ce5..8423fcb52892 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -204,6 +204,8 @@ def relabel_aliases(self, change_map): Relabel the alias values of any children. 'change_map' is a dictionary mapping old (current) alias values to the new values. """ + if not change_map: + return self for pos, child in enumerate(self.children): if hasattr(child, "relabel_aliases"): # For example another WhereNode @@ -225,6 +227,8 @@ def relabeled_clone(self, change_map): return clone def replace_expressions(self, replacements): + if not replacements: + return self if replacement := replacements.get(self): return replacement clone = self.create(connector=self.connector, negated=self.negated) From f92641a636a8cb75fc9851396cef4345510a4b52 Mon Sep 17 00:00:00 2001 From: Aivars Kalvans Date: Sun, 10 Dec 2023 21:43:34 +0200 Subject: [PATCH 325/748] Fixed #28344 -- Allowed customizing queryset in Model.refresh_from_db()/arefresh_from_db(). The from_queryset parameter can be used to: - use a custom Manager - lock the row until the end of transaction - select additional related objects --- django/db/models/base.py | 28 ++++++++----- docs/ref/models/instances.txt | 25 ++++++++++- docs/releases/5.1.txt | 5 +++ tests/async/test_async_model_methods.py | 11 +++++ tests/basic/tests.py | 56 ++++++++++++++++++++++++- 5 files changed, 111 insertions(+), 14 deletions(-) diff --git a/django/db/models/base.py b/django/db/models/base.py index 46ac762ccd46..b15bdd032ab0 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -673,7 +673,7 @@ def get_deferred_fields(self): if f.attname not in self.__dict__ } - def refresh_from_db(self, using=None, fields=None): + def refresh_from_db(self, using=None, fields=None, from_queryset=None): """ Reload field values from the database. @@ -705,10 +705,13 @@ def refresh_from_db(self, using=None, fields=None): "are not allowed in fields." % LOOKUP_SEP ) - hints = {"instance": self} - db_instance_qs = self.__class__._base_manager.db_manager( - using, hints=hints - ).filter(pk=self.pk) + if from_queryset is None: + hints = {"instance": self} + from_queryset = self.__class__._base_manager.db_manager(using, hints=hints) + elif using is not None: + from_queryset = from_queryset.using(using) + + db_instance_qs = from_queryset.filter(pk=self.pk) # Use provided fields, if not set then reload all non-deferred fields. deferred_fields = self.get_deferred_fields() @@ -729,9 +732,12 @@ def refresh_from_db(self, using=None, fields=None): # This field wasn't refreshed - skip ahead. continue setattr(self, field.attname, getattr(db_instance, field.attname)) - # Clear cached foreign keys. - if field.is_relation and field.is_cached(self): - field.delete_cached_value(self) + # Clear or copy cached foreign keys. + if field.is_relation: + if field.is_cached(db_instance): + field.set_cached_value(self, field.get_cached_value(db_instance)) + elif field.is_cached(self): + field.delete_cached_value(self) # Clear cached relations. for field in self._meta.related_objects: @@ -745,8 +751,10 @@ def refresh_from_db(self, using=None, fields=None): self._state.db = db_instance._state.db - async def arefresh_from_db(self, using=None, fields=None): - return await sync_to_async(self.refresh_from_db)(using=using, fields=fields) + async def arefresh_from_db(self, using=None, fields=None, from_queryset=None): + return await sync_to_async(self.refresh_from_db)( + using=using, fields=fields, from_queryset=from_queryset + ) def serializable_value(self, field_name): """ diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 45af7f244fc1..6d1a7e5db458 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -142,8 +142,8 @@ value from the database: >>> del obj.field >>> obj.field # Loads the field from the database -.. method:: Model.refresh_from_db(using=None, fields=None) -.. method:: Model.arefresh_from_db(using=None, fields=None) +.. method:: Model.refresh_from_db(using=None, fields=None, from_queryset=None) +.. method:: Model.arefresh_from_db(using=None, fields=None, from_queryset=None) *Asynchronous version*: ``arefresh_from_db()`` @@ -197,6 +197,27 @@ all of the instance's fields when a deferred field is reloaded:: fields = fields.union(deferred_fields) super().refresh_from_db(using, fields, **kwargs) +The ``from_queryset`` argument allows using a different queryset than the one +created from :attr:`~django.db.models.Model._base_manager`. It gives you more +control over how the model is reloaded. For example, when your model uses soft +deletion you can make ``refresh_from_db()`` to take this into account:: + + obj.refresh_from_db(from_queryset=MyModel.active_objects.all()) + +You can cache related objects that otherwise would be cleared from the reloaded +instance:: + + obj.refresh_from_db(from_queryset=MyModel.objects.select_related("related_field")) + +You can lock the row until the end of transaction before reloading a model's +values:: + + obj.refresh_from_db(from_queryset=MyModel.objects.select_for_update()) + +.. versionchanged:: 5.1 + + The ``from_queryset`` argument was added. + .. method:: Model.get_deferred_fields() A helper method that returns a set containing the attribute names of all those diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 2f672e3dce0c..30317eaa1916 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -208,6 +208,11 @@ Models :class:`~django.contrib.postgres.fields.ArrayField` can now be :ref:`sliced `. +* The new ``from_queryset`` argument of :meth:`.Model.refresh_from_db` and + :meth:`.Model.arefresh_from_db` allows customizing the queryset used to + reload a model's value. This can be used to lock the row before reloading or + to select related objects. + Requests and Responses ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/async/test_async_model_methods.py b/tests/async/test_async_model_methods.py index 94e0370e3560..d988d7befcb4 100644 --- a/tests/async/test_async_model_methods.py +++ b/tests/async/test_async_model_methods.py @@ -23,3 +23,14 @@ async def test_arefresh_from_db(self): await SimpleModel.objects.filter(pk=self.s1.pk).aupdate(field=20) await self.s1.arefresh_from_db() self.assertEqual(self.s1.field, 20) + + async def test_arefresh_from_db_from_queryset(self): + await SimpleModel.objects.filter(pk=self.s1.pk).aupdate(field=20) + with self.assertRaises(SimpleModel.DoesNotExist): + await self.s1.arefresh_from_db( + from_queryset=SimpleModel.objects.filter(field=0) + ) + await self.s1.arefresh_from_db( + from_queryset=SimpleModel.objects.filter(field__gt=0) + ) + self.assertEqual(self.s1.field, 20) diff --git a/tests/basic/tests.py b/tests/basic/tests.py index 990549edfc4b..8a304e9ace04 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -4,7 +4,14 @@ from unittest import mock from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist -from django.db import DEFAULT_DB_ALIAS, DatabaseError, connections, models +from django.db import ( + DEFAULT_DB_ALIAS, + DatabaseError, + connection, + connections, + models, + transaction, +) from django.db.models.manager import BaseManager from django.db.models.query import MAX_GET_RESULTS, EmptyQuerySet from django.test import ( @@ -13,7 +20,8 @@ TransactionTestCase, skipUnlessDBFeature, ) -from django.test.utils import ignore_warnings +from django.test.utils import CaptureQueriesContext, ignore_warnings +from django.utils.connection import ConnectionDoesNotExist from django.utils.deprecation import RemovedInDjango60Warning from django.utils.translation import gettext_lazy @@ -1003,3 +1011,47 @@ def test_prefetched_cache_cleared(self): # Cache was cleared and new results are available. self.assertCountEqual(a2_prefetched.selfref_set.all(), [s]) self.assertCountEqual(a2_prefetched.cited.all(), [s]) + + @skipUnlessDBFeature("has_select_for_update") + def test_refresh_for_update(self): + a = Article.objects.create(pub_date=datetime.now()) + for_update_sql = connection.ops.for_update_sql() + + with transaction.atomic(), CaptureQueriesContext(connection) as ctx: + a.refresh_from_db(from_queryset=Article.objects.select_for_update()) + self.assertTrue( + any(for_update_sql in query["sql"] for query in ctx.captured_queries) + ) + + def test_refresh_with_related(self): + a = Article.objects.create(pub_date=datetime.now()) + fa = FeaturedArticle.objects.create(article=a) + + from_queryset = FeaturedArticle.objects.select_related("article") + with self.assertNumQueries(1): + fa.refresh_from_db(from_queryset=from_queryset) + self.assertEqual(fa.article.pub_date, a.pub_date) + with self.assertNumQueries(2): + fa.refresh_from_db() + self.assertEqual(fa.article.pub_date, a.pub_date) + + def test_refresh_overwrites_queryset_using(self): + a = Article.objects.create(pub_date=datetime.now()) + + from_queryset = Article.objects.using("nonexistent") + with self.assertRaises(ConnectionDoesNotExist): + a.refresh_from_db(from_queryset=from_queryset) + a.refresh_from_db(using="default", from_queryset=from_queryset) + + def test_refresh_overwrites_queryset_fields(self): + a = Article.objects.create(pub_date=datetime.now()) + headline = "headline" + Article.objects.filter(pk=a.pk).update(headline=headline) + + from_queryset = Article.objects.only("pub_date") + with self.assertNumQueries(1): + a.refresh_from_db(from_queryset=from_queryset) + self.assertNotEqual(a.headline, headline) + with self.assertNumQueries(1): + a.refresh_from_db(fields=["headline"], from_queryset=from_queryset) + self.assertEqual(a.headline, headline) From 4fec1d2ce37241fb8fa001971c441d360ed2a196 Mon Sep 17 00:00:00 2001 From: jordanbae Date: Tue, 9 Jan 2024 23:04:31 +0900 Subject: [PATCH 326/748] Fixed #34949 -- Clarified when UniqueConstraints with include/nulls_distinct are not created. --- docs/ref/models/constraints.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ref/models/constraints.txt b/docs/ref/models/constraints.txt index efe63a8ac183..cc308cedf225 100644 --- a/docs/ref/models/constraints.txt +++ b/docs/ref/models/constraints.txt @@ -229,7 +229,8 @@ For example:: will allow filtering on ``room`` and ``date``, also selecting ``full_name``, while fetching data only from the index. -``include`` is supported only on PostgreSQL. +Unique constraints with non-key columns are ignored for databases besides +PostgreSQL. Non-key columns have the same database restrictions as :attr:`Index.include`. @@ -272,7 +273,8 @@ For example:: creates a unique constraint that only allows one row to store a ``NULL`` value in the ``ordering`` column. -``nulls_distinct`` is ignored for databases besides PostgreSQL 15+. +Unique constraints with ``nulls_distinct`` are ignored for databases besides +PostgreSQL 15+. ``violation_error_code`` ------------------------ From 5a46f3fad7a1a0955d68e76a9b48daf7c4f7c1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Lazarevi=C4=87?= Date: Tue, 16 Jan 2024 14:30:01 +0700 Subject: [PATCH 327/748] Fixed #35112 -- Removed previous/next month animation in admin calendar widget. --- django/contrib/admin/static/admin/css/rtl.css | 12 +--- .../admin/static/admin/css/widgets.css | 12 +--- .../admin/static/admin/img/calendar-icons.svg | 69 ++++++++++++++++--- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/django/contrib/admin/static/admin/css/rtl.css b/django/contrib/admin/static/admin/css/rtl.css index bf636720fdda..1ab09fd10f5d 100644 --- a/django/contrib/admin/static/admin/css/rtl.css +++ b/django/contrib/admin/static/admin/css/rtl.css @@ -197,12 +197,7 @@ fieldset .fieldBox { top: 0; left: auto; right: 10px; - background: url(../img/calendar-icons.svg) 0 -30px no-repeat; -} - -.calendarbox .calendarnav-previous:focus, -.calendarbox .calendarnav-previous:hover { - background-position: 0 -45px; + background: url(../img/calendar-icons.svg) 0 -15px no-repeat; } .calendarnav-next { @@ -212,11 +207,6 @@ fieldset .fieldBox { background: url(../img/calendar-icons.svg) 0 0 no-repeat; } -.calendarbox .calendarnav-next:focus, -.calendarbox .calendarnav-next:hover { - background-position: 0 -15px; -} - .calendar caption, .calendarbox h2 { text-align: center; } diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css index d3d4732cb355..cc64811a2b4c 100644 --- a/django/contrib/admin/static/admin/css/widgets.css +++ b/django/contrib/admin/static/admin/css/widgets.css @@ -519,19 +519,9 @@ span.clearable-file-input label { background: url(../img/calendar-icons.svg) 0 0 no-repeat; } -.calendarbox .calendarnav-previous:focus, -.calendarbox .calendarnav-previous:hover { - background-position: 0 -15px; -} - .calendarnav-next { right: 10px; - background: url(../img/calendar-icons.svg) 0 -30px no-repeat; -} - -.calendarbox .calendarnav-next:focus, -.calendarbox .calendarnav-next:hover { - background-position: 0 -45px; + background: url(../img/calendar-icons.svg) 0 -15px no-repeat; } .calendar-cancel { diff --git a/django/contrib/admin/static/admin/img/calendar-icons.svg b/django/contrib/admin/static/admin/img/calendar-icons.svg index dbf21c39d238..04c02741ad30 100644 --- a/django/contrib/admin/static/admin/img/calendar-icons.svg +++ b/django/contrib/admin/static/admin/img/calendar-icons.svg @@ -1,14 +1,63 @@ - - - - + + + + + + - - + + - - - - + + From 561f77041542f9081288af2af97a289544a57e2a Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sat, 13 Jan 2024 15:54:56 -0500 Subject: [PATCH 328/748] Refs #22288 -- Corrected __range lookup test names. --- tests/expressions/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index cbb441601cd4..5ba786db135d 100644 --- a/tests/expressions/tests.py +++ b/tests/expressions/tests.py @@ -1151,7 +1151,7 @@ def test_in_lookup_allows_F_expressions_and_expressions_for_integers(self): [self.c5040, self.c5050, self.c5060], ) - def test_expressions_in_lookups_join_choice(self): + def test_expressions_range_lookups_join_choice(self): midpoint = datetime.time(13, 0) t1 = Time.objects.create(time=datetime.time(12, 0)) t2 = Time.objects.create(time=datetime.time(14, 0)) @@ -1225,7 +1225,7 @@ def test_expressions_not_introduce_sql_injection_via_untrusted_string_inclusion( queryset = Company.objects.filter(name__in=[F("num_chairs") + "1)) OR ((1==1"]) self.assertQuerySetEqual(queryset, [], ordered=False) - def test_in_lookup_allows_F_expressions_and_expressions_for_datetimes(self): + def test_range_lookup_allows_F_expressions_and_expressions_for_datetimes(self): start = datetime.datetime(2016, 2, 3, 15, 0, 0) end = datetime.datetime(2016, 2, 5, 15, 0, 0) experiment_1 = Experiment.objects.create( From 0fcee1676c7f14bb08e2cc662898dee56d9cf207 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sat, 13 Jan 2024 16:16:58 -0500 Subject: [PATCH 329/748] Fixed #35111 -- Fixed compilation of DateField __in/__range rhs on SQLite and MySQL. Also removed tests that ensured that adapt_(date)timefield backend operations where able to deal with expressions when it's not the case for any other adapt methods. --- django/db/backends/base/operations.py | 8 -------- django/db/backends/oracle/operations.py | 8 -------- django/db/backends/sqlite3/operations.py | 8 -------- django/db/models/lookups.py | 11 ++++++++--- tests/backends/base/test_operations.py | 10 +--------- tests/expressions/tests.py | 18 ++++++++++++++---- 6 files changed, 23 insertions(+), 40 deletions(-) diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py index d5a40eb46eed..9f40ec5e4fc5 100644 --- a/django/db/backends/base/operations.py +++ b/django/db/backends/base/operations.py @@ -562,10 +562,6 @@ def adapt_datetimefield_value(self, value): """ if value is None: return None - # Expression values are adapted by the database. - if hasattr(value, "resolve_expression"): - return value - return str(value) def adapt_timefield_value(self, value): @@ -575,10 +571,6 @@ def adapt_timefield_value(self, value): """ if value is None: return None - # Expression values are adapted by the database. - if hasattr(value, "resolve_expression"): - return value - if timezone.is_aware(value): raise ValueError("Django does not support timezone-aware times.") return str(value) diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index 9e8172b80a10..aedfeea2367d 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -590,10 +590,6 @@ def adapt_datetimefield_value(self, value): if value is None: return None - # Expression values are adapted by the database. - if hasattr(value, "resolve_expression"): - return value - # oracledb doesn't support tz-aware datetimes if timezone.is_aware(value): if settings.USE_TZ: @@ -610,10 +606,6 @@ def adapt_timefield_value(self, value): if value is None: return None - # Expression values are adapted by the database. - if hasattr(value, "resolve_expression"): - return value - if isinstance(value, str): return datetime.datetime.strptime(value, "%H:%M:%S") diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py index dfc9857b84c3..29a5c0391e40 100644 --- a/django/db/backends/sqlite3/operations.py +++ b/django/db/backends/sqlite3/operations.py @@ -263,10 +263,6 @@ def adapt_datetimefield_value(self, value): if value is None: return None - # Expression values are adapted by the database. - if hasattr(value, "resolve_expression"): - return value - # SQLite doesn't support tz-aware datetimes if timezone.is_aware(value): if settings.USE_TZ: @@ -283,10 +279,6 @@ def adapt_timefield_value(self, value): if value is None: return None - # Expression values are adapted by the database. - if hasattr(value, "resolve_expression"): - return value - # SQLite doesn't support tz-aware datetimes if timezone.is_aware(value): raise ValueError("SQLite backend does not support timezone-aware times.") diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py index c0bcc1b3bf13..4a6e2b324147 100644 --- a/django/db/models/lookups.py +++ b/django/db/models/lookups.py @@ -268,11 +268,16 @@ def get_db_prep_lookup(self, value, connection): getattr(field, "get_db_prep_value", None) or self.lhs.output_field.get_db_prep_value ) + if not self.get_db_prep_lookup_value_is_iterable: + value = [value] return ( "%s", - [get_db_prep_value(v, connection, prepared=True) for v in value] - if self.get_db_prep_lookup_value_is_iterable - else [get_db_prep_value(value, connection, prepared=True)], + [ + v + if hasattr(v, "as_sql") + else get_db_prep_value(v, connection, prepared=True) + for v in value + ], ) diff --git a/tests/backends/base/test_operations.py b/tests/backends/base/test_operations.py index d2a3fb67657f..8df02ee76b44 100644 --- a/tests/backends/base/test_operations.py +++ b/tests/backends/base/test_operations.py @@ -4,7 +4,7 @@ from django.core.management.color import no_style from django.db import NotSupportedError, connection, transaction from django.db.backends.base.operations import BaseDatabaseOperations -from django.db.models import DurationField, Value +from django.db.models import DurationField from django.db.models.expressions import Col from django.db.models.lookups import Exact from django.test import ( @@ -89,17 +89,9 @@ def test_adapt_unknown_value_time(self): def test_adapt_timefield_value_none(self): self.assertIsNone(self.ops.adapt_timefield_value(None)) - def test_adapt_timefield_value_expression(self): - value = Value(timezone.now().time()) - self.assertEqual(self.ops.adapt_timefield_value(value), value) - def test_adapt_datetimefield_value_none(self): self.assertIsNone(self.ops.adapt_datetimefield_value(None)) - def test_adapt_datetimefield_value_expression(self): - value = Value(timezone.now()) - self.assertEqual(self.ops.adapt_datetimefield_value(value), value) - def test_adapt_timefield_value(self): msg = "Django does not support timezone-aware times." with self.assertRaisesMessage(ValueError, msg): diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index 5ba786db135d..909e317dca11 100644 --- a/tests/expressions/tests.py +++ b/tests/expressions/tests.py @@ -1225,7 +1225,7 @@ def test_expressions_not_introduce_sql_injection_via_untrusted_string_inclusion( queryset = Company.objects.filter(name__in=[F("num_chairs") + "1)) OR ((1==1"]) self.assertQuerySetEqual(queryset, [], ordered=False) - def test_range_lookup_allows_F_expressions_and_expressions_for_datetimes(self): + def test_range_lookup_allows_F_expressions_and_expressions_for_dates(self): start = datetime.datetime(2016, 2, 3, 15, 0, 0) end = datetime.datetime(2016, 2, 5, 15, 0, 0) experiment_1 = Experiment.objects.create( @@ -1256,9 +1256,19 @@ def test_range_lookup_allows_F_expressions_and_expressions_for_datetimes(self): experiment=experiment_2, result_time=datetime.datetime(2016, 1, 8, 5, 0, 0), ) - within_experiment_time = [F("experiment__start"), F("experiment__end")] - queryset = Result.objects.filter(result_time__range=within_experiment_time) - self.assertSequenceEqual(queryset, [r1]) + tests = [ + # Datetimes. + ([F("experiment__start"), F("experiment__end")], "result_time__range"), + # Dates. + ( + [F("experiment__start__date"), F("experiment__end__date")], + "result_time__date__range", + ), + ] + for within_experiment_time, lookup in tests: + with self.subTest(lookup=lookup): + queryset = Result.objects.filter(**{lookup: within_experiment_time}) + self.assertSequenceEqual(queryset, [r1]) class FTests(SimpleTestCase): From 1592f0ac220c1fd37779f6d33efb28ebd60e2e66 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Tue, 16 Jan 2024 20:09:50 +0100 Subject: [PATCH 330/748] Used more specific link to email backends in EMAIL_BACKEND docs. --- docs/ref/settings.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index ed956f70c79c..eee1c66d5695 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1390,7 +1390,7 @@ This is only used if ``CommonMiddleware`` is installed (see Default: ``'``:class:`django.core.mail.backends.smtp.EmailBackend`\ ``'`` The backend to use for sending emails. For the list of available backends see -:doc:`/topics/email`. +:ref:`topic-email-backends`. .. setting:: EMAIL_FILE_PATH From 6debeac9e7538e0e32883dc36abe6fc40a35c874 Mon Sep 17 00:00:00 2001 From: David Sanders Date: Wed, 17 Jan 2024 06:30:27 +1100 Subject: [PATCH 331/748] Improved --list-tags help text for check management command. Co-authored-by: David Sanders --- django/core/management/commands/check.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/django/core/management/commands/check.py b/django/core/management/commands/check.py index 7624b853909d..6348c9fc6814 100644 --- a/django/core/management/commands/check.py +++ b/django/core/management/commands/check.py @@ -21,7 +21,10 @@ def add_arguments(self, parser): parser.add_argument( "--list-tags", action="store_true", - help="List available tags.", + help=( + "List available tags. Specify --deploy to include available deployment " + "tags." + ), ) parser.add_argument( "--deploy", From c7e986fc9f4848bd757d4b9b70a40586d2cee9fb Mon Sep 17 00:00:00 2001 From: Alexis Athlani Date: Mon, 15 Jan 2024 23:16:12 +0100 Subject: [PATCH 332/748] Fixed #35117 -- Added support for the hectare unit in Area. --- django/contrib/gis/measure.py | 9 +++++++-- docs/ref/contrib/gis/measure.txt | 13 +++++++++++++ docs/releases/5.1.txt | 2 ++ tests/gis_tests/test_measure.py | 11 +++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/django/contrib/gis/measure.py b/django/contrib/gis/measure.py index 590a80293ab0..707c061a29a5 100644 --- a/django/contrib/gis/measure.py +++ b/django/contrib/gis/measure.py @@ -347,8 +347,13 @@ def __mul__(self, other): class Area(MeasureBase): STANDARD_UNIT = AREA_PREFIX + Distance.STANDARD_UNIT # Getting the square units values and the alias dictionary. - UNITS = {"%s%s" % (AREA_PREFIX, k): v**2 for k, v in Distance.UNITS.items()} - ALIAS = {k: "%s%s" % (AREA_PREFIX, v) for k, v in Distance.ALIAS.items()} + UNITS = {"%s%s" % (AREA_PREFIX, k): v**2 for k, v in Distance.UNITS.items()} | { + "ha": 10000, + } + ALIAS = {k: "%s%s" % (AREA_PREFIX, v) for k, v in Distance.ALIAS.items()} | { + "hectare": "ha", + } + LALIAS = {k.lower(): v for k, v in ALIAS.items()} def __truediv__(self, other): diff --git a/docs/ref/contrib/gis/measure.txt b/docs/ref/contrib/gis/measure.txt index ad02db87facc..cee421122064 100644 --- a/docs/ref/contrib/gis/measure.txt +++ b/docs/ref/contrib/gis/measure.txt @@ -116,6 +116,19 @@ Unit Attribute Full name or alias(es) For example, ``Area(sq_m=2)`` creates an :class:`Area` object representing two square meters. +In addition to unit with the ``sq_`` prefix, the following units are also +supported on :class:`Area`: + +================================= ======================================== +Unit Attribute Full name or alias(es) +================================= ======================================== +``ha`` Hectare +================================= ======================================== + +.. versionchanged:: 5.1 + + Support for the ``ha`` unit was added. + Measurement API =============== diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 30317eaa1916..1f7d26d7b829 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -70,6 +70,8 @@ Minor features ``metro_code`` and ``region_code``, but the previous keys are also retained for backward compatibility. +* :class:`~django.contrib.gis.measure.Area` now supports the ``ha`` unit. + :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/test_measure.py b/tests/gis_tests/test_measure.py index 2a922abf9b0f..19644c1f98de 100644 --- a/tests/gis_tests/test_measure.py +++ b/tests/gis_tests/test_measure.py @@ -38,6 +38,10 @@ def test_init_invalid(self): with self.assertRaises(AttributeError): D(banana=100) + def test_init_invalid_area_only_units(self): + with self.assertRaises(AttributeError): + D(ha=100) + def test_access(self): "Testing access in different units" d = D(m=100) @@ -294,6 +298,13 @@ def test_units_str(self): self.assertEqual(repr(a1), "Area(sq_m=100.0)") self.assertEqual(repr(a2), "Area(sq_km=3.5)") + def test_hectare(self): + a = A(sq_m=10000) + self.assertEqual(a.ha, 1) + + def test_hectare_unit_att_name(self): + self.assertEqual(A.unit_attname("Hectare"), "ha") + def test_hash(self): a1 = A(sq_m=100) a2 = A(sq_m=1000000) From 27a3eee72170f1eb994a213db985f42c6cf5f994 Mon Sep 17 00:00:00 2001 From: Amir Karimi Date: Sat, 16 Sep 2023 05:41:22 +0330 Subject: [PATCH 333/748] Fixed #31700 -- Made makemigrations command display meaningful symbols for each operation. --- django/contrib/postgres/operations.py | 13 +++- .../management/commands/makemigrations.py | 4 +- django/db/migrations/operations/base.py | 20 ++++++ django/db/migrations/operations/fields.py | 10 ++- django/db/migrations/operations/models.py | 17 ++++- django/db/migrations/operations/special.py | 5 +- docs/intro/tutorial02.txt | 4 +- docs/ref/contrib/gis/tutorial.txt | 2 +- docs/ref/migration-operations.txt | 42 ++++++++++- docs/releases/5.1.txt | 9 ++- docs/topics/migrations.txt | 2 +- tests/migrations/test_commands.py | 12 ++-- tests/migrations/test_operations.py | 72 +++++++++++++++++++ tests/postgres_tests/test_operations.py | 21 ++++++ 14 files changed, 214 insertions(+), 19 deletions(-) diff --git a/django/contrib/postgres/operations.py b/django/contrib/postgres/operations.py index 5ac396bedf38..1ee5fbc2e2c2 100644 --- a/django/contrib/postgres/operations.py +++ b/django/contrib/postgres/operations.py @@ -5,12 +5,13 @@ ) from django.db import NotSupportedError, router from django.db.migrations import AddConstraint, AddIndex, RemoveIndex -from django.db.migrations.operations.base import Operation +from django.db.migrations.operations.base import Operation, OperationCategory from django.db.models.constraints import CheckConstraint class CreateExtension(Operation): reversible = True + category = OperationCategory.ADDITION def __init__(self, name): self.name = name @@ -120,6 +121,7 @@ class AddIndexConcurrently(NotInTransactionMixin, AddIndex): """Create an index using PostgreSQL's CREATE INDEX CONCURRENTLY syntax.""" atomic = False + category = OperationCategory.ADDITION def describe(self): return "Concurrently create index %s on field(s) %s of model %s" % ( @@ -145,6 +147,7 @@ class RemoveIndexConcurrently(NotInTransactionMixin, RemoveIndex): """Remove an index using PostgreSQL's DROP INDEX CONCURRENTLY syntax.""" atomic = False + category = OperationCategory.REMOVAL def describe(self): return "Concurrently remove index %s from %s" % (self.name, self.model_name) @@ -213,6 +216,8 @@ def remove_collation(self, schema_editor): class CreateCollation(CollationOperation): """Create a collation.""" + category = OperationCategory.ADDITION + def database_forwards(self, app_label, schema_editor, from_state, to_state): if schema_editor.connection.vendor != "postgresql" or not router.allow_migrate( schema_editor.connection.alias, app_label @@ -236,6 +241,8 @@ def migration_name_fragment(self): class RemoveCollation(CollationOperation): """Remove a collation.""" + category = OperationCategory.REMOVAL + def database_forwards(self, app_label, schema_editor, from_state, to_state): if schema_editor.connection.vendor != "postgresql" or not router.allow_migrate( schema_editor.connection.alias, app_label @@ -262,6 +269,8 @@ class AddConstraintNotValid(AddConstraint): NOT VALID syntax. """ + category = OperationCategory.ADDITION + def __init__(self, model_name, constraint): if not isinstance(constraint, CheckConstraint): raise TypeError( @@ -293,6 +302,8 @@ def migration_name_fragment(self): class ValidateConstraint(Operation): """Validate a table NOT VALID constraint.""" + category = OperationCategory.ALTERATION + def __init__(self, model_name, name): self.model_name = model_name self.name = name diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py index 22498af3c07d..a4e4d520e61b 100644 --- a/django/core/management/commands/makemigrations.py +++ b/django/core/management/commands/makemigrations.py @@ -348,7 +348,7 @@ def write_migration_files(self, changes, update_previous_migration_paths=None): migration_string = self.get_relative_path(writer.path) self.log(" %s\n" % self.style.MIGRATE_LABEL(migration_string)) for operation in migration.operations: - self.log(" - %s" % operation.describe()) + self.log(" %s" % operation.formatted_description()) if self.scriptable: self.stdout.write(migration_string) if not self.dry_run: @@ -456,7 +456,7 @@ def all_items_equal(seq): for migration in merge_migrations: self.log(self.style.MIGRATE_LABEL(" Branch %s" % migration.name)) for operation in migration.merged_operations: - self.log(" - %s" % operation.describe()) + self.log(" %s" % operation.formatted_description()) if questioner.ask_merge(app_label): # If they still want to merge it, then write out an empty # file depending on the migrations needing merging. diff --git a/django/db/migrations/operations/base.py b/django/db/migrations/operations/base.py index 7d4dff259746..3bd9546bd72f 100644 --- a/django/db/migrations/operations/base.py +++ b/django/db/migrations/operations/base.py @@ -1,6 +1,17 @@ +import enum + from django.db import router +class OperationCategory(str, enum.Enum): + ADDITION = "+" + REMOVAL = "-" + ALTERATION = "~" + PYTHON = "p" + SQL = "s" + MIXED = "?" + + class Operation: """ Base class for migration operations. @@ -33,6 +44,8 @@ class Operation: serialization_expand_args = [] + category = None + def __new__(cls, *args, **kwargs): # We capture the arguments to make returning them trivial self = object.__new__(cls) @@ -85,6 +98,13 @@ def describe(self): """ return "%s: %s" % (self.__class__.__name__, self._constructor_args) + def formatted_description(self): + """Output a description prefixed by a category symbol.""" + description = self.describe() + if self.category is None: + return f"{OperationCategory.MIXED.value} {description}" + return f"{self.category.value} {description}" + @property def migration_name_fragment(self): """ diff --git a/django/db/migrations/operations/fields.py b/django/db/migrations/operations/fields.py index fc5640bea99f..34b441a24724 100644 --- a/django/db/migrations/operations/fields.py +++ b/django/db/migrations/operations/fields.py @@ -2,7 +2,7 @@ from django.db.models import NOT_PROVIDED from django.utils.functional import cached_property -from .base import Operation +from .base import Operation, OperationCategory class FieldOperation(Operation): @@ -75,6 +75,8 @@ def reduce(self, operation, app_label): class AddField(FieldOperation): """Add a field to a model.""" + category = OperationCategory.ADDITION + def __init__(self, model_name, name, field, preserve_default=True): self.preserve_default = preserve_default super().__init__(model_name, name, field) @@ -154,6 +156,8 @@ def reduce(self, operation, app_label): class RemoveField(FieldOperation): """Remove a field from a model.""" + category = OperationCategory.REMOVAL + def deconstruct(self): kwargs = { "model_name": self.model_name, @@ -201,6 +205,8 @@ class AlterField(FieldOperation): new field. """ + category = OperationCategory.ALTERATION + def __init__(self, model_name, name, field, preserve_default=True): self.preserve_default = preserve_default super().__init__(model_name, name, field) @@ -270,6 +276,8 @@ def reduce(self, operation, app_label): class RenameField(FieldOperation): """Rename a field on the model. Might affect db_column too.""" + category = OperationCategory.ALTERATION + def __init__(self, model_name, old_name, new_name): self.old_name = old_name self.new_name = new_name diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py index df153da7e5a2..b24a8f6557b9 100644 --- a/django/db/migrations/operations/models.py +++ b/django/db/migrations/operations/models.py @@ -1,5 +1,5 @@ from django.db import models -from django.db.migrations.operations.base import Operation +from django.db.migrations.operations.base import Operation, OperationCategory from django.db.migrations.state import ModelState from django.db.migrations.utils import field_references, resolve_relation from django.db.models.options import normalize_together @@ -41,6 +41,7 @@ def can_reduce_through(self, operation, app_label): class CreateModel(ModelOperation): """Create a model's table.""" + category = OperationCategory.ADDITION serialization_expand_args = ["fields", "options", "managers"] def __init__(self, name, fields, options=None, bases=None, managers=None): @@ -347,6 +348,8 @@ def reduce(self, operation, app_label): class DeleteModel(ModelOperation): """Drop a model's table.""" + category = OperationCategory.REMOVAL + def deconstruct(self): kwargs = { "name": self.name, @@ -382,6 +385,8 @@ def migration_name_fragment(self): class RenameModel(ModelOperation): """Rename a model.""" + category = OperationCategory.ALTERATION + def __init__(self, old_name, new_name): self.old_name = old_name self.new_name = new_name @@ -499,6 +504,8 @@ def reduce(self, operation, app_label): class ModelOptionOperation(ModelOperation): + category = OperationCategory.ALTERATION + def reduce(self, operation, app_label): if ( isinstance(operation, (self.__class__, DeleteModel)) @@ -849,6 +856,8 @@ def model_name_lower(self): class AddIndex(IndexOperation): """Add an index on a model.""" + category = OperationCategory.ADDITION + def __init__(self, model_name, index): self.model_name = model_name if not index.name: @@ -911,6 +920,8 @@ def reduce(self, operation, app_label): class RemoveIndex(IndexOperation): """Remove an index from a model.""" + category = OperationCategory.REMOVAL + def __init__(self, model_name, name): self.model_name = model_name self.name = name @@ -954,6 +965,8 @@ def migration_name_fragment(self): class RenameIndex(IndexOperation): """Rename an index.""" + category = OperationCategory.ALTERATION + def __init__(self, model_name, new_name, old_name=None, old_fields=None): if not old_name and not old_fields: raise ValueError( @@ -1104,6 +1117,7 @@ def reduce(self, operation, app_label): class AddConstraint(IndexOperation): + category = OperationCategory.ADDITION option_name = "constraints" def __init__(self, model_name, constraint): @@ -1154,6 +1168,7 @@ def reduce(self, operation, app_label): class RemoveConstraint(IndexOperation): + category = OperationCategory.REMOVAL option_name = "constraints" def __init__(self, model_name, name): diff --git a/django/db/migrations/operations/special.py b/django/db/migrations/operations/special.py index 94a6ec72de9a..196f24fcd671 100644 --- a/django/db/migrations/operations/special.py +++ b/django/db/migrations/operations/special.py @@ -1,6 +1,6 @@ from django.db import router -from .base import Operation +from .base import Operation, OperationCategory class SeparateDatabaseAndState(Operation): @@ -11,6 +11,7 @@ class SeparateDatabaseAndState(Operation): that affect the state or not the database, or so on. """ + category = OperationCategory.MIXED serialization_expand_args = ["database_operations", "state_operations"] def __init__(self, database_operations=None, state_operations=None): @@ -68,6 +69,7 @@ class RunSQL(Operation): by this SQL change, in case it's custom column/table creation/deletion. """ + category = OperationCategory.SQL noop = "" def __init__( @@ -138,6 +140,7 @@ class RunPython(Operation): Run Python code in a context suitable for doing versioned ORM operations. """ + category = OperationCategory.PYTHON reduces_to_sql = False def __init__( diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index 6ba70ddc1c1a..d558a3eb1dc1 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -241,8 +241,8 @@ You should see something similar to the following: Migrations for 'polls': polls/migrations/0001_initial.py - - Create model Question - - Create model Choice + + Create model Question + + Create model Choice By running ``makemigrations``, you're telling Django that you've made some changes to your models (in this case, you've made new ones) and that diff --git a/docs/ref/contrib/gis/tutorial.txt b/docs/ref/contrib/gis/tutorial.txt index eb62df56a86e..53c961561cdc 100644 --- a/docs/ref/contrib/gis/tutorial.txt +++ b/docs/ref/contrib/gis/tutorial.txt @@ -241,7 +241,7 @@ create a database migration: $ python manage.py makemigrations Migrations for 'world': world/migrations/0001_initial.py: - - Create model WorldBorder + + Create model WorldBorder Let's look at the SQL that will generate the table for the ``WorldBorder`` model: diff --git a/docs/ref/migration-operations.txt b/docs/ref/migration-operations.txt index e8d863085167..3c39d27873bf 100644 --- a/docs/ref/migration-operations.txt +++ b/docs/ref/migration-operations.txt @@ -475,6 +475,42 @@ operations. For an example using ``SeparateDatabaseAndState``, see :ref:`changing-a-manytomanyfield-to-use-a-through-model`. +Operation category +================== + +.. versionadded:: 5.1 + +.. currentmodule:: django.db.migrations.operations.base + +.. class:: OperationCategory + + Categories of migration operation used by the :djadmin:`makemigrations` + command to display meaningful symbols. + + .. attribute:: ADDITION + + *Symbol*: ``+`` + + .. attribute:: REMOVAL + + *Symbol*: ``-`` + + .. attribute:: ALTERATION + + *Symbol*: ``~`` + + .. attribute:: PYTHON + + *Symbol*: ``p`` + + .. attribute:: SQL + + *Symbol*: ``s`` + + .. attribute:: MIXED + + *Symbol*: ``?`` + .. _writing-your-own-migration-operation: Writing your own @@ -495,6 +531,10 @@ structure of an ``Operation`` looks like this:: # If this is False, Django will refuse to reverse past this operation. reversible = False + # This categorizes the operation. The corresponding symbol will be + # display by the makemigrations command. + category = OperationCategory.ADDITION + def __init__(self, arg1, arg2): # Operations are usually instantiated with arguments in migration # files. Store the values of them on self for later use. @@ -516,7 +556,7 @@ structure of an ``Operation`` looks like this:: pass def describe(self): - # This is used to describe what the operation does in console output. + # This is used to describe what the operation does. return "Custom Operation" @property diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 1f7d26d7b829..f8393303be2e 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -178,12 +178,17 @@ Logging Management Commands ~~~~~~~~~~~~~~~~~~~ -* ... +* :djadmin:`makemigrations` command now displays meaningful symbols for each + operation to highlight :class:`operation categories + `. Migrations ~~~~~~~~~~ -* ... +* The new ``Operation.category`` attribute allows specifying an + :class:`operation category + ` used by the + :djadmin:`makemigrations` to display a meaningful symbol for the operation. Models ~~~~~~ diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt index 9e339852b674..5fb5019c1ec0 100644 --- a/docs/topics/migrations.txt +++ b/docs/topics/migrations.txt @@ -118,7 +118,7 @@ field and remove a model - and then run :djadmin:`makemigrations`: $ python manage.py makemigrations Migrations for 'books': books/migrations/0003_auto.py: - - Alter field author on book + ~ Alter field author on book Your models will be scanned and compared to the versions currently contained in your migration files, and then a new set of migrations diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index a9c1cdf8938d..263b25ab6145 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -2141,7 +2141,7 @@ class Meta: ) # Normal --dry-run output - self.assertIn("- Add field silly_char to sillymodel", out.getvalue()) + self.assertIn("+ Add field silly_char to sillymodel", out.getvalue()) # Additional output caused by verbosity 3 # The complete migrations file that would be written @@ -2171,7 +2171,7 @@ def test_makemigrations_scriptable(self): ) initial_file = os.path.join(migration_dir, "0001_initial.py") self.assertEqual(out.getvalue(), f"{initial_file}\n") - self.assertIn(" - Create model ModelWithCustomBase\n", err.getvalue()) + self.assertIn(" + Create model ModelWithCustomBase\n", err.getvalue()) @mock.patch("builtins.input", return_value="Y") def test_makemigrations_scriptable_merge(self, mock_input): @@ -2216,7 +2216,7 @@ class Meta: self.assertTrue(os.path.exists(initial_file)) # Command output indicates the migration is created. - self.assertIn(" - Create model SillyModel", out.getvalue()) + self.assertIn(" + Create model SillyModel", out.getvalue()) @override_settings(MIGRATION_MODULES={"migrations": "some.nonexistent.path"}) def test_makemigrations_migrations_modules_nonexistent_toplevel_package(self): @@ -2321,12 +2321,12 @@ def test_makemigrations_merge_dont_output_dependency_operations(self): out.getvalue().lower(), "merging conflicting_app_with_dependencies\n" " branch 0002_conflicting_second\n" - " - create model something\n" + " + create model something\n" " branch 0002_second\n" " - delete model tribble\n" " - remove field silly_field from author\n" - " - add field rating to author\n" - " - create model book\n" + " + add field rating to author\n" + " + create model book\n" "\n" "merging will only work if the operations printed above do not " "conflict\n" diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 2d9d3a38f05c..52e43d20f90a 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -4,6 +4,7 @@ from django.core.exceptions import FieldDoesNotExist from django.db import IntegrityError, connection, migrations, models, transaction from django.db.migrations.migration import Migration +from django.db.migrations.operations.base import Operation from django.db.migrations.operations.fields import FieldOperation from django.db.migrations.state import ModelState, ProjectState from django.db.models import F @@ -47,6 +48,7 @@ def test_create_model(self): ], ) self.assertEqual(operation.describe(), "Create model Pony") + self.assertEqual(operation.formatted_description(), "+ Create model Pony") self.assertEqual(operation.migration_name_fragment, "pony") # Test the state alteration project_state = ProjectState() @@ -710,6 +712,7 @@ def test_delete_model(self): # Test the state alteration operation = migrations.DeleteModel("Pony") self.assertEqual(operation.describe(), "Delete model Pony") + self.assertEqual(operation.formatted_description(), "- Delete model Pony") self.assertEqual(operation.migration_name_fragment, "delete_pony") new_state = project_state.clone() operation.state_forwards("test_dlmo", new_state) @@ -790,6 +793,9 @@ def test_rename_model(self): # Test the state alteration operation = migrations.RenameModel("Pony", "Horse") self.assertEqual(operation.describe(), "Rename model Pony to Horse") + self.assertEqual( + operation.formatted_description(), "~ Rename model Pony to Horse" + ) self.assertEqual(operation.migration_name_fragment, "rename_pony_horse") # Test initial state and database self.assertIn(("test_rnmo", "pony"), project_state.models) @@ -1350,6 +1356,9 @@ def test_add_field(self): models.FloatField(null=True, default=5), ) self.assertEqual(operation.describe(), "Add field height to Pony") + self.assertEqual( + operation.formatted_description(), "+ Add field height to Pony" + ) self.assertEqual(operation.migration_name_fragment, "pony_height") project_state, new_state = self.make_test_state("test_adfl", operation) self.assertEqual(len(new_state.models["test_adfl", "pony"].fields), 6) @@ -1906,6 +1915,9 @@ def test_remove_field(self): # Test the state alteration operation = migrations.RemoveField("Pony", "pink") self.assertEqual(operation.describe(), "Remove field pink from Pony") + self.assertEqual( + operation.formatted_description(), "- Remove field pink from Pony" + ) self.assertEqual(operation.migration_name_fragment, "remove_pony_pink") new_state = project_state.clone() operation.state_forwards("test_rmfl", new_state) @@ -1952,6 +1964,10 @@ def test_alter_model_table(self): self.assertEqual( operation.describe(), "Rename table for Pony to test_almota_pony_2" ) + self.assertEqual( + operation.formatted_description(), + "~ Rename table for Pony to test_almota_pony_2", + ) self.assertEqual(operation.migration_name_fragment, "alter_pony_table") new_state = project_state.clone() operation.state_forwards("test_almota", new_state) @@ -2093,6 +2109,9 @@ def test_alter_field(self): "Pony", "pink", models.IntegerField(null=True) ) self.assertEqual(operation.describe(), "Alter field pink on Pony") + self.assertEqual( + operation.formatted_description(), "~ Alter field pink on Pony" + ) self.assertEqual(operation.migration_name_fragment, "alter_pony_pink") new_state = project_state.clone() operation.state_forwards("test_alfl", new_state) @@ -2403,6 +2422,9 @@ def test_alter_model_table_comment(self): # Add table comment. operation = migrations.AlterModelTableComment("Pony", "Custom pony comment") self.assertEqual(operation.describe(), "Alter Pony table comment") + self.assertEqual( + operation.formatted_description(), "~ Alter Pony table comment" + ) self.assertEqual(operation.migration_name_fragment, "alter_pony_table_comment") new_state = project_state.clone() operation.state_forwards(app_label, new_state) @@ -3073,6 +3095,9 @@ def test_rename_field(self): project_state = self.set_up_test_model("test_rnfl") operation = migrations.RenameField("Pony", "pink", "blue") self.assertEqual(operation.describe(), "Rename field pink on Pony to blue") + self.assertEqual( + operation.formatted_description(), "~ Rename field pink on Pony to blue" + ) self.assertEqual(operation.migration_name_fragment, "rename_pink_pony_blue") new_state = project_state.clone() operation.state_forwards("test_rnfl", new_state) @@ -3326,6 +3351,10 @@ def test_alter_unique_together(self): self.assertEqual( operation.describe(), "Alter unique_together for Pony (1 constraint(s))" ) + self.assertEqual( + operation.formatted_description(), + "~ Alter unique_together for Pony (1 constraint(s))", + ) self.assertEqual( operation.migration_name_fragment, "alter_pony_unique_together", @@ -3478,6 +3507,10 @@ def test_add_index(self): operation.describe(), "Create index test_adin_pony_pink_idx on field(s) pink of model Pony", ) + self.assertEqual( + operation.formatted_description(), + "+ Create index test_adin_pony_pink_idx on field(s) pink of model Pony", + ) self.assertEqual( operation.migration_name_fragment, "pony_test_adin_pony_pink_idx", @@ -3511,6 +3544,9 @@ def test_remove_index(self): self.assertIndexExists("test_rmin_pony", ["pink", "weight"]) operation = migrations.RemoveIndex("Pony", "pony_test_idx") self.assertEqual(operation.describe(), "Remove index pony_test_idx from Pony") + self.assertEqual( + operation.formatted_description(), "- Remove index pony_test_idx from Pony" + ) self.assertEqual( operation.migration_name_fragment, "remove_pony_pony_test_idx", @@ -3565,6 +3601,10 @@ def test_rename_index(self): operation.describe(), "Rename index pony_pink_idx on Pony to new_pony_test_idx", ) + self.assertEqual( + operation.formatted_description(), + "~ Rename index pony_pink_idx on Pony to new_pony_test_idx", + ) self.assertEqual( operation.migration_name_fragment, "rename_pony_pink_idx_new_pony_test_idx", @@ -3807,6 +3847,10 @@ def test_alter_index_together_remove(self): self.assertEqual( operation.describe(), "Alter index_together for Pony (0 constraint(s))" ) + self.assertEqual( + operation.formatted_description(), + "~ Alter index_together for Pony (0 constraint(s))", + ) def test_add_constraint(self): project_state = self.set_up_test_model("test_addconstraint") @@ -3819,6 +3863,10 @@ def test_add_constraint(self): gt_operation.describe(), "Create constraint test_add_constraint_pony_pink_gt_2 on model Pony", ) + self.assertEqual( + gt_operation.formatted_description(), + "+ Create constraint test_add_constraint_pony_pink_gt_2 on model Pony", + ) self.assertEqual( gt_operation.migration_name_fragment, "pony_test_add_constraint_pony_pink_gt_2", @@ -4024,6 +4072,10 @@ def test_remove_constraint(self): gt_operation.describe(), "Remove constraint test_remove_constraint_pony_pink_gt_2 from model Pony", ) + self.assertEqual( + gt_operation.formatted_description(), + "- Remove constraint test_remove_constraint_pony_pink_gt_2 from model Pony", + ) self.assertEqual( gt_operation.migration_name_fragment, "remove_pony_test_remove_constraint_pony_pink_gt_2", @@ -4564,6 +4616,9 @@ def test_alter_model_options(self): "Pony", {"permissions": [("can_groom", "Can groom")]} ) self.assertEqual(operation.describe(), "Change Meta options on Pony") + self.assertEqual( + operation.formatted_description(), "~ Change Meta options on Pony" + ) self.assertEqual(operation.migration_name_fragment, "alter_pony_options") new_state = project_state.clone() operation.state_forwards("test_almoop", new_state) @@ -4630,6 +4685,10 @@ def test_alter_order_with_respect_to(self): self.assertEqual( operation.describe(), "Set order_with_respect_to on Rider to pony" ) + self.assertEqual( + operation.formatted_description(), + "~ Set order_with_respect_to on Rider to pony", + ) self.assertEqual( operation.migration_name_fragment, "alter_rider_order_with_respect_to", @@ -4705,6 +4764,7 @@ def test_alter_model_managers(self): ], ) self.assertEqual(operation.describe(), "Change managers on Pony") + self.assertEqual(operation.formatted_description(), "~ Change managers on Pony") self.assertEqual(operation.migration_name_fragment, "alter_pony_managers") managers = project_state.models["test_almoma", "pony"].managers self.assertEqual(managers, []) @@ -4840,6 +4900,7 @@ def test_run_sql(self): ], ) self.assertEqual(operation.describe(), "Raw SQL operation") + self.assertEqual(operation.formatted_description(), "s Raw SQL operation") # Test the state alteration new_state = project_state.clone() operation.state_forwards("test_runsql", new_state) @@ -5034,6 +5095,7 @@ def inner_method_reverse(models, schema_editor): inner_method, reverse_code=inner_method_reverse ) self.assertEqual(operation.describe(), "Raw Python operation") + self.assertEqual(operation.formatted_description(), "p Raw Python operation") # Test the state alteration does nothing new_state = project_state.clone() operation.state_forwards("test_runpython", new_state) @@ -5565,6 +5627,10 @@ def test_separate_database_and_state(self): self.assertEqual( operation.describe(), "Custom state/database change combination" ) + self.assertEqual( + operation.formatted_description(), + "? Custom state/database change combination", + ) # Test the state alteration new_state = project_state.clone() operation.state_forwards("test_separatedatabaseandstate", new_state) @@ -6073,3 +6139,9 @@ def test_reference_field_by_through_fields(self): self.assertIs( operation.references_field("Through", "second", "migrations"), True ) + + +class BaseOperationTests(SimpleTestCase): + def test_formatted_description_no_category(self): + operation = Operation() + self.assertEqual(operation.formatted_description(), "? Operation: ((), {})") diff --git a/tests/postgres_tests/test_operations.py b/tests/postgres_tests/test_operations.py index f395198533c9..ff344e3cb0c4 100644 --- a/tests/postgres_tests/test_operations.py +++ b/tests/postgres_tests/test_operations.py @@ -59,6 +59,10 @@ def test_add(self): operation.describe(), "Concurrently create index pony_pink_idx on field(s) pink of model Pony", ) + self.assertEqual( + operation.formatted_description(), + "+ Concurrently create index pony_pink_idx on field(s) pink of model Pony", + ) operation.state_forwards(self.app_label, new_state) self.assertEqual( len(new_state.models[self.app_label, "pony"].options["indexes"]), 1 @@ -154,6 +158,10 @@ def test_remove(self): operation.describe(), "Concurrently remove index pony_pink_idx from Pony", ) + self.assertEqual( + operation.formatted_description(), + "- Concurrently remove index pony_pink_idx from Pony", + ) operation.state_forwards(self.app_label, new_state) self.assertEqual( len(new_state.models[self.app_label, "pony"].options["indexes"]), 0 @@ -190,6 +198,9 @@ class CreateExtensionTests(PostgreSQLTestCase): @override_settings(DATABASE_ROUTERS=[NoMigrationRouter()]) def test_no_allow_migrate(self): operation = CreateExtension("tablefunc") + self.assertEqual( + operation.formatted_description(), "+ Creates extension tablefunc" + ) project_state = ProjectState() new_state = project_state.clone() # Don't create an extension. @@ -287,6 +298,7 @@ def test_create(self): operation = CreateCollation("C_test", locale="C") self.assertEqual(operation.migration_name_fragment, "create_collation_c_test") self.assertEqual(operation.describe(), "Create collation C_test") + self.assertEqual(operation.formatted_description(), "+ Create collation C_test") project_state = ProjectState() new_state = project_state.clone() # Create a collation. @@ -418,6 +430,7 @@ def test_remove(self): operation = RemoveCollation("C_test", locale="C") self.assertEqual(operation.migration_name_fragment, "remove_collation_c_test") self.assertEqual(operation.describe(), "Remove collation C_test") + self.assertEqual(operation.formatted_description(), "- Remove collation C_test") project_state = ProjectState() new_state = project_state.clone() # Remove a collation. @@ -470,6 +483,10 @@ def test_add(self): operation.describe(), f"Create not valid constraint {constraint_name} on model Pony", ) + self.assertEqual( + operation.formatted_description(), + f"+ Create not valid constraint {constraint_name} on model Pony", + ) self.assertEqual( operation.migration_name_fragment, f"pony_{constraint_name}_not_valid", @@ -530,6 +547,10 @@ def test_validate(self): operation.describe(), f"Validate constraint {constraint_name} on model Pony", ) + self.assertEqual( + operation.formatted_description(), + f"~ Validate constraint {constraint_name} on model Pony", + ) self.assertEqual( operation.migration_name_fragment, f"pony_validate_{constraint_name}", From 12c71bff8300f5b1252dab099f4753dc86903fbd Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Thu, 18 Jan 2024 05:21:15 +0100 Subject: [PATCH 334/748] Fixed typo in docs/ref/migration-operations.txt. --- docs/ref/migration-operations.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/migration-operations.txt b/docs/ref/migration-operations.txt index 3c39d27873bf..cd852f537dfd 100644 --- a/docs/ref/migration-operations.txt +++ b/docs/ref/migration-operations.txt @@ -532,7 +532,7 @@ structure of an ``Operation`` looks like this:: reversible = False # This categorizes the operation. The corresponding symbol will be - # display by the makemigrations command. + # displayed by the makemigrations command. category = OperationCategory.ADDITION def __init__(self, arg1, arg2): From cfacd69ab81c37564799a7b8a0a765e3c1f40941 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 17 Jan 2024 06:48:45 +0000 Subject: [PATCH 335/748] Refs #35058 -- Added is_3d and set_3d() to OGRGeometry. --- django/contrib/gis/gdal/geometries.py | 27 ++++++++++++++++------ django/contrib/gis/gdal/prototypes/geom.py | 3 +++ django/contrib/gis/utils/layermapping.py | 4 ++-- docs/ref/contrib/gis/gdal.txt | 21 +++++++++++++++++ docs/releases/5.1.txt | 6 +++++ tests/gis_tests/gdal_tests/test_geom.py | 13 +++++++++++ 6 files changed, 65 insertions(+), 9 deletions(-) diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py index 509441364238..516c6c711393 100644 --- a/django/contrib/gis/gdal/geometries.py +++ b/django/contrib/gis/gdal/geometries.py @@ -268,6 +268,20 @@ def extent(self): "Return the envelope as a 4-tuple, instead of as an Envelope object." return self.envelope.tuple + @property + def is_3d(self): + """Return True if the geometry has Z coordinates.""" + return capi.is_3d(self.ptr) + + def set_3d(self, value): + """Set if this geometry has Z coordinates.""" + if value is True: + capi.set_3d(self.ptr, 1) + elif value is False: + capi.set_3d(self.ptr, 0) + else: + raise ValueError(f"Input to 'set_3d' must be a boolean, got '{value!r}'.") + # #### SpatialReference-related Properties #### # The SRS property @@ -546,16 +560,15 @@ def y(self): @property def z(self): "Return the Z coordinate for this Point." - if self.coord_dim == 3: + if self.is_3d: return capi.getz(self.ptr, 0) @property def tuple(self): "Return the tuple of this point." - if self.coord_dim == 2: - return (self.x, self.y) - elif self.coord_dim == 3: + if self.is_3d: return (self.x, self.y, self.z) + return self.x, self.y coords = tuple @@ -566,13 +579,13 @@ def __getitem__(self, index): if 0 <= index < self.point_count: x, y, z = c_double(), c_double(), c_double() capi.get_point(self.ptr, index, byref(x), byref(y), byref(z)) + if self.is_3d: + return x.value, y.value, z.value dim = self.coord_dim if dim == 1: return (x.value,) elif dim == 2: return (x.value, y.value) - elif dim == 3: - return (x.value, y.value, z.value) else: raise IndexError( "Index out of range when accessing points of a line string: %s." % index @@ -609,7 +622,7 @@ def y(self): @property def z(self): "Return the Z coordinates in a list." - if self.coord_dim == 3: + if self.is_3d: return self._listarr(capi.getz) diff --git a/django/contrib/gis/gdal/prototypes/geom.py b/django/contrib/gis/gdal/prototypes/geom.py index d5fb1a5c9983..aaada31d5bdb 100644 --- a/django/contrib/gis/gdal/prototypes/geom.py +++ b/django/contrib/gis/gdal/prototypes/geom.py @@ -4,6 +4,7 @@ from django.contrib.gis.gdal.libgdal import GDAL_VERSION, lgdal from django.contrib.gis.gdal.prototypes.errcheck import check_envelope from django.contrib.gis.gdal.prototypes.generation import ( + bool_output, const_string_output, double_output, geom_output, @@ -79,6 +80,8 @@ def topology_func(f): geom_intersection = geom_output(lgdal.OGR_G_Intersection, [c_void_p, c_void_p]) geom_sym_diff = geom_output(lgdal.OGR_G_SymmetricDifference, [c_void_p, c_void_p]) geom_union = geom_output(lgdal.OGR_G_Union, [c_void_p, c_void_p]) +is_3d = bool_output(lgdal.OGR_G_Is3D, [c_void_p]) +set_3d = void_output(lgdal.OGR_G_Set3D, [c_void_p, c_int], errcheck=False) # Geometry modification routines. add_geom = void_output(lgdal.OGR_G_AddGeometry, [c_void_p, c_void_p]) diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index 2dcf8396035d..3e33c27772f5 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -489,8 +489,8 @@ def verify_geom(self, geom, model_field): the mapped shapefile only contains Polygons). """ # Downgrade a 3D geom to a 2D one, if necessary. - if self.coord_dim != geom.coord_dim: - geom.coord_dim = self.coord_dim + if self.coord_dim == 2 and geom.is_3d: + geom.set_3d(False) if self.make_multi(geom.geom_type, model_field): # Constructing a multi-geometry type to contain the single geometry diff --git a/docs/ref/contrib/gis/gdal.txt b/docs/ref/contrib/gis/gdal.txt index c0221a17a939..c98c76cd101c 100644 --- a/docs/ref/contrib/gis/gdal.txt +++ b/docs/ref/contrib/gis/gdal.txt @@ -556,6 +556,27 @@ coordinate transformation: Returns or sets the coordinate dimension of this geometry. For example, the value would be 2 for two-dimensional geometries. + .. attribute:: is_3d + + .. versionadded:: 5.1 + + A boolean indicating if this geometry has Z coordinates. + + .. method:: set_3d(value) + + .. versionadded:: 5.1 + + A method that adds or removes the Z coordinate dimension. + + .. code-block:: pycon + + >>> p = OGRGeometry("POINT (1 2 3)") + >>> p.is_3d + True + >>> p.set_3d(False) + >>> p.wkt + "POINT (1 2)" + .. attribute:: geom_count Returns the number of elements in this geometry: diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index f8393303be2e..be609cea7f21 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -72,6 +72,12 @@ Minor features * :class:`~django.contrib.gis.measure.Area` now supports the ``ha`` unit. +* The new :attr:`.OGRGeometry.is_3d` attribute allows checking if a geometry + has a ``Z`` coordinate dimension. + +* The new :meth:`.OGRGeometry.set_3d` method allows addition and removal of the + ``Z`` coordinate dimension. + :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/gdal_tests/test_geom.py b/tests/gis_tests/gdal_tests/test_geom.py index 372a38544321..eba90504b26f 100644 --- a/tests/gis_tests/gdal_tests/test_geom.py +++ b/tests/gis_tests/gdal_tests/test_geom.py @@ -714,3 +714,16 @@ def test_geometry_types(self): msg = f"Unsupported geometry type: {type_}" with self.assertRaisesMessage(TypeError, msg): OGRGeometry(f"{geom_type} EMPTY") + + def test_is_3d_and_set_3d(self): + geom = OGRGeometry("POINT (1 2)") + self.assertIs(geom.is_3d, False) + geom.set_3d(True) + self.assertIs(geom.is_3d, True) + self.assertEqual(geom.wkt, "POINT (1 2 0)") + geom.set_3d(False) + self.assertIs(geom.is_3d, False) + self.assertEqual(geom.wkt, "POINT (1 2)") + msg = "Input to 'set_3d' must be a boolean, got 'None'" + with self.assertRaisesMessage(ValueError, msg): + geom.set_3d(None) From 51967b56c404358c4fb1a47731fefb4171aea5ee Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Thu, 18 Jan 2024 12:20:19 +0100 Subject: [PATCH 336/748] Corrected forms imports in forms_tests/tests/test_forms.py. --- tests/forms_tests/tests/test_forms.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 19ca978c4523..a86d443e33c5 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -14,6 +14,7 @@ DateField, DateTimeField, EmailField, + Field, FileField, FileInput, FloatField, @@ -35,10 +36,9 @@ TextInput, TimeField, ValidationError, - forms, ) from django.forms.renderers import DjangoTemplates, get_default_renderer -from django.forms.utils import ErrorList +from django.forms.utils import ErrorDict, ErrorList from django.http import QueryDict from django.template import Context, Template from django.test import SimpleTestCase @@ -1656,7 +1656,7 @@ def clean(self): self._errors = e.update_error_dict(self._errors) try: - raise ValidationError({"code": forms.ErrorList(["Code error 3."])}) + raise ValidationError({"code": ErrorList(["Code error 3."])}) except ValidationError as e: self._errors = e.update_error_dict(self._errors) @@ -1680,7 +1680,7 @@ def clean(self): self.assertFalse(form.is_valid()) # update_error_dict didn't lose track of the ErrorDict type. - self.assertIsInstance(form._errors, forms.ErrorDict) + self.assertIsInstance(form._errors, ErrorDict) self.assertEqual( dict(form.errors), @@ -2738,7 +2738,7 @@ class Person(Form): self.assertNotIn("birthday", p.changed_data) # A field raising ValidationError is always in changed_data - class PedanticField(forms.Field): + class PedanticField(Field): def to_python(self, value): raise ValidationError("Whatever") @@ -2879,7 +2879,7 @@ def now(self): microseconds, ) - class DateTimeForm(forms.Form): + class DateTimeForm(Form): dt = DateTimeField(initial=FakeTime().now, disabled=disabled) return DateTimeForm({}) @@ -2914,7 +2914,7 @@ def test_datetime_clean_disabled_callable_initial_bound_field(self): self.assertEqual(cleaned, bf.initial) def test_datetime_changed_data_callable_with_microseconds(self): - class DateTimeForm(forms.Form): + class DateTimeForm(Form): dt = DateTimeField( initial=lambda: datetime.datetime(2006, 10, 25, 14, 30, 45, 123456), disabled=True, @@ -3570,8 +3570,8 @@ class FileForm(Form): ) def test_filefield_initial_callable(self): - class FileForm(forms.Form): - file1 = forms.FileField(initial=lambda: "resume.txt") + class FileForm(Form): + file1 = FileField(initial=lambda: "resume.txt") f = FileForm({}) self.assertEqual(f.errors, {}) @@ -3579,7 +3579,7 @@ class FileForm(forms.Form): def test_filefield_with_fileinput_required(self): class FileForm(Form): - file1 = forms.FileField(widget=FileInput) + file1 = FileField(widget=FileInput) f = FileForm(auto_id=False) self.assertHTMLEqual( @@ -4057,7 +4057,7 @@ def to_python(self, value): return {} return super().to_python(value) - class JSONForm(forms.Form): + class JSONForm(Form): json = CustomJSONField() form = JSONForm(data={"json": "{}"}) From 10c7c7320baf1c655fcb91202169d77725c9c4bd Mon Sep 17 00:00:00 2001 From: Salvo Polizzi Date: Thu, 18 Jan 2024 10:21:12 +0100 Subject: [PATCH 337/748] Fixed #35121 -- Corrected color for links in the admin. Thanks Collin Anderson for the report. Regression in 6ad2738a8f32b94cbae742f212080fadf2dee421. --- django/contrib/admin/static/admin/css/base.css | 2 +- docs/releases/5.0.2.txt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css index aaa9c3441a55..daf4699cac17 100644 --- a/django/contrib/admin/static/admin/css/base.css +++ b/django/contrib/admin/static/admin/css/base.css @@ -102,7 +102,7 @@ body { /* LINKS */ a:link, a:visited { - color: var(--body-fg); + color: var(--link-fg); text-decoration: none; transition: color 0.15s, background 0.15s; } diff --git a/docs/releases/5.0.2.txt b/docs/releases/5.0.2.txt index facfed26f509..a5e2b6eee407 100644 --- a/docs/releases/5.0.2.txt +++ b/docs/releases/5.0.2.txt @@ -12,3 +12,6 @@ Bugfixes * Reallowed, following a regression in Django 5.0.1, filtering against local foreign keys not included in :attr:`.ModelAdmin.list_filter` (:ticket:`35087`). + +* Fixed a regression in Django 5.0 where links in the admin had an incorrect + color (:ticket:`35121`). From 8a1727dc7f66db7f0131d545812f77544f35aa57 Mon Sep 17 00:00:00 2001 From: Hisham Mahmood <45965466+Hisham-Pak@users.noreply.github.com> Date: Fri, 19 Jan 2024 09:41:53 +0500 Subject: [PATCH 338/748] Fixed #34910 -- Improved color contrast for add/change icons in admin. --- django/contrib/admin/static/admin/img/icon-addlink.svg | 2 +- django/contrib/admin/static/admin/img/icon-changelink.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/django/contrib/admin/static/admin/img/icon-addlink.svg b/django/contrib/admin/static/admin/img/icon-addlink.svg index e004fb162633..8d5c6a3a9f6c 100644 --- a/django/contrib/admin/static/admin/img/icon-addlink.svg +++ b/django/contrib/admin/static/admin/img/icon-addlink.svg @@ -1,3 +1,3 @@ - + diff --git a/django/contrib/admin/static/admin/img/icon-changelink.svg b/django/contrib/admin/static/admin/img/icon-changelink.svg index bbb137aa0866..592b093bc3c3 100644 --- a/django/contrib/admin/static/admin/img/icon-changelink.svg +++ b/django/contrib/admin/static/admin/img/icon-changelink.svg @@ -1,3 +1,3 @@ - + From 4879907223d70ee1a82474d9286ccfa5dae96971 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 19 Jan 2024 08:55:50 +0100 Subject: [PATCH 339/748] Fixed #35127 -- Made Model.full_clean() ignore GeneratedFields. Thanks Claude Paroz for the report. Regression in f333e3513e8bdf5ffeb6eeb63021c230082e6f95. --- django/db/models/base.py | 2 +- docs/releases/5.0.2.txt | 3 +++ tests/model_fields/models.py | 4 ++-- tests/model_fields/test_generatedfield.py | 8 ++++++++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/django/db/models/base.py b/django/db/models/base.py index b15bdd032ab0..61925f63ea30 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1628,7 +1628,7 @@ def clean_fields(self, exclude=None): errors = {} for f in self._meta.fields: - if f.name in exclude: + if f.name in exclude or f.generated: continue # Skip validation for empty fields with blank=True. The developer # is responsible for making sure they have a valid value. diff --git a/docs/releases/5.0.2.txt b/docs/releases/5.0.2.txt index a5e2b6eee407..05f80bb00f92 100644 --- a/docs/releases/5.0.2.txt +++ b/docs/releases/5.0.2.txt @@ -15,3 +15,6 @@ Bugfixes * Fixed a regression in Django 5.0 where links in the admin had an incorrect color (:ticket:`35121`). + +* Fixed a bug in Django 5.0 that caused a crash of ``Model.full_clean()`` on + models with a ``GeneratedField`` (:ticket:`35127`). diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py index 69b4e26145ce..e34f3c8947aa 100644 --- a/tests/model_fields/models.py +++ b/tests/model_fields/models.py @@ -502,7 +502,7 @@ class GeneratedModel(models.Model): output_field=models.IntegerField(), db_persist=True, ) - fk = models.ForeignKey(Foo, on_delete=models.CASCADE, null=True) + fk = models.ForeignKey(Foo, on_delete=models.CASCADE, null=True, blank=True) class Meta: required_db_features = {"supports_stored_generated_columns"} @@ -516,7 +516,7 @@ class GeneratedModelVirtual(models.Model): output_field=models.IntegerField(), db_persist=False, ) - fk = models.ForeignKey(Foo, on_delete=models.CASCADE, null=True) + fk = models.ForeignKey(Foo, on_delete=models.CASCADE, null=True, blank=True) class Meta: required_db_features = {"supports_virtual_generated_columns"} diff --git a/tests/model_fields/test_generatedfield.py b/tests/model_fields/test_generatedfield.py index 589f78cbb042..a636e984fdbe 100644 --- a/tests/model_fields/test_generatedfield.py +++ b/tests/model_fields/test_generatedfield.py @@ -168,6 +168,14 @@ def test_unsaved_error(self): with self.assertRaisesMessage(AttributeError, msg): m.field + def test_full_clean(self): + m = self.base_model(a=1, b=2) + # full_clean() ignores GeneratedFields. + m.full_clean() + m.save() + m = self._refresh_if_needed(m) + self.assertEqual(m.field, 3) + def test_create(self): m = self.base_model.objects.create(a=1, b=2) m = self._refresh_if_needed(m) From 12ffcfc350a19bbfbc203126a9b6c84b5e0d0ba2 Mon Sep 17 00:00:00 2001 From: Emmanuel Katchy Date: Thu, 18 Jan 2024 22:03:20 +0000 Subject: [PATCH 340/748] Updated "Dive Into Python" links. --- AUTHORS | 2 +- docs/intro/contributing.txt | 8 ++++---- docs/intro/index.txt | 8 +++++--- docs/ref/django-admin.txt | 8 +++----- docs/ref/templates/builtins.txt | 4 ++-- docs/topics/settings.txt | 3 +-- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/AUTHORS b/AUTHORS index 8319353cf551..82aab46439b5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1087,6 +1087,6 @@ A big THANK YOU goes to: Ian Bicking for convincing Adrian to ditch code generation. - Mark Pilgrim for "Dive Into Python" (https://www.diveinto.org/python3/). + Mark Pilgrim for "Dive Into Python" (https://diveintopython3.net/). Guido van Rossum for creating Python. diff --git a/docs/intro/contributing.txt b/docs/intro/contributing.txt index 652bca0cb257..06230b8ee30d 100644 --- a/docs/intro/contributing.txt +++ b/docs/intro/contributing.txt @@ -27,8 +27,8 @@ For this tutorial, we expect that you have at least a basic understanding of how Django works. This means you should be comfortable going through the existing tutorials on :doc:`writing your first Django app`. In addition, you should have a good understanding of Python itself. But if you -don't, `Dive Into Python`__ is a fantastic (and free) online book for -beginning Python programmers. +don't, `Dive Into Python`_ is a fantastic (and free) online book for beginning +Python programmers. Those of you who are unfamiliar with version control systems and Trac will find that this tutorial and its links include just enough information to get started. @@ -45,8 +45,8 @@ so that it can be of use to the widest audience. `#django-dev on irc.libera.chat`__ to chat with other Django users who might be able to help. -__ https://diveinto.org/python3/table-of-contents.html __ https://web.libera.chat/#django-dev +.. _Dive Into Python: https://diveintopython3.net/ .. _Django Forum: https://forum.djangoproject.com/ What does this tutorial cover? @@ -350,7 +350,7 @@ This test checks that the ``make_toast()`` returns ``'toast'``. * After reading those, if you want something a little meatier to sink your teeth into, there's always the Python :mod:`unittest` documentation. -__ https://diveinto.org/python3/unit-testing.html +__ https://diveintopython3.net/unit-testing.html Running your new test --------------------- diff --git a/docs/intro/index.txt b/docs/intro/index.txt index ca4836367f5e..a68d4876b704 100644 --- a/docs/intro/index.txt +++ b/docs/intro/index.txt @@ -32,10 +32,12 @@ place: read this material to quickly get up and running. `list of Python resources for non-programmers`_ If you already know a few other languages and want to get up to speed with - Python quickly, we recommend `Dive Into Python`_. If that's not quite your - style, there are many other `books about Python`_. + Python quickly, we recommend referring the official + `Python documentation`_, which provides comprehensive and authoritative + information about the language, as well as links to other resources such as + a list of `books about Python`_. .. _python: https://www.python.org/ .. _list of Python resources for non-programmers: https://wiki.python.org/moin/BeginnersGuide/NonProgrammers - .. _Dive Into Python: https://diveinto.org/python3/table-of-contents.html + .. _Python documentation: https://docs.python.org/3/ .. _books about Python: https://wiki.python.org/moin/PythonBooks diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 7f3fa271c874..3fba67bf20fc 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1797,9 +1797,9 @@ allows for the following options by default: .. django-admin-option:: --pythonpath PYTHONPATH -Adds the given filesystem path to the Python `import search path`_. If this -isn't provided, ``django-admin`` will use the :envvar:`PYTHONPATH` environment -variable. +Adds the given filesystem path to the Python :py:data:`sys.path` module +attribute. If this isn't provided, ``django-admin`` will use the +:envvar:`PYTHONPATH` environment variable. This option is unnecessary in ``manage.py``, because it takes care of setting the Python path for you. @@ -1810,8 +1810,6 @@ Example usage: django-admin migrate --pythonpath='/home/djangoprojects/myproject' -.. _import search path: https://diveinto.org/python3/your-first-python-program.html#importsearchpath - .. django-admin-option:: --settings SETTINGS Specifies the settings module to use. The settings module should be in Python diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 03c8e61d2078..a10af9310f7a 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -2484,8 +2484,8 @@ individual elements of the sequence. Returns a slice of the list. -Uses the same syntax as Python's list slicing. See -https://diveinto.org/python3/native-datatypes.html#slicinglists for an +Uses the same syntax as Python's list slicing. See the `Python documentation +`_ for an introduction. Example: diff --git a/docs/topics/settings.txt b/docs/topics/settings.txt index dc6cd945b392..553f788e61ee 100644 --- a/docs/topics/settings.txt +++ b/docs/topics/settings.txt @@ -44,9 +44,8 @@ by using an environment variable, :envvar:`DJANGO_SETTINGS_MODULE`. The value of :envvar:`DJANGO_SETTINGS_MODULE` should be in Python path syntax, e.g. ``mysite.settings``. Note that the settings module should be on the -Python `import search path`_. +Python :py:data:`sys.path`. -.. _import search path: https://diveinto.org/python3/your-first-python-program.html#importsearchpath The ``django-admin`` utility ---------------------------- From a5622f84ab0ba0ebb30c2b85f2b85d8aef75f337 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 22 Jan 2024 04:25:28 +0000 Subject: [PATCH 341/748] Fixed tutorial 'background.gif' reference. Missed in 76fda7729e4cdfec715cd92b2c80d851797b05f7. --- docs/intro/reusable-apps.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro/reusable-apps.txt b/docs/intro/reusable-apps.txt index f40daf4dfbdf..0ca63830ea0a 100644 --- a/docs/intro/reusable-apps.txt +++ b/docs/intro/reusable-apps.txt @@ -76,7 +76,7 @@ After the previous tutorials, our project should look like this: static/ polls/ images/ - background.gif + background.png style.css templates/ polls/ From 1c3a9b9f968295848baa56670797fb424096efaf Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 19 Jan 2024 20:15:29 +0000 Subject: [PATCH 342/748] Added more WKT and WKB tests. --- tests/gis_tests/gdal_tests/test_geom.py | 83 +++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/gis_tests/gdal_tests/test_geom.py b/tests/gis_tests/gdal_tests/test_geom.py index eba90504b26f..9d62bcd07b31 100644 --- a/tests/gis_tests/gdal_tests/test_geom.py +++ b/tests/gis_tests/gdal_tests/test_geom.py @@ -727,3 +727,86 @@ def test_is_3d_and_set_3d(self): msg = "Input to 'set_3d' must be a boolean, got 'None'" with self.assertRaisesMessage(ValueError, msg): geom.set_3d(None) + + def test_wkt_and_wkb_output(self): + tests = [ + # 2D + ("POINT (1 2)", "0101000000000000000000f03f0000000000000040"), + ( + "LINESTRING (30 10,10 30)", + "0102000000020000000000000000003e400000000000002" + "44000000000000024400000000000003e40", + ), + ( + "POLYGON ((30 10,40 40,20 40,30 10))", + "010300000001000000040000000000000000003e400000000000002440000000000000" + "44400000000000004440000000000000344000000000000044400000000000003e4000" + "00000000002440", + ), + ( + "MULTIPOINT (10 40,40 30)", + "0104000000020000000101000000000000000000244000000000000044400101000000" + "00000000000044400000000000003e40", + ), + ( + "MULTILINESTRING ((10 10,20 20),(40 40,30 30,40 20))", + "0105000000020000000102000000020000000000000000002440000000000000244000" + "0000000000344000000000000034400102000000030000000000000000004440000000" + "00000044400000000000003e400000000000003e400000000000004440000000000000" + "3440", + ), + ( + "MULTIPOLYGON (((30 20,45 40,10 40,30 20)),((15 5,40 10,10 20,15 5)))", + "010600000002000000010300000001000000040000000000000000003e400000000000" + "0034400000000000804640000000000000444000000000000024400000000000004440" + "0000000000003e40000000000000344001030000000100000004000000000000000000" + "2e40000000000000144000000000000044400000000000002440000000000000244000" + "000000000034400000000000002e400000000000001440", + ), + ( + "GEOMETRYCOLLECTION (POINT (40 10))", + "010700000001000000010100000000000000000044400000000000002440", + ), + # 3D + ( + "POINT (1 2 3)", + "0101000080000000000000f03f00000000000000400000000000000840", + ), + ( + "LINESTRING (30 10 3,10 30 3)", + "0102000080020000000000000000003e40000000000000244000000000000008400000" + "0000000024400000000000003e400000000000000840", + ), + ( + "POLYGON ((30 10 3,40 40 3,30 10 3))", + "010300008001000000030000000000000000003e400000000000002440000000000000" + "08400000000000004440000000000000444000000000000008400000000000003e4000" + "000000000024400000000000000840", + ), + ( + "MULTIPOINT (10 40 3,40 30 3)", + "0104000080020000000101000080000000000000244000000000000044400000000000" + "000840010100008000000000000044400000000000003e400000000000000840", + ), + ( + "MULTILINESTRING ((10 10 3,20 20 3))", + "0105000080010000000102000080020000000000000000002440000000000000244000" + "00000000000840000000000000344000000000000034400000000000000840", + ), + ( + "MULTIPOLYGON (((30 20 3,45 40 3,30 20 3)))", + "010600008001000000010300008001000000030000000000000000003e400000000000" + "0034400000000000000840000000000080464000000000000044400000000000000840" + "0000000000003e4000000000000034400000000000000840", + ), + ( + "GEOMETRYCOLLECTION (POINT (40 10 3))", + "0107000080010000000101000080000000000000444000000000000024400000000000" + "000840", + ), + ] + for geom, wkb in tests: + with self.subTest(geom=geom): + g = OGRGeometry(geom) + self.assertEqual(g.wkt, geom) + self.assertEqual(g.wkb.hex(), wkb) From 184d82d8488b1b31ade5b5a68b0040f1c267f2be Mon Sep 17 00:00:00 2001 From: Salvo Polizzi <101474753+salvo-polizzi@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:31:50 +0100 Subject: [PATCH 343/748] Fixed #35130 -- Doc'd django.db.close_old_connections(). This also adds close_db_connections() to the django.db.__all__. --- django/db/__init__.py | 1 + docs/ref/databases.txt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/django/db/__init__.py b/django/db/__init__.py index f3cf4574a961..eb8118adb5c9 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -17,6 +17,7 @@ from django.utils.connection import ConnectionProxy __all__ = [ + "close_old_connections", "connection", "connections", "router", diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 5a31ceeaaead..d98d523db581 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -99,7 +99,8 @@ connections. If a connection is created in a long-running process, outside of Django’s request-response cycle, the connection will remain open until explicitly -closed, or timeout occurs. +closed, or timeout occurs. You can use ``django.db.close_old_connections()`` to +close all old or unusable connections. Encoding -------- From 8570e091d025c4aacc6b76597a3322030c2f8162 Mon Sep 17 00:00:00 2001 From: Adrienne Franke <18449966+adriennefranke@users.noreply.github.com> Date: Mon, 22 Jan 2024 10:43:13 -0600 Subject: [PATCH 344/748] Fixed typo in docs/topics/auth/default.txt. --- docs/topics/auth/default.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index a8d32b2fea49..75d33b5e7ee0 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -1608,7 +1608,7 @@ Helper functions Defaults to :setting:`settings.LOGIN_URL ` if not supplied. * ``redirect_field_name``: The name of a ``GET`` field containing the - URL to redirect to after log out. Overrides ``next`` if the given + URL to redirect to after login. Overrides ``next`` if the given ``GET`` parameter is passed. .. _built-in-auth-forms: From f4c597346453c44769726d2ac061fbc028b2fd5b Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 22 Jan 2024 17:55:40 +0000 Subject: [PATCH 345/748] Refs #35058 -- Deprecated OGRGeometry.coord_dim setter. Reflecting a change in the underlying GDAL library (since GDAL 2.1) using coord_dim to set a geometries dimensions is deprecated in favor of set_3d(). --- django/contrib/gis/gdal/geometries.py | 13 +++++++++---- docs/internals/deprecation.txt | 3 +++ docs/ref/contrib/gis/gdal.txt | 8 ++++++-- docs/releases/5.1.txt | 3 +++ tests/gis_tests/gdal_tests/test_geom.py | 10 ++++++++++ 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py index 516c6c711393..57f81ca476e8 100644 --- a/django/contrib/gis/gdal/geometries.py +++ b/django/contrib/gis/gdal/geometries.py @@ -39,6 +39,7 @@ True True """ import sys +import warnings from binascii import b2a_hex from ctypes import byref, c_char_p, c_double, c_ubyte, c_void_p, string_at @@ -50,6 +51,7 @@ from django.contrib.gis.gdal.prototypes import srs as srs_api from django.contrib.gis.gdal.srs import CoordTransform, SpatialReference from django.contrib.gis.geometry import hex_regex, json_regex, wkt_regex +from django.utils.deprecation import RemovedInDjango60Warning from django.utils.encoding import force_bytes @@ -206,18 +208,21 @@ def dimension(self): "Return 0 for points, 1 for lines, and 2 for surfaces." return capi.get_dims(self.ptr) - def _get_coord_dim(self): + @property + def coord_dim(self): "Return the coordinate dimension of the Geometry." return capi.get_coord_dim(self.ptr) - def _set_coord_dim(self, dim): + # RemovedInDjango60Warning + @coord_dim.setter + def coord_dim(self, dim): "Set the coordinate dimension of this Geometry." + msg = "coord_dim setter is deprecated. Use set_3d() instead." + warnings.warn(msg, RemovedInDjango60Warning, stacklevel=2) if dim not in (2, 3): raise ValueError("Geometry dimension must be either 2 or 3") capi.set_coord_dim(self.ptr, dim) - coord_dim = property(_get_coord_dim, _set_coord_dim) - @property def geom_count(self): "Return the number of elements in this Geometry." diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 07e0f46856a2..e91ac062cb90 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -71,6 +71,9 @@ details on these changes. * Support for passing positional arguments to ``Model.save()`` and ``Model.asave()`` will be removed. +* The setter for ``django.contrib.gis.gdal.OGRGeometry.coord_dim`` will be + removed. + .. _deprecation-removed-in-5.1: 5.1 diff --git a/docs/ref/contrib/gis/gdal.txt b/docs/ref/contrib/gis/gdal.txt index c98c76cd101c..4b7a706fc44a 100644 --- a/docs/ref/contrib/gis/gdal.txt +++ b/docs/ref/contrib/gis/gdal.txt @@ -553,8 +553,12 @@ coordinate transformation: .. attribute:: coord_dim - Returns or sets the coordinate dimension of this geometry. For example, the - value would be 2 for two-dimensional geometries. + Returns the coordinate dimension of this geometry. For example, the value + would be 2 for two-dimensional geometries. + + .. deprecated:: 5.1 + + The ``coord_dim`` setter is deprecated. Use :meth:`.set_3d` instead. .. attribute:: is_3d diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index be609cea7f21..0cf249f3cf2a 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -378,6 +378,9 @@ Miscellaneous * Passing positional arguments to :meth:`.Model.save` and :meth:`.Model.asave` is deprecated in favor of keyword-only arguments. +* Setting ``django.contrib.gis.gdal.OGRGeometry.coord_dim`` is deprecated. Use + :meth:`~django.contrib.gis.gdal.OGRGeometry.set_3d` instead. + Features removed in 5.1 ======================= diff --git a/tests/gis_tests/gdal_tests/test_geom.py b/tests/gis_tests/gdal_tests/test_geom.py index 9d62bcd07b31..13e7d3e70df3 100644 --- a/tests/gis_tests/gdal_tests/test_geom.py +++ b/tests/gis_tests/gdal_tests/test_geom.py @@ -11,6 +11,7 @@ from django.template import Context from django.template.engine import Engine from django.test import SimpleTestCase +from django.utils.deprecation import RemovedInDjango60Warning from ..test_data import TestDataMixin @@ -810,3 +811,12 @@ def test_wkt_and_wkb_output(self): g = OGRGeometry(geom) self.assertEqual(g.wkt, geom) self.assertEqual(g.wkb.hex(), wkb) + + +class DeprecationTests(SimpleTestCase): + def test_coord_setter_deprecation(self): + geom = OGRGeometry("POINT (1 2)") + msg = "coord_dim setter is deprecated. Use set_3d() instead." + with self.assertWarnsMessage(RemovedInDjango60Warning, msg): + geom.coord_dim = 3 + self.assertEqual(geom.coord_dim, 3) From bbfbf0ab686030f8da5ea22a65c41b9574681262 Mon Sep 17 00:00:00 2001 From: Hisham Mahmood <45965466+Hisham-Pak@users.noreply.github.com> Date: Tue, 23 Jan 2024 13:09:24 +0500 Subject: [PATCH 346/748] Refs #33517 -- Prevented __second lookup from returning fractional seconds on Oracle. --- django/db/backends/oracle/features.py | 6 ------ django/db/backends/oracle/operations.py | 9 +++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py index 002e03e2b534..47bdf37efaae 100644 --- a/django/db/backends/oracle/features.py +++ b/django/db/backends/oracle/features.py @@ -95,12 +95,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests." "test_trunc_week_before_1000", }, - "Oracle extracts seconds including fractional seconds (#33517).": { - "db_functions.datetime.test_extract_trunc.DateFunctionTests." - "test_extract_second_func_no_fractional", - "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests." - "test_extract_second_func_no_fractional", - }, "Oracle doesn't support bitwise XOR.": { "expressions.tests.ExpressionOperatorTests.test_lefthand_bitwise_xor", "expressions.tests.ExpressionOperatorTests.test_lefthand_bitwise_xor_null", diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index aedfeea2367d..541128ec5004 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -165,6 +165,9 @@ def datetime_cast_time_sql(self, sql, params, tzname): def datetime_extract_sql(self, lookup_type, sql, params, tzname): sql, params = self._convert_sql_to_tz(sql, params, tzname) + if lookup_type == "second": + # Truncate fractional seconds. + return f"FLOOR(EXTRACT(SECOND FROM {sql}))", params return self.date_extract_sql(lookup_type, sql, params) def datetime_trunc_sql(self, lookup_type, sql, params, tzname): @@ -188,6 +191,12 @@ def datetime_trunc_sql(self, lookup_type, sql, params, tzname): return f"CAST({sql} AS DATE)", params return f"TRUNC({sql}, %s)", (*params, trunc_param) + def time_extract_sql(self, lookup_type, sql, params): + if lookup_type == "second": + # Truncate fractional seconds. + return f"FLOOR(EXTRACT(SECOND FROM {sql}))", params + return self.date_extract_sql(lookup_type, sql, params) + def time_trunc_sql(self, lookup_type, sql, params, tzname=None): # The implementation is similar to `datetime_trunc_sql` as both # `DateTimeField` and `TimeField` are stored as TIMESTAMP where From d9b91e38361696014bdc98434d6d018eae809519 Mon Sep 17 00:00:00 2001 From: Syed Waheed <105697767+Waheedsys@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:17:31 +0530 Subject: [PATCH 347/748] Fixed #32923 -- Refactored out Field._clean_bound_field(). --- django/forms/fields.py | 8 ++++++++ django/forms/forms.py | 9 ++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/django/forms/fields.py b/django/forms/fields.py index 62d68985c073..4ec7b7aee74f 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -261,6 +261,10 @@ def __deepcopy__(self, memo): result.validators = self.validators[:] return result + def _clean_bound_field(self, bf): + value = bf.initial if self.disabled else bf.data + return self.clean(value) + class CharField(Field): def __init__( @@ -694,6 +698,10 @@ def bound_data(self, _, initial): def has_changed(self, initial, data): return not self.disabled and data is not None + def _clean_bound_field(self, bf): + value = bf.initial if self.disabled else bf.data + return self.clean(value, bf.initial) + class ImageField(FileField): default_validators = [validators.validate_image_file_extension] diff --git a/django/forms/forms.py b/django/forms/forms.py index 5de14d598af3..452f554e1eca 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -6,7 +6,7 @@ import datetime from django.core.exceptions import NON_FIELD_ERRORS, ValidationError -from django.forms.fields import Field, FileField +from django.forms.fields import Field from django.forms.utils import ErrorDict, ErrorList, RenderableFormMixin from django.forms.widgets import Media, MediaDefiningClass from django.utils.datastructures import MultiValueDict @@ -329,13 +329,8 @@ def full_clean(self): def _clean_fields(self): for name, bf in self._bound_items(): field = bf.field - value = bf.initial if field.disabled else bf.data try: - if isinstance(field, FileField): - value = field.clean(value, bf.initial) - else: - value = field.clean(value) - self.cleaned_data[name] = value + self.cleaned_data[name] = field._clean_bound_field(bf) if hasattr(self, "clean_%s" % name): value = getattr(self, "clean_%s" % name)() self.cleaned_data[name] = value From 0450c9bdf1773297c61b4e36850ab997ffd5dde2 Mon Sep 17 00:00:00 2001 From: duranbe Date: Sun, 19 Nov 2023 20:32:31 +0100 Subject: [PATCH 348/748] Fixed #34971 -- Doc'd additional loggers. Co-authored-by: duranbe Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> --- docs/ref/logging.txt | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/ref/logging.txt b/docs/ref/logging.txt index a15e2ac91f1d..6d8861299fcc 100644 --- a/docs/ref/logging.txt +++ b/docs/ref/logging.txt @@ -199,6 +199,39 @@ This logging does not include framework-level initialization (e.g. ``SET TIMEZONE``). Turn on query logging in your database if you wish to view all database queries. +.. _django-utils-autoreloader-logger: + +``django.utils.autoreload`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Log messages related to automatic code reloading during the execution of the +Django development server. This logger generates an ``INFO`` message upon +detecting a modification in a source code file and may produce ``WARNING`` +messages during filesystem inspection and event subscription processes. + +.. _django-contrib-gis-logger: + +``django.contrib.gis`` +~~~~~~~~~~~~~~~~~~~~~~ + +Log messages related to :doc:`contrib/gis/index` at various points: during the +loading of external GeoSpatial libraries (GEOS, GDAL, etc.) and when reporting +errors. Each ``ERROR`` log record includes the caught exception and relevant +contextual data. + +.. _django-dispatch-logger: + +``django.dispatch`` +~~~~~~~~~~~~~~~~~~~ + +This logger is used in :doc:`signals`, specifically within the +:mod:`~django.dispatch.Signal` class, to report issues when dispatching a +signal to a connected receiver. The ``ERROR`` log record includes the caught +exception as ``exc_info`` and adds the following extra context: + +* ``receiver``: The name of the receiver. +* ``err``: The exception that occurred when calling the receiver. + .. _django-security-logger: ``django.security.*`` From e412d85b4626bc56eb25206a40c3529162ce5dfc Mon Sep 17 00:00:00 2001 From: Marijke Luttekes Date: Wed, 24 Jan 2024 14:11:54 +0100 Subject: [PATCH 349/748] Fixed #35115 -- Made admin's footer render in
tag. --- django/contrib/admin/static/admin/css/base.css | 5 ----- django/contrib/admin/static/admin/css/responsive.css | 6 +----- django/contrib/admin/templates/admin/base.html | 2 +- docs/releases/5.1.txt | 4 ++++ tests/admin_views/tests.py | 7 +++++++ 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css index daf4699cac17..3a80e3a3c98e 100644 --- a/django/contrib/admin/static/admin/css/base.css +++ b/django/contrib/admin/static/admin/css/base.css @@ -893,11 +893,6 @@ a.deletelink:focus, a.deletelink:hover { } } -#footer { - clear: both; - padding: 10px; -} - /* COLUMN TYPES */ .colMS { diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css index b9553415d71a..df426ed991b9 100644 --- a/django/contrib/admin/static/admin/css/responsive.css +++ b/django/contrib/admin/static/admin/css/responsive.css @@ -451,14 +451,10 @@ input[type="submit"], button { @media (max-width: 767px) { /* Layout */ - #header, #content, #footer { + #header, #content { padding: 15px; } - #footer:empty { - padding: 0; - } - div.breadcrumbs { padding: 10px 15px; } diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index 1ca50e508d1d..f01a7ab61ce2 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -108,9 +108,9 @@

- {% block footer %}{% endblock %} +
{% block footer %}{% endblock %}
diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 0cf249f3cf2a..9e8ff8c04999 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -348,6 +348,10 @@ Miscellaneous * In order to improve accessibility, the admin's changelist filter is now rendered in a ``