[ACCEPTED]-Readonly for existing items only in Django admin inline-django-admin
Having the same problem, I came across this 18 fix:
Create two inline objects, one with 17 no change permission, and the other with 16 all the fields read-only. Include both in 15 the model admin.
class SubscriptionInline(admin.TabularInline): model = Subscription extra = 0 readonly_fields = ['subscription', 'usedPtsStr', 'isActive', 'activationDate', 'purchaseDate'] def has_add_permission(self, request): return False class AddSupscriptionInline(admin.TabularInline): model = Subscription extra = 0 fields = ['subscription', 'usedPoints', 'isActive', 'activationDate', 'purchaseDate'] def has_change_permission(self, request, obj=None): return False # For Django Version > 2.1 there is a "view permission" that needs to be disabled too (https://docs.djangoproject.com/en/2.2/releases/2.1/#what-s-new-in-django-2-1) def has_view_permission(self, request, obj=None): return False
Include them in the same 14 model admin:
class UserAdmin(admin.ModelAdmin): inlines = [ AddSupscriptionInline, SubscriptionInline]
To add a new subscription I 13 use the
AddSubscriptionInline in the admin. Once it is saved, the 12 new subscription disappears from that inline, but 11 now does appear in the
SubscriptionInline, as read only.
SubscriptionInline, it is important to mention
extra = 0, so it won't 9 show junk read-only subscriptions.
It is 8 better also to hide the add option for
SubscriptionInline, to 7 allow adding only via
AddSubscriptionInline, by setting the
has_add_permission to 6 always return
Not perfect at all, but it's 5 the best option for me, since I must provide 4 the ability to add subscriptions on the 3 user admin page, but after one is added, it 2 should be changed only via the internal 1 app logic.
You can achieve this with only a single 1 inline like so:
class MyInline(admin.TabularInline): fields = [...] extra = 0 def has_change_permission(self, request, obj): return False
I actually came across another solution 13 that seems to work really well (I can't 12 take credit for this, but link here).
You can define 11 the
get_readonly_fields method on your
TabularInline and set the read only 10 fields appropriately when there is an object 9 (editing) vs when there is not one (creating).
def get_readonly_fields(self, request, obj=None): if obj is not None: # You may have to check some other attrs as well # Editing an object return ('field_name', ) else: # Creating a new object return ()
This 8 has the effect of making your target field 7 readonly when you're editing an exiting 6 instance while allowing it to be editable 5 when creating a new instance.
As pointed 4 out below in the comment, this doesn't quite 3 work as intended because the
obj passed is 2 is actually the parent... There's an old 1 django ticket that discusses this here.
According to this post this issue has been reported 8 as a bug in Ticket15602.
A workaround would be to override 7 the
clean method of the inline model in forms.py 6 and raise an error when an existing inline 5 is changed:
class NoteForm(forms.ModelForm): def clean(self): if self.has_changed() and self.initial: raise ValidationError( 'You cannot change this inline', code='Forbidden' ) return super().clean() class Meta(object): model = Note fields='__all__'
The above gives a solution on 4 the model level.
To raise an error when 3 a specific field is changed, the
clean_<field> method 2 can help. For example, if the field is a 1
class MyModelForm(forms.Form): pass # Several lines of code here for the needs of the Model Form # The following form will be called from the admin inline class only class MyModelInlineForm(MyModelForm): def clean_category(self): category = self.cleaned_data.get('category', None) initial_value = getattr( self.fields.get('category', None), 'initial', None ) if all( ( self.has_changed(), category.id != initial_value, ) ): raise forms.ValidationError( _('You cannot change this'), code='Forbidden' ) return category class Meta: # Copy here the Meta class of the parent model
This code work perfectly according to your 8 requirements.
Actually i got this answer 7 from my own question but specific to my 6 problem and i removed some lines related 5 to my problem. And credit goes to @YellowShark. Check here my question.
Once 4 you created new inline then you will be 3 not able to edit existing inline.
class XYZ_Inline(admin.TabularInline): model = YourModel class RequestAdmin(admin.ModelAdmin): inlines = [XYZ_Inline, ] # If you wanted to manipulate the inline forms, to make one of the fields read-only: def get_inline_formsets(self, request, formsets, inline_instances, obj=None): inline_admin_formsets =  for inline, formset in zip(inline_instances, formsets): fieldsets = list(inline.get_fieldsets(request, obj)) readonly = list(inline.get_readonly_fields(request, obj)) prepopulated = dict(inline.get_prepopulated_fields(request, obj)) inline_admin_formset = helpers.InlineAdminFormSet( inline, formset, fieldsets, prepopulated, readonly, model_admin=self, ) if isinstance(inline, XYZ_Inline): for form in inline_admin_formset.forms: #Here we change the fields read only. form.fields['some_fields'].widget.attrs['readonly'] = True inline_admin_formsets.append(inline_admin_formset) return inline_admin_formsets
You can 2 add only new inline and read only all existing 1 inline.
This is possible with a monkey patch.
The 7 following example will make the "note" field 6 to be read only for existing AdminNote objects. Unlike 5 converting fields to be hidden like suggested 4 in other answers, this will actually remove 3 fields from the submit/validation workflow 2 (which is more secure and uses existing 1 field renderers).
# # app/models.py # class Order(models.Model): pass class AdminNote(models.Model): order = models.ForeignKey(Order) time = models.DateTimeField(auto_now_add=True) note = models.TextField() # # app/admin.py # import monkey_patches.admin_fieldset ... class AdminNoteForm(forms.ModelForm): class Meta: model = AdminNote def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field in self.get_readonly_fields(): del self.fields[field] def get_readonly_fields(self): if self.instance.pk: return ['note'] return  class AdminNoteInline(admin.TabularInline): model = AdminNote form = AdminNoteForm extra = 1 fields = 'note', 'time' readonly_fields = 'time', @admin.register(Order) class OrderAdmin(admin.ModelAdmin): inlines = AdminNoteInline, # # monkey_patches/admin_fieldset.py # import django.contrib.admin.helpers class Fieldline(django.contrib.admin.helpers.Fieldline): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if hasattr(self.form, 'get_readonly_fields'): self.readonly_fields = list(self.readonly_fields) + list(self.form.get_readonly_fields()) django.contrib.admin.helpers.Fieldline = Fieldline
Here's a better read-only widget that I've 1 used before:
from django_readonlywidget.widgets import ReadOnlyWidget class TestAdmin(admin.ModelAdmin): def formfield_for_dbfield(self, db_field, **kwargs): field = super(TestAdmin, self).formfield_for_dbfield(db_field, **kwargs) if field: field.widget = ReadOnlyWidget(db_field=db_field) return field
More Related questions