Django ModelForms:将ManyToMany字段显示为单选

时间:2012-09-28 14:45:20

标签: django django-forms

在Django应用程序中,我有一个模型Bet,其中包含与Django用户模型的ManyToMany关系:

class Bet(models.Model):
    ...
    participants = models.ManyToManyField(User)

用户应该可以使用表单开始新的投注。到目前为止,投注只有两个参与者,其中一个是自己创建投注的用户。这意味着在新赌注的形式中你必须选择一个参与者。在保存表单数据时,投注创建者将被添加为参与者。

我正在为我的NewBetForm使用ModelForm:

class NewBetForm(forms.ModelForm):
    class Meta:
        model = Bet
        widgets = {
            'participants': forms.Select()
        }

    def save(self, user):
        ... # save user as participant

注意参与者字段的重新定义的小部件,确保您只能选择一个参与者。

但是,这给了我一个验证错误:

Enter a list of values.

我不确定这是从哪里来的。如果我查看开发人员工具中的POST数据,它似乎与我使用默认小部件并仅选择一个参与者完全相同。但是,似乎ManyToManyField的to_python()方法存在此数据的问题。如果我启用了“选择”小部件,则至少没有创建用户对象。

我知道我可以通过从表单中排除参与者字段并自己定义它来解决这个问题,但如果仍然可以使用ModelForm的容量,那将会更好(毕竟,它只是一个小部件更改)。如果我知道如何,也许我可以用某种方式操纵传递的数据。

任何人都可以告诉我问题究竟是什么,以及是否有一个好方法可以解决它?

提前致谢!

修改

根据评论中的建议:视图的(相关)代码。

def new_bet(request):
    if request.method == 'POST':
        form = NewBetForm(request.POST)
        if form.is_valid():
            form.save(request.user)
            ... # success message and redirect
    else:
        form = NewBetForm()
    return render(request, 'bets/new.html', {'form': form})

6 个答案:

答案 0 :(得分:2)

在挖掘Django代码后,我可以回答我自己的问题。

问题是Django的ModelForm将模型中的ManyToManyField映射到表单的ModelMultipleChoiceField。这种表单字段要求widget对象从其value_from_datadict()方法返回一个序列。 ModelMultipleChoiceField的默认窗口小部件(SelectMultiple)会覆盖value_from_datadict()以从用户提供的数据返回列表。但是如果我使用Select小部件,则使用超类的默认value_from_datadict()方法,它只返回一个字符串。 ModelMultipleChoiceField根本不喜欢这样,因此验证错误。

我能想到的解决方案:

  1. 通过继承或某些类装饰器覆盖value_from_datadict() Select
  2. 通过创建新的表单字段并调整save()的{​​{1}}方法来手动处理m2m字段,以便将其数据保存为m2m关系。
  3. 秒解决方案似乎不那么冗长,所以这就是我将要使用的。

答案 1 :(得分:2)

我并不是要重新解决一个已解决的问题,但我正在制定一个这样的解决方案,并认为我会分享我的代码以帮助其他人。

在j0ker的回答中,他列出了两种方法来实现这一点。我使用方法1.我从SelectMultiple小部件中借用了'value_from_datadict'方法。

forms.py

from django.utils.datastructures import MultiValueDict, MergeDict

class M2MSelect(forms.Select):
    def value_from_datadict(self, data, files, name):
        if isinstance(data, (MultiValueDict, MergeDict)):
            return data.getlist(name)
        return data.get(name, None)    

class WindowsSubnetForm(forms.ModelForm):
    port_group = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=PortGroup.objects.all())
    class Meta:
        model = Subnet

答案 2 :(得分:1)

问题是ManyToMany是这种关系的错误数据类型。

从某种意义上说,赌注本身是多对多的关系。让参与者成为一个多人的领域是没有意义的。你需要的是两个ForeignKeys,对于用户:一个用于创建者,一个用于另一个用户('acceptor'?)

答案 3 :(得分:1)

您可以在Form.clean_field_name之前(期间)修改提交的值。您可以使用此方法将select的单个值包装在列表中。

class NewBetForm(forms.ModelForm):
    class Meta:
        model = Bet
        widgets = {
            'participants': forms.Select()
        }

    def save(self, user):
        ... # save user as participant

    def clean_participants(self):
        data = self.cleaned_data['participants']
        return [data]

我实际上只是猜测选择所提供的值是什么样的,所以这可能需要一些调整,但我认为它会起作用。

Here are the docs.

答案 4 :(得分:0)

受到@Ryan Currah的启发,我发现这个开箱即用:

class M2MSelect(forms.SelectMultiple):
    def render(self, name, value, attrs=None, choices=()):
        rendered = super(M2MSelect, self).render(name, value=value, attrs=attrs, choices=choices)
        return rendered.replace(u'multiple="multiple"', u'')

显示多对多中的第一个,保存时仅保留所选值。

答案 5 :(得分:0)

受@Ryan Currah 的启发,我找到了一种更简单的方法来做到这一点: 您只需要覆盖 SelectMultiple 类中的“allow_multiple_selected”属性

class M2MSelect(forms.SelectMultiple):
    allow_multiple_selected = False


class NewBetForm(forms.ModelForm):
    class Meta:
        model = Bet

    participants = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=User.objects.all())