[Fixed]-Get_readonly_fields in a TabularInline class in Django?


Careful – “obj” is not the inline object, it’s the parent. That’s arguably a bug – see for example this Django ticket


As a workaround to this issue I have associated a form and a Widget to my Inline:



class MasterCouponFileInline(admin.TabularInline):
    model = MasterCouponFile
    form = MasterCouponFileForm
    extra = 0

in Django 2.0:


from django import forms
from . import models
from feedback.widgets import DisablePopulatedText

class FeedbackCommentForm(forms.ModelForm):
    class Meta:
        model = models.MasterCouponFile
        fields = ('Comment', ....)
        widgets = {
            'Comment':  DisablePopulatedText,

in widgets.py

from django import forms

class DisablePopulatedText(forms.TextInput):
    def render(self, name, value, attrs=None, renderer=None):
        """Render the widget as an HTML string."""
        if value is not None:
            # Just return the value, as normal read_only fields do
            # Add Hidden Input otherwise the old fields are still required
            HiddenInput = forms.HiddenInput()
            return format_html("{}\n"+HiddenInput.render(name, value), self.format_value(value))
            return super().render(name, value, attrs, renderer)

older Django Versions:



class MasterCouponFileForm(forms.ModelForm):
    class Meta:
        model = MasterCouponFile       

    def __init__(self, *args, **kwargs):
        super(MasterCouponFileForm, self).__init__(*args, **kwargs)
        self.fields['range'].widget = DisablePopulatedText(self.instance)
        self.fields['quantity'].widget = DisablePopulatedText(self.instance)

in widgets.py


from django import forms
from django.forms.util import flatatt
from django.utils.encoding import force_text

class DisablePopulatedText(forms.TextInput):
    def __init__(self, obj, attrs=None):
        self.object = obj
        super(DisablePopulatedText, self).__init__(attrs)
    def render(self, name, value, attrs=None):
        if value is None:
            value = ''
        final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
        if value != '':
            # Only add the 'value' attribute if a value is non-empty.
            final_attrs['value'] = force_text(self._format_value(value))
        if "__prefix__" not in name and not value:
            return format_html('<input{0} disabled />', flatatt(final_attrs))
            return format_html('<input{0} />', flatatt(final_attrs))


This is still currently not easily doable due to the fact that obj is the parent model instance not the instance displayed by the inline.

What I did in order to solve this, was to make all the fields, in the inline form, read only and provide a Add/Edit link to a ChangeForm for the inlined model.

Like this

class ChangeFormLinkMixin(object):
    def change_form_link(self, instance):
        url = reverse('admin:%s_%s_change' % (instance._meta.app_label,
            instance._meta.module_name), args=(instance.id,))
        # Id == None implies and empty inline object
        url = url.replace('None', 'add')
        command = _('Add') if url.find('add') > -1 else _('Edit')
        return format_html(u'<a href="{}">%s</a>' % command, url)

And then in the inline I will have something like this

class ItemInline(ChangeFormLinkMixin, admin.StackedInline):
    model = Item
    extra = 5
    readonly_fields = ['field1',...,'fieldN','change_form_link']

Then in the ChangeForm I’ll be able to control the changes the way I want to (I have several states, each of them with a set of editable fields associated).


As others have added, this is a design flaw in django as seen in this Django ticket (thanks Danny W). get_readonly_fields returns the parent object, which is not what we want here.

Since we can’t make it readonly, here is my solution to validate it can’t be set by the form, using a formset and a clean method:

class ItemInline(admin.TabularInline):
    model = Item
    formset = ItemInlineFormset

class ItemInlineFormset(forms.models.BaseInlineFormSet):
    def clean(self):
        super(ItemInlineFormset, self).clean()
        for form in self.forms:
            if form.instance.some_condition:
                form.add_error('some_condition', 'Nope')


You are on the right track. Update self.readonly_fields with a tuple of what fields you want to set as readonly.

class ItemInline(admin.TabularInline):
    model = Item
    extra = 5

    def get_readonly_fields(self, request, obj=None):
        # add a tuple of readonly fields
        self.readonly_fields += ('field_a', 'field_b')
        return self.readonly_fields

Leave a comment