Commit 049b3ff8 authored by Ramiro Morales's avatar Ramiro Morales
Browse files

[1.2.X] Fixed #15424 -- Corrected lookup of callables listed in admin inlines'...

[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
parent 9a5ebbca
Loading
Loading
Loading
Loading
+4 −4
Original line number Diff line number Diff line
@@ -207,14 +207,14 @@ class InlineAdminFormSet(object):
        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)
@@ -223,7 +223,7 @@ class InlineAdminFormSet(object):
                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
                    },
+1 −1
Original line number Diff line number Diff line
@@ -300,7 +300,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"):
+16 −10
Original line number Diff line number Diff line
@@ -121,16 +121,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
@@ -191,6 +182,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

@@ -376,3 +370,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))
+40 −0
Original line number Diff line number Diff line
@@ -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)
+19 −0
Original line number Diff line number Diff line
@@ -84,6 +84,25 @@ class TestInline(TestCase):
        # 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']

Loading