diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index d5d9acff886a..723f348824fc 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -89,6 +89,22 @@ def _clean_credentials(credentials): return credentials +def _set_auth_user(request, user=None): + from django.contrib.auth.models import AnonymousUser + + if user is None: + user = AnonymousUser() + + if hasattr(request, "user"): + request.user = user + if hasattr(request, "auser"): + + async def auser(): + return user + + request.auser = auser + + def _get_user_session_key(request): # This value in the session is always serialized to a string, so we need # to convert it back to Python whenever we access it. @@ -177,8 +193,7 @@ def login(request, user, backend=None): request.session[SESSION_KEY] = user._meta.pk.value_to_string(user) request.session[BACKEND_SESSION_KEY] = backend request.session[HASH_SESSION_KEY] = session_auth_hash - if hasattr(request, "user"): - request.user = user + _set_auth_user(request, user) rotate_token(request) user_logged_in.send(sender=user.__class__, request=request, user=user) @@ -207,14 +222,7 @@ async def alogin(request, user, backend=None): await request.session.aset(SESSION_KEY, user._meta.pk.value_to_string(user)) await request.session.aset(BACKEND_SESSION_KEY, backend) await request.session.aset(HASH_SESSION_KEY, session_auth_hash) - if hasattr(request, "user"): - request.user = user - if hasattr(request, "auser"): - - async def auser(): - return user - - request.auser = auser + _set_auth_user(request, user) rotate_token(request) await user_logged_in.asend(sender=user.__class__, request=request, user=user) @@ -231,10 +239,8 @@ def logout(request): user = None user_logged_out.send(sender=user.__class__, request=request, user=user) request.session.flush() - if hasattr(request, "user"): - from django.contrib.auth.models import AnonymousUser - request.user = AnonymousUser() + _set_auth_user(request) async def alogout(request): @@ -249,20 +255,7 @@ async def alogout(request): await user_logged_out.asend(sender=user.__class__, request=request, user=user) await request.session.aflush() - has_user = hasattr(request, "user") - has_auser = hasattr(request, "auser") - if has_user or has_auser: - from django.contrib.auth.models import AnonymousUser - - anon = AnonymousUser() - if has_user: - request.user = anon - if has_auser: - - async def auser(): - return anon - - request.auser = auser + _set_auth_user(request) def get_user_model(): diff --git a/django/http/response.py b/django/http/response.py index 12cf47b1c14d..3b948d6eed43 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -20,6 +20,10 @@ from django.http.cookie import SimpleCookie from django.utils import timezone from django.utils.datastructures import CaseInsensitiveMapping +from django.utils.deprecation import ( + RemovedInDjango71Warning, + django_file_prefixes, +) from django.utils.encoding import iri_to_uri from django.utils.functional import cached_property from django.utils.http import ( @@ -755,10 +759,21 @@ def __init__( self, data, encoder=DjangoJSONEncoder, - safe=True, + # RemovedInDjango71Warning: Remove the safe parameter. + safe=None, json_dumps_params=None, **kwargs, ): + # RemovedInDjango71Warning. + if safe is None: + safe = False + else: + warnings.warn( + "The safe parameter is deprecated.", + category=RemovedInDjango71Warning, + skip_file_prefixes=django_file_prefixes(), + ) + # RemovedInDjango71Warning. if safe and not isinstance(data, dict): raise TypeError( "In order to allow non-dict objects to be serialized set the " diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 906da679c7b0..eebd0cd053f0 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -1281,6 +1281,14 @@ using non-dict objects in JSON-encoded response. modern browsers implement ECMAScript 5 which removes this attack vector. Therefore it is possible to disable this security precaution. +.. versionchanged:: 6.2 + + In earlier versions, the ``safe`` parameter defaulted to ``True``. + +.. deprecated:: 6.2 + + The ``safe`` parameter is deprecated. + Changing the default JSON encoder ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/2.2.26.txt b/docs/releases/2.2.26.txt index 7fbdc02089de..f46002802b71 100644 --- a/docs/releases/2.2.26.txt +++ b/docs/releases/2.2.26.txt @@ -19,7 +19,7 @@ In order to mitigate this issue, relatively long values are now ignored by ``UserAttributeSimilarityValidator``. This issue has severity "medium" according to the :ref:`Django security policy -`. +`. CVE-2021-45116: Potential information disclosure in ``dictsort`` template filter ================================================================================ @@ -35,7 +35,7 @@ dictionaries. As a reminder, all untrusted user input should be validated before use. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2021-45452: Potential directory-traversal via ``Storage.save()`` ==================================================================== @@ -44,4 +44,4 @@ CVE-2021-45452: Potential directory-traversal via ``Storage.save()`` crafted file names. This issue has severity "low" according to the :ref:`Django security policy -`. +`. diff --git a/docs/releases/3.2.11.txt b/docs/releases/3.2.11.txt index bb68e14a8e88..6261c2146127 100644 --- a/docs/releases/3.2.11.txt +++ b/docs/releases/3.2.11.txt @@ -19,7 +19,7 @@ In order to mitigate this issue, relatively long values are now ignored by ``UserAttributeSimilarityValidator``. This issue has severity "medium" according to the :ref:`Django security policy -`. +`. CVE-2021-45116: Potential information disclosure in ``dictsort`` template filter ================================================================================ @@ -35,7 +35,7 @@ dictionaries. As a reminder, all untrusted user input should be validated before use. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2021-45452: Potential directory-traversal via ``Storage.save()`` ==================================================================== @@ -44,4 +44,4 @@ CVE-2021-45452: Potential directory-traversal via ``Storage.save()`` crafted file names. This issue has severity "low" according to the :ref:`Django security policy -`. +`. diff --git a/docs/releases/4.0.1.txt b/docs/releases/4.0.1.txt index dba3f1783fb0..0ad2c80775a2 100644 --- a/docs/releases/4.0.1.txt +++ b/docs/releases/4.0.1.txt @@ -19,7 +19,7 @@ In order to mitigate this issue, relatively long values are now ignored by ``UserAttributeSimilarityValidator``. This issue has severity "medium" according to the :ref:`Django security policy -`. +`. CVE-2021-45116: Potential information disclosure in ``dictsort`` template filter ================================================================================ @@ -35,7 +35,7 @@ dictionaries. As a reminder, all untrusted user input should be validated before use. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2021-45452: Potential directory-traversal via ``Storage.save()`` ==================================================================== @@ -44,7 +44,7 @@ CVE-2021-45452: Potential directory-traversal via ``Storage.save()`` crafted file names. This issue has severity "low" according to the :ref:`Django security policy -`. +`. Bugfixes ======== diff --git a/docs/releases/4.2.28.txt b/docs/releases/4.2.28.txt index 1d81095b3eee..f95d6927f5b1 100644 --- a/docs/releases/4.2.28.txt +++ b/docs/releases/4.2.28.txt @@ -16,7 +16,7 @@ The ``django.contrib.auth.handlers.modwsgi.check_password()`` function for allowed remote attackers to enumerate users via a timing attack. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2025-14550: Potential denial-of-service vulnerability via repeated headers when using ASGI ============================================================================================== @@ -28,7 +28,7 @@ repeated string concatenation while combining repeated headers, which produced super-linear computation resulting in service degradation or outage. This issue has severity "moderate" according to the :ref:`Django security -policy `. +policy `. CVE-2026-1207: Potential SQL injection via raster lookups on PostGIS ==================================================================== @@ -40,7 +40,7 @@ index. As a reminder, all untrusted user input should be validated before use. This issue has severity "high" according to the :ref:`Django security policy -`. +`. CVE-2026-1285: Potential denial-of-service vulnerability in ``django.utils.text.Truncator`` HTML methods ======================================================================================================== @@ -52,7 +52,7 @@ denial-of-service attack via certain inputs with a large number of unmatched HTML end tags, which could cause quadratic time complexity during HTML parsing. This issue has severity "moderate" according to the :ref:`Django security -policy `. +policy `. CVE-2026-1287: Potential SQL injection in column aliases via control characters =============================================================================== @@ -65,7 +65,7 @@ expansion, as the ``**kwargs`` passed to :meth:`.QuerySet.annotate`, :meth:`~.QuerySet.alias`. This issue has severity "high" according to the :ref:`Django security policy -`. +`. CVE-2026-1312: Potential SQL injection via ``QuerySet.order_by`` and ``FilteredRelation`` ========================================================================================= @@ -75,4 +75,4 @@ containing periods when the same alias was, using a suitably crafted dictionary, with dictionary expansion, used in :class:`.FilteredRelation`. This issue has severity "high" according to the :ref:`Django security policy -`. +`. diff --git a/docs/releases/4.2.29.txt b/docs/releases/4.2.29.txt index 71170a576398..b45b64a219f1 100644 --- a/docs/releases/4.2.29.txt +++ b/docs/releases/4.2.29.txt @@ -27,7 +27,7 @@ validation, but if you rely on custom validators, ensure they do not depend on the previous behavior of ``URLField.to_python()``. This issue has severity "moderate" according to the :ref:`Django security -policy `. +policy `. CVE-2026-25674: Potential incorrect permissions on newly created file system objects ==================================================================================== @@ -42,4 +42,4 @@ Django now applies the requested permissions via :func:`~os.chmod` after :func:`~os.mkdir`, removing the dependency on the process-wide umask. This issue has severity "low" according to the :ref:`Django security policy -`. +`. diff --git a/docs/releases/4.2.30.txt b/docs/releases/4.2.30.txt index 8382907068f1..be484ef1d724 100644 --- a/docs/releases/4.2.30.txt +++ b/docs/releases/4.2.30.txt @@ -25,7 +25,7 @@ Headers containing underscores are now ignored by ``ASGIRequest``, matching the behavior of :pypi:`Daphne `, the reference server for ASGI. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-4277: Privilege abuse in ``GenericInlineModelAdmin`` ============================================================= @@ -35,7 +35,7 @@ forged ``POST`` data in :class:`~django.contrib.contenttypes.admin.GenericInlineModelAdmin`. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-4292: Privilege abuse in ``ModelAdmin.list_editable`` ============================================================== @@ -45,7 +45,7 @@ Admin changelist forms using instances to be created via forged ``POST`` data. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-33033: Potential denial-of-service vulnerability in ``MultiPartParser`` via base64-encoded file upload =============================================================================================================== @@ -55,7 +55,7 @@ with ``Content-Transfer-Encoding: base64`` that include excessive whitespace may trigger repeated memory copying, potentially degrading performance. This issue has severity "moderate" according to the :ref:`Django security -policy `. +policy `. CVE-2026-33034: Potential denial-of-service vulnerability in ASGI requests via memory upload limit bypass ========================================================================================================= @@ -66,4 +66,4 @@ bypass the :setting:`DATA_UPLOAD_MAX_MEMORY_SIZE` limit when reading memory and causing service degradation. This issue has severity "low" according to the :ref:`Django security policy -`. +`. diff --git a/docs/releases/5.2.11.txt b/docs/releases/5.2.11.txt index 76efc4aa8d25..75fa68317ef3 100644 --- a/docs/releases/5.2.11.txt +++ b/docs/releases/5.2.11.txt @@ -16,7 +16,7 @@ The ``django.contrib.auth.handlers.modwsgi.check_password()`` function for allowed remote attackers to enumerate users via a timing attack. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2025-14550: Potential denial-of-service vulnerability via repeated headers when using ASGI ============================================================================================== @@ -28,7 +28,7 @@ repeated string concatenation while combining repeated headers, which produced super-linear computation resulting in service degradation or outage. This issue has severity "moderate" according to the :ref:`Django security -policy `. +policy `. CVE-2026-1207: Potential SQL injection via raster lookups on PostGIS ==================================================================== @@ -40,7 +40,7 @@ index. As a reminder, all untrusted user input should be validated before use. This issue has severity "high" according to the :ref:`Django security policy -`. +`. CVE-2026-1285: Potential denial-of-service vulnerability in ``django.utils.text.Truncator`` HTML methods ======================================================================================================== @@ -52,7 +52,7 @@ denial-of-service attack via certain inputs with a large number of unmatched HTML end tags, which could cause quadratic time complexity during HTML parsing. This issue has severity "moderate" according to the :ref:`Django security -policy `. +policy `. CVE-2026-1287: Potential SQL injection in column aliases via control characters =============================================================================== @@ -65,7 +65,7 @@ expansion, as the ``**kwargs`` passed to :meth:`.QuerySet.annotate`, :meth:`~.QuerySet.alias`. This issue has severity "high" according to the :ref:`Django security policy -`. +`. CVE-2026-1312: Potential SQL injection via ``QuerySet.order_by`` and ``FilteredRelation`` ========================================================================================= @@ -75,4 +75,4 @@ containing periods when the same alias was, using a suitably crafted dictionary, with dictionary expansion, used in :class:`.FilteredRelation`. This issue has severity "high" according to the :ref:`Django security policy -`. +`. diff --git a/docs/releases/5.2.12.txt b/docs/releases/5.2.12.txt index 177bfd1cedec..2208315a6897 100644 --- a/docs/releases/5.2.12.txt +++ b/docs/releases/5.2.12.txt @@ -28,7 +28,7 @@ validation, but if you rely on custom validators, ensure they do not depend on the previous behavior of ``URLField.to_python()``. This issue has severity "moderate" according to the :ref:`Django security -policy `. +policy `. CVE-2026-25674: Potential incorrect permissions on newly created file system objects ==================================================================================== @@ -43,7 +43,7 @@ Django now applies the requested permissions via :func:`~os.chmod` after :func:`~os.mkdir`, removing the dependency on the process-wide umask. This issue has severity "low" according to the :ref:`Django security policy -`. +`. Bugfixes ======== diff --git a/docs/releases/5.2.13.txt b/docs/releases/5.2.13.txt index 9b7ce3155a0e..bbac90208701 100644 --- a/docs/releases/5.2.13.txt +++ b/docs/releases/5.2.13.txt @@ -25,7 +25,7 @@ Headers containing underscores are now ignored by ``ASGIRequest``, matching the behavior of :pypi:`Daphne `, the reference server for ASGI. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-4277: Privilege abuse in ``GenericInlineModelAdmin`` ============================================================= @@ -35,7 +35,7 @@ forged ``POST`` data in :class:`~django.contrib.contenttypes.admin.GenericInlineModelAdmin`. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-4292: Privilege abuse in ``ModelAdmin.list_editable`` ============================================================== @@ -45,7 +45,7 @@ Admin changelist forms using instances to be created via forged ``POST`` data. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-33033: Potential denial-of-service vulnerability in ``MultiPartParser`` via base64-encoded file upload =============================================================================================================== @@ -55,7 +55,7 @@ with ``Content-Transfer-Encoding: base64`` that include excessive whitespace may trigger repeated memory copying, potentially degrading performance. This issue has severity "moderate" according to the :ref:`Django security -policy `. +policy `. CVE-2026-33034: Potential denial-of-service vulnerability in ASGI requests via memory upload limit bypass ========================================================================================================= @@ -66,4 +66,4 @@ bypass the :setting:`DATA_UPLOAD_MAX_MEMORY_SIZE` limit when reading memory and causing service degradation. This issue has severity "low" according to the :ref:`Django security policy -`. +`. diff --git a/docs/releases/5.2.14.txt b/docs/releases/5.2.14.txt index 1fe882ebea1f..c3c869a4e827 100644 --- a/docs/releases/5.2.14.txt +++ b/docs/releases/5.2.14.txt @@ -18,7 +18,7 @@ As a reminder, Django :ref:`expects a limit to be configured relying on :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE`. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-35192: Session fixation via public cached pages and ``SESSION_SAVE_EVERY_REQUEST`` =========================================================================================== @@ -29,7 +29,7 @@ session was not modified, but :setting:`SESSION_SAVE_EVERY_REQUEST` was a cached public page. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-6907: Potential exposure of private data due to incorrect handling of ``Vary: *`` in ``UpdateCacheMiddleware`` ======================================================================================================================= @@ -39,4 +39,4 @@ erroneously cache requests where the ``Vary`` header contained an asterisk (``'*'``). This could lead to private data being stored and served. This issue has severity "low" according to the :ref:`Django security policy -`. +`. diff --git a/docs/releases/5.2.15.txt b/docs/releases/5.2.15.txt new file mode 100644 index 000000000000..9163528f3ff7 --- /dev/null +++ b/docs/releases/5.2.15.txt @@ -0,0 +1,7 @@ +=========================== +Django 5.2.15 release notes +=========================== + +*June 3, 2026* + +Django 5.2.15 fixes five security issues with severity "low" in 5.2.14. diff --git a/docs/releases/6.0.2.txt b/docs/releases/6.0.2.txt index 0372cf9eb0c6..be57399eb568 100644 --- a/docs/releases/6.0.2.txt +++ b/docs/releases/6.0.2.txt @@ -16,7 +16,7 @@ The ``django.contrib.auth.handlers.modwsgi.check_password()`` function for allowed remote attackers to enumerate users via a timing attack. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2025-14550: Potential denial-of-service vulnerability via repeated headers when using ASGI ============================================================================================== @@ -28,7 +28,7 @@ repeated string concatenation while combining repeated headers, which produced super-linear computation resulting in service degradation or outage. This issue has severity "moderate" according to the :ref:`Django security -policy `. +policy `. CVE-2026-1207: Potential SQL injection via raster lookups on PostGIS ==================================================================== @@ -40,7 +40,7 @@ index. As a reminder, all untrusted user input should be validated before use. This issue has severity "high" according to the :ref:`Django security policy -`. +`. CVE-2026-1285: Potential denial-of-service vulnerability in ``django.utils.text.Truncator`` HTML methods ======================================================================================================== @@ -52,7 +52,7 @@ denial-of-service attack via certain inputs with a large number of unmatched HTML end tags, which could cause quadratic time complexity during HTML parsing. This issue has severity "moderate" according to the :ref:`Django security -policy `. +policy `. CVE-2026-1287: Potential SQL injection in column aliases via control characters =============================================================================== @@ -65,7 +65,7 @@ expansion, as the ``**kwargs`` passed to :meth:`.QuerySet.annotate`, :meth:`~.QuerySet.alias`. This issue has severity "high" according to the :ref:`Django security policy -`. +`. CVE-2026-1312: Potential SQL injection via ``QuerySet.order_by`` and ``FilteredRelation`` ========================================================================================= @@ -75,7 +75,7 @@ containing periods when the same alias was, using a suitably crafted dictionary, with dictionary expansion, used in :class:`.FilteredRelation`. This issue has severity "high" according to the :ref:`Django security policy -`. +`. Bugfixes ======== diff --git a/docs/releases/6.0.3.txt b/docs/releases/6.0.3.txt index 31f12b179232..78474d0ef497 100644 --- a/docs/releases/6.0.3.txt +++ b/docs/releases/6.0.3.txt @@ -27,7 +27,7 @@ validation, but if you rely on custom validators, ensure they do not depend on the previous behavior of ``URLField.to_python()``. This issue has severity "moderate" according to the :ref:`Django security -policy `. +policy `. CVE-2026-25674: Potential incorrect permissions on newly created file system objects ==================================================================================== @@ -42,7 +42,7 @@ Django now applies the requested permissions via :func:`~os.chmod` after :func:`~os.mkdir`, removing the dependency on the process-wide umask. This issue has severity "low" according to the :ref:`Django security policy -`. +`. Bugfixes ======== diff --git a/docs/releases/6.0.4.txt b/docs/releases/6.0.4.txt index f6a677d47b17..7e041e96f2c3 100644 --- a/docs/releases/6.0.4.txt +++ b/docs/releases/6.0.4.txt @@ -25,7 +25,7 @@ Headers containing underscores are now ignored by ``ASGIRequest``, matching the behavior of :pypi:`Daphne `, the reference server for ASGI. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-4277: Privilege abuse in ``GenericInlineModelAdmin`` ============================================================= @@ -35,7 +35,7 @@ forged ``POST`` data in :class:`~django.contrib.contenttypes.admin.GenericInlineModelAdmin`. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-4292: Privilege abuse in ``ModelAdmin.list_editable`` ============================================================== @@ -45,7 +45,7 @@ Admin changelist forms using instances to be created via forged ``POST`` data. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-33033: Potential denial-of-service vulnerability in ``MultiPartParser`` via base64-encoded file upload =============================================================================================================== @@ -55,7 +55,7 @@ with ``Content-Transfer-Encoding: base64`` that include excessive whitespace may trigger repeated memory copying, potentially degrading performance. This issue has severity "moderate" according to the :ref:`Django security -policy `. +policy `. CVE-2026-33034: Potential denial-of-service vulnerability in ASGI requests via memory upload limit bypass ========================================================================================================= @@ -66,7 +66,7 @@ bypass the :setting:`DATA_UPLOAD_MAX_MEMORY_SIZE` limit when reading memory and causing service degradation. This issue has severity "low" according to the :ref:`Django security policy -`. +`. Bugfixes ======== diff --git a/docs/releases/6.0.5.txt b/docs/releases/6.0.5.txt index 08bce66e6a76..707b116f19c3 100644 --- a/docs/releases/6.0.5.txt +++ b/docs/releases/6.0.5.txt @@ -19,7 +19,7 @@ As a reminder, Django :ref:`expects a limit to be configured relying on :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE`. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-35192: Session fixation via public cached pages and ``SESSION_SAVE_EVERY_REQUEST`` =========================================================================================== @@ -30,7 +30,7 @@ session was not modified, but :setting:`SESSION_SAVE_EVERY_REQUEST` was a cached public page. This issue has severity "low" according to the :ref:`Django security policy -`. +`. CVE-2026-6907: Potential exposure of private data due to incorrect handling of ``Vary: *`` in ``UpdateCacheMiddleware`` ======================================================================================================================= @@ -40,7 +40,7 @@ erroneously cache requests where the ``Vary`` header contained an asterisk (``'*'``). This could lead to private data being stored and served. This issue has severity "low" according to the :ref:`Django security policy -`. +`. Bugfixes ======== diff --git a/docs/releases/6.0.6.txt b/docs/releases/6.0.6.txt index 9a7bd4e8b56c..110f89932a88 100644 --- a/docs/releases/6.0.6.txt +++ b/docs/releases/6.0.6.txt @@ -2,9 +2,10 @@ Django 6.0.6 release notes ========================== -*Expected June 3, 2026* +*June 3, 2026* -Django 6.0.6 fixes several bugs in 6.0.5. +Django 6.0.6 fixes five security issues with severity "low" and one bug in +6.0.5. Bugfixes ======== diff --git a/docs/releases/6.2.txt b/docs/releases/6.2.txt index ba9396313d71..1193f7a84d89 100644 --- a/docs/releases/6.2.txt +++ b/docs/releases/6.2.txt @@ -251,7 +251,10 @@ backends. Miscellaneous ------------- -* ... +* To facilitate the deprecation of the ``safe`` parameter of + :class:`~django.http.JsonResponse`, it now defaults to ``False``, because the + pollution vulnerability in the ``Array`` prototype was fixed in + `ES5 `_. .. _deprecated-features-6.2: @@ -261,4 +264,5 @@ Features deprecated in 6.2 Miscellaneous ------------- -* ... +* The ``safe`` parameter is deprecated from :class:`~django.http.JsonResponse`. + Omitting the argument is equivalent to the prior ``safe=False`` usage. diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 1b715ba9884c..45f50bd3f007 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -52,6 +52,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 5.2.15 5.2.14 5.2.13 5.2.12 diff --git a/docs/topics/async.txt b/docs/topics/async.txt index 2607381ad274..6d4e22de3717 100644 --- a/docs/topics/async.txt +++ b/docs/topics/async.txt @@ -117,6 +117,26 @@ For example:: @never_cache async def my_async_view(request): ... +:func:`~django.utils.decorators.method_decorator` can be used with asynchronous +methods, including ``async def`` view handlers. Note, though, that if +decorating :meth:`~django.views.generic.base.View.dispatch` on a ``View`` with +asynchronous handlers, you will need to override ``dispatch`` to make it +``async def`` as well:: + + class MyClass(View): + @method_decorator(never_cache) + async def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + + async def get(self, request): ... + + async def post(self, request): ... + +Here, because we are decorating ``dispatch``, rather than the individual +handler methods, we need to make ``dispatch`` asynchronous so that +``method_decorator`` can correctly mark the resulting method as a coroutine +function. + Queries & the ORM ----------------- diff --git a/scripts/do_django_release.py b/scripts/do_django_release.py index 3d5bbb52108a..8f7450147e05 100755 --- a/scripts/do_django_release.py +++ b/scripts/do_django_release.py @@ -13,25 +13,12 @@ import subprocess from datetime import date -PGP_KEY_ID = os.getenv("PGP_KEY_ID") -PGP_KEY_URL = os.getenv("PGP_KEY_URL") -PGP_EMAIL = os.getenv("PGP_EMAIL") -DEST_FOLDER = os.path.expanduser(os.getenv("DEST_FOLDER")) - -assert ( - PGP_KEY_ID -), "Missing PGP_KEY_ID: Set this env var to your PGP key ID (used for signing)." -assert ( - PGP_KEY_URL -), "Missing PGP_KEY_URL: Set this env var to your PGP public key URL (for fetching)." -assert DEST_FOLDER and os.path.exists( - DEST_FOLDER -), "Missing DEST_FOLDER: Set this env var to the local path to place the artifacts." - - checksum_file_text = """This file contains MD5, SHA1, and SHA256 checksums for the source-code tarball and wheel files of Django {django_version}, released {release_date}. +It also includes the commit hash of the release tag, identifying the exact +source revision the artifacts were built from. + To use this file, you will need a working install of PGP or other compatible public-key encryption software. You will also need to have the Django release manager's public key in your keyring. This key has @@ -77,6 +64,10 @@ {sha256_tarball} {tarball_name} {sha256_wheel} {wheel_name} +Git tag +======= + +{commit_hash} {django_version} """ @@ -86,138 +77,193 @@ def build_artifacts(): build_main([]) -def do_checksum(checksum_algo, release_file): +def do_checksum(checksum_algo, release_file, dist_path): with open(os.path.join(dist_path, release_file), "rb") as f: return checksum_algo(f.read()).hexdigest() -# Ensure the working directory is clean. -subprocess.call(["git", "clean", "-fdx"]) - -django_repo_path = os.path.abspath(os.path.curdir) -dist_path = os.path.join(django_repo_path, "dist") - -# Build release files. -build_artifacts() -release_files = os.listdir(dist_path) -wheel_name = None -tarball_name = None -for f in release_files: - if f.endswith(".whl"): - wheel_name = f - if f.endswith(".tar.gz"): - tarball_name = f - -assert wheel_name is not None -assert tarball_name is not None - -django_version = wheel_name.split("-")[1] -django_major_version = ".".join(django_version.split(".")[:2]) - -artifacts_path = os.path.join(os.path.expanduser(DEST_FOLDER), django_version) -os.makedirs(artifacts_path, exist_ok=True) - -# Chop alpha/beta/rc suffix -match = re.search("[abrc]", django_major_version) -if match: - django_major_version = django_major_version[: match.start()] - -release_date = date.today().strftime("%B %-d, %Y") -checksum_file_name = f"Django-{django_version}.checksum.txt" -checksum_file_kwargs = dict( - release_date=release_date, - pgp_key_id=PGP_KEY_ID, - django_version=django_version, - pgp_key_url=PGP_KEY_URL, - checksum_file_name=checksum_file_name, - wheel_name=wheel_name, - tarball_name=tarball_name, -) -checksums = ( - ("md5", hashlib.md5), - ("sha1", hashlib.sha1), - ("sha256", hashlib.sha256), -) -for checksum_name, checksum_algo in checksums: - checksum_file_kwargs[f"{checksum_name}_tarball"] = do_checksum( - checksum_algo, tarball_name +def get_commit_hash(): + return subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() + + +def parse_major_version(django_version): + major = ".".join(django_version.split(".")[:2]) + match = re.search("[abrc]", major) + if match: + major = major[: match.start()] + return major + + +def find_release_artifacts(dist_path): + wheel_name = None + tarball_name = None + for f in os.listdir(dist_path): + if f.endswith(".whl"): + wheel_name = f + elif f.endswith(".tar.gz"): + tarball_name = f + return wheel_name, tarball_name + + +def create_checksum_file( + *, + django_version, + release_date, + checksum_file_path, + tarball_name, + wheel_name, + commit_hash, + dist_path, + pgp_key_id, + pgp_key_url, +): + kwargs = dict( + release_date=release_date, + pgp_key_id=pgp_key_id, + django_version=django_version, + pgp_key_url=pgp_key_url, + checksum_file_name=os.path.basename(checksum_file_path), + wheel_name=wheel_name, + tarball_name=tarball_name, + commit_hash=commit_hash, + ) + for checksum_name, checksum_algo in ( + ("md5", hashlib.md5), + ("sha1", hashlib.sha1), + ("sha256", hashlib.sha256), + ): + kwargs[f"{checksum_name}_tarball"] = do_checksum( + checksum_algo, tarball_name, dist_path + ) + kwargs[f"{checksum_name}_wheel"] = do_checksum( + checksum_algo, wheel_name, dist_path + ) + with open(checksum_file_path, "wb") as f: + f.write(checksum_file_text.format(**kwargs).encode("ascii")) + + +def main(): + pgp_key_id = os.getenv("PGP_KEY_ID") + pgp_key_url = os.getenv("PGP_KEY_URL") + pgp_email = os.getenv("PGP_EMAIL") + dest_folder = os.path.expanduser(os.getenv("DEST_FOLDER")) + + assert ( + pgp_key_id + ), "Missing PGP_KEY_ID: Set this env var to your PGP key ID (used for signing)." + assert ( + pgp_key_url + ), "Missing PGP_KEY_URL: Set this env var to your PGP public key URL." + assert dest_folder and os.path.exists( + dest_folder + ), "Missing DEST_FOLDER: Set this env var to the path to place the artifacts." + + # Ensure the working directory is clean. + subprocess.call(["git", "clean", "-fdx"]) + + commit_hash = get_commit_hash() + + django_repo_path = os.path.abspath(os.path.curdir) + dist_path = os.path.join(django_repo_path, "dist") + + # Build release files. + build_artifacts() + wheel_name, tarball_name = find_release_artifacts(dist_path) + + assert wheel_name is not None + assert tarball_name is not None + + django_version = wheel_name.split("-")[1] + django_major_version = parse_major_version(django_version) + artifacts_path = os.path.join(dest_folder, django_version) + os.makedirs(artifacts_path, exist_ok=True) + release_date = date.today().strftime("%B %-d, %Y") + checksum_file_path = os.path.join( + artifacts_path, f"Django-{django_version}.checksum.txt" ) - checksum_file_kwargs[f"{checksum_name}_wheel"] = do_checksum( - checksum_algo, wheel_name + + create_checksum_file( + django_version=django_version, + release_date=release_date, + checksum_file_path=checksum_file_path, + wheel_name=wheel_name, + tarball_name=tarball_name, + commit_hash=commit_hash, + dist_path=dist_path, + pgp_key_id=pgp_key_id, + pgp_key_url=pgp_key_url, ) -# Create the checksum file -checksum_file_text = checksum_file_text.format(**checksum_file_kwargs) -checksum_file_path = os.path.join(artifacts_path, checksum_file_name) -with open(checksum_file_path, "wb") as f: - f.write(checksum_file_text.encode("ascii")) - -print("\n\nDiffing release with checkout for sanity check.") - -# Unzip and diff... -unzip_command = [ - "unzip", - "-q", - os.path.join(dist_path, wheel_name), - "-d", - os.path.join(dist_path, django_major_version), -] -subprocess.run(unzip_command) -diff_command = [ - "diff", - "-qr", - "./django/", - os.path.join(dist_path, django_major_version, "django"), -] -subprocess.run(diff_command) -subprocess.run( - [ - "rm", - "-rf", + print("\n\nDiffing release with checkout for sanity check.") + + # Unzip and diff... + unzip_command = [ + "unzip", + "-q", + os.path.join(dist_path, wheel_name), + "-d", os.path.join(dist_path, django_major_version), ] -) - -print("\n\n=> Commands to run NOW:") - -# Sign the checksum file, this may prompt for a passphrase. -pgp_email = f"-u {PGP_EMAIL} " if PGP_EMAIL else "" -print(f"gpg --clearsign {pgp_email}--digest-algo SHA256 {checksum_file_path}") -# Create, verify and push tag -print(f'git tag --sign --message="Tag {django_version}" {django_version}') -print(f"git tag --verify {django_version}") - -# Copy binaries outside the current repo tree to avoid lossing them. -subprocess.run(["cp", "-r", dist_path, artifacts_path]) - -# Make the binaries available to the world -print( - "\n\n=> These ONLY 15 MINUTES BEFORE RELEASE TIME (consider new terminal " - "session with isolated venv)!" -) - -# Upload the checksum file and release artifacts to the djangoproject admin. -print( - "\n==> ACTION Add tarball, wheel, and checksum files to the Release entry at:" - f"https://www.djangoproject.com/admin/releases/release/{django_version}" -) -print( - f"* Tarball and wheel from {artifacts_path}\n" - f"* Signed checksum {checksum_file_path}.asc" -) - -# Verify the release artifacts (GPG signature, checksums, and smoke test). -print("\n==> ACTION Verify the release artifacts:") -print(f"VERSION={django_version} verify_release.sh") - -# Upload to PyPI. -print("\n==> ACTION Upload to PyPI, ensure your release venv is activated:") -print(f"cd {artifacts_path}") -print("pip install -U pip twine") -print("twine upload --repository django dist/*") - -# Push the tags. -print("\n==> ACTION Push the tags:") -print("git push --tags") - -print("\n\nDONE!!!") + subprocess.run(unzip_command) + diff_command = [ + "diff", + "-qr", + "./django/", + os.path.join(dist_path, django_major_version, "django"), + ] + subprocess.run(diff_command) + subprocess.run( + [ + "rm", + "-rf", + os.path.join(dist_path, django_major_version), + ] + ) + + print("\n\n=> Commands to run NOW:") + + # Sign the checksum file, this may prompt for a passphrase. + pgp_email_flag = f"-u {pgp_email} " if pgp_email else "" + print(f"gpg --clearsign {pgp_email_flag}--digest-algo SHA256 {checksum_file_path}") + # Create, verify and push tag. + print(f'git tag --sign --message="Tag {django_version}" {django_version}') + print(f"git tag --verify {django_version}") + + # Copy binaries outside the current repo tree to avoid lossing them. + subprocess.run(["cp", "-r", dist_path, artifacts_path]) + + # Make the binaries available to the world + print( + "\n\n=> These ONLY 15 MINUTES BEFORE RELEASE TIME (consider new terminal " + "session with isolated venv)!" + ) + + # Upload the checksum file and artifacts to the djangoproject admin. + print( + "\n==> ACTION Add tarball, wheel, and checksum files to the Release entry at:" + f"https://www.djangoproject.com/admin/releases/release/{django_version}" + ) + print( + f"* Tarball and wheel from {artifacts_path}\n" + f"* Signed checksum {checksum_file_path}.asc" + ) + + # Verify the release artifacts (GPG signature, checksums, and smoke test). + print("\n==> ACTION Verify the release artifacts:") + print(f"VERSION={django_version} verify_release.sh") + + # Upload to PyPI. + print("\n==> ACTION Upload to PyPI, ensure your release venv is activated:") + print(f"cd {artifacts_path}") + print("pip install -U pip twine") + print("twine upload --repository django dist/*") + + # Push the tags. + print("\n==> ACTION Push the tags:") + print("git push --tags") + + print("\n\nDONE!!!") + + +if __name__ == "__main__": + main() diff --git a/scripts/tests.py b/scripts/tests.py index b36c2937039c..10afce658895 100644 --- a/scripts/tests.py +++ b/scripts/tests.py @@ -12,11 +12,110 @@ $ PYTHONPATH=scripts/ python -m unittest scripts/tests.py """ +import hashlib +import os +import tempfile import unittest +from do_django_release import ( + create_checksum_file, + find_release_artifacts, + parse_major_version, +) from prepare_commit_msg import process_commit_message +class ParseMajorVersionTests(unittest.TestCase): + def test_final_patch_release(self): + self.assertEqual(parse_major_version("5.2.4"), "5.2") + + def test_final_dot_zero_release(self): + self.assertEqual(parse_major_version("6.0"), "6.0") + + def test_alpha(self): + self.assertEqual(parse_major_version("6.0a1"), "6.0") + + def test_beta(self): + self.assertEqual(parse_major_version("6.0b1"), "6.0") + + def test_release_candidate(self): + self.assertEqual(parse_major_version("6.0rc1"), "6.0") + + def test_two_digit_minor(self): + self.assertEqual(parse_major_version("5.10a1"), "5.10") + + +class CreateChecksumFileTests(unittest.TestCase): + WHEEL_CONTENT = b"fake wheel content" + TARBALL_CONTENT = b"fake tarball content" + + def generate_checksum_file(self, **overrides): + with tempfile.TemporaryDirectory() as tmp: + dist_path = os.path.join(tmp, "dist") + os.mkdir(dist_path) + with open( + os.path.join(dist_path, "Django-5.2.4-py3-none-any.whl"), "wb" + ) as f: + f.write(self.WHEEL_CONTENT) + with open(os.path.join(dist_path, "django-5.2.4.tar.gz"), "wb") as f: + f.write(self.TARBALL_CONTENT) + artifacts_path = os.path.join(tmp, "artifacts") + os.mkdir(artifacts_path) + checksum_file_path = os.path.join( + artifacts_path, "Django-5.2.4.checksum.txt" + ) + kwargs = dict( + django_version="5.2.4", + release_date="May 7, 2025", + checksum_file_path=checksum_file_path, + wheel_name="Django-5.2.4-py3-none-any.whl", + tarball_name="django-5.2.4.tar.gz", + commit_hash="abc123def456abc123def456abc123def456abc1", + dist_path=dist_path, + pgp_key_id="ABCD1234ABCD1234", + pgp_key_url="https://github.com/releaser.gpg", + ) + kwargs.update(overrides) + create_checksum_file(**kwargs) + with open(checksum_file_path) as f: + return f.read() + + def test_release_metadata(self): + result = self.generate_checksum_file() + self.assertIn("Django 5.2.4", result) + self.assertIn("May 7, 2025", result) + self.assertIn("ABCD1234ABCD1234", result) + self.assertIn("https://github.com/releaser.gpg", result) + self.assertIn("Django-5.2.4.checksum.txt", result) + self.assertIn("abc123def456abc123def456abc123def456abc1 5.2.4", result) + + def test_artifact_checksums(self): + result = self.generate_checksum_file() + for algo in (hashlib.md5, hashlib.sha1, hashlib.sha256): + expected_tarball = algo(self.TARBALL_CONTENT).hexdigest() + expected_wheel = algo(self.WHEEL_CONTENT).hexdigest() + self.assertIn(f"{expected_tarball} django-5.2.4.tar.gz", result) + self.assertIn(f"{expected_wheel} Django-5.2.4-py3-none-any.whl", result) + + +class FindReleaseArtifactsTests(unittest.TestCase): + def test_finds_wheel_and_tarball(self): + with tempfile.TemporaryDirectory() as d: + with open(os.path.join(d, "Django-5.2.4-py3-none-any.whl"), "w"): + pass + with open(os.path.join(d, "django-5.2.4.tar.gz"), "w"): + pass + wheel, tarball = find_release_artifacts(d) + self.assertEqual(wheel, "Django-5.2.4-py3-none-any.whl") + self.assertEqual(tarball, "django-5.2.4.tar.gz") + + def test_empty_directory_returns_none(self): + with tempfile.TemporaryDirectory() as d: + wheel, tarball = find_release_artifacts(d) + self.assertIsNone(wheel) + self.assertIsNone(tarball) + + class ProcessCommitMessageTests(unittest.TestCase): def test_non_stable_branch_no_prefix_added(self): lines = ["Fixed #123 -- Added a feature.\n"] diff --git a/tests/auth_tests/test_middleware.py b/tests/auth_tests/test_middleware.py index 894b49548b27..a1c047abbd0a 100644 --- a/tests/auth_tests/test_middleware.py +++ b/tests/auth_tests/test_middleware.py @@ -1,5 +1,7 @@ +from asgiref.sync import sync_to_async + from django.conf import settings -from django.contrib.auth import REDIRECT_FIELD_NAME, alogin, alogout +from django.contrib.auth import REDIRECT_FIELD_NAME, alogin, alogout, login, logout from django.contrib.auth.middleware import ( AuthenticationMiddleware, LoginRequiredMiddleware, @@ -68,6 +70,14 @@ async def test_auser_after_alogin(self): auser_second = await self.request.auser() self.assertEqual(auser_second, self.user2) + async def test_auser_after_login(self): + self.middleware(self.request) + auser = await self.request.auser() + self.assertEqual(auser, self.user) + await sync_to_async(login)(self.request, self.user2) + auser_second = await self.request.auser() + self.assertEqual(auser_second, self.user2) + async def test_auser_after_alogout(self): self.middleware(self.request) auser = await self.request.auser() @@ -76,6 +86,14 @@ async def test_auser_after_alogout(self): auser_second = await self.request.auser() self.assertTrue(auser_second.is_anonymous) + async def test_auser_after_logout(self): + self.middleware(self.request) + auser = await self.request.auser() + self.assertEqual(auser, self.user) + await sync_to_async(logout)(self.request) + auser_second = await self.request.auser() + self.assertTrue(auser_second.is_anonymous) + class TestAsyncLoginLogoutAfterSyncMiddleware(TestCase): @classmethod diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index 151088909c59..e2eb9af30f8c 100644 --- a/tests/httpwrappers/tests.py +++ b/tests/httpwrappers/tests.py @@ -24,6 +24,7 @@ parse_cookie, ) from django.test import SimpleTestCase +from django.utils.deprecation import RemovedInDjango71Warning from django.utils.encoding import iri_to_uri from django.utils.functional import lazystr from django.utils.http import MAX_URL_REDIRECT_LENGTH @@ -703,25 +704,31 @@ def test_json_response_non_ascii(self): response = JsonResponse(data) self.assertEqual(json.loads(response.text), data) - def test_json_response_raises_type_error_with_default_setting(self): - with self.assertRaisesMessage( - TypeError, - "In order to allow non-dict objects to be serialized set the " - "safe parameter to False", + # RemovedInDjango71Warning: When the deprecation ends, remove this test. + def test_json_response_raises_type_error_with_safe_arg(self): + with ( + self.assertRaisesMessage( + TypeError, + "In order to allow non-dict objects to be serialized set the " + "safe parameter to False", + ), + self.assertWarnsMessage( + RemovedInDjango71Warning, "The safe parameter is deprecated." + ), ): - JsonResponse([1, 2, 3]) + JsonResponse([1, 2, 3], safe=True) def test_json_response_text(self): - response = JsonResponse("foobar", safe=False) + response = JsonResponse("foobar") self.assertEqual(json.loads(response.text), "foobar") def test_json_response_list(self): - response = JsonResponse(["foo", "bar"], safe=False) + response = JsonResponse(["foo", "bar"]) self.assertEqual(json.loads(response.text), ["foo", "bar"]) def test_json_response_uuid(self): u = uuid.uuid4() - response = JsonResponse(u, safe=False) + response = JsonResponse(u) self.assertEqual(json.loads(response.text), str(u)) def test_json_response_custom_encoder(self):