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

django表单:在单个表单中编辑多组相关对象,django,django-forms,django-orm,Django,Django Forms,Django Orm,我正在尝试做一些非常常见的事情:以单一形式添加/编辑一组相关模型。例如: Visitor Details: Select destinations and activities: Miami [] - swimming [], clubbing [], sunbathing[] Cancun [] - swimming [], clubbing [], sunbathing[] 我的模型是Visitor、Destination和Activity,Visitor通过

我正在尝试做一些非常常见的事情:以单一形式添加/编辑一组相关模型。例如:

Visitor Details:
Select destinations and activities:
    Miami  []   -  swimming [], clubbing [], sunbathing[]
    Cancun []   -  swimming [], clubbing [], sunbathing[]
我的模型是Visitor、Destination和Activity,Visitor通过中介模型VisitorDestination将多个字段转换为Destination,该模型包含要在目的地上执行的活动的详细信息(本身就是多个字段转换为Activity)

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

对我来说,这似乎是一种非常常见的情况(与其他模型中的FK或M2M字段的附加细节存在多对多关系),这看起来是最合理的建模,但如果我错了,请纠正我

我花了几天时间搜索Django docs/SO/google,但还没有找到解决方法。我尝试了几种方法:

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

  • 使用
    inlineformset\u工厂
    生成目标/活动表单集,使用
    inlineformset\u工厂(目标,访问者)
    。这会中断,因为访问者与目的地的关系是M2M,而不是FK

  • 使用
    formset\u factory
    自定义普通表单集,例如
    DestinationActivityFormSet=formset\u factory(DestinationActivityForm,extra=2)
    。但是如何设计目标活动形式呢?我对这方面的研究还不够深入,但它看起来不太有希望:我不想输入目的地和活动列表,我想要一个带有标签的复选框列表,标签设置为我想要选择的目的地/活动,但是
    formset\u工厂
    将返回一个带有相同标签的表单列表

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


    谢谢

    因此,正如您所看到的,inlineformset_工厂的一个特点是它需要两个模型—一个父模型和一个与父模型有外键关系的子模型。对于中间模型中的额外数据,如何动态地将额外数据传递给表单

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

    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)
    
    form类“ChildModelForm”需要有一个init重写,用于从参数中添加“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
    

    希望这能帮助您走上正轨。

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

    在同一个视图中处理多个表单变得足够简单(至少在这种情况下,不存在跨字段验证问题)

    我仍然感到惊讶的是,对于中介模型的多对多关系没有内置的支持,在网络上四处查看,我发现没有直接引用它。我会发布代码,以防对任何人有所帮助

    首先是自定义表单:

    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
    
    我通过传递
    访问者
    目的地
    对象(以及为方便起见在外部计算的“已访问”标志)来定制每个表单

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

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

    然后查看代码:

    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))
    
    我只需在一个列表中收集我的目标表单,并将该列表传递给我的模板,这样它就可以迭代并显示它们。只要您不忘记为构造函数中的每个前缀传递不同的前缀,它就可以正常工作

    我将把这个问题留待几天,以防有人有更干净的方法


    谢谢

    django 1.9支持向表单集表单传递自定义参数:

    只需将form_kwargs添加到FormSet init,如下所示:

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

    我喜欢使用curry在模型表单集中使用自定义构造函数的想法,但仍然存在inlineformset_factory要求两个模型通过FK链接的问题,而我的模型通过M2M链接。如果我尝试使用它们,我会得到一个错误,说子模型没有到父模型的FK,或者我从错误的角度来看这个问题,也许我应该让我的表单使用VisitorDestination作为基础模型,它有到Visitor和Destination的FK,以及到Activity的M2M,并以此作为我的表单集的基础如果没有看到确切的模型、视图、工作流程等,我不确定你的应用程序的正确解决方案是什么,但是,听起来你可能朝着阻力最小的方向前进。看起来在Django 1.10中,此解决方案不再有效。现在出现此错误:AttributeError:“函数”对象没有属性“\u meta”。寻找解决方案,如果我发现有用的东西,我会添加一个答案。@martync让我知道你找到了什么感谢这篇文章-我一直认为文档和教程没有为初学者提供足够清晰的信息。从“民意调查”或“博客”到在大多数web应用程序中真正有用的表单种类有了巨大的飞跃。在编辑模型的单个实例后,处理表单中的多个对象可能是最常见的应用程序
    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})