Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[1.2.X] 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.

Backport of [15650] from trunk.

git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.2.X@15651 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 049b3ff8a25a3907e7791091cb6a87910cff95df 1 parent 9a5ebbc
Ramiro Morales authored February 26, 2011
8  django/contrib/admin/helpers.py
@@ -207,14 +207,14 @@ def __iter__(self):
207 207
         for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
208 208
             yield InlineAdminForm(self.formset, form, self.fieldsets,
209 209
                 self.opts.prepopulated_fields, original, self.readonly_fields,
210  
-                model_admin=self.model_admin)
  210
+                model_admin=self.opts)
211 211
         for form in self.formset.extra_forms:
212 212
             yield InlineAdminForm(self.formset, form, self.fieldsets,
213 213
                 self.opts.prepopulated_fields, None, self.readonly_fields,
214  
-                model_admin=self.model_admin)
  214
+                model_admin=self.opts)
215 215
         yield InlineAdminForm(self.formset, self.formset.empty_form,
216 216
             self.fieldsets, self.opts.prepopulated_fields, None,
217  
-            self.readonly_fields, model_admin=self.model_admin)
  217
+            self.readonly_fields, model_admin=self.opts)
218 218
 
219 219
     def fields(self):
220 220
         fk = getattr(self.formset, "fk", None)
@@ -223,7 +223,7 @@ def fields(self):
223 223
                 continue
224 224
             if field in self.readonly_fields:
225 225
                 yield {
226  
-                    'label': label_for_field(field, self.opts.model, self.model_admin),
  226
+                    'label': label_for_field(field, self.opts.model, self.opts),
227 227
                     'widget': {
228 228
                         'is_hidden': False
229 229
                     },
2  django/contrib/admin/util.py
@@ -300,7 +300,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False):
300 300
             else:
301 301
                 message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name)
302 302
                 if model_admin:
303  
-                    message += " or %s" % (model_admin.__name__,)
  303
+                    message += " or %s" % (model_admin.__class__.__name__,)
304 304
                 raise AttributeError(message)
305 305
 
306 306
             if hasattr(attr, "short_description"):
26  django/contrib/admin/validation.py
@@ -121,16 +121,7 @@ def validate(cls, model):
121 121
             get_field(cls, model, opts, 'ordering[%d]' % idx, field)
122 122
 
123 123
     if hasattr(cls, "readonly_fields"):
124  
-        check_isseq(cls, "readonly_fields", cls.readonly_fields)
125  
-        for idx, field in enumerate(cls.readonly_fields):
126  
-            if not callable(field):
127  
-                if not hasattr(cls, field):
128  
-                    if not hasattr(model, field):
129  
-                        try:
130  
-                            opts.get_field(field)
131  
-                        except models.FieldDoesNotExist:
132  
-                            raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r."
133  
-                                % (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
  124
+        check_readonly_fields(cls, model, opts)
134 125
 
135 126
     # list_select_related = False
136 127
     # save_as = False
@@ -191,6 +182,9 @@ def validate_inline(cls, parent, parent_model):
191 182
                     "'%s' - this is the foreign key to the parent model "
192 183
                     "%s." % (cls.__name__, fk.name, parent_model.__name__))
193 184
 
  185
+    if hasattr(cls, "readonly_fields"):
  186
+        check_readonly_fields(cls, cls.model, cls.model._meta)
  187
+
194 188
 def validate_base(cls, model):
195 189
     opts = model._meta
196 190
 
@@ -376,3 +370,15 @@ def fetch_attr(cls, model, opts, label, field):
376 370
     except AttributeError:
377 371
         raise ImproperlyConfigured("'%s.%s' refers to '%s' that is neither a field, method or property of model '%s'."
378 372
             % (cls.__name__, label, field, model.__name__))
  373
+
  374
+def check_readonly_fields(cls, model, opts):
  375
+    check_isseq(cls, "readonly_fields", cls.readonly_fields)
  376
+    for idx, field in enumerate(cls.readonly_fields):
  377
+        if not callable(field):
  378
+            if not hasattr(cls, field):
  379
+                if not hasattr(model, field):
  380
+                    try:
  381
+                        opts.get_field(field)
  382
+                    except models.FieldDoesNotExist:
  383
+                        raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r."
  384
+                            % (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 049b3ff

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