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!
Related
One of the fields in my inline needs to be calculated. I overrode the BaseInLineFormSet clean method and can do the calculation there and apparently set the field's value there but it doesn't get saved to the DB, and consequently is also not displayed. The field defaults to zero, btw, in case that matters. Here's a hard-coded version:
class EmployeeAssignmentInLineFormSet(BaseInlineFormSet):
def clean(self):
super(EmployeeAssignmentInLineFormSet, self).clean()
self.cleaned_data[0]['cost'] = 5000
I also tried overriding save_formset in the ModelAdmin - same result:
def save_formset(self, request, form, formset, change):
formset.cleaned_data[0]['cost'] = 5000
formset.save()
When I set the value just in clean I can see it's been set when it gets to save_formset, but it still ends up being zero in the DB. Am I in the wrong place or what?
I don't think a ModelFormSet has a cleaned_data attribute like that
https://github.com/django/django/blob/master/django/forms/models.py#L623
I'm not sure why you're not getting an exception from your code above in that case, but it looks to me like you should try instead:
def clean(self):
super(EmployeeAssignmentInLineFormSet, self).clean()
self.forms[0].cleaned_data['cost'] = 5000
Doesn't seem to be much interest in this but for the sake of completeness, this is what I ended up doing. All my gazillion attempts at trying to save the field when the record is saved failed. Simply putting something into cleaned_data doesn't work. I tried overriding clean, save_model, save_related, save_formset - no joy. So now I'm in the Inline options:
readonly_fields = ('get_cost', )
fields = ('project', 'start_date', 'end_date', 'effort', 'role', 'get_cost')
def get_cost(self, obj):
if obj.effort and obj.cost == 0.0:
obj.cost = obj.employee._calculate_cost(obj.effort)
obj.save()
return obj.cost
get_cost.short_description = 'Cost'
Need the conditional of course to avoid saving the cost every time the inline formset is loaded. Seems odd to save the field after the rest of the record has been saved, but it works.
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(...)
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! ^^
I have this models:
class Balanta(models.Model):
data = models.DateField()
class Conturi(models.Model):
cont=models.PositiveIntegerField()
cont_debit=models.DecimalField(default=0, max_digits=30, decimal_places=2)
cont_credit=models.DecimalField(default=0, max_digits=30, decimal_places=2)
balanta = models.ForeignKey(Balanta)
And i have formsets working ok in a template and this view:
def balanta_introducere(request):
balanta=Balanta()
ConturiInlineFormSet=inlineformset_factory(Balanta, Conturi, extra=3)
if request.method=='POST':
balanta_form=BalantaForm(request.POST, instance=balanta)
if balanta_form.is_valid():
balanta, created=Balanta.objects.get_or_create(**balanta_form.cleaned_data)
#return HttpResponseRedirect('/sitfin/balantaok')
formset=ConturiInlineFormSet(request.POST, request.FILES, instance=balanta)
if formset.is_valid():
for form in formset:
data={
'cont':form.cleaned_data.get('cont'),
'cont_debit':form.cleaned_data.get('cont_debit'),
'cont_credit':form.cleaned_data.get('cont_credit'),
'balanta':form.cleaned_data.get('balanta'),
}
try:
c=Conturi.objects.get(cont=data['cont'])
except Conturi.DoesNotExist:
cont_complete,created=Conturi.objects.get_or_create(**data)
else:
cont_complete,created=Conturi.objects.get_or_create(cont=data['cont'],cont_debit=data['cont_debit'],cont_credit=data['cont_credit'],balanta=data['balanta'])
else:
balanta_form=BalantaForm()
formset=ConturiInlineFormSet(instance=balanta)
return render_to_response('sitfin/balanta_introducere.html',{'balanta_form':balanta_form,'formset':formset}, context_instance=RequestContext(request))
If i hit the first submit, all the data goes in the database (foreignkey and all)
After the second submit with the same data, the form doesn't do anything and this is ok.
If i change a value in the form (in a "cont_credit" of a "cont" for example) and hit submit again, i get another Conturi object with only the modified "cont" with the updated "cont_credit" value and this is not good!
What is the approach for updating only some fields of an existing Conturi model with the help of a form?
Something like:
If the cont it is not in the database,
create a Conturi objects with the data in the form,
If the "cont" is already in the database,
Update the cont_credit and cont_debit data with the new values entered in the form
Thank you very much.
get_or_create is trying a get with all the parameters you pass it, so if anything changes on the form, it won't find the existing object, and instead will create a new one.
If your forms are ModelForms, then you can just use form.save() to save the instance bound to the form, and formset.save() to save all the instances bound to the formset.
EDIT:
I now noticed another thing: you are using
balanta=Balanta()
and then
balanta_form=BalantaForm(request.POST, instance=balanta)
so you are forcing the form to use a new instance. try getting the specific Balanta you're editing, and pass that as the instance.
Going nuts over here...
From within the shell, I can do:
product.tags.add("a_new_tag")
The tag is added to the db, and the tag association with the product works correctly. (i.e., when I do Product.objects.filter(tags__name__in=["a_new_tag"] the appropriate product spits out)
What I need to do is add some tags in the admin when the form is processed.
Here is my form code (read the comments in lines 4 and 5):
class ProductForm(ModelForm):
def save(self, commit=True):
product = super(ProductForm, self).save(commit=False)
product.type="New Type to Confirm Info is being Saved Correctly" //this is saved to the product.
product.tags.add('a_new_tag_1') //the tag is saved to the taggit db, but the association with the product isn't kept.
product.save()
self.save_m2m()
return m
I tried to do the saving in the admin class instead, but this doesn't work either:
class ProductAdmin(admin.ModelAdmin):
form = ProductForm
def save_model(self, request, obj, form, change):
obj.type="new_type" //this works
obj.tags.add("a_new_tag_2") //tag association not saved
obj.save()
form.save_m2m()
What am I doing wrong? Thanks in advance!
So it turns out that form.save_m2m() was the culprit. If I took it out of my own code, and commented it out in django.contrib.admin.options.py (line 983), the associations as well as the tag were saved.
Obviously it's not a good idea to change django's code, so I ended up overriding change_view() in my ProductAdmin (as well as add_view()). I added the tags after calling super(), so form.save_m2m() wouldn't overwrite my tag associations.
This is strange because it goes directly against django-taggit's documentation which emphasizes how important it is to call form.save_m2m() : http://django-taggit.readthedocs.org/en/latest/forms.html
Well I dunno what's up, I'll probably go on the taggit google groups and notify 'em. In any case thanks David for your help, if nothing less pdb is AWESOME and I did not know about it before :)
Which tagging system are you using? Possibly you need to use product.tags.save()?