Django CreateView 结合 modelformset 和 inlineformset

Django CreateView combine modelformset and inlineformset

我的 CreateView 中有两个表单:

  1. RequisitionModelForm(物料申请模型)-> 这节省了
  2. RequisitionItemsModelForm(MaterialRequisitionItems 模型)--> 这个不保存

第二个表单可以有多个条目保存到它们自己的 MaterialRequisitionItems 对象中。

我的目标是在一个 MaterialRequisition 对象下保存多个项目对象。

这是我的模板的屏幕截图:

我使用 inlineformset_factory 在同一视图中显示两个表单,但我不确定如何为创建多个项目对象的第二个表单创建另一个表单集。我想我需要 formset_factory 来保存多个项目对象。

forms.py

RequisitionInlineFormSet = forms.inlineformset_factory(
    MaterialRequisition,
    MaterialRequisitionItems,
    form = RequisitionItemsModelForm,
    extra=1,
    can_delete=False,
    can_order=False,
    )

如果有人能指出我的大致方向,将不胜感激。 :)


其他代码供参考

views.py

class RequisitionAddView(LoginRequiredMixin, generic.CreateView):
    template_name = "requisition/requisition_add.html"
    form_class = RequisitionModelForm
    model = MaterialRequisition
    
    def form_valid(self, form):
        print(form.data)
        ctx = self.get_context_data()
        inlines = ctx['inlines']
        
        if inlines.is_valid() and form.is_valid():
            req = form.save(commit = False)
            req.site = self.request.user.site
            req.save()
            for form in inlines:
                form.save()
        return super(RequisitionAddView, self).form_valid(form)

    def get_context_data(self, **kwargs):
        ctx=super(RequisitionAddView,self).get_context_data(**kwargs)
        ctx['item_list'] = ReqItemModelForm()
        if self.request.POST:
            ctx['form']=RequisitionModelForm(self.request.POST)
            ctx['inlines']=RequisitionInlineFormSet(self.request.POST)
        else:
            ctx['form']=RequisitionModelForm()
            ctx['inlines']=RequisitionInlineFormSet()
        return ctx

forms.py

class RequisitionModelForm(forms.ModelForm):
    class Meta:
        model = MaterialRequisition
        fields = (
            'reqDescription',
            'reqDateNeeded',
        )

class RequisitionItemsModelForm(forms.ModelForm):
    class Meta:
        model = MaterialRequisitionItems
        fields = (
            'requisition',
            'item',
            'itemQuantity',
        )

requisition_add.html

<form
          id="reqForm"
          class="form-inline"
          method="post"
          action=""
          {% csrf_token %}
          {{ form.reqDescription|as_crispy_field }}
          {{ form.reqDateNeeded|as_crispy_field }}

          {{ inlines.management_form }}
          <div class="flex mb-5">
            <a
              class="flex-grow text-indigo-500 border-b-2 border-indigo-500 py-2 text-lg"
              >Items Requested</a
            >
          </div>
          <div id="form_set">
            {% for form in inlines.forms %}
            <div class="flex flex-row">
                <div class='w-full grid grid-cols-2 gap-5 no_error'>
                    {{ form|crispy }}
                </div>
                <input class="delete cursor-pointer ml-5 my-auto text-center text-gray-900 cursor-pointer text-sm font-medium h-full [TEST] text-white bg-rose-500 hover:bg-rose-600 ease-in duration-100 border-0 py-1 px-3 focus:outline-none rounded-full" value="Delete" type="button" formnovalidate></input>
          </div>
            {% endfor %}
          </div>
          
          <input class="flex mt-3 mx-auto text-center text-gray-900 cursor-pointer text-sm font-medium h-full [TEST] text-white bg-slate-500 hover:bg-slate-600 ease-in duration-100 border-0 py-2 px-5 focus:outline-none rounded-full" type="button" value="Add an Item +" id="add_more">

          <div id="empty_form" style="display:none">
            <div class="flex flex-row">
                <div class='w-full grid grid-cols-2 gap-5 no_error'>
                    {{ inlines.empty_form|crispy }}
                    
                </div>
                <input class="delete cursor-pointer ml-5 my-auto text-center text-gray-900 cursor-pointer text-sm font-medium h-full [TEST] text-white bg-rose-500 hover:bg-rose-600 ease-in duration-100 border-0 py-1 px-3 focus:outline-none rounded-full" value="Delete" type="button" formnovalidate></input>
          </div>
        </div>

          <div class="flex justify-end mt-6 ml-auto">
            <div id="buttons" class="flex">
              <input
                type="submit"
                value="Submit"
                class="flex ml-3 text-white bg-indigo-500 cursor-pointer border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded ease-in duration-100"
              />
              <a href="{% url 'requisition:list-requisition' %}" class="flex ml-3 text-black bg-gray-200 border-0 py-2 px-6 focus:outline-none hover:bg-gray-300 rounded ease-in duration-100">
                Cancel
              </a>
            </div>
          </div>
        </form>
        
      </div>
    </div>
    <script>
      $('#add_more').click(function() {
          var form_idx = $('#id_form-TOTAL_FORMS').val();
          $('#form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
          $('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
      });

      $(document).on("click", ".delete", function() {
        $(this).parent().remove(); 
});
  </script>

adding/removing 字段的 JS

$('#add_more').click(function(ev) {
          // var form_idx = $('#id_form-TOTAL_FORMS').val();
          // $('#form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
          // $('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
        ev.preventDefault();
        var count = $('#form_set').children().length;
        var tmplMarkup = $('#empty_form').html();
        var compiledTmpl = tmplMarkup.replace(/__prefix__/g, count);
        $('div#form_set').append(compiledTmpl);
        $('#id_materialrequisitionitems_set-TOTAL_FORMS').attr('value', count+1);
      });

      $(document).on("click", ".delete", function() {
        var delcount = $('#form_set').children().length;
        $('#id_materialrequisitionitems_set-TOTAL_FORMS').attr('value', delcount-1);
        $(this).parent().remove();
      });

在这种情况下,我通常做的是将内联表单嵌入到主表单中,这样当主表单保存时,内联表单也会被保存。

问题: 要指出您的代码问题,您似乎在保存内联表单集时没有设置实例。此外,您正在将表单一张一张地保存在表单集中。这不是必需的,因为 formset class 具有为您完成的保存方法。最后,由于您使用 crispy-forms 呈现内联表单,它们会自动将表单标签添加到呈现的表单中。由于 HTML 中的 <form> 标签不能有子表单,这可能是您的表单未保存的实际原因。您可以通过配置 crispy forms helper 来避免这种情况。

让我为您提供两个解决方案,如果它们有效,请告诉我。

解决方案 #1 - 修补现有代码:

def form_valid(self, form):
        print(form.data)
        ctx = self.get_context_data()
        inlines = ctx['inlines']
        
        if inlines.is_valid() and form.is_valid():
            # Better to do this like this
            # See https://www.django-antipatterns.com/antipattern/using-commit-false-when-altering-the-instance-in-a-modelform.html           
            form.instance.site = self.request.user.site
            req = form.save()

            # Setting the instance so the formset knows how to set the FK
            # and saving the formset in one line
            inlines.instance = req
            inlines.save()

        return super(RequisitionAddView, self).form_valid(form)

配置助手:

class RequisitionItemsModelForm(forms.ModelForm):
    class Meta:
        model = MaterialRequisitionItems
        fields = (
            'requisition',
            'item',
            'itemQuantity',
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.helper = FormHelper()  # imported from crispy_forms.helper
        self.form_tag = False  # solves the form tag problem

为了安全起见,这也应该以其他模型形式完成。

解决方案 #2 - 在您的 RequisitionModelForm:

中嵌入内联表单集
class RequisitionModelForm(forms.ModelForm):
    class Meta:
        model = MaterialRequisition
        fields = (
            'reqDescription',
            'reqDateNeeded',
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.requisition_formset = RequisitionInlineFormSet(
            data=kwargs.get('data'), instance=self.instance
        )

        self.helper = FormHelper()
        self.form_tag = False

    # Now you need to override save to save the inline formset, too
    def save(self, **kwargs):
        # If any operations fail, we rollback
        with transaction.atomic():
            # Saving the MaterialRequisition first
            saved_req = super().save(**kwargs)

            # Saving the inline formsets
            self.requisition_formset.instance = saved_req
            self.requisition_formset.save()

            return saved_req

    # Also needs to be overridden in case any clean method are implemented
    def clean(self):
        self.requisition_formset.clean()
        super().clean()

        return self.cleaned_data

    # is_valid sets the cleaned_data attribute so we need to override that too
    def is_valid(self):
        is_valid = True
        is_valid &= self.requisition_formset.is_valid()
        is_valid &= super().is_valid()

        return is_valid

     # In case you're using the form for updating, you need to do this too
     # because nothing will be saved if you only update field in the inner
     # formset
     def has_changed(self):
         has_changed = False

         has_changed |= self.requisition_formset.has_changed()
         has_changed |= super().has_changed()

         return has_changed

所以,这个解决方案有点长,但是在表单代码中保留了表单逻辑,所以我认为它更清晰。现在在视图代码中你可以简单地这样做:

class RequisitionAddView(LoginRequiredMixin, generic.CreateView):
    template_name = "requisition/requisition_add.html"
    form_class = RequisitionModelForm
    model = MaterialRequisition

    # No need for form_valid or get_context_data!

PS。另一个干净的代码提示:不要使用 if self.request.POST 检查请求是否是 POST 请求。更好的做法是使用 if self.request.method == 'POST'。有关详细信息,请参阅 this

views.py

class RequisitionAddView(LoginRequiredMixin, generic.CreateView):
    template_name = "requisition/requisition_add.html"
    form_class = RequisitionModelForm
    model = MaterialRequisition
    

    def get_context_data(self, **kwargs):
        ctx=super(RequisitionAddView,self).get_context_data(**kwargs)
        ctx['inlines']=RequisitionInlineFormSet()
        return ctx

    def post(self, request, *args, **kwargs):
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        formset = RequisitionInlineFormSet(self.request.POST)
        if form.is_valid() and formset .is_valid():
            return self.form_valid(form, formset )
        else:
            return self.form_invalid(form, formset )

    def form_valid(self, form, formset ):
        self.object = form.save(commit=False)
        self.object.save()
        inlines = formset .save(commit=False)
        for form in inlines :
            form.<your_foreignkey_field> = self.object
            form.save()
        return redirect(reverse("product:product_list"))

我在 here

提到了这个