django forms:在一个表单中编辑多组相关对象

Posted

技术标签:

【中文标题】django forms:在一个表单中编辑多组相关对象【英文标题】:django forms: editing multiple sets of related objects in a single form 【发布时间】:2012-01-10 02:42:36 【问题描述】:

我正在尝试做一些应该很常见的事情:在一个表单中添加/编辑一堆相关模型。例如:

Visitor Details:
Select destinations and activities:
    Miami  []   -  swimming [], clubbing [], sunbathing[]
    Cancun []   -  swimming [], clubbing [], sunbathing[]

我的模型是 Visitor、Destination 和 Activity,Visitor 通过中间模型 VisitorDestination 在 Destination 中有一个 ManyToMany 字段,该模型包含要在目的地上完成的活动的详细信息(本身就是 Activity 中的 ManyToMany 字段)。

Visitor ---->(M2M though VisitorDestination) -------------> Destination
                                            |
                       activities            ---->(M2M)---> Activity  

请注意,我不想输入 目的地/活动值,只需从数据库中可用的值中选择(但这是对 M2M 字段的完全合法使用对吧?)

对我来说,这看起来是一种极其常见的情况(与其他模型的 FK 或 M2M 字段的附加细节存在多对多关系),这看起来是最明智的建模,但如果我'我错了。

我花了几天时间搜索 Django 文档/SO/谷歌搜索,但无法弄清楚如何处理这个问题。我尝试了几种方法:

    访客的自定义模型表单,我在其中为目的地和活动添加了多项选择字段。如果可以独立选择 Destination 和 Activity,那就可以了,但在这里它们是相关,即我想为 每个目的地选择一个或多个活动

    李>

    使用inlineformset_factory 生成一组目的地/活动表单,使用inlineformset_factory(Destination, Visitor)。这会中断,因为 Visitor 与 Destination 有 M2M 关系,而不是 FK。

    使用formset_factory 自定义普通表单集,例如DestinationActivityFormSet = formset_factory(DestinationActivityForm, extra=2)。但是如何设计DestinationActivityForm?我没有对此进行足够的探索,但它看起来不太有希望:我不想输入目的地和活动列表,我想要一个带有设置为目的地/我想要的活动标签的复选框列表选择,但formset_factory 将返回具有相同标签的表单列表。

我是 django 的完整新手,所以解决方案可能很明显,但我发现这方面的文档非常薄弱 - 如果有人有一些关于表单/表单集使用示例的指针,也会有帮助

谢谢!

【问题讨论】:

【参考方案1】:

所以,正如您所看到的,关于 inlineformset_factory 的其中一件事是它需要两个模型 - 父模型和子模型,它们与父模型具有外键关系。对于中间模型中的额外数据,您如何将额外数据动态传递给表单?

我如何做到这一点是使用咖喱:

from django.utils.functional import curry

from my_app.models import ParentModel, ChildModel, SomeOtherModel

def some_view(request, child_id, extra_object_id):
    instance = ChildModel.objects.get(pk=child_id)
    some_extra_model = SomeOtherModel.objects.get(pk=extra_object_id)

    MyFormset = inlineformset_factory(ParentModel, ChildModel, form=ChildModelForm)

    #This is where the object "some_extra_model" gets passed to each form via the
    #static method
    MyFormset.form = staticmethod(curry(ChildModelForm,
        some_extra_model=some_extra_model))

    formset = MyFormset(request.POST or None, request.FILES or None,
        queryset=SomeObject.objects.filter(something=something), instance=instance)

表单类“ChildModelForm”需要有一个从参数中添加“some_extra_model”对象的初始化覆盖:

def ChildModelForm(forms.ModelForm):
    class Meta:
        model = ChildModel

    def __init__(self, some_extra_model, *args, **kwargs):
        super(ChildModelForm, self).__init__(*args, **kwargs)
        #do something with "some_extra_model" here

希望能帮助您走上正轨。

【讨论】:

我喜欢使用 curry 以便能够在模型表单集中使用自定义构造函数的想法,但是仍然存在 inlineformset_factory 需要通过 FK 链接的两个模型的问题,而我的模型通过M2M。如果我尝试使用它们,我会收到一条错误消息,指出子模型没有父模型的 FK 或者我从错误的角度看待这个问题,也许我应该让我的表单使用 VisitorDestination 作为基本模型,它对 Visitor 和 Destination 有 FK,对 Activity 有 M2M,并使用它作为我的表单集的基础 如果没有看到确切的模型、视图、工作流程等,我不确定适合您的应用的解决方案是什么,但是,听起来您可能正朝着阻力最小的方向前进。 看起来在 Django 1.10 中这个解决方案不再起作用了。现在得到这个错误:AttributeError:'function'对象没有属性'_meta'。寻找解决方案,如果我发现有用的东西,我会添加一个答案。 @martync 告诉我你发现了什么【参考方案2】:

最后我选择在同一个视图中处理多个表单,一个用于访问者详细信息的访客模型表单,然后是每个目的地的自定义表单列表。

在同一个视图中处理多个表单非常简单(至少在这种情况下,没有跨字段验证问题)。

我仍然很惊讶没有内置支持与中间模型的多对多关系,并且在网络上环顾四周,我发现没有直接引用它。我会发布代码以防它对任何人有帮助。

首先是自定义表单:

class VisitorForm(ModelForm):
    class Meta:
      model = Visitor
      exclude = ['destinations']

class VisitorDestinationForm(Form):
    visited = forms.BooleanField(required=False)
    activities = forms.MultipleChoiceField(choices = [(obj.pk, obj.name) for obj in Activity.objects.all()], required=False, 
                                                      widget = CheckboxSelectMultipleInline(attrs='style' : 'display:inline'))

    def __init__(self, visitor, destination, visited,  *args, **kwargs):
        super(VisitorDestinationForm, self).__init__(*args, **kwargs)
        self.destination = destination
        self.fields['visited'].initial = visited
        self.fields['visited'].label= destination.destination

        # load initial choices for activities
        activities_initial = []
        try:
            visitorDestination_entry = VisitorDestination.objects.get(visitor=visitor, destination=destination)
            activities = visitorDestination_entry.activities.all()
            for activity in Activity.objects.all():
                if activity in activities: 
                    activities_initial.append(activity.pk)
        except VisitorDestination.DoesNotExist:
            pass
        self.fields['activities'].initial = activities_initial

我通过传递 VisitorDestination 对象(以及为方便起见在外部计算的“已访问”标志)来自定义每个表单

我使用布尔字段来允许用户选择每个目的地。该字段称为“已访问”,但是我将标签设置为目标,以便很好地显示。

活动由通常的 MultipleChoiceField 处理(我使用自定义小部件让复选框显示在表格上,非常简单,但如果有人需要,可以发布它)

然后查看代码:

def edit_visitor(request, pk):
    visitor_obj = Visitor.objects.get(pk=pk)
    visitorDestinations = visitor_obj.destinations.all()
    if request.method == 'POST':
        visitorForm = VisitorForm(request.POST, instance=visitor_obj)

        # set up the visitor destination forms
        destinationForms = []
        for destination in Destination.objects.all():
            visited = destination in visitorDestinations
            destinationForms.append(VisitorDestinationForm(visitor_obj, destination, visited, request.POST, prefix=destination.destination))

        if visitorForm.is_valid() and all([form.is_valid() for form in destinationForms]):
            visitor_obj = visitorForm.save()
            # clear any existing entries,
            visitor_obj.destinations.clear()
            for form in destinationForms:
                if form.cleaned_data['visited']: 
                    visitorDestination_entry = VisitorDestination(visitor = visitor_obj, destination=form.destination)
                    visitorDestination_entry.save()
                    for activity_pk in form.cleaned_data['activities']: 
                        activity = Activity.objects.get(pk=activity_pk)
                        visitorDestination_entry.activities.add(activity)
                    print 'activities: %s' % visitorDestination_entry.activities.all()
                    visitorDestination_entry.save()

            success_url = reverse('visitor_detail', kwargs='pk' : visitor_obj.pk)
            return HttpResponseRedirect(success_url)
    else:
        visitorForm = VisitorForm(instance=visitor_obj)
        # set up the visitor destination forms
        destinationForms = []
        for destination in Destination.objects.all():
            visited = destination in visitorDestinations
            destinationForms.append(VisitorDestinationForm(visitor_obj, destination, visited,  prefix=destination.destination))

    return render_to_response('testapp/edit_visitor.html', 'form': visitorForm, 'destinationForms' : destinationForms, 'visitor' : visitor_obj, context_instance= RequestContext(request))

我只是将我的目标表单收集在一个列表中,然后将此列表传递给我的模板,以便它可以遍历它们并显示它们。只要您不要忘记在构造函数中为每个前缀传递不同的前缀,它就可以正常工作

如果有人有更简洁的方法,我会将这个问题留几天。

谢谢!

【讨论】:

感谢这篇文章 - 我一直认为文档和教程对于使用表单的初学者来说提供的信息不够清楚。从“民意调查”或“博客”到在大多数网络应用程序中真正有用的表单种类有了巨大的飞跃。在使用表单编辑模型的单个实例之后,处理表单中的多个对象可能是最常见的应用程序。【参考方案3】:

从 django 1.9 开始,支持将自定义参数传递给 formset 表单: https://docs.djangoproject.com/en/1.9/topics/forms/formsets/#passing-custom-parameters-to-formset-forms

只需像这样将 form_kwargs 添加到您的 FormSet 初始化中:

from my_app.models import ParentModel, ChildModel, SomeOtherModel

def some_view(request, child_id, extra_object_id):
    instance = ChildModel.objects.get(pk=child_id)
    some_extra_model = SomeOtherModel.objects.get(pk=extra_object_id)

    MyFormset = inlineformset_factory(ParentModel, ChildModel, form=ChildModelForm)
    formset = MyFormset(request.POST or None, request.FILES or None,
        queryset=SomeObject.objects.filter(something=something), instance=instance,
        form_kwargs="some_extra_model": some_extra_model)

【讨论】:

以上是关于django forms:在一个表单中编辑多组相关对象的主要内容,如果未能解决你的问题,请参考以下文章

Django - 禁用表单集中现有表单的编辑,但允许在新表单中编辑

Django 表单中的可编辑选择字段/下拉框

如何在 Django 中创建用于编辑的填充表单

使用Ajax验证并提交Django表单(django-crispy-forms)

react antd form多组表单数据处理

如何在没有模型的 Django 表单中设置空白=假