自定义Django脆皮表单

时间:2020-07-12 13:03:46

标签: python django forms validation django-crispy-forms

我有一个相当复杂的Django表单,它影响3个模型,其中一部分包含一个内联表单集。我在https://dev.to/zxenia/django-inline-formsets-with-class-based-views-and-crispy-forms-14o6找到了一个很好的解决方案来构建表单。我扩展了该解决方案,并以与添加表单集相似的方式添加了第三个模型(使用自定义Django Crispy Form,然后使用Crispy Forms Layout功能插入表单)。

我的问题是,在两个插入的表单(表单集和小型子表单)中的任何一个上引发的任何验证错误都将被忽略-主表单正确张贴,并且引发的ValidationErrors在表单中显示为错误,允许用户更正任何错误及其数据均已正确保存到数据库中。如果子窗体和表单集有效,则它们的数据也将正确保存。但是,如果子表单和表单集中的数据无效,则表单将永远不会显示错误,从而使用户有机会纠正他们的错误,并且数据将被忽略并且永远不会保存到数据库中-主模型的数据可以很好地保存但是。

我的问题是,如何使表单刷新,其中包含在添加的子表单和表单集中显示的错误,从而允许用户纠正错误?

下面的大多数代码来自上面引用的相当不错的文章,并添加了第三个模型

型号:

from django.db import models
from django.contrib.auth.models import User


class Collection(models.Model):
    subject = models.CharField(max_length=300, blank=True)
    owner = models.CharField(max_length=300, blank=True)
    note = models.TextField(blank=True)
    created_by = models.ForeignKey(User,
        related_name="collections", blank=True, null=True,
        on_delete=models.SET_NULL)

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


class CollectionTitle(models.Model):
    """
    A Class for Collection titles.

    """
    collection = models.ForeignKey(Collection,
        related_name="has_titles", on_delete=models.CASCADE)
    name = models.CharField(max_length=500, verbose_name="Title")
    language = models.CharField(max_length=3)

Class CollectionTxn(models.Model):
    """
    A Class for Collection transactions.

    """
    collection = models.ForeignKey(Collection,
        related_name="has_txn", on_delete=models.CASCADE)
    number_received= models.IntegerField()
    date_received= models.DateField()

    class Meta:
        '''
        If 2 rows are entered with the same information, a validation error is raised, but it just
        doesn't save the data at all instead of refreshing the form showing the error.
        '''
        unique_together = ('number_received', 'date_received')

forms.py:

from django import forms
from .models import Collection, CollectionTitle
from django.forms.models import inlineformset_factory
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Fieldset, Div, Row, HTML, ButtonHolder, Submit
from .custom_layout_object import Formset, Subform

import re


class CollectionTitleForm(forms.ModelForm):

    class Meta:
        model = CollectionTitle
        exclude = ()

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

        formtag_prefix = re.sub('-[0-9]+$', '', kwargs.get('prefix', ''))

        self.helper = FormHelper()
        self.helper.form_tag = False
        self.helper.layout = Layout(
            Row(
                Field('name'),
                Field('language'),
                Field('DELETE'),
                css_class='formset_row-{}'.format(formtag_prefix)
            )
        )


CollectionTitleFormSet = inlineformset_factory(
    Collection, CollectionTitle, form=CollectionTitleForm,
    fields=['name', 'language'], extra=1, can_delete=True
)


class CollectionForm(forms.ModelForm):

    class Meta:
        model = Collection
        exclude = ['created_by', ]

    def __init__(self, *args, **kwargs):
        super(CollectionForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_tag = True
        self.helper.form_class = 'form-horizontal'
        self.helper.label_class = 'col-md-3 create-label'
        self.helper.field_class = 'col-md-9'
        self.helper.layout = Layout(
            Div(
                Field('subject'),
                Field('owner'),
                Fieldset('Add titles',
                         Formset('titles')),
                Field('note'),
                Subform('transactions'),
                HTML("<br>"),
                ButtonHolder(Submit('submit', 'Save')),
            )
        )

class CollectionTxnForm(forms.ModelForm):
    
    class Meta:
        model = CollectionTxn
        exclude = ()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['collection'].widget = HiddenInput()
        self.helper = FormHelper()
        self.helper.form_tag = False
        self.helper.layout = Layout(
            Row(
                Field('number_received'),
                Field('date_received'),
            )
        )

views.py:

from .models import *
from .forms import *
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
from django.db import transaction

class CollectionCreate(CreateView):
    model = Collection
    template_name = 'mycollections/collection_create.html'
    form_class = CollectionForm
    success_url = None

    def get_context_data(self, **kwargs):
        data = super(CollectionCreate, self).get_context_data(**kwargs)
        if self.request.POST:
            data['titles'] = CollectionTitleFormSet(self.request.POST)
            data['transactions'] = CollectionTrxForm(self.request.POST)
        else:
            data['titles'] = CollectionTitleFormSet()
            data['transactions'] = CollectionTrxForm()
        return data

    def form_valid(self, form):
        context = self.get_context_data()
        titles = context['titles']
        transactions = context['transactions']
        with transaction.atomic():
            form.instance.created_by = self.request.user
            self.object = form.save()
            if titles.is_valid():
                titles.instance = self.object
                titles.save()
            if transactions.is_valid():
                transactions.save()
        return super(CollectionCreate, self).form_valid(form)

    def get_success_url(self):
        return reverse_lazy('mycollections:collection_detail', kwargs={'pk': self.object.pk})

custom_layout_object.py

from crispy_forms.layout import LayoutObject, TEMPLATE_PACK
from django.shortcuts import render
from django.template.loader import render_to_string

class Formset(LayoutObject):
    template = "mycollections/formset.html"

    def __init__(self, formset_name_in_context, template=None):
        self.formset_name_in_context = formset_name_in_context
        self.fields = []
        if template:
            self.template = template

    def render(self, form, form_style, context, template_pack=TEMPLATE_PACK):
        formset = context[self.formset_name_in_context]
        return render_to_string(self.template, {'formset': formset})

class SubForm(LayoutObject):
    template = "mycollections/subform.html"

    def __init__(self, subform_name_in_context, template=None):
        self.subform_name_in_context = subform_name_in_context
        self.fields = []
        if template:
            self.template = template

    def render(self, subform, form_style, context, template_pack=TEMPLATE_PACK):
        subform = context[self.subform_name_in_context]
        return render_to_string(self.template, {'subform': subform})

formset.html

{% load crispy_forms_tags %}
{% load staticfiles %}

<style type="text/css">
  .delete-row {
    align-self: center;
  }
</style>

{{ formset.management_form|crispy }}

{% for form in formset.forms %}
  {% for hidden in form.hidden_fields %}
      {{ hidden|as_crispy_field }}
  {% endfor %}
  {% crispy form %}
{% endfor %}

<br>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="{% static 'mycollections/libraries/django-dynamic-formset/jquery.formset.js' %}"></script>
<script type="text/javascript">
    $('.formset_row-{{ formset.prefix }}').formset({
        addText: 'add another',
        deleteText: 'remove',
        prefix: '{{ formset.prefix }}',
    });
</script>

subform.html

{% load crispy_forms_tags %}
{% crispy subform %}

collection_create.html

{% extends "mycollections/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container">
    <div class="card">
        <div class="card-header">
            Create collection
        </div>
        <div class="card-body">
             {% crispy form %}
        </div>
    </div>
</div>
{% endblock content %}

基本上,对于与添加到布局中的表单集和子表单相关联的字段,仍然会引发验证错误,但是它们不会冒出到表单级别以显示错误,它们只会被忽略,并且永远不会保存数据。 “主要”模型可以正常工作,并且会为其字段显示验证错误。如果没有无效数据,则主表单,子表单和表单集的数据都将正确保存。如果表单集或子表单中存在无效数据,则用户将永远没有机会更正该数据。

任何有关我将所需代码添加到何处的帮助,如果在表单集或子表单中输入的任何无效数据将导致表单刷新显示错误,而不仅仅是忽略而不保存无效数据,将不胜感激。

1 个答案:

答案 0 :(得分:0)

经过大量调试和分析代码(包括我自己和Crispy),我已经解决了自己的问题。只需检查子表单和表单集验证,如果无效,则重新呈现表单。

这是执行此操作的视图中的新form_valid()方法:

    def form_valid(self, form):
        context = self.get_context_data()
        titles = context['titles']
        transactions = context['transactions']
        with transaction.atomic():
            form.instance.created_by = self.request.user
            if titles.is_valid() and transactions_is_valid():
                self.object = form.save() #only save form if other subforms validate
                titles.instance = self.object
                # Any other field processing goes here
                titles.save()
                transactions.save()
            else:
                # If any subform or subformset is invalid, re-render the form showing errors
                context.update({'titles': titles})
                context.update({'transactions': transactions})
                return self.render_to_response(context)
        return super(CollectionCreate, self).form_valid(form)

使用此方法,formset和子窗体中的任何干净方法,或者如果存在任何其他错误(例如,如果由于setmodel中声明了unique_together(),而使setset的两行都不能相同),则该窗体将刷新以显示所有所有三个组合的表单/表单集上的错误,使用户可以纠正这些错误。

现在-使用Ajax进行操作,以使页面不会刷新:)