javascript - Django formsets no data on post


Keywords:javascript 


Question: 

I have a form that uses multiple formsets. The formset forms are added dynamically via JS. I have been looking at a few different places to help myself along.

Add a dynamic form to a django formset using javascript in a right way

And

A nice post by Kevin Dias - Django class-based views with multiple inline formsets

The problem I am having is that when I post my data the outer form has data, but non of my formsets actually have any data in the cleaned_data dictionary when I start looping through them. Any thoughts on what I may be missing? The second formset is added with a very similar JS method.

Forms

class ShippingForm(Form):
    is_partial = BooleanField(label='Partial?')


class ShippingActualProduct(Form):

    box_no = CharField(label='Box Number', max_length=3)
    upc = CharField(
        widget=forms.TextInput(attrs={'class':'upcAjax'}),
    )
    serial_no = CharField(
        label='Serial Number',
        widget=forms.TextInput(attrs={'class':'serial'}),
    )
    sku = CharField(
        widget=forms.TextInput(attrs={'class':'skuAjax'}),
    )
    description=CharField(label='Description')
    on_hand=CharField(label='On Hand')

    def __init__(self, *args, **kwargs):
        super(ShippingActualProduct,self).__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_show_labels = True
        self.helper.form_class = 'form-inline'


class ShippingNonInventoryProduct(Form):

    non_box_no = CharField(label='Box Number', max_length=3)
    quantity = IntegerField()
    description = CharField()
    serial_no = CharField()

    def __init__(self, *args, **kwargs):
        super(ShippingNonInventoryProduct,self).__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_show_labels = True
        self.helper.form_class = 'form-inline'



ActualProductFormSet = formset_factory(ShippingActualProduct, extra=1,       can_delete=True)
NonInventoryProductFormSet = formset_factory(ShippingNonInventoryProduct, extra=1, can_delete=True)

Views

    class ShippingCreate(FormView):
    template_name = 'jinja2/Shipping/shipping_create.html'
    form_class = ShippingForm
    success_url = reverse_lazy('shipping_create')


    def get_context_data(self, **kwargs):

        context = super(ShippingCreate, self).get_context_data(**kwargs)
        input_invoice_no = self.request.GET['invoice_no']
        try:
            self.object = Invoice.objects.get(invoice_no = input_invoice_no)
            context['invoice'] = self.object
        except Invoice.DoesNotExist:
            messages.error(self.request, 'We were unable to find invoice number %s, please try again' % input_invoice_no)

        try:
            context['voucher'] = Voucher.objects.get(voucher_no = self.object.voucher_no)
        except Voucher.DoesNotExist:
            messages.error(self.request, 'We were unable to find an installation voucher for claim number %s' % self.object.voucher_no)

        context['actual_items_forms'] = ActualProductFormSet(prefix='actual')
        context['non_inventory_items_forms'] = NonInventoryProductFormSet(prefix='non')
        context['form'] = ShippingForm()
        return context

    def get(self, request, *args, **kwargs):

        self.object = None
        context = self.get_context_data()
        return render(request, self.template_name, context)

    def post(self, request, *args, **kwargs):
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        actual_product = ActualProductFormSet(self.request.POST, prefix='actual')
        non_inv_product = NonInventoryProductFormSet(self.request.POST, prefix='non')
        if actual_product.is_valid():
            for product in actual_product:
                data = product.cleaned_data
                sku = data.get('sku')
        context={}
        return render(request, self.template_name, context)

Template

    {% extends "base.html" %}
{% load crispy_forms_tags %}
{%  load static from staticfiles %}
{%  load socialaccount %}
{%  load sitetree %}
{% block headcss %}
    {{ block.super }}
    <link rel="stylesheet" href=""css/shipping.css" %}">
{% endblock headcss %}
{%  block headjs %}
    {{ block.super }}
    <script src=""js/shipping.js" %}"></script>

{%  endblock headjs %}
{% block content %}
    {% block messages %}
        {{ block.super }}
    {% endblock messages %}

    <div class="container-fluid">
        <form action="." method="post">
            <div>
                <h3>Item information:</h3>
                        {%  csrf_token %}
                        {{ actual_items_forms.management_form }}
                        <div id="actual-items-form-container">
                        </div>
                        <a href="" id="actual-item-btn" class="btn btn-info fa fa-plus-square add-item"> Add Item</a>
            </div>
            <div>
                <h3>Non Inventory Items Shipped:</h3>
                    {{ non_inventory_items_forms.management_form }}
                    <div id="non-inv-items-form-container">
                    </div>
                    <a href="" id="add-non-inv-item-btn" class="btn btn-info fa fa-plus-square add-non-item"> Add Item</a>
            </div>
            {{  form.as_p }}
            <input type="submit" value="Complete" class="submit btn btn-success" />
        </form>

    </div>

    {%  include "jinja2/hub/loading_modal.html" %}
{% endblock content %}
</html>

The JavaScript

    function getNewActualItemForm() {
        // unbind this ajax call from the overlay displayed when looking data up.
        $(document).unbind(".items");
        var count = $('actual-items-form-container').children().length;
        $.get("/forms/actualitemform",function(data, status){
            var form = data.replace(/__prefix__/g, count);
            // Get the html contents of the form, all together to iterate over.
            var htmlForm = $('<form>').html(form).contents();
            // Just grab the children of that form
            var rows = htmlForm.children();
            // loop through the inputs locating the DELETE input and label.
            $(rows).each( function(index, value) {
                var row = $(this);
                var del = $(row).find('input:checkbox[id $= "-DELETE"]');
                // Only move forward if we have found the DELETE input
                if (del.length){
                    //Write the form ot the Dom so the search for the label will succeed.
                    $('divactual-items-form-container').append(form);
                    var label ='label[for="id_form-' + count + '-DELETE"]';
                    $(label).hide();
                }
            });

            // update form count
            $('id_actual-TOTAL_FORMS').attr('value', count+1);

            // some animate to scroll to view our new form
            $('html, body').animate({
                scrollTop: $('actual-item-btn').position().top-200
            }, 800);

            // get the max box number.
            var maxBoxNo = getBoxNo();

            // Set focus to the next UPC
            var boxID = 'id_form-var-box_no';
            var upcID = 'id_form-var-upc';
            var nextBox = boxID.replace('var',count);
            var nextUpc = upcID.replace('var',count);
            // set the box number for the new line.
            $(nextBox).val(maxBoxNo);
            $(nextUpc).focus();


        });
        return count;
    }

1 Answer: 

There were two issues that were causing the troubles here.

  1. I had the crispy forms helper rendering the form tags for each formset. This is a bad idea. Setting form_tag = False fixed that.
  2. I had forgotten to set the prefix argument on my formsets in the views I created to grab the next form via JavaScript.

Once both of these were implemented the data was now available from the forms on submission.