发布于 2026-01-06 0 阅读
0

使用基于类的视图和 Crispy Forms 的 Django 内联表单集向任务添加文章

Django 内联表单集,支持基于类的视图和清晰表单

将文章添加到任务

最近我在一个 Django 项目中使用了内联表单集,效果非常好。我决定分享一下我将内联表单集与基于类的视图、crispy-formsdjango-dynamic-formset jQuery 插件集成的示例。在研究这个主题时,我发现相关的示例并不多,Django 官方文档在这方面也比较简略,所以我写了这篇文章,希望能帮助到想尝试这种解决方案的人,也方便自己日后参考。

首先,为什么要使用内联表单集
允许用户在一页上,通过外键从被引用对象的创建/更新视图创建和更新相关的对象。

假设我们有一个集合,其中可以包含多种语言的标题,但我们无法确切知道用​​户会提供多少个标题翻译。我们希望允许用户通过点击“添加”按钮来添加所需的标题数量,该按钮会在集合创建表单中添加一个新行。
这就是我们的模型。

models.py:

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)

Enter fullscreen mode Exit fullscreen mode

现在让我们为 CollectionTitle 创建一个表单和一个表单集(使用inlineformset_factory),其中包含父模型 Collection 和 FK 相关模型 CollectionTitle。

forms.py

from django import forms
from .models import *
from django.forms.models import inlineformset_factory


class CollectionTitleForm(forms.ModelForm):

    class Meta:
        model = CollectionTitle
        exclude = ()

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

Enter fullscreen mode Exit fullscreen mode

接下来,我们将此表单集添加到 CollectionCreate 视图中。

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)
        else:
            data['titles'] = CollectionTitleFormSet()
        return data

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

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

Enter fullscreen mode Exit fullscreen mode

CollectionUpdate 视图看起来类似,只是在get_context_data()中需要传递实例对象。

views.py:

def get_context_data(self, **kwargs):
        data = super(CollectionUpdate, self).get_context_data(**kwargs)
        if self.request.POST:
            data['titles'] = CollectionTitleFormSet(self.request.POST, instance=self.object)
        else:
            data['titles'] = CollectionTitleFormSet(instance=self.object)
        return data

Enter fullscreen mode Exit fullscreen mode

接下来,我们需要创建一个 CollectionForm,并将表单集渲染成其中的字段。这并非易事,因为 crispy-forms 没有像 Div 或 HTML 那样为表单集提供布局对象。
最佳解决方案(非常感谢!)是创建一个自定义的 crispy 布局对象。

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

Enter fullscreen mode Exit fullscreen mode

下一步是添加一个模板来渲染表单集。
具体来说,我想要渲染的内容是:对于每个新标题,生成一行包含“名称”和“语言”字段的表单集,以及一个用于删除该行的“删除”按钮(并在更新集合时删除数据库中的数据),行下方还有一个按钮,用于为新标题添加另一行。
我使用django-dynamic-formset jQuery 插件来动态添加更多行。
我建议使用前缀文档),以便为所有内联表单集情况(例如,在一个表单中添加多个内联表单集时)使用同一个表单集模板。表单集前缀是引用类的相关名称。因此,在我的示例中,它是“has_titles”。

formset.html:


{% load crispy_forms_tags %}
<table>
{{ formset.management_form|crispy }}

    {% for form in formset.forms %}
            <tr class="{% cycle 'row1' 'row2' %} formset_row-{{ formset.prefix }}">
                {% for field in form.visible_fields %}
                <td>
                    {# Include the hidden fields in the form #}
                    {% if forloop.first %}
                        {% for hidden in form.hidden_fields %}
                            {{ hidden }}
                        {% endfor %}
                    {% endif %}
                    {{ field.errors.as_ul }}
                    {{ field|as_crispy_field }}
                </td>
                {% endfor %}
            </tr>
    {% endfor %}

</table>
<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>
Enter fullscreen mode Exit fullscreen mode

最后,我们可以结合使用现有的布局对象和自定义 Formset 对象,为 CollectionForm 构建我们自己的表单布局。

forms.py:

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Fieldset, Div, HTML, ButtonHolder, Submit
from .custom_layout_object import *


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'),
                HTML("<br>"),
                ButtonHolder(Submit('submit', 'save')),
                )
            )

Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

现在一切就绪,我们只需点击一次“保存”按钮,即可在一个页面上的一个表单中创建一个收藏集及其标题。

内联表单集

本文的源代码在这里

感谢这篇精彩的博客文章,它帮助我解决了内联表单集的问题。

文章来源:https://dev.to/zxenia/django-inline-formsets-with-class-based-views-and-crispy-forms-14o6