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

发布于 2024-12-19 03:52:47 字数 1560 浏览 1 评论 0原文

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

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 文档/SO/谷歌搜索,但一直无法弄清楚如何处理这个问题。我尝试了几种方法:

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

  2. 使用inlineformset_factory 使用 inlineformset_factory(Destination, Visitor) 生成目的地/活动表单集。这会破坏,因为访问者与目的地具有 M2M 关系,而不是 FK。

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

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

谢谢!

I'm trying to do something that should be very common: add/edit a bunch of related models in a single form. For example:

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

My models are Visitor, Destination and Activity, with Visitor having a ManyToMany field into Destination through an intermediary model, VisitorDestination, which has the details of the activities to be done on the destination (in itself a ManyToMany field into Activity).

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

Note that I don't want to enter new destination / activity values, just choose from those available in the db (but that's a perfectly legit use of M2M fields right?)

To me this looks like an extremely common situation (a many to many relation with additional details which are a FK or M2M field into some other model), and this looks like the most sensible modelling, but please correct me if I'm wrong.

I've spent a few days searching Django docs / SO / googling but haven't been able to work out how to deal with this. I tried several approaches:

  1. Custom Model form for Visitor, where I add multiple choice fields for Destination and Activity. That works ok if Destination and Activity could be selected independently, but here they are correlated, ie I want to choose one or several activities for each destination

  2. Using inlineformset_factory to generate the set of destination / activities forms, with inlineformset_factory(Destination, Visitor). This breaks, because Visitor has a M2M relation to Destination, rather than a FK.

  3. Customizing a plain formset, using formset_factory, eg DestinationActivityFormSet = formset_factory(DestinationActivityForm, extra=2). But how to design DestinationActivityForm? I haven't explored this enough, but it doesn't look very promising: I don't want to type in the destination and a list of activities, I want a list of checkboxes with the labels set to the destination / activities I want to select, but the formset_factory would return a list of forms with identical labels.

I'm a complete newbie with django so maybe the solution is obvious, but I find that the documentation in this area is very weak - if anyone has some pointers to examples of use for forms / formsets that would be also helpful

thanks!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

饮湿 2024-12-26 03:52:47

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

事实证明,在同一视图中处理多个表单非常简单(至少在本例中,不存在跨字段验证问题)。

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

首先是自定义表单:

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

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

我会将问题保留几天,以防有人有更干净的方法。

谢谢!

In the end I opted for processing multiple forms within the same view, a Visitor model form for the visitor details, then a list of custom forms for each of the destinations.

Processing multiple forms in the same view turned out to be simple enough (at least in this case, where there were no cross-field validation issues).

I'm still surprised there is no built-in support for many to many relationships with an intermediary model, and looking around in the web I found no direct reference to it. I'll post the code in case it helps anyone.

First the custom forms:

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

I customize each form by passing a Visitor and Destination objects (and a 'visited' flag which is calculated outside for convenience)

I use a boolean field to allow the user to select each destination. The field is called 'visited', however I set the label to the destination so it gets nicely displayed.

The activities get handled by the usual MultipleChoiceField (I used I customized widget to get the checkboxes to display on a table, pretty simple but can post it if somebody needs that)

Then the view code:

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

I simply collect my destination forms in a list and pass this list to my template, so that it can iterate over them and display them. It works well as long as you don't forget to pass a different prefix for each one in the constructor

I'll leave the question open for a few days in case some one has a cleaner method.

Thanks!

爱殇璃 2024-12-26 03:52:47

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

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

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

希望这可以帮助您走上正确的轨道。

So, as you've seen, one of the things about inlineformset_factory is that it expects two models - a parent, and child, which has a foreign key relationship to the parent. How do you pass extra data on the fly to the form, for extra data in the intermediary model?

How I do this is by using curry:

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)

The form class "ChildModelForm" would need to have an init override that adds the "some_extra_model" object from the arguments:

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

Hope that helps get you on the right track.

夏天碎花小短裙 2024-12-26 03:52:47

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

From django 1.9, there is a support for passing custom parameters to formset forms :
https://docs.djangoproject.com/en/1.9/topics/forms/formsets/#passing-custom-parameters-to-formset-forms

Just add form_kwargs to your FormSet init like this :

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})
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文