Django multiple formsets in a single view only saves last value entered

1.9k views Asked by At

I am trying to use multiple formsets in a single view, but I am not able to implement this functionality properly. For example: When I try to add say 3 values in each formset,i.e. 3 entries for 1st formset and 3 entries for 2nd formset only last enties are stored in the database and first 2 values are discarded and not saved.

Please find the code written so far as below:

1) models.py

from __future__ import unicode_literals
from django.db import models
from bokeh.themes import default
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import AbstractUser
from phonenumber_field.modelfields import PhoneNumberField

# Create your models here.
class FeeForService(models.Model):
CHOICES = (
    ('Yes', 'Yes'),
    ('No', 'No'),
)

REQUEST_STATUS = (
    ('Pending', 'Pending'),
    ('Approved', 'Approved'),
    ('Denied', 'Denied'),
)

requestor_name = models.CharField(max_length=240, blank=False, null=False)
requestor_RU_or_PL = models.CharField(max_length=240, blank=False, null=False)
vendor_procurement_system = models.CharField(max_length=3, choices=CHOICES, blank=False, null=False)
vendor_name = models.CharField(max_length=254, blank=False, null=False)
vendor_address = models.CharField(max_length=480, blank=False, null=False)
vendor_email = models.EmailField(max_length=254, blank=False, null=False)
phone_number = PhoneNumberField(blank=True)
proposed_start_date =  models.DateTimeField(blank=False, null=False)
proposed_end_date =  models.DateTimeField(blank=False, null=False)
brief_proposal = models.CharField(max_length=480, blank=False, null=False)

status = models.CharField(max_length=20, choices=REQUEST_STATUS, default='Pending')
user = models.ForeignKey(User, on_delete=models.CASCADE)

def __str__(self):
    return str(self.id)


class DeliverablesFeeForService(models.Model):
    milestone_deliverable = models.CharField('MileStone/Deliverable', max_length=480, blank=False, null=False)
    poa_institution = models.CharField('POA/Institution', max_length=480, blank=False, null=False)
    duration_to_complete = models.CharField(max_length=240, blank=False, null=False)
    feeForService = models.ForeignKey(FeeForService, on_delete=models.CASCADE)

    def __str__(self):
        return str(self.id)


class PaymentScheduleFeeForService(models.Model):
    milestone_deliverable1 = models.CharField('MileStone/Deliverable', max_length=480, blank=False, null=False)
    cost = models.DecimalField(max_digits=14, decimal_places=2, blank=False, null=False, default=0)
    estimated_payment_date = models.DateTimeField(blank=False, null=False)
    feeForService = models.ForeignKey(FeeForService, on_delete=models.CASCADE)

    def __str__(self):
        return str(self.id)

2) forms.py

from django import forms
from .models import FeeForService, DeliverablesFeeForService, PaymentScheduleFeeForService
from datetime import datetime
from file_resubmit.admin import AdminResubmitImageWidget, AdminResubmitFileWidget
from django.forms.formsets import BaseFormSet

class DeliverablesFeeForServiceForm(forms.ModelForm):
    milestone_deliverable = forms.CharField(
                                    widget=forms.TextInput(attrs={
                                        'class': 'form-control',
                                        'placeholder': 'Enter milestone/deliverables here',
                                    }),
                                    required=True)
    poa_institution = forms.CharField(
                                widget=forms.TextInput(attrs={
                                    'class': 'form-control',
                                    'placeholder': 'Enter POA/Institution here',
                                }),
                                required=True)
    duration_to_complete = forms.CharField(
                                    widget=forms.TextInput(attrs={
                                        'class': 'form-control',
                                        'placeholder': 'Enter duration to complete here',
                                    }),
                                    required=True)
    class Meta:
    model = DeliverablesFeeForService
    exclude = ('feeForService', )


class PaymentScheduleFeeForServiceForm(forms.ModelForm):
    milestone_deliverable1 = forms.CharField(
                                    widget=forms.TextInput(attrs={
                                        'class': 'form-control',
                                        'placeholder': 'Enter milestone/deliverables here',
                                    }),
                                    required=True)
    cost = forms.CharField(
                    widget=forms.TextInput(attrs={
                        'class': 'form-control',
                        'placeholder': 'Enter cost here',
                    }),
                    required=True)
    estimated_payment_date = forms.CharField(
                                    widget=forms.TextInput(attrs={
                                        'class': 'form-control',
                                        'placeholder': 'Enter estimated payment date here',
                                    }),
                                    required=True)
    class Meta:
        model = PaymentScheduleFeeForService
        exclude = ('feeForService', )



class DeliverablesFeeForServiceFormset(BaseFormSet):
    def clean(self):
        """
        Adds validation to check that no two deliverables have the same milestone or institution
        and that all deliverables have both an milestone and institution.
        """
        if any(self.errors):
            return

        milestone_deliverables = []
        poa_institutions = []
        durations_to_complete = []
        duplicates = False

        for form in self.forms:
            if form.cleaned_data:
                milestone_deliverable = form.cleaned_data['milestone_deliverable']
                poa_institution = form.cleaned_data['poa_institution']
                duration_to_complete = form.cleaned_data['duration_to_complete']

                # Check that no two deliverables have the same milestone or institution
                if milestone_deliverable and poa_institution:
                    if milestone_deliverable in milestone_deliverables:
                        duplicates = True
                    milestone_deliverables.append(milestone_deliverable)

                    if poa_institution in poa_institutions:
                        duplicates = True
                    poa_institutions.append(poa_institution)

                if duplicates:
                    raise forms.ValidationError(
                        'Deliverables must have unique milestones and institutions.',
                        code='duplicate_deliverables'
                    )

                # Check that all deliverables have both an milestone and institution
                if milestone_deliverable and not poa_institution:
                    raise forms.ValidationError(
                        'All deliverables must have an institution.',
                        code='missing_institution'
                    )
                elif poa_institution and not milestone_deliverable:
                    raise forms.ValidationError(
                        'All deliverables must have a milestone.',
                        code='missing_milestone'
                    )


class PaymentScheduleFeeForServiceFormset(BaseFormSet):
    def clean(self):
        """
        Adds validation to check that no two payment schedules have the same milestone
        and that all payment schedules have both a milestone and cost.
        """
        if any(self.errors):
            return

        milestone_deliverables1 = []
        costs = []
        estimated_payment_dates = []
        duplicates = False

        for form in self.forms:
            if form.cleaned_data:
                milestone_deliverable1 = form.cleaned_data['milestone_deliverable1']
                cost = form.cleaned_data['cost']
                estimated_payment_date = form.cleaned_data['estimated_payment_date']

                # Check that no two deliverables have the same milestone
                if milestone_deliverable1:
                    if milestone_deliverable1 in milestone_deliverables1:
                        duplicates = True
                    milestone_deliverables1.append(milestone_deliverable1)

                if duplicates:
                    raise forms.ValidationError(
                        'Payment schedule must have unique milestones',
                        code='duplicate_schedules'
                    )

                # Check that all payment schedules have both a milestone and cost
                if milestone_deliverable1 and not cost:
                    raise forms.ValidationError(
                        'All payemnt schedules must have a cost.',
                        code='missing_cost'
                    )
                elif cost and not milestone_deliverable1:
                    raise forms.ValidationError(
                        'All payemnt schedules must have a milestone.',
                        code='missing_milestone'

3) views.py

def create_FeeForService(request):
    currentUser = User.objects.get(id=request.user.id)
    # Create the formset, specifying the form and formset we want to use.
    DeliverablesFormSet = formset_factory(DeliverablesFeeForServiceForm, formset=DeliverablesFeeForServiceFormset)
    PaymentScheduleFormSet = formset_factory(PaymentScheduleFeeForServiceForm, formset=PaymentScheduleFeeForServiceFormset)
    # This is used as initial data.
    deliverable_data = []
    paymentSchedule_data = []
    if request.method == 'POST': #If the form has been submitted...
        feeForService_form = FeeForServiceForm(request.POST) # A form bound to the POST data
        deliverables_formset = DeliverablesFormSet(request.POST, prefix='deliverables')
        paymentSchedule_formset = PaymentScheduleFormSet(request.POST, prefix='paymentSchedule')

        if feeForService_form.is_valid() and deliverables_formset.is_valid() and paymentSchedule_formset.is_valid(): # all validation rules pass
            # Save Fee For Service info
            feeForService = feeForService_form.save(commit=False)
            feeForService.user = request.user
            feeForService.save()

            # Now save the data for each form in the formset
            new_deliverables = []

            for deliverable_form in deliverables_formset:
                print("Hi i am deliverable for loop")
                milestone_deliverable = deliverable_form.cleaned_data.get('milestone_deliverable')
                poa_institution = deliverable_form.cleaned_data.get('poa_institution')
                duration_to_complete = deliverable_form.cleaned_data.get('duration_to_complete')

                if milestone_deliverable and poa_institution and duration_to_complete:
                    new_deliverables.append(DeliverablesFeeForService(feeForService=feeForService, milestone_deliverable=milestone_deliverable, poa_institution=poa_institution, duration_to_complete=duration_to_complete))


            new_paymentSchedules = []

            for paymentSchedule_form in paymentSchedule_formset:
                milestone_deliverable1 = paymentSchedule_form.cleaned_data.get('milestone_deliverable1')
                cost = paymentSchedule_form.cleaned_data.get('cost')
                estimated_payment_date = paymentSchedule_form.cleaned_data.get('estimated_payment_date')

                if milestone_deliverable1 and cost and estimated_payment_date:
                    print("2nd Details are:", milestone_deliverable1, cost, estimated_payment_date)
                    new_paymentSchedules.append(PaymentScheduleFeeForService(feeForService=feeForService, milestone_deliverable1=milestone_deliverable1, cost=cost, estimated_payment_date=estimated_payment_date)) 

            try:
                with transaction.atomic():
                    #Add all the new values
                    DeliverablesFeeForService.objects.bulk_create(new_deliverables)
                    PaymentScheduleFeeForService.objects.bulk_create(new_paymentSchedules)

            except IntegrityError: #If the transaction failed
                messages.error(request, 'There was an error saving your FeeForService.')
                return redirect(reverse('create_FeeForService'))

            feeForService_form = FeeForServiceForm()
            deliverables_formset = DeliverablesFormSet(initial=deliverable_data, prefix='deliverables')
            paymentSchedule_formset = PaymentScheduleFormSet(initial=paymentSchedule_data, prefix='paymentSchedule')
    else:
        feeForService_form = FeeForServiceForm()
        deliverables_formset = DeliverablesFormSet(initial=deliverable_data, prefix='deliverables')
        paymentSchedule_formset = PaymentScheduleFormSet(initial=paymentSchedule_data, prefix='paymentSchedule')

    return render(request, 'createFeeForService.html', {'feeForService_form': feeForService_form, 'deliverables_formset': deliverables_formset, 'paymentSchedule_formset': paymentSchedule_formset})

4) html code

{% extends "header.html" %}
{% load widget_tweaks %}
{% block content %}

<script type="text/javascript">
    $(function() {
        $(".inline.{{ deliverables_formset.prefix }}").formset({
            prefix: "{{ deliverables_formset.prefix }}",
        })
        $(".inline.{{ paymentSchedule_formset.prefix }}").formset({
            prefix: "{{ paymentSchedule_formset.prefix }}",
        })
    })    
</script>


<script type="text/javascript">
function updateElementIndex(el, prefix, ndx) {
    var id_regex = new RegExp('(' + prefix + '-\\d+)');
    var replacement = prefix + '-' + ndx;
    if ($(el).attr("for")) $(el).attr("for", $(el).attr("for").replace(id_regex, replacement));
    if (el.id) el.id = el.id.replace(id_regex, replacement);
    if (el.name) el.name = el.name.replace(id_regex, replacement);
}
function cloneMore(selector, prefix) {
    var newElement = $(selector).clone(true);
    var total = $('#id_' + prefix + '-TOTAL_FORMS').val();
    newElement.find(':input').each(function() {
        var name = $(this).attr('name')
        if(name) {
            name = name.replace('-' + (total-1) + '-', '-' + total + '-');
            var id = 'id_' + name;
            $(this).attr({'name': name, 'id': id}).val('').removeAttr('checked');
        }
    });
    total++;
    $('#id_' + prefix + '-TOTAL_FORMS').val(total);
    $(selector).after(newElement);
    var conditionRow = $('.form-row.deliverables:not(:last)');
    conditionRow.find('.btn.add-form-row')
    .removeClass('btn-success').addClass('btn-danger')
    .removeClass('add-form-row').addClass('remove-form-row')
    .html('-');
    return false;
}

function cloneMore1(selector, prefix) {
    var newElement = $(selector).clone(true);
    var total = $('#id_' + prefix + '-TOTAL_FORMS').val();
    newElement.find(':input').each(function() {
        var name = $(this).attr('name')
        if(name) {
            name = name.replace('-' + (total-1) + '-', '-' + total + '-');
            var id = 'id_' + name;
            $(this).attr({'name': name, 'id': id}).val('').removeAttr('checked');
        }
    });
    total++;
    $('#id_' + prefix + '-TOTAL_FORMS').val(total);
    $(selector).after(newElement);
    var conditionRow = $('.form-row.payments:not(:last)');
    conditionRow.find('.btn.add-form-row1')
    .removeClass('btn-success').addClass('btn-danger')
    .removeClass('add-form-row1').addClass('remove-form-row1')
    .html('-');
    return false;
}


function deleteForm(prefix, btn) {
    var total = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val());
    if (total > 1){
        btn.closest('.form-row').remove();
        var forms = $('.form-row');
        $('#id_' + prefix + '-TOTAL_FORMS').val(forms.length);
        for (var i=0, formCount=forms.length; i<formCount; i++) {
            $(forms.get(i)).find(':input').each(function() {
                updateElementIndex(this, prefix, i);
            });
        }
    }
    return false;
}
$(document).on('click', '.add-form-row', function(e){
    e.preventDefault();
    cloneMore('.form-row.deliverables:last', 'form');
    return false;
});
$(document).on('click', '.remove-form-row', function(e){
    e.preventDefault();
    deleteForm('form', $(this));
    return false;
});
$(document).on('click', '.add-form-row1', function(e){
    e.preventDefault();
    cloneMore1('.form-row.payments:last', 'form');
    return false;
});
$(document).on('click', '.remove-form-row1', function(e){
    e.preventDefault();
    deleteForm('form', $(this));
    return false;
});

</script>

{% include 'messages.html' %}
<div class="container" align="center">
    <h1 class="display-5">Fee For Service</h1>
</div>
<br/>

<form id="form-id" method="post" novalidate>
    {% csrf_token %}

    {% for hidden_field in feeForService_form.hidden_fields %}
        {{ hidden_field }}
    {% endfor %}

    {% if feeForService_form.non_field_errors %}
    <div class="alert alert-danger" role="alert">
        {% for error in feeForService_form.non_field_errors %}
            {{ error }}
        {% endfor %}
    </div>
    {% endif %}

    {% for field in feeForService_form.visible_fields %}
    <div class="form-group">
        <div class="row">
            <div class="col-md-8">{{ field.label_tag }}</div>
            <div class="col-md-4">
                {% if feeForService_form.is_bound %}
                    {% if field.errors %}
                        {% render_field field class="form-control is-invalid" %}
                        {% for error in field.errors %}
                            <div class="invalid-feedback">
                                {{ error }}
                            </div>
                        {% endfor %}
                    {% else %}
                        {% render_field field class="form-control is-valid" %}
                    {% endif %}
                {% else %}
                    {% render_field field class="form-control" %}
                {% endif %}

                {% if field.help_text %}
                    <small class="form-text text-muted">{{ field.help_text }}</small>
                {% endif %}
            </div>
        </div>
    </div>
    {% endfor %}

    <div class="form-group">
        <div class="row"><div class="col-md-12"><p><b>Describe the milestones/deliverables</b></p></div></div>
    </div>

    <div class="form-group">
        <div class="row form-row spacer">
            <div class="col-4">
                <label>Milestone/Deliverable</label>
            </div>
            <div class="col-4">
                <label>POA/Institution</label>
            </div>
            <div class="col-4">
                <label>Duration to complete</label>
            </div>
        </div>
    </div>

    {{ deliverables_formset.management_form }}
    {% for deliverables_form in deliverables_formset %}
    {{ deliverables_form.id }}
    <div class="form-group">
        <div class="row form-row spacer deliverables">
            <div class="col-4">
                <div class="input-group">
                    {{deliverables_form.milestone_deliverable}}
                </div>
            </div>
            <div class="col-4">
                <div class="input-group">
                    {{deliverables_form.poa_institution}}
                </div>
            </div>
            <div class="col-4">
                <div class="input-group">
                    {{deliverables_form.duration_to_complete}}
                    <div class="input-group-append">
                        <button class="btn btn-success add-form-row">+</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
    {% endfor %}

    <div class="form-group">
        <div class="row"><div class="col-md-12"><p><b>Describe the payment schedule</b></p></div></div>
    </div>

    <div class="form-group">
        <div class="row form-row spacer">
            <div class="col-4">
                <label>Milestone/Deliverable</label>
            </div>
            <div class="col-4">
                <label>Cost</label>
            </div>
            <div class="col-4">
                <label>Estimated payment date</label>
            </div>
        </div>
    </div>

    {{ paymentSchedule_formset.management_form }}
    {% for form in paymentSchedule_formset %}
    {{ form.id }}
    <div class="form-group">
        <div class="row form-row spacer payments">
            <div class="col-4">
                <div class="input-group">
                    {{form.milestone_deliverable1}}
                </div>
            </div>
            <div class="col-4">
                <div class="input-group">
                    {{form.cost}}
                </div>
            </div>
            <div class="col-4">
                <div class="input-group">
                    {{form.estimated_payment_date}}
                    <div class="input-group-append">
                        <button class="btn btn-success add-form-row1">+</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
    {% endfor %}

    <div class="row">
        <div class="col-xs-12" style="height: 14px;"></div>
    </div>

    <div class="row">
        <div class="col-md-4"></div>
        <div class="col-md-4" align="center">
            <button type="submit" class="btn btn-primary" onclick="return confirm('Do you want to submit this form?')">Submit</button>
        </div>
        <div class="col-md-4"></div>
    </div>
</form>

{% endblock %}

Please let me know if you require any more information regarding the issue. Any help or advises will be appreciated!

2

There are 2 answers

0
Amey Kelekar On BEST ANSWER

After debugging using the tips from @dirkgroten, I figured out the issue was with the javascript code written by me. When I tried adding a new formset, I was getting NaN value.

Instead, I used django-dynamic-formset [https://github.com/elo80ka/django-dynamic-formset] to implement multiple formset.

Please find below the script code implemented

<script src="{% static 'js/jquery.formset.js' %}"></script>
<script type="text/javascript">
    $(function() {
        $(".inline.{{ deliverables_formset.prefix }}").formset({
            addText: 'Add Deliverables',
            deleteText: 'Remove',
            prefix: "{{ deliverables_formset.prefix }}",
        })
        $(".inline.{{ paymentSchedule_formset.prefix }}").formset({
            addText: 'Add Payment Schedules',
            deleteText: 'Remove',
            prefix: "{{ paymentSchedule_formset.prefix }}",
        })
    })    
</script>

I removed my jQuery code and used django-dynamic-formset.

Regards, Amey Kelekar

0
Ashish Kapil On

As you figured out the issue was with the javascript code. When you tried adding a new formset and was getting NaN value because your javascript code's formset name was not matching original formset name as it was prefixed by deliverables and second one with paymentSchedule. So you need to edit below functions in your javascript code:

$(document).on('click', '.add-form-row', function(e){
    e.preventDefault();
    cloneMore('.form-row.deliverables:last', 'form');
    return false;
 });

$(document).on('click', '.add-form-row1', function(e){
    e.preventDefault();
    cloneMore1('.form-row.payments:last', 'form');
    return false;
});

Replace 'form' in cloneMore and clonemore1 function with deliverables and paymentSchedule:

$(document).on('click', '.add-form-row', function(e){
    e.preventDefault();
    cloneMore('.form-row.deliverables:last', 'deliverables');
    return false;
});

$(document).on('click', '.add-form-row1', function(e){
    e.preventDefault();
    cloneMore1('.form-row.payments:last', 'paymentSchedule');
    return false;
});

Similarly do it for remove functions. In case any one gets stuck in future, it'll help :)