arpa
arpa

Reputation: 328

One form for two models in django

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: page before submit

But when I press the submit button it clean up Order related fields and mark them as blank enter image description here

Upvotes: 0

Views: 1030

Answers (1)

Abdul Aziz Barkat
Abdul Aziz Barkat

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

Related Questions