[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.
For 10 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 False
.
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 ForeignKey
called category
:
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:
https://bitbucket.org/stephrdev/django-readonlywidget/
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
We use cookies to improve the performance of the site. By staying on our site, you agree to the terms of use of cookies.