Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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 ramiro authored
View
8 django/contrib/admin/helpers.py
@@ -206,14 +206,14 @@ def __iter__(self):
for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
yield InlineAdminForm(self.formset, form, self.fieldsets,
self.opts.prepopulated_fields, original, self.readonly_fields,
- model_admin=self.model_admin)
+ model_admin=self.opts)
for form in self.formset.extra_forms:
yield InlineAdminForm(self.formset, form, self.fieldsets,
self.opts.prepopulated_fields, None, self.readonly_fields,
- model_admin=self.model_admin)
+ model_admin=self.opts)
yield InlineAdminForm(self.formset, self.formset.empty_form,
self.fieldsets, self.opts.prepopulated_fields, None,
- self.readonly_fields, model_admin=self.model_admin)
+ self.readonly_fields, model_admin=self.opts)
def fields(self):
fk = getattr(self.formset, "fk", None)
@@ -222,7 +222,7 @@ def fields(self):
continue
if field in self.readonly_fields:
yield {
- 'label': label_for_field(field, self.opts.model, self.model_admin),
+ 'label': label_for_field(field, self.opts.model, self.opts),
'widget': {
'is_hidden': False
},
View
2  django/contrib/admin/util.py
@@ -249,7 +249,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False):
else:
message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name)
if model_admin:
- message += " or %s" % (model_admin.__name__,)
+ message += " or %s" % (model_admin.__class__.__name__,)
raise AttributeError(message)
if hasattr(attr, "short_description"):
View
26 django/contrib/admin/validation.py
@@ -129,16 +129,7 @@ def validate(cls, model):
get_field(cls, model, opts, 'ordering[%d]' % idx, field)
if hasattr(cls, "readonly_fields"):
- check_isseq(cls, "readonly_fields", cls.readonly_fields)
- for idx, field in enumerate(cls.readonly_fields):
- if not callable(field):
- if not hasattr(cls, field):
- if not hasattr(model, field):
- try:
- opts.get_field(field)
- except models.FieldDoesNotExist:
- raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r."
- % (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
+ check_readonly_fields(cls, model, opts)
# list_select_related = False
# save_as = False
@@ -199,6 +190,9 @@ def validate_inline(cls, parent, parent_model):
"'%s' - this is the foreign key to the parent model "
"%s." % (cls.__name__, fk.name, parent_model.__name__))
+ if hasattr(cls, "readonly_fields"):
+ check_readonly_fields(cls, cls.model, cls.model._meta)
+
def validate_base(cls, model):
opts = model._meta
@@ -384,3 +378,15 @@ def fetch_attr(cls, model, opts, label, field):
except AttributeError:
raise ImproperlyConfigured("'%s.%s' refers to '%s' that is neither a field, method or property of model '%s'."
% (cls.__name__, label, field, model.__name__))
+
+def check_readonly_fields(cls, model, opts):
+ check_isseq(cls, "readonly_fields", cls.readonly_fields)
+ for idx, field in enumerate(cls.readonly_fields):
+ if not callable(field):
+ if not hasattr(cls, field):
+ if not hasattr(model, field):
+ try:
+ opts.get_field(field)
+ except models.FieldDoesNotExist:
+ raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r."
+ % (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
View
40 tests/regressiontests/admin_inlines/models.py
@@ -151,3 +151,43 @@ class TitleInline(admin.TabularInline):
extra = 1
admin.site.register(TitleCollection, inlines=[TitleInline])
+
+# Models for #15424
+
+class Poll(models.Model):
+ name = models.CharField(max_length=40)
+
+class Question(models.Model):
+ poll = models.ForeignKey(Poll)
+
+class QuestionInline(admin.TabularInline):
+ model = Question
+ readonly_fields=['call_me']
+
+ def call_me(self, obj):
+ return 'Callable in QuestionInline'
+
+class PollAdmin(admin.ModelAdmin):
+ inlines = [QuestionInline]
+
+ def call_me(self, obj):
+ return 'Callable in PollAdmin'
+
+class Novel(models.Model):
+ name = models.CharField(max_length=40)
+
+class Chapter(models.Model):
+ novel = models.ForeignKey(Novel)
+
+class ChapterInline(admin.TabularInline):
+ model = Chapter
+ readonly_fields=['call_me']
+
+ def call_me(self, obj):
+ return 'Callable in ChapterInline'
+
+class NovelAdmin(admin.ModelAdmin):
+ inlines = [ChapterInline]
+
+admin.site.register(Poll, PollAdmin)
+admin.site.register(Novel, NovelAdmin)
View
19 tests/regressiontests/admin_inlines/tests.py
@@ -84,6 +84,25 @@ def test_tabular_non_field_errors(self):
# Here colspan is "4": two fields (title1 and title2), one hidden field and the delete checkbock.
self.assertContains(response, '<tr><td colspan="4"><ul class="errorlist"><li>The two titles must be the same</li></ul></td></tr>')
+ def test_no_parent_callable_lookup(self):
+ """Admin inline `readonly_field` shouldn't invoke parent ModelAdmin callable"""
+ # Identically named callable isn't present in the parent ModelAdmin,
+ # rendering of the add view shouldn't explode
+ response = self.client.get('/test_admin/admin/admin_inlines/novel/add/')
+ self.assertEqual(response.status_code, 200)
+ # View should have the child inlines section
+ self.assertContains(response, '<div class="inline-group" id="chapter_set-group">')
+
+ def test_callable_lookup(self):
+ """Admin inline should invoke local callable when its name is listed in readonly_fields"""
+ response = self.client.get('/test_admin/admin/admin_inlines/poll/add/')
+ self.assertEqual(response.status_code, 200)
+ # Add parent object view should have the child inlines section
+ self.assertContains(response, '<div class="inline-group" id="question_set-group">')
+ # The right callabe should be used for the inline readonly_fields
+ # column cells
+ self.assertContains(response, '<p>Callable in QuestionInline</p>')
+
class TestInlineMedia(TestCase):
fixtures = ['admin-views-users.xml']
View
8 tests/regressiontests/admin_validation/models.py
@@ -45,3 +45,11 @@ class Book(models.Model):
class AuthorsBooks(models.Model):
author = models.ForeignKey(Author)
book = models.ForeignKey(Book)
+
+
+class State(models.Model):
+ name = models.CharField(max_length=15)
+
+
+class City(models.Model):
+ state = models.ForeignKey(State)
View
16 tests/regressiontests/admin_validation/tests.py
@@ -4,7 +4,7 @@
ImproperlyConfigured
from django.test import TestCase
-from models import Song, Book, Album, TwoAlbumFKAndAnE
+from models import Song, Book, Album, TwoAlbumFKAndAnE, State, City
class SongForm(forms.ModelForm):
pass
@@ -162,6 +162,16 @@ class SongAdmin(admin.ModelAdmin):
validate,
SongAdmin, Song)
+ def test_nonexistant_field_on_inline(self):
+ class CityInline(admin.TabularInline):
+ model = City
+ readonly_fields=['i_dont_exist'] # Missing attribute
+
+ self.assertRaisesMessage(ImproperlyConfigured,
+ "CityInline.readonly_fields[0], 'i_dont_exist' is not a callable or an attribute of 'CityInline' or found in the model 'City'.",
+ validate_inline,
+ CityInline, None, State)
+
def test_extra(self):
class SongAdmin(admin.ModelAdmin):
def awesome_song(self, instance):
@@ -241,7 +251,3 @@ class FieldsOnFormOnlyAdmin(admin.ModelAdmin):
fields = ['title', 'extra_data']
validate(FieldsOnFormOnlyAdmin, Song)
-
-
-
-
Please sign in to comment.
Something went wrong with that request. Please try again.