django admin make a field read-only when modifying obj but required when adding new obj

DjangoDjango Admin

Django Problem Overview


In admin I would like to disable a field when modifying object, but make it required when adding new object.

Whats the django way to go about this one?

Django Solutions


Solution 1 - Django

You can override the admin's get_readonly_fields method:

class MyModelAdmin(admin.ModelAdmin):

    def get_readonly_fields(self, request, obj=None):
        if obj: # editing an existing object
            return self.readonly_fields + ('field1', 'field2')
        return self.readonly_fields

Solution 2 - Django

If you want to set all fields as read only just on the change view, override the admin's get_readonly_fields:

def get_readonly_fields(self, request, obj=None):
    if obj: # editing an existing object
        # All model fields as read_only
        return self.readonly_fields + tuple([item.name for item in obj._meta.fields])
    return self.readonly_fields

And if you want to hide save buttons on change view:

  1. Change the view

    def change_view(self, request, object_id, form_url='', extra_context=None):
        ''' customize edit form '''
        extra_context = extra_context or {}
        extra_context['show_save_and_continue'] = False
        extra_context['show_save'] = False
        extra_context['show_save_and_add_another'] = False # this not works if has_add_permision is True
        return super(TransferAdmin, self).change_view(request, object_id, extra_context=extra_context)
    
  2. Change permissions if user is trying to edit:

    def has_add_permission(self, request, obj=None):
       # Not too much elegant but works to hide show_save_and_add_another button
        if '/change/' in str(request):
            return False
        return True
    

This solution has been tested over Django 1.11

Solution 3 - Django

A variation based on the previous excellent suggestion of Bernhard Vallant, which also preserves any possible customization provided by the base class (if any):

class MyModelAdmin(BaseModelAdmin):

    def get_readonly_fields(self, request, obj=None):
        readonly_fields = super(MyModelAdmin, self).get_readonly_fields(request, obj)
        if obj: # editing an existing object
            return readonly_fields + ['field1', ..]
        return readonly_fields

Solution 4 - Django

FYI: in case someone else runs into the same two problems I encountered:

  1. You should still declare any permanently readonly_fields in the body of the class, as the readonly_fields class attribute will be accessed from validation (see django.contrib.admin.validation: validate_base(), line.213 appx)

  2. This won't work with Inlines as the obj passed to get_readonly_fields() is the parent obj (I have two rather hacky and low-security solutions using css or js)

Solution 5 - Django

A more pluggable Solution to the great solutions of Bernhard and Mario, adding support for createonly_fields analog to readonly_fields:

class MyModelAdmin(admin.ModelAdmin):

    # ModelAdmin configuration as usual goes here

    createonly_fields = ['title', ]

    def get_readonly_fields(self, request, obj=None):
        readonly_fields = list(super(MyModelAdmin, self).get_readonly_fields(request, obj))
        createonly_fields = list(getattr(self, 'createonly_fields', []))
            
        if obj:  # editing an existing object
            readonly_fields.extend(createonly_fields)
        return readonly_fields

Solution 6 - Django

The situation with inline forms is still not fixed for Django 2.2.x but the solution from John is actually pretty smart.

Code slightly tuned to my situation:

class NoteListInline(admin.TabularInline):
""" Notes list, readonly """
    model = Note
    verbose_name = _('Note')
    verbose_name_plural = _('Notes')
    extra = 0
    fields = ('note', 'created_at')
    readonly_fields = ('note', 'created_at')
    
    def has_add_permission(self, request, obj=None):
	""" Only add notes through AddInline """
	return False

class NoteAddInline(admin.StackedInline):
    """ Notes edit field """
    model = Note
    verbose_name = _('Note')
    verbose_name_plural = _('Notes')
    extra = 1
    fields = ('note',)
    can_delete = False

    def get_queryset(self, request):
	    queryset = super().get_queryset(request)
	    return queryset.none()  # no existing records will appear

@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
    # ...
    inlines = (NoteListInline, NoteAddInline)
    # ...

Solution 7 - Django

You can do this by overriding the formfield_for_foreignkey method of the ModelAdmin:

from django import forms
from django.contrib import admin

from yourproject.yourapp.models import YourModel

class YourModelAdmin(admin.ModelAdmin):

    class Meta:
        model = YourModel

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
        # Name of your field here
        if db_field.name == 'add_only':
            if request:
                add_opts = (self._meta.app_label, self._meta.module_name)
                add = u'/admin/%s/%s/add/' % add_opts
                if request.META['PATH_INFO'] == add:
                    field = db_field.formfield(**kwargs)
                else:
                    kwargs['widget'] = forms.HiddenInput()
                    field = db_field.formfield(**kwargs)
            return field
        return admin.ModelAdmin(self, db_field, request, **kwargs)

Solution 8 - Django

Got a similar problem. I solved it with "add_fieldsets" and "restricted_fieldsets" in the ModelAdmin.

from django.contrib import admin  
class MyAdmin(admin.ModelAdmin):
 declared_fieldsets = None
 restricted_fieldsets = (
    (None, {'fields': ('mod_obj1', 'mod_obj2')}),
    ( 'Text', {'fields': ('mod_obj3', 'mod_obj4',)}),
 )
 
 add_fieldsets = (
            (None, {
             'classes': ('wide',),
             'fields': ('add_obj1', 'add_obj2', )}),
             )

Please see e.g.: http://code.djangoproject.com/svn/django/trunk/django/contrib/auth/admin.py

But this doesn't protect your model from later changes of "add_objX". If you want this too, I think you have to go the way over the Model class "save" function and check for changes there.

See: www.djangoproject.com/documentation/models/save_delete_hooks/

Greez, Nick

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionfrnhrView Question on Stackoverflow
Solution 1 - DjangoBernhard VallantView Answer on Stackoverflow
Solution 2 - DjangoDVicenteRView Answer on Stackoverflow
Solution 3 - DjangoMario OrlandiView Answer on Stackoverflow
Solution 4 - DjangoTim DigginsView Answer on Stackoverflow
Solution 5 - DjangodirkboView Answer on Stackoverflow
Solution 6 - DjangojlapoutreView Answer on Stackoverflow
Solution 7 - DjangoDavid MillerView Answer on Stackoverflow
Solution 8 - DjangoNick Ma.View Answer on Stackoverflow