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

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

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


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

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)