[Django]-Different fields for add and change pages in admin

36👍

First have a look at source of ModelAdmin class’ get_form and get_formsets methods located in django.contrib.admin.options.py. You can override those methods and use kwargs to get the behavior you want. For example:

class SoftwareVersionAdmin(ModelAdmin):
    def get_form(self, request, obj=None, **kwargs):
        # Proper kwargs are form, fields, exclude, formfield_callback
        if obj: # obj is not None, so this is a change page
            kwargs['exclude'] = ['foo', 'bar',]
        else: # obj is None, so this is an add page
            kwargs['fields'] = ['foo',]
        return super(SoftwareVersionAdmin, self).get_form(request, obj, **kwargs)
👤shanyu

39👍

This is an old question but I wanted to add that the add_view and change_view methods can be modified for this purpose:

class SoftwareVersionAdmin(ModelAdmin):
     ...
     def add_view(self,request,extra_content=None):
         self.exclude = ('product','version_number',)
         return super(SoftwareVersionAdmin,self).add_view(request)

     def change_view(self,request,object_id,extra_content=None):
         self.exclude = ('product','description',)
         return super(SoftwareVersionAdmin,self).change_view(request,object_id)

8👍

I couldn’t get this working in django 1.6.5 using the above solutions. So I tried creating forms and having get_form serve those pre-defined forms depending on if the object exists or not:

models.py:

from django.db import models

class Project(models.Model):
    name = models.CharField('Project Name', max_length=255)
    slug = models.SlugField('Project Slug', max_length=255, unique=True)

forms.py:
from django import forms
from models import Project

class ProjectAddForm(forms.ModelForm):

    test = forms.Field()

    class Meta:
        model = Project


class ProjectEditForm(forms.ModelForm):

    class Meta:
        model = Project
        fields = ("name", 'slug')

admin.py

from django.contrib import admin
from models import Project
from forms import ProjectAddForm, ProjectEditForm


class ProjectAdmin(admin.ModelAdmin):

    def get_form(self, request, obj=None, **kwargs):
        # Proper kwargs are form, fields, exclude, formfield_callback
        if obj:
            self.form = ProjectEditForm
        else:
            self.form = ProjectAddForm
        return super(ProjectAdmin, self).get_form(request, obj, **kwargs)


admin.site.register(Project, ProjectAdmin)

Now I can intercept the non-persistent test field in the forms clean and do as I wish with it, just overwrite clean in the ProjectAddForm:

def clean(self):
    cleaned_data = super(ProjectAddForm, self).clean()
    test = cleaned_data.get("test")
    # Do logic here
    #raise forms.ValidationError("Passwords don't match.")
    return cleaned_data
👤radtek

4👍

I don’t think it’s a good idea to override fields or exclude or form, because they are config attributes, so they would not initialize for every request.
I think the accepted answer by shanyu is a good solution.

Or we can use the method from UserAdmin:

def get_fieldsets(self, request, obj=None):                                  
    if not obj:                                                                                                 
        return self.add_fieldsets                                            
    return super(UserAdmin, self).get_fieldsets(request, obj)  

Remember to assign the add_fieldsets yourself. Unfortunately it doesn’t fit my use case.

For Django 1.7. I don’t know how they are implemented in other versions.

4👍

This is how it’s done in Django 1.10. Just override get_form and return add_form when object is None:

class FoobarAddForm(forms.ModelForm):
    class Meta:
        model = Foobar
        fields = ['some_field',]

@register(Foobar)
class AdminFoobar(admin.ModelAdmin):
    add_form = FoobarAddForm

    def get_form(self, request, obj=None, **kwargs):
        defaults = {}
        if obj is None:
            defaults['form'] = self.add_form
        defaults.update(kwargs)
        return super(AdminFoobar, self).get_form(request, obj, **defaults)

2👍

This specific code did not work for me.
I simply change it a little:

if obj: # obj is not None, so this is a change page
        #kwargs['exclude'] = ['owner']
        self.fields = ['id', 'family_name', 'status', 'owner']
    else: # obj is None, so this is an add page
        #kwargs['fields'] = ['id', 'family_name', 'status']
        self.fields = ['id', 'family_name', 'status']
    return super(YourAdmin, self).get_form(request, obj, **kwargs)

2👍

dpawlows‘ solution above is the clearest, I think.

However, I encountered an additional issue in that type of structure.

If change_view() makes changes to the model, e.g. specifies readonly_fields that have been filled in in add_view(), these changes persist in add_view() after change_view() has been called. For example:

def add_view(self, request, extra_context=None):
    return super().add_view(request)

def change_view(self, request, object_id, extra_context=None):
    self.readonly_fields = ['name']  # this change persists in add_view()
    return super().change_view(self, request, object_id)

In this case, after change_view() has been called on any instance, invoking add_view() will show readonly_fields (“name”, in this case) set by change_view() and thus protect these fields from filling in.

This can be solved by adding a ‘roll back’ assignment in add_view():

def add_view(self, request, extra_context=None):
    self.readonly_fields = []  # 'roll back' for changes made by change_view()
    return super().add_view(request)

2👍

With more modern Django versions (3.2 at the time of writing), you can override some methods from BaseModelAdmin to achieve having different fields on the "change" and the "add" model admin page:

class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
    """Functionality common to both ModelAdmin and InlineAdmin."""
    
    # ...

    def get_exclude(self, request, obj=None):
        """
        Hook for specifying exclude.
        """
        return self.exclude

    def get_fields(self, request, obj=None):
        """
        Hook for specifying fields.
        """
        if self.fields:
            return self.fields
        # _get_form_for_get_fields() is implemented in subclasses.
        form = self._get_form_for_get_fields(request, obj)
        return [*form.base_fields, *self.get_readonly_fields(request, obj)]

    def get_fieldsets(self, request, obj=None):
        """
        Hook for specifying fieldsets.
        """
        if self.fieldsets:
            return self.fieldsets
        return [(None, {'fields': self.get_fields(request, obj)})]

    def get_readonly_fields(self, request, obj=None):
        """
        Hook for specifying custom readonly fields.
        """
        return self.readonly_fields

For example, to add some read-only fields on the change page (obj exists), but not on the add page:

class MyModelAdmin(admin.ModelAdmin):
    
    # Readonly_fields only on change page
    def get_readonly_fields(self, request, obj=None):
        if obj:  # obj is not None, so this is a change page
            return 'field_1', 'field_2'
        return ()  # obj is None, so this is the add page

0👍

Using formsets in Django 1.6 I ended up with the following:

def get_formsets(self, request, obj=None):
    if obj is None:
        # It's a new object
        for field, fieldset in {'hide_me_from_the_first_fieldset': 0,
                                'me_from_the_second': 1,
                                'and_me_too': 1}.items():
            self.fieldsets[fieldset][1]['fields'].remove(field)

    return super().get_formsets(request, obj)

EDIT:
Perhaps a more intuitive way is to specify a separate add_fieldsets property and do:

def get_formsets(self, request, obj=None):
    if obj is None:
        self.fieldsets = self.add_fieldsets

    return super().get_formsets(request, obj)

0👍

An easy way is to use fieldsets for the Change Page and add_fieldsets for the Add Page.

0👍

base django 3.2, you can try this

class SoftwareVersionAdmin(ModelAdmin):
    fields = ("product", "version_number", "description",
      "media", "relative_url", "current_version")
    list_display = ["product", "version_number", "size",
      "current_version", "number_of_clients", "percent_of_clients"]
    list_display_links = ("version_number",)
    list_filter = ['product',]

    # add 
    add_fieldsets = (
        (None, {
            'description': 'add fields',
            'fields': ('product', 'description'),
        }),
    )

    def get_fieldsets(self, request, obj=None):
        if not obj:
            return self.add_fieldsets
        return super().get_fieldsets(request, obj)

learn this code from django.contrib.auth.admin.UserAdmin

https://docs.djangoproject.com/zh-hans/3.2/topics/auth/customizing/#custom-users-and-django-contrib-admin

Leave a comment