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
This commit is contained in:
Ramiro Morales 2011-02-26 01:44:41 +00:00
parent 4d70d48ecb
commit 0a9b5d7ade
7 changed files with 99 additions and 20 deletions

View File

@ -206,14 +206,14 @@ class InlineAdminFormSet(object):
for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()): for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
yield InlineAdminForm(self.formset, form, self.fieldsets, yield InlineAdminForm(self.formset, form, self.fieldsets,
self.opts.prepopulated_fields, original, self.readonly_fields, self.opts.prepopulated_fields, original, self.readonly_fields,
model_admin=self.model_admin) model_admin=self.opts)
for form in self.formset.extra_forms: for form in self.formset.extra_forms:
yield InlineAdminForm(self.formset, form, self.fieldsets, yield InlineAdminForm(self.formset, form, self.fieldsets,
self.opts.prepopulated_fields, None, self.readonly_fields, 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, yield InlineAdminForm(self.formset, self.formset.empty_form,
self.fieldsets, self.opts.prepopulated_fields, None, 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): def fields(self):
fk = getattr(self.formset, "fk", None) fk = getattr(self.formset, "fk", None)
@ -222,7 +222,7 @@ class InlineAdminFormSet(object):
continue continue
if field in self.readonly_fields: if field in self.readonly_fields:
yield { yield {
'label': label_for_field(field, self.opts.model, self.model_admin), 'label': label_for_field(field, self.opts.model, self.opts),
'widget': { 'widget': {
'is_hidden': False 'is_hidden': False
}, },

View File

@ -249,7 +249,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False):
else: else:
message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name) message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name)
if model_admin: if model_admin:
message += " or %s" % (model_admin.__name__,) message += " or %s" % (model_admin.__class__.__name__,)
raise AttributeError(message) raise AttributeError(message)
if hasattr(attr, "short_description"): if hasattr(attr, "short_description"):

View File

@ -129,16 +129,7 @@ def validate(cls, model):
get_field(cls, model, opts, 'ordering[%d]' % idx, field) get_field(cls, model, opts, 'ordering[%d]' % idx, field)
if hasattr(cls, "readonly_fields"): if hasattr(cls, "readonly_fields"):
check_isseq(cls, "readonly_fields", cls.readonly_fields) check_readonly_fields(cls, model, opts)
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))
# list_select_related = False # list_select_related = False
# save_as = 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' - this is the foreign key to the parent model "
"%s." % (cls.__name__, fk.name, parent_model.__name__)) "%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): def validate_base(cls, model):
opts = model._meta opts = model._meta
@ -384,3 +378,15 @@ def fetch_attr(cls, model, opts, label, field):
except AttributeError: except AttributeError:
raise ImproperlyConfigured("'%s.%s' refers to '%s' that is neither a field, method or property of model '%s'." raise ImproperlyConfigured("'%s.%s' refers to '%s' that is neither a field, method or property of model '%s'."
% (cls.__name__, label, field, model.__name__)) % (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 File

@ -151,3 +151,43 @@ class TitleInline(admin.TabularInline):
extra = 1 extra = 1
admin.site.register(TitleCollection, inlines=[TitleInline]) 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 File

@ -84,6 +84,25 @@ class TestInline(TestCase):
# Here colspan is "4": two fields (title1 and title2), one hidden field and the delete checkbock. # 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>') 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): class TestInlineMedia(TestCase):
fixtures = ['admin-views-users.xml'] fixtures = ['admin-views-users.xml']

View File

@ -45,3 +45,11 @@ class Book(models.Model):
class AuthorsBooks(models.Model): class AuthorsBooks(models.Model):
author = models.ForeignKey(Author) author = models.ForeignKey(Author)
book = models.ForeignKey(Book) book = models.ForeignKey(Book)
class State(models.Model):
name = models.CharField(max_length=15)
class City(models.Model):
state = models.ForeignKey(State)

View File

@ -4,7 +4,7 @@ from django.contrib.admin.validation import validate, validate_inline, \
ImproperlyConfigured ImproperlyConfigured
from django.test import TestCase 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): class SongForm(forms.ModelForm):
pass pass
@ -162,6 +162,16 @@ class ValidationTestCase(TestCase):
validate, validate,
SongAdmin, Song) 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): def test_extra(self):
class SongAdmin(admin.ModelAdmin): class SongAdmin(admin.ModelAdmin):
def awesome_song(self, instance): def awesome_song(self, instance):
@ -241,7 +251,3 @@ class ValidationTestCase(TestCase):
fields = ['title', 'extra_data'] fields = ['title', 'extra_data']
validate(FieldsOnFormOnlyAdmin, Song) validate(FieldsOnFormOnlyAdmin, Song)