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")