Reputation: 328
Django==3.1.7
django-crispy-forms==1.11.2
I 've 2 models: Order and OrderList
Order is a header and OrderList is a tabular section of the related Order
class Order(models.Model):
print_number = models.PositiveIntegerField(
verbose_name=_("Number"),
default=get_todays_free_print_number,
)
# ... some other fields
class OrderList(models.Model):
order = models.ForeignKey(
Order,
blank=False,
null=False,
on_delete=models.CASCADE
)
item = models.ForeignKey(
Item,
verbose_name=_("item"),
blank=True,
null=True,
on_delete=models.CASCADE
)
# ... some other OrderList fields
The question is how to create a form containing both models and provide the ability to add an OrderList positions within an Order into the form and save them both.
What I did:
forms.py - I used inline formset factory for the OrderList
from django.forms import ModelForm
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from .models import Order, OrderList
class OrderForm(ModelForm):
class Meta:
model = Order
fields = [
'__all__',
]
class OrderListForm(ModelForm):
class Meta:
model = OrderList
fields = [
'__all__',
]
class OrderListFormSetHelper(FormHelper):
"""Use class to display the formset as a table"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.template = 'bootstrap4/table_inline_formset.html'
# I am not sure we should add a button here
####################################################
self.add_input(Submit('submit', 'Submit',
css_class='btn btn-primary offset4'))
views.py
@login_required
def orders(request):
template = f'{APP_NAME}/index.html'
list_helper = OrderListFormSetHelper()
list_formset = inlineformset_factory(Order,
OrderList,
OrderListForm,)
if request.method == 'POST':
form = OrderForm(request.POST, prefix="header")
if form.is_valid() and list_formset.is_valid():
order = form.save()
order_list = list_formset.save(commit=False)
order_list.order = order
order_list.save()
return HttpResponseRedirect(reverse('order_created'))
else: # all other methods means we should create a blank form
form = OrderForm()
return render(request, template, {'form': form,
'list_form': list_formset,
'list_helper': list_helper})
index.html
<form method="post">
{% csrf_token %}
{% crispy form %}
{% crispy list_form list_helper %}
<!-- the button below doesn't make sense because it does nothing.
the self.add_input in forms.py already adds a submit button.
-->
<button type="submit" class="btn btn-primary">
{% translate "Send an order" %}
</button>
</form>
The resulting html renders the page like that:
But when I press the submit button
it clean up Order related fields and mark them as blank
Upvotes: 0
Views: 1030
Reputation: 21807
You use the crispy
template tag to render your forms. It uses the FormHelper
class to help render your forms, which by default has the attribute form_tag
set to True
which makes it render a form
tag for you. Meaning you are nesting form tags which does not work and is not possible with the HTML5 standard. You need to set this attribute to False
to prevent this:
class OrderForm(ModelForm):
class Meta:
model = Order
fields = [
'__all__',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper(self) # Explicitly set helper to prevent automatic creation
self.helper.form_tag = False # Don't render form tag
self.helper.disable_csrf = True # Don't render CSRF token
Next in the helper you make in the view you also have to set these attributes. Furthermore what you call as list_formset
is not an instance
of a formset but a class, hence you actually need to instantiate the formset class and use it:
@login_required
def orders(request):
template = f'{APP_NAME}/index.html'
list_helper = OrderListFormSetHelper()
list_helper.form_tag = False # Don't render form tag
list_helper.disable_csrf = True # Don't render CSRF token
OrderListFormSet = inlineformset_factory(Order,
OrderList,
OrderListForm,)
if request.method == 'POST':
form = OrderForm(request.POST, prefix="header")
list_formset = OrderListFormSet(request.POST, instance=form.instance) # Instantiate formset
if form.is_valid() and list_formset.is_valid():
order = form.save()
order_list = list_formset.save()
# Remove below two line, have already instantiated formset with `form.instance` and called save without `commit=False`
# order_list.order = order
# order_list.save()
return HttpResponseRedirect(reverse('order_created'))
else: # all other methods means we should create a blank form
form = OrderForm()
list_formset = OrderListFormSet(instance=form.instance) # Instantiate formset
return render(request, template, {'form': form,
'list_form': list_formset,
'list_helper': list_helper})
Upvotes: 1