get_readonly_fields in a TabularInline class in Dj

2019-04-19 07:13发布

I'm trying to use get_readonly_fields in a TabularInline class in Django:

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

    def get_readonly_fields(self, request, obj=None):
        if obj:
            return ['name']

        return self.readonly_fields

This code was taken from another StackOverflow question: Django admin site: prevent fields from being edited?

However, when it's put in a TabularInline class, the new object forms don't render properly. The goal is to make certain fields read only while still allowing data to be entered in new objects. Any ideas for a workaround or different strategy?

5条回答
做个烂人
2楼-- · 2019-04-19 07:15

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')
查看更多
干净又极端
3楼-- · 2019-04-19 07:22

Careful - "obj" is not the inline object, it's the parent. That's arguably a bug - see for example this Django ticket

查看更多
Evening l夕情丶
4楼-- · 2019-04-19 07:24

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

admin.py:

...

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

in Django 2.0:

forms.py

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))
        else:
            return super().render(name, value, attrs, renderer)

older Django Versions:

forms.py

....

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))
        else:
            return format_html('<input{0} />', flatatt(final_attrs))
查看更多
兄弟一词,经得起流年.
5楼-- · 2019-04-19 07:24

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).

查看更多
干净又极端
6楼-- · 2019-04-19 07:39

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
查看更多
登录 后发表回答