Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #15103 - SuspiciousOperation with limit_choices_to and raw_id_f…

…ields

Thanks to natrius for the report.

This patch also fixes some unicode bugs in affected code.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@15347 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit c24bdf044ba23f2aa09ea4637a368ea86fd1c128 1 parent 22eaf77
Luke Plant authored January 28, 2011
12  django/contrib/admin/options.py
@@ -205,7 +205,16 @@ def queryset(self, request):
205 205
             qs = qs.order_by(*ordering)
206 206
         return qs
207 207
 
208  
-    def lookup_allowed(self, lookup):
  208
+    def lookup_allowed(self, lookup, value):
  209
+        model = self.model
  210
+        # Check FKey lookups that are allowed, so that popups produced by
  211
+        # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
  212
+        # are allowed to work.
  213
+        for l in model._meta.related_fkey_lookups:
  214
+            for k, v in widgets.url_params_from_lookup_dict(l).items():
  215
+                if k == lookup and v == value:
  216
+                    return True
  217
+
209 218
         parts = lookup.split(LOOKUP_SEP)
210 219
 
211 220
         # Last term in lookup is a query term (__exact, __startswith etc)
@@ -217,7 +226,6 @@ def lookup_allowed(self, lookup):
217 226
         # if foo has been specificially included in the lookup list; so
218 227
         # drop __id if it is the last part. However, first we need to find
219 228
         # the pk attribute name.
220  
-        model = self.model
221 229
         pk_attr_name = None
222 230
         for part in parts[:-1]:
223 231
             field, _, _, _ = model._meta.get_field_by_name(part)
10  django/contrib/admin/views/main.py
@@ -183,16 +183,18 @@ def get_query_set(self):
183 183
 
184 184
             # if key ends with __in, split parameter into separate values
185 185
             if key.endswith('__in'):
186  
-                lookup_params[key] = value.split(',')
  186
+                value = value.split(',')
  187
+                lookup_params[key] = value
187 188
 
188 189
             # if key ends with __isnull, special case '' and false
189 190
             if key.endswith('__isnull'):
190 191
                 if value.lower() in ('', 'false'):
191  
-                    lookup_params[key] = False
  192
+                    value = False
192 193
                 else:
193  
-                    lookup_params[key] = True
  194
+                    value = True
  195
+                lookup_params[key] = value
194 196
 
195  
-            if not self.model_admin.lookup_allowed(key):
  197
+            if not self.model_admin.lookup_allowed(key, value):
196 198
                 raise SuspiciousOperation(
197 199
                     "Filtering by %s not allowed" % key
198 200
                 )
36  django/contrib/admin/widgets.py
@@ -91,6 +91,22 @@ class AdminFileWidget(forms.ClearableFileInput):
91 91
     template_with_clear = (u'<span class="clearable-file-input">%s</span>'
92 92
                            % forms.ClearableFileInput.template_with_clear)
93 93
 
  94
+def url_params_from_lookup_dict(lookups):
  95
+    """
  96
+    Converts the type of lookups specified in a ForeignKey limit_choices_to
  97
+    attribute to a dictionary of query parameters
  98
+    """
  99
+    params = {}
  100
+    if lookups and hasattr(lookups, 'items'):
  101
+        items = []
  102
+        for k, v in lookups.items():
  103
+            if isinstance(v, list):
  104
+                v = u','.join([str(x) for x in v])
  105
+            else:
  106
+                v = unicode(v)
  107
+            items.append((k, v))
  108
+        params.update(dict(items))
  109
+    return params
94 110
 
95 111
 class ForeignKeyRawIdWidget(forms.TextInput):
96 112
     """
@@ -108,33 +124,23 @@ def render(self, name, value, attrs=None):
108 124
         related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower())
109 125
         params = self.url_parameters()
110 126
         if params:
111  
-            url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in params.items()])
  127
+            url = u'?' + u'&amp;'.join([u'%s=%s' % (k, v) for k, v in params.items()])
112 128
         else:
113  
-            url = ''
  129
+            url = u''
114 130
         if "class" not in attrs:
115 131
             attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook.
116 132
         output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)]
117 133
         # TODO: "id_" is hard-coded here. This should instead use the correct
118 134
         # API to determine the ID dynamically.
119  
-        output.append('<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \
  135
+        output.append(u'<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \
120 136
             (related_url, url, name))
121  
-        output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup')))
  137
+        output.append(u'<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup')))
122 138
         if value:
123 139
             output.append(self.label_for_value(value))
124 140
         return mark_safe(u''.join(output))
125 141
 
126 142
     def base_url_parameters(self):
127  
-        params = {}
128  
-        if self.rel.limit_choices_to and hasattr(self.rel.limit_choices_to, 'items'):
129  
-            items = []
130  
-            for k, v in self.rel.limit_choices_to.items():
131  
-                if isinstance(v, list):
132  
-                    v = ','.join([str(x) for x in v])
133  
-                else:
134  
-                    v = str(v)
135  
-                items.append((k, v))
136  
-            params.update(dict(items))
137  
-        return params
  143
+        return url_params_from_lookup_dict(self.rel.limit_choices_to)
138 144
 
139 145
     def url_parameters(self):
140 146
         from django.contrib.admin.views.main import TO_FIELD_VAR
2  django/db/models/fields/related.py
@@ -901,6 +901,8 @@ def contribute_to_related_class(self, cls, related):
4  django/db/models/options.py
@@ -55,6 +55,10 @@ def __init__(self, meta, app_label=None):
55 55
         self.abstract_managers = []
56 56
         self.concrete_managers = []
57 57
 
  58
+        # List of all lookups defined in ForeignKey 'limit_choices_to' options
  59
+        # from *other* models. Needed for some admin checks. Internal use only.
  60
+        self.related_fkey_lookups = []
  61
+
58 62
     def contribute_to_class(self, cls, name):
59 63
         from django.db import connection
60 64
         from django.db.backends.util import truncate_name
23  tests/regressiontests/admin_views/models.py
@@ -178,6 +178,26 @@ def __unicode__(self):
178 178
 class ThingAdmin(admin.ModelAdmin):
179 179
     list_filter = ('color__warm', 'color__value')
180 180
 
  181
+class Actor(models.Model):
  182
+    name = models.CharField(max_length=50)
  183
+    age = models.IntegerField()
  184
+    def __unicode__(self):
  185
+        return self.name
  186
+
  187
+class Inquisition(models.Model):
  188
+    expected = models.BooleanField()
  189
+    leader = models.ForeignKey(Actor)
  190
+    def __unicode__(self):
  191
+        return self.expected
  192
+
  193
+class Sketch(models.Model):
  194
+    title = models.CharField(max_length=100)
  195
+    inquisition = models.ForeignKey(Inquisition, limit_choices_to={'leader__name': 'Palin',
  196
+                                                                   'leader__age': 27,
  197
+                                                                   })
  198
+    def __unicode__(self):
  199
+        return self.title
  200
+
181 201
 class Fabric(models.Model):
182 202
     NG_CHOICES = (
183 203
         ('Textured', (
@@ -642,6 +662,9 @@ def __unicode__(self):
642 662
 admin.site.register(ModelWithStringPrimaryKey)
643 663
 admin.site.register(Color)
644 664
 admin.site.register(Thing, ThingAdmin)
  665
+admin.site.register(Actor)
  666
+admin.site.register(Inquisition)
  667
+admin.site.register(Sketch)
645 668
 admin.site.register(Person, PersonAdmin)
646 669
 admin.site.register(Persona, PersonaAdmin)
647 670
 admin.site.register(Subscriber, SubscriberAdmin)
11  tests/regressiontests/admin_views/tests.py
@@ -393,6 +393,17 @@ def test_disallowed_filtering(self):
393 393
         response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk)
394 394
         self.assertEqual(response.status_code, 200)
395 395
 
  396
+    def test_allowed_filtering_15103(self):
  397
+        """
  398
+        Regressions test for ticket 15103 - filtering on fields defined in a
  399
+        ForeignKey 'limit_choices_to' should be allowed, otherwise raw_id_fields
  400
+        can break.
  401
+        """
  402
+        try:
  403
+            self.client.get("/test_admin/admin/admin_views/inquisition/?leader__name=Palin&leader__age=27")
  404
+        except SuspiciousOperation:
  405
+            self.fail("Filters should be allowed if they are defined on a ForeignKey pointing to this model")
  406
+
396 407
 class SaveAsTests(TestCase):
397 408
     fixtures = ['admin-views-users.xml','admin-views-person.xml']
398 409
 

0 notes on commit c24bdf0

Please sign in to comment.
Something went wrong with that request. Please try again.