[Fixed]-Django admin: exclude field on change form only


class SubSectionAdmin(admin.ModelAdmin):
    # ...
    def change_view(self, request, object_id, extra_context=None):       
        self.exclude = ('field', )
        return super(SubSectionAdmin, self).change_view(request, object_id, extra_context)


orwellian’s answer will make the whole SubSectionAdmin singleton change its exclude property.

A way to ensure that fields are excluded on a per-request basis is to do something like:

class SubSectionAdmin(admin.ModelAdmin):
    # ...
    def get_form(self, request, obj=None, **kwargs):
        """Override the get_form and extend the 'exclude' keyword arg"""
        if obj:
                'exclude': getattr(kwargs, 'exclude', tuple()) + ('field',),
        return super(SubSectionAdmin, self).get_form(request, obj, **kwargs)

which will just inform the Form to exclude those extra fields.

Not sure how this will behave given a required field being excluded…


Setting self.exclude does as @steve-pike mentions, make the whole SubSectionAdmin singleton change its exclude property.
A singleton is a class that will reuse the same instance every time the class is instantiated, so an instance is only created on the first use of the constructor, and subsequent use of the constructor will return the same instance. See the wiki page for a more indept description.
This means that if you write code to exclude the field on change it will have the implication that if you first add an item, the field will be there, but if you open an item for change, the field will be excluded for your following visits to the add page.

The simplest way to achieve a per request behaviour, is to use get_fields and test on the obj argument, which is None if we are adding an object, and an instance of an object if we are changing an object. The get_fields method is available from Django 1.7.

class SubSectionAdmin(admin.ModelAdmin):
    def get_fields(self, request, obj=None):
        fields = super(SubSectionAdmin, self).get_fields(request, obj)
        if obj:  # obj will be None on the add page, and something on change pages
        return fields


Please note that get_fields may return a tuple, so you may need to convert fields into a list to remove elements.
You may also encounter an error if the field name you try to remove is not in the list. Therefore it may, in some cases where you have other factors that exclude fields, be better to build a set of excludes and remove using a list comprehension:

class SubSectionAdmin(admin.ModelAdmin):
    def get_fields(self, request, obj=None):
        fields = list(super(SubSectionAdmin, self).get_fields(request, obj))
        exclude_set = set()
        if obj:  # obj will be None on the add page, and something on change pages
        return [f for f in fields if f not in exclude_set]

Alternatively you can also make a deepcopy of the result in the get_fieldsets method, which in other use cases may give you access to better context for excluding stuff. Most obviously this will be useful if you need to act on the fieldset name. Also, this is the only way to go if you actually use fieldsets since that will omit the call to get_fields.

from copy import deepcopy

class SubSectionAdmin(admin.ModelAdmin):
    def get_fieldsets(self, request, obj=None):
        """Custom override to exclude fields"""
        fieldsets = deepcopy(super(SubSectionAdmin, self).get_fieldsets(request, obj))

        # Append excludes here instead of using self.exclude.
        # When fieldsets are defined for the user admin, so self.exclude is ignored.
        exclude = ()

        if not request.user.is_superuser:
            exclude += ('accepted_error_margin_alert', 'accepted_error_margin_warning')

        # Iterate fieldsets
        for fieldset in fieldsets:
            fieldset_fields = fieldset[1]['fields']

            # Remove excluded fields from the fieldset
            for exclude_field in exclude:
                if exclude_field in fieldset_fields:
                    fieldset_fields = tuple(field for field in fieldset_fields if field != exclude_field)  # Filter
                    fieldset[1]['fields'] = fieldset_fields  # Store new tuple

        return fieldsets


The approach below has the advantage of not overriding the object wide exclude property; instead it is reset based on each type of request

class SubSectionAdmin(admin.ModelAdmin):
    add_exclude = ('field1', 'field2')
    edit_exclude = ('field2',)

    def add_view(self, *args, **kwargs):
        self.exclude = getattr(self, 'add_exclude', ())
        return super(SubSectionAdmin, self).add_view(*args, **kwargs)

    def change_view(self, *args, **kwargs):
        self.exclude = getattr(self, 'edit_exclude', ())
        return super(SubSectionAdmin, self).change_view(*args, **kwargs)


I believe you can override get_fieldsets method of ModeAdmin class. See the example below, in the code example below, I only want to display country field in the form when adding a new country, In order to check if object is being added, we simply need to check if obj == None, I am specifying the fields I need. Now otherwise obj != None means existing object is being changed, so you can specify which fields you want to exclude from the change form.

def get_fieldsets(self, request: HttpRequest, obj=None):
    fieldset = super().get_fieldsets(request, obj=obj)
    if obj == None: # obj is None when you are adding new object.
        fieldset[0][1]["fields"] = ["country"]
        fieldset[0][1]["fields"] = [
            for f in self.model._meta.fields
            if f.name not in ["id", "country"]
    return fieldset


You can override the get_exclude method of the admin.ModelAdmin class:

    def get_exclude(self, request, obj):
        if "change" in request.path.split("/"):
            return [

        return super().get_exclude(request, obj)

I think this is cleaner than the provided answers. It doesn’t override the exclude field of the Class explicitly, but rather only contextually provides the fields you wish to exclude depending on what view you’re on.

Leave a comment