Allowing further overriding of save_formset on a ModelAdmin - django

Pretty basic usage scenario here. I want to save the user who created an object and the user who last modified it. However, it's an inlined model so I, of course need to use save_formset. The Django docs have the following example code:
class ArticleAdmin(admin.ModelAdmin):
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
instance.user = request.user
instance.save()
formset.save_m2m()
The thing is, if you notice, since super is never called, this is a dead-end. If the ModelAdmin is subclassed and this method is overridden in the same way, you lose the functionality inherent in the parent. This matters because this is such a common usage scenario that I want to factor out the functionality, so I created the following:
class TrackableInlineAdminMixin(admin.ModelAdmin):
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
if hasattr(instance, 'created_by') and hasattr(instance, 'modified_by'):
if not instance.pk:
instance.created_by = request.user
instance.modified_by = request.user
instance.save()
formset.save_m2m()
super(TrackableInlineAdminMixin, self).save_formset(request, form, formset, change)
I tacked on the call to super out of habit more than anything else, not thinking that it actually will cause the formset to save twice. Nevertheless, it still works in every scenario except one: deleting. As soon as you try to delete an inline in the admin, you get an error. The error's pretty vague and not really relavent to my question here, but I believe it's related to trying to save the formset again after you've just deleted one of the instances in it. The code works just fine when the call to super is removed.
Long and short, is there any way that I'm missing to both customize the formset saving behavior and allow subclasses to do their own overriding?

This is a doozie.
I had some fun poking around and it appears all of the action happens here in django.forms.models.BaseModelFormSet.
The problem is that ModelFormSet.save() deletes instances regardless of of the commit flag and does not modify the forms to reflect the deleted state.
If you call save() again it iterates over the forms and in ModelChoiceField cleaning tries to pull up the referenced ID and throws an invalid choice error.
def save_existing_objects(self, commit=True):
self.changed_objects = []
self.deleted_objects = []
if not self.initial_forms:
return []
saved_instances = []
for form in self.initial_forms:
pk_name = self._pk_field.name
raw_pk_value = form._raw_value(pk_name)
# clean() for different types of PK fields can sometimes return
# the model instance, and sometimes the PK. Handle either.
pk_value = form.fields[pk_name].clean(raw_pk_value)
pk_value = getattr(pk_value, 'pk', pk_value)
obj = self._existing_object(pk_value)
if self.can_delete and self._should_delete_form(form):
self.deleted_objects.append(obj)
obj.delete()
# problem here causes `clean` 6 lines up to fail next round
# patched line here for future save()
# to not attempt a second delete
self.forms.remove(form)
The only way I was able to fix this is to patch BaseModelFormset.save_existing_objects to remove the form from self.forms if an object is deleted.
Did some testing and there doesn't appear to be any ill effects.

#Chris Pratt helped:
I tacked on the call to super out of habit more than anything else,
not thinking that it actually will cause the formset to save twice.
I was trying to further override a save_formset in order to send a post save signal. I just could not understand that calling super() was only saving the formset for second time.
In order to deal with the super() issue, I created a save_formset_now method with my custom code, that I call when I override the save_formset through the admin.ModelAdmin children.
This is the code, which seems to also take care of the deleting issue, using Django 1.10 in 2016.
class BaseMixinAdmin(object):
def save_formset_now(self, request, form, formset, change):
instances = formset.save(commit=False)
for obj in formset.deleted_objects:
obj.delete()
for instance in instances:
# *** Start Coding for Custom Needs ***
....
# *** End Coding for Custom Needs ***
instance.save()
formset.save_m2m()
class BaseAdmin(BaseMixinAdmin, admin.ModelAdmin):
def save_formset(self, request, form, formset, change):
self.save_formset_now(request, form, formset, change)
class ChildAdmin(BaseAdmin):
def save_formset(self, request, form, formset, change):
self.save_formset_now(request, form, formset, change)
my_signal.send(...)

Related

Django Admin save_model() creating duplicate objects

While clicking on save button multiple times. Multiple entries are getting created in db. How can i put a check there.
When overriding ModelAdmin.save_model() and ModelAdmin.delete_model(), your code must save/delete the object. They aren’t meant for veto purposes, rather they allow you to perform extra operations. So you should handle update action. This is example I try to fix this problem, maybe helpful with you.
def save_model(self, request, obj, form, change):
# check for update action
if obj.id is not None:
obj.save()
else: # check for create, delete action
id_li = list(EquipmentCategory.objects.values_list('id', flat=True))
if len(id_li) > 0:
max_id = max(id_li)
else:
max_id=0
obj.id = max_id+1
obj.save()
super(EquipmentCategoryAdmin, self).save_model(request, obj, form, change)

Saving multiple instance at once

Following code saving only one instance in database. I've added range so it should be 3. Is something wrong? Thanks
def save_formset(self, request, form, formset, change):
for coupon in range(3):
instances = formset.save(commit=False)
for instance in instances:
instance.name = 'test'
instance.save()
formset.save_m2m()
Whatever formset may be, each of the three times, you are taking the same elements from that formset and are saving it. So the same change is overwritten everytime, so it looks like the change is made only once.
I've solved the same issue using instance.pk = None before instance.save(). I don't know if there is a better way to do this, but it worked!

How to access inline data in django ModelAdmin

I need to process a file uploaded in a django admin form. I've added a file upload field to the form:
class ExampleInline(admin.TabularInline):
model = OtherExample
extra = 1
class ExampleForm(forms.ModelForm):
filedata = forms.FileField()
class Meta:
model = ExampleModel
class ExampleModelAdmin(admin.ModelAdmin):
form = ExampleForm
inlines = [ExampleInline,]
This renders the form exactly like I want it to render. The data returned in Request is exactly what I expect.
The issue is that I want to access the contents of the inline.
class ExampleAdmin(admin.ModelAdmin):
...
def save_model(self, Request, obj, form, change):
the_file = form.cleaned_data['filedata']
# do amazing things to contents of file
At this point I want to reference the results of what the user selected in the inline. Whatever they picked for OtherExample.
How do I access that through the form? I would prefer not to go through the Request but am willing to do that. I'm also willing to examine save_related(self,request, form, formset, change)
save_related can do this, although it's called after the form is saved so you'll end up saving the object twice. You can access the object as form.instance or formset.instance.
def save_related(self, request, form, formsets, change):
obj = form.instance
# whatever your formset dependent logic is to change obj.filedata
obj.save()
super(ExampleAdmin, self).save_related(request, form, formsets, change)

super() save method on Django auth user's user change form

I am trying to edit django.contrib.auth.forms.UserChangeForm. Basically, auth_user's user edit page.
https://github.com/django/django/blob/master/django/contrib/auth/forms.py
According to source code, the form does not have a save() method, so it should inherit from forms.ModelForm right?
For full code, see here
class MyUserAdminForm(forms.ModelForm):
class Meta:
model = User
def __init__(self, *args, **kwargs):
super(MyUserAdminForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.id: # username and user id
... the rest of the __init__ is setting readonly fields
.... some clean methods .....
def save(self, *args, **kwargs):
kwargs['commit'] = True
user = super(MyUserAdminForm, self).save(*args, **kwargs)
print user.username
print 'done'
return user
When I hit save, it said 'UserForm' object has no attribute 'save_m2m'. I've googled quite a bit, and tried to use add() but didn't work. What's causing this behaviour?
The thing is: the two print statements are printed. But the value never saved into database. I thought that the 2nd line would have saved once already.
Thanks
Remove the kwargs['commit'] = True line and see what happen.
Django Admin would invoke form.save_m2m(), which is hooked to the form when commit is False, here. The unconditional overriding of kwargs['commit'] = True would break the setattr of save_m2m() to form thus no attribute error is raised. The actual affected logic is here:
def save_form(self, request, form, change):
"""
Given a ModelForm return an unsaved instance. ``change`` is True if
the object is being changed, and False if it's being added.
"""
return form.save(commit=False)
You could find out that your version of form.save() overriding commit=False to commit=True unconditionally, thus Django Admin fails to continue as it believes form.save(commit=False) is invoked and thus form.save_m2m() needs to be called.
Refs the doc:
Another side effect of using commit=False is seen when your model has
a many-to-many relation with another model. If your model has a
many-to-many relation and you specify commit=False when you save a
form, Django cannot immediately save the form data for the
many-to-many relation. This is because it isn't possible to save
many-to-many data for an instance until the instance exists in the
database.
To work around this problem, every time you save a form using
commit=False, Django adds a save_m2m() method to your ModelForm
subclass. After you've manually saved the instance produced by the
form, you can invoke save_m2m() to save the many-to-many form data.

Override save on Django InlineModelAdmin

That question might look similar to this one, but it's not...
I have a model structure like :
class Customer(models.Model):
....
class CustomerCompany(models.Model):
customer = models.ForeignKey(Customer)
type = models.SmallIntegerField(....)
I am using InlineModels, and have two types of CustomerCompany.type. So I define two different inlines for the CustomerCompany and override InlineModelAdmin.queryset
class CustomerAdmin(admin.ModelAdmin):
inlines=[CustomerCompanyType1Inline, CustomerCompanyType2Inline]
class CustomerCompanyType1Inline(admin.TabularInline):
model = CustomerCompany
def queryset(self, request):
return super(CustomerCompanyType1Inline, self).queryset(request).filter(type=1)
class CustomerCompanyType2Inline(admin.TabularInline):
model = CustomerCompany
def queryset(self, request):
return super(CustomerCompanyType2Inline, self).queryset(request).filter(type=2)
All is nice and good up to here, but for adding new records for InlineModelAdmin, I still need to display type field of CustomerCompany on the AdminForm, since I can not override save method of an InlineModelAdmin like:
class CustomerCompanyType2Inline(admin.TabularInline):
model = CustomerCompany
def queryset(self, request):
return super(CustomerCompanyType2Inline, self).queryset(request).filter(type=2)
#Following override do not work
def save_model(self, request, obj, form, change):
obj.type=2
obj.save()
Using a signal is also not a solution since my signal sender will be the same Model, so I can not detect which InlineModelAdmin send it and what the type must be...
Is there a way that will let me set type field before save?
Alasdair's answer isn't wrong, but it has a few sore points that could cause problems. First, by looping through the formset using form as the variable name, you actually override the value passed into the method for form. It's not a huge deal, but since you can do the save without commit right from the formset, it's better to do it that way. Second, the all important formset.save_m2m() was left out of the answer. The actual Django docs recommend the following:
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
# Do something with `instance`
instance.save()
formset.save_m2m()
The problem you're going to run into is that the save_formset method must go on the parent ModelAdmin rather than the inlines, and from there, there's no way to know which inline is actually being utilized. If you have an obj with two "types" and all the fields are the same, then you should be using proxy models and you can actually override the save method of each to set the appropriate type automatically.
class CustomerCompanyType1(CustomerCompany):
class Meta:
proxy = True
def save(self, *args, **kwargs):
self.type = 1
super(CustomerCompanyType1, self).save(*args, **kwargs)
class CustomerCompanyType2(CustomerCompany):
class Meta:
proxy = True
def save(self, *args, **kwargs):
self.type = 2
super(CustomerCompanyType2, self).save(*args, **kwargs)
Then, you don't need to do anything special at all with your inlines. Just change your existing inline admin classes to use their appropriate proxy model, and everything will sort itself out.
There's a save_formset method which you could override. You'd have to work out which inline the formset represents somehow.
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
# Do something with `instance`
instance.save()
formset.save_m2m()
Other answers are right when it comes to using save_formset. They are missing a way to check what model is currently saved though. To do that, you can just:
if formset.model == CustomerCompany:
# actions for specific model
Which would make the save_formset function look like: (assuming you just want to override save for the specific model(s))
def save_formset(self, request, form, formset, change):
for obj in formset.deleted_objects:
obj.delete()
# if it's not the model we want to change
# just call the default function
if formset.model != CustomerCompany:
return super(CustomerAdmin, self).save_formset(request, form, formset, change)
# if it is, do our custom stuff
instances = formset.save(commit=False)
for instance in instances:
instance.type = 2
instance.save()
formset.save_m2m()
For the cases where you need to take an action if the registry is new, you need to do it before saving the formset.
def save_formset(self, request, form, formset, change):
for form in formset:
model = type(form.instance)
if change and hasattr(model, "created_by"):
# craeted_by will not appear in the form dictionary because
# is read_only, but we can anyway set it directly at the yet-
# to-be-saved instance.
form.instance.created_by = request.user
super().save_formset(request, form, formset, change)
In this case I'm also applying it when the model contains a "created_by" field (because this is for a mixin that I'm using in many places, not for a specific model).
Just remove the and hasattr(model, "created_by") part if you don't need it.
Some other properties that might be interesting for you when messing with formsets:
"""
The interesting fields to play with are:
for form in formset:
print("Instance str representation:", form.instance)
print("Instance dict:", form.instance.__dict__)
print("Initial for ID field:", form["id"].initial)
print("Has changed:", form.has_changed())
form["id"].initial will be None if it's a new entry.
"""
I hope my digging helps someone else! ^^