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/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/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/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/6.1.txt b/docs/releases/6.1.txt index 48f893904140..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` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -573,6 +576,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/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 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 @@ - + 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) 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")