different fields for add and change pages in admin

Question:

I have a django app with the following class in my admin.py:

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',]

I want to have these fileds for add page but different fields for change page. How can I do that?

Asked By: alexarsh

||

Answers:

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)
Answered By: shanyu

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)
Answered By: Ido Ran

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)
Answered By: tomekwi

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
Answered By: radtek

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)
Answered By: dpawlows

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.

Answered By: Craynic Cai

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)
Answered By: Sergii Shcherbak

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)
Answered By: Max Malysh

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

Answered By: Prateek Gupta

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
Answered By: scandel
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.