Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions django/contrib/gis/forms/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)

Expand Down
19 changes: 18 additions & 1 deletion django/template/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions docs/internals/deprecation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions docs/releases/5.2.14.txt
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 3 additions & 2 deletions docs/releases/6.0.5.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
========
Expand Down
7 changes: 7 additions & 0 deletions docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -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
=======================

Expand Down
1 change: 1 addition & 0 deletions docs/releases/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion js_tests/tests.html
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
<textarea id="id_multipolygon" name="multipolygon" class="vSerializedField required"
style="display:none;" rows="10" cols="150"></textarea>
</div>
<script src='https://cdn.jsdelivr.net/npm/ol@v7.2.2/dist/ol.js'></script>
<script src='https://cdn.jsdelivr.net/npm/ol@v10.9.0/dist/ol.js'></script>
<script src='../django/contrib/gis/static/gis/js/OLMapWidget.js' data-cover></script>
<script src='./gis/mapwidget.test.js'></script>

Expand Down
2 changes: 1 addition & 1 deletion tests/admin_views/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
36 changes: 19 additions & 17 deletions tests/admin_views/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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)

Expand Down
49 changes: 48 additions & 1 deletion tests/template_tests/syntax_tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Loading