Django Admin save_model() creating duplicate objects - django

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)

Related

Django: Avoid duplicates image uploads from admin panel

I have a model with an ImageField, which allow an image upload from the panel admin of Django.
I would like to check if the image already exists, before saving the model.
If it's the case, I would like to display a popup (or a warning on the same page) with both images, to allow users to compare images, and allow saving if it's a false positive.
For the image comparison, I'm going to use imagehash.average_hash() algorithm which gaves me good results from my tests.
So my questions are:
How to get the file content (to compute the aHash), before the model save.
How to display a popup or modify the modelAdmin page to allow the check of false positive.
Any help is appreciated!
I figured out to achieve this a part of this:
admin.py
# called after page validation, to switch page (we catch the ValidationError send from save_model
# if this image was already uploaded
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
try:
return super(MediaAdmin, self).changeform_view(request, object_id, form_url, extra_context)
except ValidationError as e:
self.message_user(request, e, level=messages.ERROR)
if object_id:
# if we are editing an existing media
return redirect(to=reverse('admin:web_app_media_change', args=(object_id, )))
else:
# if we try to add a new media
return redirect(to=reverse('admin:web_app_media_add'))
# check if the media was uploaded before, and raise a ValidationError if it's the case
def save_model(self, request, obj, form, change):
if not obj.hash:
obj.hash = imagehash.average_hash(Image.open(obj.media)).__str__()
duplicate = False
for media in Media.objects.all():
if media.media != obj.media and media.hash == obj.hash:
duplicate = True
if not duplicate:
super(MediaAdmin, self).save_model(request, obj, form, change)
else:
raise ValidationError("This media was already uploaded.")
With a new hash = models.CharField(max_length=20) in my Media model.
Hoping it will help :)

Django Admin save_model() creates a new object when I only try to edit

My original issue was that I was trying to attach the current user to new entries, so I override the save_model method under admin.ModelAdmin to do
def save_model(self, request, obj, form, change):
obj.submit_usr = request.user
super().save_model(request, obj, form, change)
This works great when I try to create new object or new entry in my db, but when I try to edit the existing one, it still creates a new entry
my model looks like (I took out all the settings to avoid being messy here)
class AppUser(models.Model):
app_name = model.CharField()
submit_usr = models.ForeignKey()
submit_date = model.DateTimeField()
I know that the change arg indicates if there's a change in existing entry, but I still don't have a way to tell Django that I only want to modify the current entry instead of creating a new one.
Any ideas how to achieve that?
You can try
def save_model(self, request, obj, form, change):
if not change:
obj.submit_usr = request.user
super().save_model(request, obj, form, change)

Save the related objects before the actual object being edited on django admin

Is it possible to save the related objects before the actual object being edited on a django admin form?
For example:
in models.py
class Parent(model.Model):
pass
class Child(model.Model):
parent = models.ForeignKey(Parent)
#receiver(post_save,sender = Parent)
def notify_parent_save(sender, instance=None, **kwargs):
print "Parent save"
#receiver(post_save,sender = Child)
def notify_child_save(sender, instance=None, **kwargs):
print "Child saved"
in admin.py
class ChildInline(admin.TabularInline):
model = Child
extra = 1
class ParentsAdmin(admin.ModelAdmin):
inlines = [ChildInline]
admin.site.register(Parent,ParentsAdmin)
Now, in django admin if I save a parent object, it will output on the console.
Parent save
Child save
I need this to happen in revese order:
Child save
Parent save
The following will save the children first:
class ParentAdmin(admin.ModelAdmin):
inlines = [ChildInline]
def save_model(self, request, obj, form, change):
pass # don't actually save the parent instance
def save_formset(self, request, form, formset, change):
formset.save() # this will save the children
form.instance.save() # form.instance is the parent
I was having issues with the answers in this post, so I figured out a more concise answer. I was having an issue because using django-fsm, the other answers here would try to save the model multiple times (once for every formset) rather than once at the end.
def save_model(self, request, obj, form, change):
if not obj.pk: # call super method if object has no primary key
super(YourAdmin, self).save_model(request, obj, form, change)
else:
pass # don't actually save the parent instance
def save_related(self, request, form, formsets, change):
form.save_m2m()
for formset in formsets:
self.save_formset(request, form, formset, change=change)
super(YourAdmin, self).save_model(request, form.instance, form, change)
This essential just flips the order of save_model and save_related as called in Django ModelAdmin source
ccrisan's answer brought me on the right track, but I think there is a flaw regarding save behavior of instances that do not yet exist in the database. In this case it's not possible to save the related objects first, because there is no foreign key that they can point to. For me the following extension did the trick:
class ParentAdmin(admin.ModelAdmin):
inlines = [ChildInline]
def save_model(self, request, obj, form, change):
if not obj.pk: # call super method if object has no primary key
super(ParentAdmin, self).save_model(request, obj, form, change)
else:
pass # don't actually save the parent instance
def save_formset(self, request, form, formset, change):
formset.save() # this will save the children
form.instance.save() # form.instance is the parent
Depending on what you exactly want to do in your signals, can you just change the post_save to pre_save for the Child model ?

Allowing further overriding of save_formset on a ModelAdmin

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(...)

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! ^^