Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #15424 -- Corrected lookup of callables listed in admin inlines…

…' `readonly_fields` by passing the right ModelAdmin (sub)class instance when instantiating inline forms admin wrappers. Also, added early validation of its elements. Thanks kmike for the report and Karen for the patch fixing the issue.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@15650 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 0a9b5d7adee86def16972debf6441deb96135ce2 1 parent 4d70d48
Ramiro Morales authored February 26, 2011
8  django/contrib/admin/helpers.py
@@ -206,14 +206,14 @@ def __iter__(self):
206 206
         for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
207 207
             yield InlineAdminForm(self.formset, form, self.fieldsets,
208 208
                 self.opts.prepopulated_fields, original, self.readonly_fields,
209  
-                model_admin=self.model_admin)
  209
+                model_admin=self.opts)
210 210
         for form in self.formset.extra_forms:
211 211
             yield InlineAdminForm(self.formset, form, self.fieldsets,
212 212
                 self.opts.prepopulated_fields, None, self.readonly_fields,
213  
-                model_admin=self.model_admin)
  213
+                model_admin=self.opts)
214 214
         yield InlineAdminForm(self.formset, self.formset.empty_form,
215 215
             self.fieldsets, self.opts.prepopulated_fields, None,
216  
-            self.readonly_fields, model_admin=self.model_admin)
  216
+            self.readonly_fields, model_admin=self.opts)
217 217
 
218 218
     def fields(self):
219 219
         fk = getattr(self.formset, "fk", None)
@@ -222,7 +222,7 @@ def fields(self):
222 222
                 continue
223 223
             if field in self.readonly_fields:
224 224
                 yield {
225  
-                    'label': label_for_field(field, self.opts.model, self.model_admin),
  225
+                    'label': label_for_field(field, self.opts.model, self.opts),
226 226
                     'widget': {
227 227
                         'is_hidden': False
228 228
                     },
2  django/contrib/admin/util.py
@@ -249,7 +249,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False):
249 249
             else:
250 250
                 message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name)
251 251
                 if model_admin:
252  
-                    message += " or %s" % (model_admin.__name__,)
  252
+                    message += " or %s" % (model_admin.__class__.__name__,)
253 253
                 raise AttributeError(message)
254 254
 
255 255
             if hasattr(attr, "short_description"):
26  django/contrib/admin/validation.py
@@ -129,16 +129,7 @@ def validate(cls, model):
129 129
             get_field(cls, model, opts, 'ordering[%d]' % idx, field)
130 130
 
131 131
     if hasattr(cls, "readonly_fields"):
132  
-        check_isseq(cls, "readonly_fields", cls.readonly_fields)
133  
-        for idx, field in enumerate(cls.readonly_fields):
134  
-            if not callable(field):
135  
-                if not hasattr(cls, field):
136  
-                    if not hasattr(model, field):
137  
-                        try:
138  
-                            opts.get_field(field)
139  
-                        except models.FieldDoesNotExist:
140  
-                            raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r."
141  
-                                % (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
  132
+        check_readonly_fields(cls, model, opts)
142 133
 
143 134
     # list_select_related = False
144 135
     # save_as = False
@@ -199,6 +190,9 @@ def validate_inline(cls, parent, parent_model):
199 190
                     "'%s' - this is the foreign key to the parent model "
200 191
                     "%s." % (cls.__name__, fk.name, parent_model.__name__))
201 192
 
  193
+    if hasattr(cls, "readonly_fields"):
  194
+        check_readonly_fields(cls, cls.model, cls.model._meta)
  195
+
202 196
 def validate_base(cls, model):
203 197
     opts = model._meta
204 198
 
@@ -384,3 +378,15 @@ def fetch_attr(cls, model, opts, label, field):
384 378
     except AttributeError:
385 379
         raise ImproperlyConfigured("'%s.%s' refers to '%s' that is neither a field, method or property of model '%s'."
386 380
             % (cls.__name__, label, field, model.__name__))
  381
+
  382
+def check_readonly_fields(cls, model, opts):
  383
+    check_isseq(cls, "readonly_fields", cls.readonly_fields)
  384
+    for idx, field in enumerate(cls.readonly_fields):
  385
+        if not callable(field):
  386
+            if not hasattr(cls, field):
  387
+                if not hasattr(model, field):
  388
+                    try:
  389
+                        opts.get_field(field)
  390
+                    except models.FieldDoesNotExist:
  391
+                        raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r."
  392
+                            % (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
40  tests/regressiontests/admin_inlines/models.py
@@ -151,3 +151,43 @@ class TitleInline(admin.TabularInline):
151 151
     extra = 1
152 152
 
153 153
 admin.site.register(TitleCollection, inlines=[TitleInline])
  154
+
  155
+# Models for #15424
  156
+
  157
+class Poll(models.Model):
  158
+    name = models.CharField(max_length=40)
  159
+
  160
+class Question(models.Model):
  161
+    poll = models.ForeignKey(Poll)
  162
+
  163
+class QuestionInline(admin.TabularInline):
  164
+    model = Question
  165
+    readonly_fields=['call_me']
  166
+
  167
+    def call_me(self, obj):
  168
+        return 'Callable in QuestionInline'
  169
+
  170
+class PollAdmin(admin.ModelAdmin):
  171
+    inlines = [QuestionInline]
  172
+
  173
+    def call_me(self, obj):
  174
+        return 'Callable in PollAdmin'
  175
+
  176
+class Novel(models.Model):
  177
+    name = models.CharField(max_length=40)
  178
+
  179
+class Chapter(models.Model):
  180
+    novel = models.ForeignKey(Novel)
  181
+
  182
+class ChapterInline(admin.TabularInline):
  183
+    model = Chapter
  184
+    readonly_fields=['call_me']
  185
+
  186
+    def call_me(self, obj):
  187
+        return 'Callable in ChapterInline'
  188
+
  189
+class NovelAdmin(admin.ModelAdmin):
  190
+    inlines = [ChapterInline]
  191
+
  192
+admin.site.register(Poll, PollAdmin)
  193
+admin.site.register(Novel, NovelAdmin)
19  tests/regressiontests/admin_inlines/tests.py
@@ -84,6 +84,25 @@ def test_tabular_non_field_errors(self):
84 84
         # Here colspan is "4": two fields (title1 and title2), one hidden field and the delete checkbock.
85 85
         self.assertContains(response, '<tr><td colspan="4"><ul class="errorlist"><li>The two titles must be the same</li></ul></td></tr>')
86 86
 
  87
+    def test_no_parent_callable_lookup(self):
  88
+        """Admin inline `readonly_field` shouldn't invoke parent ModelAdmin callable"""
  89
+        # Identically named callable isn't present in the parent ModelAdmin,
  90
+        # rendering of the add view shouldn't explode
  91
+        response = self.client.get('/test_admin/admin/admin_inlines/novel/add/')
  92
+        self.assertEqual(response.status_code, 200)
  93
+        # View should have the child inlines section
  94
+        self.assertContains(response, '<div class="inline-group" id="chapter_set-group">')
  95
+
  96
+    def test_callable_lookup(self):
  97
+        """Admin inline should invoke local callable when its name is listed in readonly_fields"""
  98
+        response = self.client.get('/test_admin/admin/admin_inlines/poll/add/')
  99
+        self.assertEqual(response.status_code, 200)
  100
+        # Add parent object view should have the child inlines section
  101
+        self.assertContains(response, '<div class="inline-group" id="question_set-group">')
  102
+        # The right callabe should be used for the inline readonly_fields
  103
+        # column cells
  104
+        self.assertContains(response, '<p>Callable in QuestionInline</p>')
  105
+
87 106
 class TestInlineMedia(TestCase):
88 107
     fixtures = ['admin-views-users.xml']
89 108
 
8  tests/regressiontests/admin_validation/models.py
@@ -45,3 +45,11 @@ class Book(models.Model):
45 45
 class AuthorsBooks(models.Model):
46 46
     author = models.ForeignKey(Author)
47 47
     book = models.ForeignKey(Book)
  48
+
  49
+
  50
+class State(models.Model):
  51
+    name = models.CharField(max_length=15)
  52
+
  53
+
  54
+class City(models.Model):
  55
+    state = models.ForeignKey(State)
16  tests/regressiontests/admin_validation/tests.py
@@ -4,7 +4,7 @@
4 4
                                             ImproperlyConfigured
5 5
 from django.test import TestCase
6 6
 
7  
-from models import Song, Book, Album, TwoAlbumFKAndAnE
  7
+from models import Song, Book, Album, TwoAlbumFKAndAnE, State, City
8 8
 
9 9
 class SongForm(forms.ModelForm):
10 10
     pass
@@ -162,6 +162,16 @@ class SongAdmin(admin.ModelAdmin):
162 162
             validate,
163 163
             SongAdmin, Song)
164 164
 
  165
+    def test_nonexistant_field_on_inline(self):
  166
+        class CityInline(admin.TabularInline):
  167
+            model = City
  168
+            readonly_fields=['i_dont_exist'] # Missing attribute
  169
+
  170
+        self.assertRaisesMessage(ImproperlyConfigured,
  171
+            "CityInline.readonly_fields[0], 'i_dont_exist' is not a callable or an attribute of 'CityInline' or found in the model 'City'.",
  172
+            validate_inline,
  173
+            CityInline, None, State)
  174
+
165 175
     def test_extra(self):
166 176
         class SongAdmin(admin.ModelAdmin):
167 177
             def awesome_song(self, instance):
@@ -241,7 +251,3 @@ class FieldsOnFormOnlyAdmin(admin.ModelAdmin):
241 251
             fields = ['title', 'extra_data']
242 252
 
243 253
         validate(FieldsOnFormOnlyAdmin, Song)
244  
-
245  
-
246  
-
247  
-

0 notes on commit 0a9b5d7

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