1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +00:00

[1.2.X] Fixed #15103 - SuspiciousOperation with limit_choices_to and raw_id_fields

Thanks to natrius for the report.

This patch also fixes some unicode bugs in affected code.

Backport of [15347] from trunk.

git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.2.X@15349 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Luke Plant 2011-01-28 14:59:54 +00:00
parent 8ab768f935
commit 276ecc7209
7 changed files with 78 additions and 21 deletions

View File

@ -185,7 +185,16 @@ class BaseModelAdmin(object):
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
return self.readonly_fields return self.readonly_fields
def lookup_allowed(self, lookup): def lookup_allowed(self, lookup, value):
model = self.model
# Check FKey lookups that are allowed, so that popups produced by
# ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
# are allowed to work.
for l in model._meta.related_fkey_lookups:
for k, v in widgets.url_params_from_lookup_dict(l).items():
if k == lookup and v == value:
return True
parts = lookup.split(LOOKUP_SEP) parts = lookup.split(LOOKUP_SEP)
# Last term in lookup is a query term (__exact, __startswith etc) # Last term in lookup is a query term (__exact, __startswith etc)
@ -197,7 +206,6 @@ class BaseModelAdmin(object):
# if foo has been specificially included in the lookup list; so # if foo has been specificially included in the lookup list; so
# drop __id if it is the last part. However, first we need to find # drop __id if it is the last part. However, first we need to find
# the pk attribute name. # the pk attribute name.
model = self.model
pk_attr_name = None pk_attr_name = None
for part in parts[:-1]: for part in parts[:-1]:
field, _, _, _ = model._meta.get_field_by_name(part) field, _, _, _ = model._meta.get_field_by_name(part)

View File

@ -179,16 +179,18 @@ class ChangeList(object):
# if key ends with __in, split parameter into separate values # if key ends with __in, split parameter into separate values
if key.endswith('__in'): if key.endswith('__in'):
lookup_params[key] = value.split(',') value = value.split(',')
lookup_params[key] = value
# if key ends with __isnull, special case '' and false # if key ends with __isnull, special case '' and false
if key.endswith('__isnull'): if key.endswith('__isnull'):
if value.lower() in ('', 'false'): if value.lower() in ('', 'false'):
lookup_params[key] = False value = False
else: else:
lookup_params[key] = True value = True
lookup_params[key] = value
if not self.model_admin.lookup_allowed(key): if not self.model_admin.lookup_allowed(key, value):
raise SuspiciousOperation( raise SuspiciousOperation(
"Filtering by %s not allowed" % key "Filtering by %s not allowed" % key
) )

View File

@ -100,6 +100,23 @@ class AdminFileWidget(forms.FileInput):
output.append(super(AdminFileWidget, self).render(name, value, attrs)) output.append(super(AdminFileWidget, self).render(name, value, attrs))
return mark_safe(u''.join(output)) return mark_safe(u''.join(output))
def url_params_from_lookup_dict(lookups):
"""
Converts the type of lookups specified in a ForeignKey limit_choices_to
attribute to a dictionary of query parameters
"""
params = {}
if lookups and hasattr(lookups, 'items'):
items = []
for k, v in lookups.items():
if isinstance(v, list):
v = u','.join([str(x) for x in v])
else:
v = unicode(v)
items.append((k, v))
params.update(dict(items))
return params
class ForeignKeyRawIdWidget(forms.TextInput): class ForeignKeyRawIdWidget(forms.TextInput):
""" """
A Widget for displaying ForeignKeys in the "raw_id" interface rather than A Widget for displaying ForeignKeys in the "raw_id" interface rather than
@ -116,33 +133,23 @@ class ForeignKeyRawIdWidget(forms.TextInput):
related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower()) related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower())
params = self.url_parameters() params = self.url_parameters()
if params: if params:
url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()]) url = u'?' + u'&'.join([u'%s=%s' % (k, v) for k, v in params.items()])
else: else:
url = '' url = u''
if not attrs.has_key('class'): if not attrs.has_key('class'):
attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook. attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook.
output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)]
# TODO: "id_" is hard-coded here. This should instead use the correct # TODO: "id_" is hard-coded here. This should instead use the correct
# API to determine the ID dynamically. # API to determine the ID dynamically.
output.append('<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \ output.append(u'<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \
(related_url, url, name)) (related_url, url, name))
output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup'))) output.append(u'<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup')))
if value: if value:
output.append(self.label_for_value(value)) output.append(self.label_for_value(value))
return mark_safe(u''.join(output)) return mark_safe(u''.join(output))
def base_url_parameters(self): def base_url_parameters(self):
params = {} return url_params_from_lookup_dict(self.rel.limit_choices_to)
if self.rel.limit_choices_to and hasattr(self.rel.limit_choices_to, 'items'):
items = []
for k, v in self.rel.limit_choices_to.items():
if isinstance(v, list):
v = ','.join([str(x) for x in v])
else:
v = str(v)
items.append((k, v))
params.update(dict(items))
return params
def url_parameters(self): def url_parameters(self):
from django.contrib.admin.views.main import TO_FIELD_VAR from django.contrib.admin.views.main import TO_FIELD_VAR

View File

@ -895,6 +895,8 @@ class ForeignKey(RelatedField, Field):
# don't get a related descriptor. # don't get a related descriptor.
if not self.rel.is_hidden(): if not self.rel.is_hidden():
setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
if self.rel.limit_choices_to:
cls._meta.related_fkey_lookups.append(self.rel.limit_choices_to)
if self.rel.field_name is None: if self.rel.field_name is None:
self.rel.field_name = cls._meta.pk.name self.rel.field_name = cls._meta.pk.name

View File

@ -50,6 +50,10 @@ class Options(object):
self.abstract_managers = [] self.abstract_managers = []
self.concrete_managers = [] self.concrete_managers = []
# List of all lookups defined in ForeignKey 'limit_choices_to' options
# from *other* models. Needed for some admin checks. Internal use only.
self.related_fkey_lookups = []
def contribute_to_class(self, cls, name): def contribute_to_class(self, cls, name):
from django.db import connection from django.db import connection
from django.db.backends.util import truncate_name from django.db.backends.util import truncate_name

View File

@ -151,6 +151,26 @@ class Thing(models.Model):
class ThingAdmin(admin.ModelAdmin): class ThingAdmin(admin.ModelAdmin):
list_filter = ('color',) list_filter = ('color',)
class Actor(models.Model):
name = models.CharField(max_length=50)
age = models.IntegerField()
def __unicode__(self):
return self.name
class Inquisition(models.Model):
expected = models.BooleanField()
leader = models.ForeignKey(Actor)
def __unicode__(self):
return self.expected
class Sketch(models.Model):
title = models.CharField(max_length=100)
inquisition = models.ForeignKey(Inquisition, limit_choices_to={'leader__name': 'Palin',
'leader__age': 27,
})
def __unicode__(self):
return self.title
class Fabric(models.Model): class Fabric(models.Model):
NG_CHOICES = ( NG_CHOICES = (
('Textured', ( ('Textured', (
@ -605,6 +625,9 @@ admin.site.register(Section, save_as=True, inlines=[ArticleInline])
admin.site.register(ModelWithStringPrimaryKey) admin.site.register(ModelWithStringPrimaryKey)
admin.site.register(Color) admin.site.register(Color)
admin.site.register(Thing, ThingAdmin) admin.site.register(Thing, ThingAdmin)
admin.site.register(Actor)
admin.site.register(Inquisition)
admin.site.register(Sketch)
admin.site.register(Person, PersonAdmin) admin.site.register(Person, PersonAdmin)
admin.site.register(Persona, PersonaAdmin) admin.site.register(Persona, PersonaAdmin)
admin.site.register(Subscriber, SubscriberAdmin) admin.site.register(Subscriber, SubscriberAdmin)

View File

@ -337,6 +337,17 @@ class AdminViewBasicTest(TestCase):
response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk) response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_allowed_filtering_15103(self):
"""
Regressions test for ticket 15103 - filtering on fields defined in a
ForeignKey 'limit_choices_to' should be allowed, otherwise raw_id_fields
can break.
"""
try:
self.client.get("/test_admin/admin/admin_views/inquisition/?leader__name=Palin&leader__age=27")
except SuspiciousOperation:
self.fail("Filters should be allowed if they are defined on a ForeignKey pointing to this model")
class SaveAsTests(TestCase): class SaveAsTests(TestCase):
fixtures = ['admin-views-users.xml','admin-views-person.xml'] fixtures = ['admin-views-users.xml','admin-views-person.xml']