From e8c6322b4f2ab4df610bb480003a54c88f32210e Mon Sep 17 00:00:00 2001 From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:26:47 +0200 Subject: [PATCH 1/4] Added stub release notes and release date for 6.0.5 and 5.2.14. --- docs/releases/5.2.14.txt | 7 +++++++ docs/releases/6.0.5.txt | 5 +++-- docs/releases/index.txt | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 docs/releases/5.2.14.txt diff --git a/docs/releases/5.2.14.txt b/docs/releases/5.2.14.txt new file mode 100644 index 000000000000..d51432322c7f --- /dev/null +++ b/docs/releases/5.2.14.txt @@ -0,0 +1,7 @@ +=========================== +Django 5.2.14 release notes +=========================== + +*May 5, 2026* + +Django 5.2.14 fixes three security issue with severity "low" in 5.2.13. diff --git a/docs/releases/6.0.5.txt b/docs/releases/6.0.5.txt index 7e54cbe1a4e4..91443c52aebf 100644 --- a/docs/releases/6.0.5.txt +++ b/docs/releases/6.0.5.txt @@ -2,9 +2,10 @@ Django 6.0.5 release notes ========================== -*Expected May 5, 2026* +*May 5, 2026* -Django 6.0.5 fixes several bugs in 6.0.4. +Django 6.0.5 fixes three security issues with severity "low" and several bugs +in 6.0.4. Bugfixes ======== diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 5a954cf254ca..05baf00ae0a6 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -44,6 +44,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 5.2.14 5.2.13 5.2.12 5.2.11 From 5d911f2d2fecc703be91b2b9b28acc59d34b35f3 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sun, 26 Apr 2026 07:53:13 +0100 Subject: [PATCH 2/4] Fixed #35738 -- Deprecated double-dot variable lookups. --- django/template/base.py | 19 ++++++- docs/internals/deprecation.txt | 3 ++ docs/releases/6.1.txt | 4 ++ .../template_tests/syntax_tests/test_basic.py | 49 ++++++++++++++++++- 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/django/template/base.py b/django/template/base.py index 9d75111e4240..8c6390de33fe 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -57,7 +57,7 @@ from enum import Enum from django.template.context import BaseContext -from django.utils.deprecation import django_file_prefixes +from django.utils.deprecation import RemovedInDjango70Warning, django_file_prefixes from django.utils.formats import localize from django.utils.html import conditional_escape from django.utils.inspect import lazy_annotations, signature @@ -555,6 +555,23 @@ def parse(self, parse_until=None): except TemplateSyntaxError as e: raise self.error(token, e) var_node = VariableNode(filter_expression) + if ".." in str(filter_expression.var): + warnings.warn( + "Support for double-dot lookups '..' which maps to a " + "lookup of the empty string is deprecated.\n" + f" Template: {self.origin.name}\n" + f" Line: {token.lineno}", + RemovedInDjango70Warning, + skip_file_prefixes=django_file_prefixes(), + ) + + # RemovedInDjango70Warning + # When deprecation ends elevate the warning to an error. + # raise self.error( + # token, + # ("Variable contains '..' on line %d" % token.lineno), + # ) + self.extend_nodelist(nodelist, var_node, token) elif token_type == 2: # TokenType.BLOCK try: diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index c8e467a0ee22..3cda71933b6b 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -77,6 +77,9 @@ details on these changes. ``django.contrib.postgres.aggregates.BitOr``, and ``django.contrib.postgres.aggregates.BitXor`` classes will be removed. +* Support for double-dot variable lookup, like ``{{ book..title }}``, + is removed. + .. _deprecation-removed-in-6.1: 6.1 diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index 48f893904140..3ec680702c72 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -573,6 +573,10 @@ Miscellaneous :class:`~django.db.models.BitOr`, and :class:`~django.db.models.BitXor` classes. +* Support for a double-dot variable lookup like ``{{ book..title }}`` which + maps to a lookup of the empty string before the next lookup of the named + attribute is deprecated. + Features removed in 6.1 ======================= diff --git a/tests/template_tests/syntax_tests/test_basic.py b/tests/template_tests/syntax_tests/test_basic.py index 04cf5f44012d..47bc949fdcd8 100644 --- a/tests/template_tests/syntax_tests/test_basic.py +++ b/tests/template_tests/syntax_tests/test_basic.py @@ -1,7 +1,9 @@ +from django.template import Engine from django.template.base import Origin, Template, TemplateSyntaxError from django.template.context import Context from django.template.loader_tags import BlockContext, BlockNode -from django.test import SimpleTestCase +from django.test import SimpleTestCase, ignore_warnings +from django.utils.deprecation import RemovedInDjango70Warning from django.views.debug import ExceptionReporter from ..utils import SilentAttrClass, SilentGetItemClass, SomeClass, setup @@ -393,6 +395,42 @@ def __class_getitem__(cls, key): output = self.engine.render_to_string("template", {"meals": Meals}) self.assertEqual(output, "soup is yummy.") + def test_double_dot_lookup(self): + loaders = [ + ( + "django.template.loaders.cached.Loader", + [ + ( + "django.template.loaders.locmem.Loader", + {"template": "{{ doubledot..lookup }}"}, + ), + ], + ), + ] + + msg = ( + "Support for double-dot lookups '..' which maps to a lookup of the empty " + "string is deprecated.\n Template: template\n Line: 1" + ) + + for debug in [True, False]: + with self.subTest(debug=debug): + engine = Engine(loaders=loaders, debug=debug) + with self.assertWarnsMessage(RemovedInDjango70Warning, msg): + engine.render_to_string("template", {}) + # Cached loader results in warning only on first access. + engine.render_to_string("template", {}) + + # RemovedInDjango70Warning. + # Replace the above test with the following. + # @setup({"template": "{{ doubledot..lookup }}"}) + # def test_double_dot_lookup(self): + # with self.assertRaisesMessage( + # TemplateSyntaxError, + # "Variable contains '..' on line 1", + # ): + # self.engine.render_to_string("template") + class BlockContextTests(SimpleTestCase): def test_repr(self): @@ -429,3 +467,12 @@ def test_unknown_source_template(self): Template("{% endfor %}") except TemplateSyntaxError as e: self.assertEqual(str(e), self.template_error_msg) + + +# RemovedInDjango70Warning +@ignore_warnings(category=RemovedInDjango70Warning) +class DeprecatedTests(SimpleTestCase): + @setup({"template": "{{ doubledot..lookup }}"}) + def test_double_dot_lookup(self): + context = Context({"doubledot": {"": {"lookup": "value"}}}) + self.assertEqual(self.engine.render_to_string("template", context), "value") From f3ff680c768a313d34eb2e15eb7322edec60920c Mon Sep 17 00:00:00 2001 From: VIZZARD-X Date: Thu, 16 Apr 2026 14:43:27 +0530 Subject: [PATCH 3/4] Fixed #36990 -- Bumped OpenLayers to 10.9.0. --- django/contrib/gis/forms/widgets.py | 4 ++-- docs/releases/6.1.txt | 3 +++ js_tests/tests.html | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/django/contrib/gis/forms/widgets.py b/django/contrib/gis/forms/widgets.py index 904005875083..947f25a0bbdd 100644 --- a/django/contrib/gis/forms/widgets.py +++ b/django/contrib/gis/forms/widgets.py @@ -78,12 +78,12 @@ class OpenLayersWidget(BaseGeometryWidget): class Media: css = { "all": ( - "https://cdn.jsdelivr.net/npm/ol@v7.2.2/ol.css", + "https://cdn.jsdelivr.net/npm/ol@v10.9.0/ol.css", "gis/css/ol3.css", ) } js = ( - "https://cdn.jsdelivr.net/npm/ol@v7.2.2/dist/ol.js", + "https://cdn.jsdelivr.net/npm/ol@v10.9.0/dist/ol.js", "gis/js/OLMapWidget.js", ) diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index 3ec680702c72..b4b4e60082d5 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -154,6 +154,9 @@ Minor features allow filtering geometries by the number of dimensions on PostGIS and SpatiaLite. +* :class:`~django.contrib.gis.forms.widgets.OpenLayersWidget` is now based on + OpenLayers 10.9.0 (previously 7.2.2). + :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/js_tests/tests.html b/js_tests/tests.html index b3b53925f4f5..470254b6c934 100644 --- a/js_tests/tests.html +++ b/js_tests/tests.html @@ -156,7 +156,7 @@ - + From 5b3cfce51770f46c6dc100e9be7f199a37176762 Mon Sep 17 00:00:00 2001 From: Artyom Kotovskiy Date: Sat, 25 Apr 2026 00:00:31 -0400 Subject: [PATCH 4/4] Refs #15759 -- Fixed ModelAdmin.list_editable form submission for non-editable instances. Added formset that excludes objects for which user has no permission for POST formset as well. Fixed regression test: the test was not simulating real behaviour properly. By providing full form data for the post request we skipped the part where the user was actually limited in permissions and only modified some of the rows. Improved tests by getting rid of obj.id % 2 approach for granting permissions per object for users, since it is not the safest. Instead granting permissions simply by 'alive' parameter, which is simpler and more stable. Bug in 84db026228413dda4cd195464554d51c0b208e32. --- django/contrib/admin/options.py | 13 +++++++++--- tests/admin_views/admin.py | 2 +- tests/admin_views/tests.py | 36 +++++++++++++++++---------------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index e5502c42d583..e05881b16a29 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -2026,13 +2026,16 @@ def _get_list_editable_queryset(self, request, prefix): return queryset return queryset.filter(pk__in=object_pks) - def _get_formset_with_permissions(self, request, queryset): + def _get_formset_with_permissions(self, request, queryset, for_save=False): """ Construct a changelist formset, and remove list_editable fields for objects the user cannot change. """ FormSet = self.get_changelist_formset(request) - formset = FormSet(queryset=queryset) + if for_save: + formset = FormSet(data=request.POST, files=request.FILES, queryset=queryset) + else: + formset = FormSet(queryset=queryset) for form in formset.forms: if not self.has_change_permission(request, form.instance): @@ -2158,7 +2161,11 @@ def changelist_view(self, request, extra_context=None): modified_objects = self._get_list_editable_queryset( request, FormSet.get_default_prefix() ) - cl.formset = FormSet(request.POST, request.FILES, queryset=modified_objects) + cl.formset = self._get_formset_with_permissions( + request, + queryset=modified_objects, + for_save=True, + ) if cl.formset.is_valid(): self._save_formset(request, cl.formset) diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index 5e7a055ec30c..d0448a1b640b 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -385,7 +385,7 @@ class PersonNoChangePermissionsAdmin9(admin.ModelAdmin): def has_change_permission(self, request, obj=None): if obj is None: return True - return obj.id % 2 == 0 + return obj.alive class FooAccountAdmin(admin.StackedInline): diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 1fdb90822ce3..3dba13d1851c 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -5076,14 +5076,14 @@ def test_list_editable_per_object_permissions(self): self.client.force_login(self.superuser) response = self.client.get(reverse("admin9:admin_views_person_changelist")) - # Editable fields present - self.assertContains(response, 'name="form-1-gender"') - self.assertContains(response, 'name="form-1-alive"') - # Non-editable fields should NOT have inputs - self.assertNotContains(response, 'name="form-0-gender"') - self.assertNotContains(response, 'name="form-0-alive"') - self.assertNotContains(response, 'name="form-2-gender"') - self.assertNotContains(response, 'name="form-2-alive"') + # Non-editable fields should NOT have inputs. + self.assertNotContains(response, 'name="form-1-gender"') + self.assertNotContains(response, 'name="form-1-alive"') + # Editable fields are present. + self.assertContains(response, 'name="form-0-gender"') + self.assertContains(response, 'name="form-0-alive"') + self.assertContains(response, 'name="form-2-gender"') + self.assertContains(response, 'name="form-2-alive"') def test_list_editable_per_object_permissions_submission(self): """ @@ -5092,26 +5092,28 @@ def test_list_editable_per_object_permissions_submission(self): """ self.client.logout() self.client.force_login(self.superuser) - + # Skip the instance lacking edit permission (include only its id). data = { "form-TOTAL_FORMS": "3", "form-INITIAL_FORMS": "3", "form-MAX_NUM_FORMS": "0", - "form-0-gender": "2", # Change per1 (not allowed) + "form-0-gender": "2", + "form-0-alive": "checked", "form-0-id": str(self.per1.pk), - "form-1-gender": "2", # Change per2 (allowed) - "form-1-id": str(self.per2.pk), - "form-2-gender": "2", # Change per3 (not allowed) + "form-1-id": str(self.per2.pk), # not editable + "form-2-gender": "2", + "form-2-alive": "checked", "form-2-id": str(self.per3.pk), "_save": "Save", } response = self.client.post( reverse("admin9:admin_views_person_changelist"), data, follow=True ) - # per2 and per3 were updated, but per1 was not - self.assertEqual(Person.objects.get(pk=self.per1.pk).gender, 1) # Unchanged - self.assertEqual(Person.objects.get(pk=self.per2.pk).gender, 2) - self.assertEqual(Person.objects.get(pk=self.per3.pk).gender, 1) # Unchanged + # per1 and per3 were updated, but per2 was not. + self.assertEqual(Person.objects.get(pk=self.per1.pk).gender, 2) + self.assertEqual(Person.objects.get(pk=self.per2.pk).gender, 1) # Unchanged + self.assertEqual(Person.objects.get(pk=self.per3.pk).gender, 2) + # Check for success message self.assertEqual(len(response.context["messages"]), 1)