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

时间:2011-11-30 21:24:29

标签: django django-forms django-orm

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

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

我的模型是访问者,目的地和活动,访问者通过中间模型VisitorDestination将ManyToMany字段导入Destination,其中包含要在目标上完成的活动的详细信息(本身是Activity中的ManyToMany字段)。

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

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

对我而言,这看起来是一种非常常见的情况(与其他模型中的FK或M2M字段的其他细节有很多甚至很多关系),这看起来像是最明智的建模,但如果我',请纠正我我错了。

我花了几天时间搜索Django docs / SO / googling,但还是没能弄清楚如何处理这个问题。我尝试了几种方法:

  1. 访问者的自定义模型表单,其中我为目标和活动添加了多个选项字段。如果可以单独选择目的地和活动,但这里它们相关,即我想为每个目的地选择一个或多个活动

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

  3. 使用formset_factory自定义普通表单集,例如DestinationActivityFormSet = formset_factory(DestinationActivityForm, extra=2)。但是如何设计DestinationActivityForm?我没有充分探讨这一点,但看起来并不是很有希望:我不想输入目的地和活动列表,我想要一个复选框列表,标签设置为我想要的目的地/活动选择,但formset_factory将返回具有相同标签的表单列表。

  4. 我是django的完全新手所以也许解决方案很明显,但我发现这个领域的文档非常弱 - 如果有人对表单/表单集的使用示例有一些指示也会有帮助< / p>

    谢谢!

3 个答案:

答案 0 :(得分:7)

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

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

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

首先是自定义表单:

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

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

如果有人使用更清洁的方法,我会将问题保持开放几天。

谢谢!

答案 1 :(得分: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”需要有一个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

希望这有助于您走上正确的轨道。

答案 2 :(得分:0)

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

只需将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})