单个视图中的 Django 多个表单集仅保存最后输入的值

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

我正在尝试在单个视图中使用多个表单集,但我无法正确实现此功能。 例如:当我尝试在每个表单集中添加 3 个值时,即第一个表单集的 3 个条目和第二个表单集的 3 个条目只有最后一个条目存储在数据库中,前两个值被丢弃而不保存。

请找出目前写的代码如下:

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 代码

{% 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 %}

如果您需要有关此问题的更多信息,请告诉我。 任何帮助或建议将不胜感激!

使用@dirkgroten 的提示进行调试后,我发现问题出在我编写的 javascript 代码上。当我尝试添加新的表单集时,我得到的是 NaN 值。

相反,我使用 django-dynamic-formset [https://github.com/elo80ka/django-dynamic-formset] 来实现多个 formset。

请在下面找到实现的脚本代码

<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>

我删除了我的 jQuery 代码并使用了 django-dynamic-formset。

此致, 阿米·凯勒卡

如您所见,问题出在 javascript 代码上。当您尝试添加新的表单集并获得 NaN 值时,因为您的 javascript 代码的表单集名称与原始表单集名称不匹配,因为它以 deliverables 为前缀,第二个带有 付款时间表。 因此,您需要在 javascript 代码中编辑以下函数:

$(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;
});

将 cloneMore 和 clonemore1 函数中的 'form' 替换为 deliverablespaymentSchedule:

$(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;
});

移除函数也同样如此。 万一将来有人遇到困难,它会有所帮助:)