mirror of
https://github.com/django/django.git
synced 2025-03-31 11:37:06 +00:00
Fixed #22569 -- Made ModelAdmin.lookup_allowed() respect get_list_filter().
Thank you Simon Meers for the initial patch.
This commit is contained in:
parent
57f2b935b3
commit
594fcc2b74
django/contrib
docs
tests/modeladmin
@ -436,7 +436,9 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
|
|||||||
else self.get_list_display(request)
|
else self.get_list_display(request)
|
||||||
)
|
)
|
||||||
|
|
||||||
def lookup_allowed(self, lookup, value):
|
# RemovedInDjango60Warning: when the deprecation ends, replace with:
|
||||||
|
# def lookup_allowed(self, lookup, value, request):
|
||||||
|
def lookup_allowed(self, lookup, value, request=None):
|
||||||
from django.contrib.admin.filters import SimpleListFilter
|
from django.contrib.admin.filters import SimpleListFilter
|
||||||
|
|
||||||
model = self.model
|
model = self.model
|
||||||
@ -482,7 +484,12 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
|
|||||||
# Either a local field filter, or no fields at all.
|
# Either a local field filter, or no fields at all.
|
||||||
return True
|
return True
|
||||||
valid_lookups = {self.date_hierarchy}
|
valid_lookups = {self.date_hierarchy}
|
||||||
for filter_item in self.list_filter:
|
# RemovedInDjango60Warning: when the deprecation ends, replace with:
|
||||||
|
# for filter_item in self.get_list_filter(request):
|
||||||
|
list_filter = (
|
||||||
|
self.get_list_filter(request) if request is not None else self.list_filter
|
||||||
|
)
|
||||||
|
for filter_item in list_filter:
|
||||||
if isinstance(filter_item, type) and issubclass(
|
if isinstance(filter_item, type) and issubclass(
|
||||||
filter_item, SimpleListFilter
|
filter_item, SimpleListFilter
|
||||||
):
|
):
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import warnings
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -31,7 +32,9 @@ from django.core.paginator import InvalidPage
|
|||||||
from django.db.models import Exists, F, Field, ManyToOneRel, OrderBy, OuterRef
|
from django.db.models import Exists, F, Field, ManyToOneRel, OrderBy, OuterRef
|
||||||
from django.db.models.expressions import Combinable
|
from django.db.models.expressions import Combinable
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.deprecation import RemovedInDjango60Warning
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
|
from django.utils.inspect import func_supports_parameter
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext
|
||||||
|
|
||||||
@ -174,9 +177,19 @@ class ChangeList:
|
|||||||
may_have_duplicates = False
|
may_have_duplicates = False
|
||||||
has_active_filters = False
|
has_active_filters = False
|
||||||
|
|
||||||
|
supports_request = func_supports_parameter(
|
||||||
|
self.model_admin.lookup_allowed, "request"
|
||||||
|
)
|
||||||
|
if not supports_request:
|
||||||
|
warnings.warn(
|
||||||
|
f"`request` must be added to the signature of "
|
||||||
|
f"{self.model_admin.__class__.__qualname__}.lookup_allowed().",
|
||||||
|
RemovedInDjango60Warning,
|
||||||
|
)
|
||||||
for key, value_list in lookup_params.items():
|
for key, value_list in lookup_params.items():
|
||||||
for value in value_list:
|
for value in value_list:
|
||||||
if not self.model_admin.lookup_allowed(key, value):
|
params = (key, value, request) if supports_request else (key, value)
|
||||||
|
if not self.model_admin.lookup_allowed(*params):
|
||||||
raise DisallowedModelAdminLookup(f"Filtering by {key} not allowed")
|
raise DisallowedModelAdminLookup(f"Filtering by {key} not allowed")
|
||||||
|
|
||||||
filter_specs = []
|
filter_specs = []
|
||||||
|
@ -106,10 +106,12 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
),
|
),
|
||||||
] + super().get_urls()
|
] + super().get_urls()
|
||||||
|
|
||||||
def lookup_allowed(self, lookup, value):
|
# RemovedInDjango60Warning: when the deprecation ends, replace with:
|
||||||
|
# def lookup_allowed(self, lookup, value, request):
|
||||||
|
def lookup_allowed(self, lookup, value, request=None):
|
||||||
# Don't allow lookups involving passwords.
|
# Don't allow lookups involving passwords.
|
||||||
return not lookup.startswith("password") and super().lookup_allowed(
|
return not lookup.startswith("password") and super().lookup_allowed(
|
||||||
lookup, value
|
lookup, value, request
|
||||||
)
|
)
|
||||||
|
|
||||||
@sensitive_post_parameters_m
|
@sensitive_post_parameters_m
|
||||||
|
@ -21,6 +21,9 @@ details on these changes.
|
|||||||
* Support for passing positional arguments to ``BaseConstraint`` will be
|
* Support for passing positional arguments to ``BaseConstraint`` will be
|
||||||
removed.
|
removed.
|
||||||
|
|
||||||
|
* ``request`` will be required in the signature of
|
||||||
|
``ModelAdmin.lookup_allowed()`` subclasses.
|
||||||
|
|
||||||
.. _deprecation-removed-in-5.1:
|
.. _deprecation-removed-in-5.1:
|
||||||
|
|
||||||
5.1
|
5.1
|
||||||
|
@ -1845,7 +1845,7 @@ templates used by the :class:`ModelAdmin` views:
|
|||||||
kwargs["formset"] = MyAdminFormSet
|
kwargs["formset"] = MyAdminFormSet
|
||||||
return super().get_changelist_formset(request, **kwargs)
|
return super().get_changelist_formset(request, **kwargs)
|
||||||
|
|
||||||
.. method:: ModelAdmin.lookup_allowed(lookup, value)
|
.. method:: ModelAdmin.lookup_allowed(lookup, value, request)
|
||||||
|
|
||||||
The objects in the changelist page can be filtered with lookups from the
|
The objects in the changelist page can be filtered with lookups from the
|
||||||
URL's query string. This is how :attr:`list_filter` works, for example. The
|
URL's query string. This is how :attr:`list_filter` works, for example. The
|
||||||
@ -1855,10 +1855,11 @@ templates used by the :class:`ModelAdmin` views:
|
|||||||
unauthorized data exposure.
|
unauthorized data exposure.
|
||||||
|
|
||||||
The ``lookup_allowed()`` method is given a lookup path from the query string
|
The ``lookup_allowed()`` method is given a lookup path from the query string
|
||||||
(e.g. ``'user__email'``) and the corresponding value
|
(e.g. ``'user__email'``), the corresponding value
|
||||||
(e.g. ``'user@example.com'``), and returns a boolean indicating whether
|
(e.g. ``'user@example.com'``), and the request, and returns a boolean
|
||||||
filtering the changelist's ``QuerySet`` using the parameters is permitted.
|
indicating whether filtering the changelist's ``QuerySet`` using the
|
||||||
If ``lookup_allowed()`` returns ``False``, ``DisallowedModelAdminLookup``
|
parameters is permitted. If ``lookup_allowed()`` returns ``False``,
|
||||||
|
``DisallowedModelAdminLookup``
|
||||||
(subclass of :exc:`~django.core.exceptions.SuspiciousOperation`) is raised.
|
(subclass of :exc:`~django.core.exceptions.SuspiciousOperation`) is raised.
|
||||||
|
|
||||||
By default, ``lookup_allowed()`` allows access to a model's local fields,
|
By default, ``lookup_allowed()`` allows access to a model's local fields,
|
||||||
@ -1870,6 +1871,10 @@ templates used by the :class:`ModelAdmin` views:
|
|||||||
Override this method to customize the lookups permitted for your
|
Override this method to customize the lookups permitted for your
|
||||||
:class:`~django.contrib.admin.ModelAdmin` subclass.
|
:class:`~django.contrib.admin.ModelAdmin` subclass.
|
||||||
|
|
||||||
|
.. versionchanged:: 5.0
|
||||||
|
|
||||||
|
The ``request`` argument was added.
|
||||||
|
|
||||||
.. method:: ModelAdmin.has_view_permission(request, obj=None)
|
.. method:: ModelAdmin.has_view_permission(request, obj=None)
|
||||||
|
|
||||||
Should return ``True`` if viewing ``obj`` is permitted, ``False`` otherwise.
|
Should return ``True`` if viewing ``obj`` is permitted, ``False`` otherwise.
|
||||||
|
@ -387,6 +387,10 @@ Miscellaneous
|
|||||||
:class:`~django.db.models.BaseConstraint` is deprecated in favor of
|
:class:`~django.db.models.BaseConstraint` is deprecated in favor of
|
||||||
keyword-only arguments.
|
keyword-only arguments.
|
||||||
|
|
||||||
|
* ``request`` is added to the signature of :meth:`.ModelAdmin.lookup_allowed`.
|
||||||
|
Support for ``ModelAdmin`` subclasses that do not accept this argument is
|
||||||
|
deprecated.
|
||||||
|
|
||||||
Features removed in 5.0
|
Features removed in 5.0
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
|
@ -19,8 +19,9 @@ from django.contrib.admin.widgets import (
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms.widgets import Select
|
from django.forms.widgets import Select
|
||||||
from django.test import SimpleTestCase, TestCase
|
from django.test import RequestFactory, SimpleTestCase, TestCase
|
||||||
from django.test.utils import isolate_apps
|
from django.test.utils import isolate_apps
|
||||||
|
from django.utils.deprecation import RemovedInDjango60Warning
|
||||||
|
|
||||||
from .models import Band, Concert, Song
|
from .models import Band, Concert, Song
|
||||||
|
|
||||||
@ -121,7 +122,10 @@ class ModelAdminTests(TestCase):
|
|||||||
fields = ["name"]
|
fields = ["name"]
|
||||||
|
|
||||||
ma = BandAdmin(Band, self.site)
|
ma = BandAdmin(Band, self.site)
|
||||||
self.assertTrue(ma.lookup_allowed("name__nonexistent", "test_value"))
|
self.assertIs(
|
||||||
|
ma.lookup_allowed("name__nonexistent", "test_value", request),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
@isolate_apps("modeladmin")
|
@isolate_apps("modeladmin")
|
||||||
def test_lookup_allowed_onetoone(self):
|
def test_lookup_allowed_onetoone(self):
|
||||||
@ -147,11 +151,15 @@ class ModelAdminTests(TestCase):
|
|||||||
ma = EmployeeProfileAdmin(EmployeeProfile, self.site)
|
ma = EmployeeProfileAdmin(EmployeeProfile, self.site)
|
||||||
# Reverse OneToOneField
|
# Reverse OneToOneField
|
||||||
self.assertIs(
|
self.assertIs(
|
||||||
ma.lookup_allowed("employee__employeeinfo__description", "test_value"), True
|
ma.lookup_allowed(
|
||||||
|
"employee__employeeinfo__description", "test_value", request
|
||||||
|
),
|
||||||
|
True,
|
||||||
)
|
)
|
||||||
# OneToOneField and ForeignKey
|
# OneToOneField and ForeignKey
|
||||||
self.assertIs(
|
self.assertIs(
|
||||||
ma.lookup_allowed("employee__department__code", "test_value"), True
|
ma.lookup_allowed("employee__department__code", "test_value", request),
|
||||||
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@isolate_apps("modeladmin")
|
@isolate_apps("modeladmin")
|
||||||
@ -175,13 +183,87 @@ class ModelAdminTests(TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
ma = WaiterAdmin(Waiter, self.site)
|
ma = WaiterAdmin(Waiter, self.site)
|
||||||
self.assertIs(ma.lookup_allowed("restaurant__place__country", "1"), True)
|
|
||||||
self.assertIs(
|
self.assertIs(
|
||||||
ma.lookup_allowed("restaurant__place__country__id__exact", "1"), True
|
ma.lookup_allowed("restaurant__place__country", "1", request),
|
||||||
|
True,
|
||||||
)
|
)
|
||||||
self.assertIs(
|
self.assertIs(
|
||||||
ma.lookup_allowed("restaurant__place__country__name", "test_value"), True
|
ma.lookup_allowed("restaurant__place__country__id__exact", "1", request),
|
||||||
|
True,
|
||||||
)
|
)
|
||||||
|
self.assertIs(
|
||||||
|
ma.lookup_allowed(
|
||||||
|
"restaurant__place__country__name", "test_value", request
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_lookup_allowed_considers_dynamic_list_filter(self):
|
||||||
|
class ConcertAdmin(ModelAdmin):
|
||||||
|
list_filter = ["main_band__sign_date"]
|
||||||
|
|
||||||
|
def get_list_filter(self, request):
|
||||||
|
if getattr(request, "user", None):
|
||||||
|
return self.list_filter + ["main_band__name"]
|
||||||
|
return self.list_filter
|
||||||
|
|
||||||
|
model_admin = ConcertAdmin(Concert, self.site)
|
||||||
|
request_band_name_filter = RequestFactory().get(
|
||||||
|
"/", {"main_band__name": "test"}
|
||||||
|
)
|
||||||
|
self.assertIs(
|
||||||
|
model_admin.lookup_allowed(
|
||||||
|
"main_band__sign_date", "?", request_band_name_filter
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.assertIs(
|
||||||
|
model_admin.lookup_allowed(
|
||||||
|
"main_band__name", "?", request_band_name_filter
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
request_with_superuser = request
|
||||||
|
self.assertIs(
|
||||||
|
model_admin.lookup_allowed(
|
||||||
|
"main_band__sign_date", "?", request_with_superuser
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.assertIs(
|
||||||
|
model_admin.lookup_allowed("main_band__name", "?", request_with_superuser),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_lookup_allowed_without_request_deprecation(self):
|
||||||
|
class ConcertAdmin(ModelAdmin):
|
||||||
|
list_filter = ["main_band__sign_date"]
|
||||||
|
|
||||||
|
def get_list_filter(self, request):
|
||||||
|
return self.list_filter + ["main_band__name"]
|
||||||
|
|
||||||
|
def lookup_allowed(self, lookup, value):
|
||||||
|
return True
|
||||||
|
|
||||||
|
model_admin = ConcertAdmin(Concert, self.site)
|
||||||
|
msg = (
|
||||||
|
"`request` must be added to the signature of ModelAdminTests."
|
||||||
|
"test_lookup_allowed_without_request_deprecation.<locals>."
|
||||||
|
"ConcertAdmin.lookup_allowed()."
|
||||||
|
)
|
||||||
|
request_band_name_filter = RequestFactory().get(
|
||||||
|
"/", {"main_band__name": "test"}
|
||||||
|
)
|
||||||
|
request_band_name_filter.user = User.objects.create_superuser(
|
||||||
|
username="bob", email="bob@test.com", password="test"
|
||||||
|
)
|
||||||
|
with self.assertWarnsMessage(RemovedInDjango60Warning, msg):
|
||||||
|
changelist = model_admin.get_changelist_instance(request_band_name_filter)
|
||||||
|
filterspec = changelist.get_filters(request_band_name_filter)[0][0]
|
||||||
|
self.assertEqual(filterspec.title, "sign date")
|
||||||
|
filterspec = changelist.get_filters(request_band_name_filter)[0][1]
|
||||||
|
self.assertEqual(filterspec.title, "name")
|
||||||
|
self.assertSequenceEqual(filterspec.lookup_choices, [self.band.name])
|
||||||
|
|
||||||
def test_field_arguments(self):
|
def test_field_arguments(self):
|
||||||
# If fields is specified, fieldsets_add and fieldsets_change should
|
# If fields is specified, fieldsets_add and fieldsets_change should
|
||||||
|
Loading…
x
Reference in New Issue
Block a user