I have two forms in my CreateView:
The second form can have multiple entries to be saved into their own MaterialRequisitionItems object.
My goal is to save multiple item objects under one MaterialRequisition object.
Here is a screenshot of my template:
I used inlineformset_factory
to display both forms in the same view, but I am not sure how to go about creating another formset for the second form that creates multiple item objects. I imagine I would need formset_factory
for the saving of multiple item objects.
forms.py
RequisitionInlineFormSet = forms.inlineformset_factory(
MaterialRequisition,
MaterialRequisitionItems,
form = RequisitionItemsModelForm,
extra=1,
can_delete=False,
can_order=False,
)
If someone could point me to a general direction, it would be greatly appreciated. :)
Other code for reference
views.py
class RequisitionAddView(LoginRequiredMixin, generic.CreateView):
template_name = "requisition/requisition_add.html"
form_class = RequisitionModelForm
model = MaterialRequisition
def form_valid(self, form):
print(form.data)
ctx = self.get_context_data()
inlines = ctx['inlines']
if inlines.is_valid() and form.is_valid():
req = form.save(commit = False)
req.site = self.request.user.site
req.save()
for form in inlines:
form.save()
return super(RequisitionAddView, self).form_valid(form)
def get_context_data(self, **kwargs):
ctx=super(RequisitionAddView,self).get_context_data(**kwargs)
ctx['item_list'] = ReqItemModelForm()
if self.request.POST:
ctx['form']=RequisitionModelForm(self.request.POST)
ctx['inlines']=RequisitionInlineFormSet(self.request.POST)
else:
ctx['form']=RequisitionModelForm()
ctx['inlines']=RequisitionInlineFormSet()
return ctx
forms.py
class RequisitionModelForm(forms.ModelForm):
class Meta:
model = MaterialRequisition
fields = (
'reqDescription',
'reqDateNeeded',
)
class RequisitionItemsModelForm(forms.ModelForm):
class Meta:
model = MaterialRequisitionItems
fields = (
'requisition',
'item',
'itemQuantity',
)
requisition_add.html
<form
id="reqForm"
class="form-inline"
method="post"
action=""
{% csrf_token %}
{{ form.reqDescription|as_crispy_field }}
{{ form.reqDateNeeded|as_crispy_field }}
{{ inlines.management_form }}
<div class="flex mb-5">
<a
class="flex-grow text-indigo-500 border-b-2 border-indigo-500 py-2 text-lg"
>Items Requested</a
>
</div>
<div id="form_set">
{% for form in inlines.forms %}
<div class="flex flex-row">
<div class='w-full grid grid-cols-2 gap-5 no_error'>
{{ form|crispy }}
</div>
<input class="delete cursor-pointer ml-5 my-auto text-center text-gray-900 cursor-pointer text-sm font-medium h-full [TEST] text-white bg-rose-500 hover:bg-rose-600 ease-in duration-100 border-0 py-1 px-3 focus:outline-none rounded-full" value="Delete" type="button" formnovalidate></input>
</div>
{% endfor %}
</div>
<input class="flex mt-3 mx-auto text-center text-gray-900 cursor-pointer text-sm font-medium h-full [TEST] text-white bg-slate-500 hover:bg-slate-600 ease-in duration-100 border-0 py-2 px-5 focus:outline-none rounded-full" type="button" value="Add an Item +" id="add_more">
<div id="empty_form" style="display:none">
<div class="flex flex-row">
<div class='w-full grid grid-cols-2 gap-5 no_error'>
{{ inlines.empty_form|crispy }}
</div>
<input class="delete cursor-pointer ml-5 my-auto text-center text-gray-900 cursor-pointer text-sm font-medium h-full [TEST] text-white bg-rose-500 hover:bg-rose-600 ease-in duration-100 border-0 py-1 px-3 focus:outline-none rounded-full" value="Delete" type="button" formnovalidate></input>
</div>
</div>
<div class="flex justify-end mt-6 ml-auto">
<div id="buttons" class="flex">
<input
type="submit"
value="Submit"
class="flex ml-3 text-white bg-indigo-500 cursor-pointer border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded ease-in duration-100"
/>
<a href="{% url 'requisition:list-requisition' %}" class="flex ml-3 text-black bg-gray-200 border-0 py-2 px-6 focus:outline-none hover:bg-gray-300 rounded ease-in duration-100">
Cancel
</a>
</div>
</div>
</form>
</div>
</div>
<script>
$('#add_more').click(function() {
var form_idx = $('#id_form-TOTAL_FORMS').val();
$('#form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
});
$(document).on("click", ".delete", function() {
$(this).parent().remove();
});
</script>
JS for adding/removing fields
$('#add_more').click(function(ev) {
// var form_idx = $('#id_form-TOTAL_FORMS').val();
// $('#form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
// $('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
ev.preventDefault();
var count = $('#form_set').children().length;
var tmplMarkup = $('#empty_form').html();
var compiledTmpl = tmplMarkup.replace(/__prefix__/g, count);
$('div#form_set').append(compiledTmpl);
$('#id_materialrequisitionitems_set-TOTAL_FORMS').attr('value', count+1);
});
$(document).on("click", ".delete", function() {
var delcount = $('#form_set').children().length;
$('#id_materialrequisitionitems_set-TOTAL_FORMS').attr('value', delcount-1);
$(this).parent().remove();
});
What I normally do in this cases is to embed the inline forms in the main form so when the main form saves the inline forms are saved as well.
Problem:
To point to the problem of your code, it seems that you're not setting the instance when saving the inline formset. Also, you're saving the forms in the formset one by one. This is not necessary as the formset class has save method which does it for you. Finally, since you're rendering the inline forms with crispy-forms, they automatically add a form tag to your rendered forms. Since the <form>
tag in HTML cannot have children forms, this is probably the actual cause why your forms are not saving. You can avoid this by configuring the crispy forms helper.
Let me present you with two solutions and you let me know if they work.
Solution #1 - patching your existing code:
def form_valid(self, form):
print(form.data)
ctx = self.get_context_data()
inlines = ctx['inlines']
if inlines.is_valid() and form.is_valid():
# Better to do this like this
# See https://www.django-antipatterns.com/antipattern/using-commit-false-when-altering-the-instance-in-a-modelform.html
form.instance.site = self.request.user.site
req = form.save()
# Setting the instance so the formset knows how to set the FK
# and saving the formset in one line
inlines.instance = req
inlines.save()
return super(RequisitionAddView, self).form_valid(form)
Configure the helper:
class RequisitionItemsModelForm(forms.ModelForm):
class Meta:
model = MaterialRequisitionItems
fields = (
'requisition',
'item',
'itemQuantity',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper() # imported from crispy_forms.helper
self.form_tag = False # solves the form tag problem
To be safe, this should be probably done in the other model form as well.
Solution #2 - embedding the inline formset in your RequisitionModelForm
:
class RequisitionModelForm(forms.ModelForm):
class Meta:
model = MaterialRequisition
fields = (
'reqDescription',
'reqDateNeeded',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.requisition_formset = RequisitionInlineFormSet(
data=kwargs.get('data'), instance=self.instance
)
self.helper = FormHelper()
self.form_tag = False
# Now you need to override save to save the inline formset, too
def save(self, **kwargs):
# If any operations fail, we rollback
with transaction.atomic():
# Saving the MaterialRequisition first
saved_req = super().save(**kwargs)
# Saving the inline formsets
self.requisition_formset.instance = saved_req
self.requisition_formset.save()
return saved_req
# Also needs to be overridden in case any clean method are implemented
def clean(self):
self.requisition_formset.clean()
super().clean()
return self.cleaned_data
# is_valid sets the cleaned_data attribute so we need to override that too
def is_valid(self):
is_valid = True
is_valid &= self.requisition_formset.is_valid()
is_valid &= super().is_valid()
return is_valid
# In case you're using the form for updating, you need to do this too
# because nothing will be saved if you only update field in the inner
# formset
def has_changed(self):
has_changed = False
has_changed |= self.requisition_formset.has_changed()
has_changed |= super().has_changed()
return has_changed
So, this solution is a bit longer but keeps the form logic in the form code so I consider it cleaner. Now in the view code you can simply do this:
class RequisitionAddView(LoginRequiredMixin, generic.CreateView):
template_name = "requisition/requisition_add.html"
form_class = RequisitionModelForm
model = MaterialRequisition
# No need for form_valid or get_context_data!
PS. another clean code tip: don't use if self.request.POST
for checking if the request is a POST request. A better practice is to use if self.request.method == 'POST'
. See this for more details.