I'm trying to write a dynamic form for models that allow users with different permissions to do different things. What I would like is for the below code to function properly, so that non-superusers can't edit any of the fields in the 'Merchant' block.
class CategoryModelAdmin(LWModelAdmin):
fieldsets = (
('Merchant', {'fields': ('merchant',) }),
('Category', { 'fields': ('name', 'parent',) }),
)
def get_form(self,request,obj=None, **kwargs):
if request.user.is_superuser:
self.exclude = []
else:
self.exclude = ['Merchant']
return super(CategoryModelAdmin,self).get_form(request, obj=None, **kwargs)
While I can achieve that effect via the code below, I'm really searching for the "right" way to do it and it feels like using exclude is the way to go, but I can't seem to get it right no matter what I try.
class CategoryModelAdmin(LWModelAdmin):
declared_fieldsets = None
admin_fieldsets = (
(None, {'fields': ('merchant',) }),
('Category', { 'fields': ('name', 'parent',) }),
)
restricted_fieldsets = (
('Category', { 'fields': ('name', 'parent',) }),
)
def get_fieldsets(self, request, obj=None):
if request.user.is_superuser:
fieldsets = self.admin_fieldsets
else:
fieldsets = self.restricted_fieldsets
return LWModelAdmin.fieldsets + fieldsets
def get_form(self, request, obj=None, **kwargs):
self.declared_fieldsets = self.get_fieldsets(request, obj)
return super(self.__class__, self).get_form(request, obj)
Your code is not thread-safe, you shouldn't set attributes on self (self.exclude etc) in your custom ModelAdmin methods. Instead use kwargs of ModelAdmin.get_form and InlineModelAdmin.get_formset to get what you want.
Here's a simple example:
class CategoryModelAdmin(LWModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
kwargs['exclude'] = ['foo', 'bar',]
else:
kwargs['fields'] = ['foo',]
return super(CategoryModelAdmin, self).get_form(request, obj, **kwargs)
I'm not sure if this would be more or less of a hack, but have you considered this?
Create two different models, pointing to the same table. Each model
can have it's own ModelAdmin class with whatever settings you want.
Then use Django's permissions to make one of them invisible to
non-administrators.
If you want to avoid duplicating code, you can derive both of your
classes from some common base class you create.
The advantage is that you wouldn't be doing anything outside of what's written in the documentation. I don't think we're meant to be overwriting methods like get_form and get_fieldsets. If they're not truly part of the published API, those methods may change or be removed on day, and you could get owned in an upgrade.
Just a thought...
Related
I'm having some trouble overriding the queryset for my inline admin.
Here's a bog-standard parent admin and inline admin:
class MyInlineAdmin(admin.TabularInline):
model = MyInlineModel
def queryset(self, request):
qs = super(MyInlineAdmin, self).queryset(request)
return qs
class ParentAdmin(admin.ModelAdmin):
inlines = [MyInlineAdmin]
admin.site.register(ParentAdminModel, ParentAdmin)
Now I can do qs.filter(user=request.user) or qs.filter(date__gte=datetime.today()) no problem.
But what I need is either the MyInlineModel instance or the ParentAdminModel instance (not the model!), as I need to filter my queryset based on that.
Is it possible to get something like self.instance or obj (like in get_readonly_fields() or get_formset()) inside the queryset() method?
Hope this makes sense. Any help is much appreciated.
class MyInlineAdmin(admin.TabularInline):
model = MyInlineModel
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
"""enable ordering drop-down alphabetically"""
if db_field.name == 'car':
kwargs['queryset'] = Car.objects.order_by("name")
return super(MyInlineAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
class ParentAdmin(admin.ModelAdmin):
inlines = [MyInlineAdmin]
admin.site.register(ParentAdminModel, ParentAdmin)
Im assuming your models look something like:
class MyInlineModel(models.Model):
car=models.Foreignkey(Car)
#blah
for more on this; read the django Docs on formfield_for_foreignkey-->
https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey
class Book(models.Model):
title = models.CharField(..., null=True)
type = models.CharField(...)
author = models.CharField(...)
I have a simple class in models.py. In admin I would like to hide title of the book (in book details form) when type of the saved book is 1.
How do this in a simplest way?
For Django > 1.8 one can directly set the fields to be excluded in admin:
class PostCodesAdmin(admin.ModelAdmin):
exclude = ('pcname',)
Hidden fields are directly defined in Django's ORM by setting the Field attribute: editable = False
e.g.
class PostCodes(models.Model):
gisid = models.IntegerField(primary_key=True)
pcname = models.CharField(max_length=32, db_index=True, editable=False)
...
However, setting or changing the model's fields directly may not always be possible or advantegous. In principle the following admin.py setup could work, but won't since exclude is an InlineModelAdmin option.
class PostCodesAdmin(admin.ModelAdmin):
exclude = ('pcname',)
....
A solution working at least in Django 1.4 (and likely later version numbers) is:
class PostCodesAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super(PostCodesAdmin, self).get_form(request, obj, **kwargs)
del form.base_fields['enable_comments']
return form
For the admin list-view of the items, it suffices to simply leave out fields not required:
e.g.
class PostCodesAdmin(admin.ModelAdmin):
list_display = ('id', 'gisid', 'title', )
You are to create admin.py in your module (probably book)
class BookAdmin(admin.ModelAdmin):
list_display = ("pk", "get_title_or_nothing")
In Book class:
class Book:
...
def get_title_or_nothing(self):
if self.type == WEIRD_TYPE:
return ""
return self.title
UPDATED:
class BookAdmin(admin.ModelAdmin):
list_display = ("pk", "get_title_or_nothing")
def get_form(self, request, obj=None, **kwargs):
if obj.type == "1":
self.exclude = ("title", )
form = super(BookAdmin, self).get_form(request, obj, **kwargs)
return form
I tried to override get_form() function but some mix up errors occur when I switch in different records. I found there is a get_exclude() function we can override.
Use:
class BookAdmin(admin.ModelAdmin):
def get_exclude(self, request, obj=None):
if obj and obj.type == "1":
# When you create new data the obj is None
return ("title", )
return super().get_exclude(request, obj)
class BookAdmin(admin.ModelAdmin):
exclude = ("fieldname",) # hide fields which you want
Apropos #Lorenz #mrts answer
with Django 2.1 I found that exclude does not work if the field is already specified via fields = .
In that case you may use
self.fields.remove('title')
fields will have to be defined as a list [] for this to work
If you want to maintain the value in the form (for example set a value, i.e. user, based on the request) and hide the field, you can change the widget to forms.HiddenInput():
from django import forms
...
def get_form(self, request, obj=None, **kwargs):
"""Set defaults based on request user"""
# update user field with logged user as default
form = super().get_form(request, obj, **kwargs)
form.base_fields["user"].initial = request.user.id
form.base_fields["user"].widget = forms.HiddenInput()
return form
Here is a working example
class BookAdmin(admin.ModelAdmin):
def get_fieldsets(self, request, obj):
if obj is None:
return [
(
None,
{'fields': ('type', 'description',)}
)
]
elif request.user.is_superuser:
return [
(
None,
{'fields': ('type', 'status', 'author', 'store', 'description',)}
)
]
else:
return [
(
None,
{'fields': ('type', 'date', 'author', 'store',)}
)
]
I've tried various methods to achieve this.
I decided against overriding formfield_for_dbfield as it doesn't get a copy of the request object and I was hoping to avoid the thread_locals hack.
I settled on overriding get_form in my ModelAdmin class and tried the following:
class PageOptions(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
self.fieldsets = ((None, {'fields': ('title','name',),}),)
else:
self.fieldsets = ((None, {'fields': ('title',),}),)
return super(PageOptions,self).get_form(request, obj=None, **kwargs)
When I print fieldsets or declared_fieldsets from within get_form I get None (or whatever I set as an initial value in PageOptions).
Why doesn't this work and is there a better way to do this?
I have some sample code from a recent project of mine that I believe may help you. In this example, super users can edit every field, while everyone else has the "description" field excluded.
Note that I think it's expected that you return a Form class from get_form, which could be why yours was not working quite right.
Here's the example:
class EventForm(forms.ModelForm):
class Meta:
model = models.Event
exclude = ['description',]
class EventAdminForm(forms.ModelForm):
class Meta:
model = models.Event
class EventAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
return EventAdminForm
else:
return EventForm
admin.site.register(models.Event, EventAdmin)
I have no idea why printing the property doesn't give you want you just assigned (I guess may be that depends on where you print, exactly), but try overriding get_fieldsets instead. The base implementation looks like this:
def get_fieldsets(self, request, obj=None):
if self.declared_fieldsets:
return self.declared_fieldsets
form = self.get_formset(request).form
return [(None, {'fields': form.base_fields.keys()})]
I.e. you should be able to just return your tuples.
EDIT by andybak. 4 years on and I found my own question again when trying to do something similar on another project. This time I went with this approach although modified slightly to avoid having to repeat fieldsets definition:
def get_fieldsets(self, request, obj=None):
# Add 'item_type' on add forms and remove it on changeforms.
fieldsets = super(ItemAdmin, self).get_fieldsets(request, obj)
if not obj: # this is an add form
if 'item_type' not in fieldsets[0][1]['fields']:
fieldsets[0][1]['fields'] += ('item_type',)
else: # this is a change form
fieldsets[0][1]['fields'] = tuple(x for x in fieldsets[0][1]['fields'] if x!='item_type')
return fieldsets
This is my solution:
class MyModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
self.exclude = ()
else:
self.exclude = ('field_to_exclude',)
return super(MyModelAdmin, self).get_form(request, obj=None, **kwargs)
Hope can help
For creating customized admin forms we have defined a new class which can be used as mixin. The approach is quite flexible:
ModelAdmin: define a fieldset containing all fields
ModelForm: narrow the fields being shown
FlexibleModelAdmin: overriding get_fieldsets-method of ModelAdmin; returns a reduced fieldset that only contains the fields defined in the admin form
class FlexibleModelAdmin(object):
'''
adds the possibility to use a fieldset as template for the generated form
this class should be used as mix-in
'''
def _filterFieldset(self, proposed, form):
'''
remove fields from a fieldset that do not
occur in form itself.
'''
allnewfields = []
fields = form.base_fields.keys()
fieldset = []
for fsname, fdict in proposed:
newfields = []
for field in fdict.get('fields'):
if field in fields:
newfields.append(field)
allnewfields.extend(newfields)
if newfields:
newentry = {'fields': newfields}
fieldset.append([fsname, newentry])
# nice solution but sets are not ordered ;)
# don't forget fields that are in a form but were forgotten
# in fieldset template
lostfields = list(set(fields).difference(allnewfields))
if len(lostfields):
fieldset.append(['lost in space', {'fields': lostfields}])
return fieldset
def get_fieldsets(self, request, obj=None):
'''
Hook for specifying fieldsets for the add form.
'''
if hasattr(self, 'fieldsets_proposed'):
form = self.get_form(request, obj)
return self._filterFieldset(self.fieldsets_proposed, form)
else:
return super(FlexibleModelAdmin, self).get_fieldsets(request, obj)
In the admin model you define fieldsets_proposed which serves as template and contains all fields.
class ReservationAdmin(FlexibleModelAdmin, admin.ModelAdmin):
list_display = ['id', 'displayFullName']
list_display_links = ['id', 'displayFullName']
date_hierarchy = 'reservation_start'
ordering = ['-reservation_start', 'vehicle']
exclude = ['last_modified_by']
# considered by FlexibleModelAdmin as template
fieldsets_proposed = (
(_('General'), {
'fields': ('vehicle', 'reservation_start', 'reservation_end', 'purpose') # 'added_by'
}),
(_('Report'), {
'fields': ('mileage')
}),
(_('Status'), {
'fields': ('active', 'editable')
}),
(_('Notes'), {
'fields': ('note')
}),
)
....
def get_form(self, request, obj=None, **kwargs):
'''
set the form depending on the role of the user for the particular group
'''
if request.user.is_superuser:
self.form = ReservationAdminForm
else:
self.form = ReservationUserForm
return super(ReservationAdmin, self).get_form(request, obj, **kwargs)
admin.site.register(Reservation, ReservationAdmin)
In your model forms you can now define the fields to be excluded/included. get_fieldset() of the mixin-class makes sure that only the fields defined in the form are being returned.
class ReservationAdminForm(ModelForm):
class Meta:
model = Reservation
exclude = ('added_by', 'last_modified_by')
class ReservationUserForm(BaseReservationForm):
class Meta:
model = Reservation
fields = ('vehicle', 'reservation_start', 'reservation_end', 'purpose', 'note')
Don't change the value of self attributes because it's not thread-safe. You need to use whatever hooks to override those values.
In my case, with Django 2.1 you could do the following
In forms.py
class ObjectAddForm(forms.ModelForm):
class Meta:
model = Object
exclude = []
class ObjectChangeForm(forms.ModelForm):
class Meta:
model = Object
exclude = []
And then in the admin.py
from your.app import ObjectAddForm, ObjectChangeForm
class ObjectAdmin(admin.ModelAdmin):
....
def get_form(self, request, obj=None, **kwargs):
if obj is None:
kwargs['form'] = ObjectAddForm
else:
kwargs['form'] = ObjectChangeForm
return super().get_form(request, obj, **kwargs)
You can use get_fields or get_fieldset methods for that purpose
You could make fieldsetsand form properties and have them emit signals to get the desired forms/fieldsets.
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?
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:
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)
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
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
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)
# ...
FYI: in case someone else runs into the same two problems I encountered:
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)
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)
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)
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
I've tried various methods to achieve this.
I decided against overriding formfield_for_dbfield as it doesn't get a copy of the request object and I was hoping to avoid the thread_locals hack.
I settled on overriding get_form in my ModelAdmin class and tried the following:
class PageOptions(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
self.fieldsets = ((None, {'fields': ('title','name',),}),)
else:
self.fieldsets = ((None, {'fields': ('title',),}),)
return super(PageOptions,self).get_form(request, obj=None, **kwargs)
When I print fieldsets or declared_fieldsets from within get_form I get None (or whatever I set as an initial value in PageOptions).
Why doesn't this work and is there a better way to do this?
I have some sample code from a recent project of mine that I believe may help you. In this example, super users can edit every field, while everyone else has the "description" field excluded.
Note that I think it's expected that you return a Form class from get_form, which could be why yours was not working quite right.
Here's the example:
class EventForm(forms.ModelForm):
class Meta:
model = models.Event
exclude = ['description',]
class EventAdminForm(forms.ModelForm):
class Meta:
model = models.Event
class EventAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
return EventAdminForm
else:
return EventForm
admin.site.register(models.Event, EventAdmin)
I have no idea why printing the property doesn't give you want you just assigned (I guess may be that depends on where you print, exactly), but try overriding get_fieldsets instead. The base implementation looks like this:
def get_fieldsets(self, request, obj=None):
if self.declared_fieldsets:
return self.declared_fieldsets
form = self.get_formset(request).form
return [(None, {'fields': form.base_fields.keys()})]
I.e. you should be able to just return your tuples.
EDIT by andybak. 4 years on and I found my own question again when trying to do something similar on another project. This time I went with this approach although modified slightly to avoid having to repeat fieldsets definition:
def get_fieldsets(self, request, obj=None):
# Add 'item_type' on add forms and remove it on changeforms.
fieldsets = super(ItemAdmin, self).get_fieldsets(request, obj)
if not obj: # this is an add form
if 'item_type' not in fieldsets[0][1]['fields']:
fieldsets[0][1]['fields'] += ('item_type',)
else: # this is a change form
fieldsets[0][1]['fields'] = tuple(x for x in fieldsets[0][1]['fields'] if x!='item_type')
return fieldsets
This is my solution:
class MyModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
self.exclude = ()
else:
self.exclude = ('field_to_exclude',)
return super(MyModelAdmin, self).get_form(request, obj=None, **kwargs)
Hope can help
For creating customized admin forms we have defined a new class which can be used as mixin. The approach is quite flexible:
ModelAdmin: define a fieldset containing all fields
ModelForm: narrow the fields being shown
FlexibleModelAdmin: overriding get_fieldsets-method of ModelAdmin; returns a reduced fieldset that only contains the fields defined in the admin form
class FlexibleModelAdmin(object):
'''
adds the possibility to use a fieldset as template for the generated form
this class should be used as mix-in
'''
def _filterFieldset(self, proposed, form):
'''
remove fields from a fieldset that do not
occur in form itself.
'''
allnewfields = []
fields = form.base_fields.keys()
fieldset = []
for fsname, fdict in proposed:
newfields = []
for field in fdict.get('fields'):
if field in fields:
newfields.append(field)
allnewfields.extend(newfields)
if newfields:
newentry = {'fields': newfields}
fieldset.append([fsname, newentry])
# nice solution but sets are not ordered ;)
# don't forget fields that are in a form but were forgotten
# in fieldset template
lostfields = list(set(fields).difference(allnewfields))
if len(lostfields):
fieldset.append(['lost in space', {'fields': lostfields}])
return fieldset
def get_fieldsets(self, request, obj=None):
'''
Hook for specifying fieldsets for the add form.
'''
if hasattr(self, 'fieldsets_proposed'):
form = self.get_form(request, obj)
return self._filterFieldset(self.fieldsets_proposed, form)
else:
return super(FlexibleModelAdmin, self).get_fieldsets(request, obj)
In the admin model you define fieldsets_proposed which serves as template and contains all fields.
class ReservationAdmin(FlexibleModelAdmin, admin.ModelAdmin):
list_display = ['id', 'displayFullName']
list_display_links = ['id', 'displayFullName']
date_hierarchy = 'reservation_start'
ordering = ['-reservation_start', 'vehicle']
exclude = ['last_modified_by']
# considered by FlexibleModelAdmin as template
fieldsets_proposed = (
(_('General'), {
'fields': ('vehicle', 'reservation_start', 'reservation_end', 'purpose') # 'added_by'
}),
(_('Report'), {
'fields': ('mileage')
}),
(_('Status'), {
'fields': ('active', 'editable')
}),
(_('Notes'), {
'fields': ('note')
}),
)
....
def get_form(self, request, obj=None, **kwargs):
'''
set the form depending on the role of the user for the particular group
'''
if request.user.is_superuser:
self.form = ReservationAdminForm
else:
self.form = ReservationUserForm
return super(ReservationAdmin, self).get_form(request, obj, **kwargs)
admin.site.register(Reservation, ReservationAdmin)
In your model forms you can now define the fields to be excluded/included. get_fieldset() of the mixin-class makes sure that only the fields defined in the form are being returned.
class ReservationAdminForm(ModelForm):
class Meta:
model = Reservation
exclude = ('added_by', 'last_modified_by')
class ReservationUserForm(BaseReservationForm):
class Meta:
model = Reservation
fields = ('vehicle', 'reservation_start', 'reservation_end', 'purpose', 'note')
Don't change the value of self attributes because it's not thread-safe. You need to use whatever hooks to override those values.
In my case, with Django 2.1 you could do the following
In forms.py
class ObjectAddForm(forms.ModelForm):
class Meta:
model = Object
exclude = []
class ObjectChangeForm(forms.ModelForm):
class Meta:
model = Object
exclude = []
And then in the admin.py
from your.app import ObjectAddForm, ObjectChangeForm
class ObjectAdmin(admin.ModelAdmin):
....
def get_form(self, request, obj=None, **kwargs):
if obj is None:
kwargs['form'] = ObjectAddForm
else:
kwargs['form'] = ObjectChangeForm
return super().get_form(request, obj, **kwargs)
You can use get_fields or get_fieldset methods for that purpose
You could make fieldsetsand form properties and have them emit signals to get the desired forms/fieldsets.