首页 > 解决方案 > 在 ModelForm 中手动设置模型字段

问题描述

我有一个带有外键和唯一约束的模型,如下所示:

class Menu(models.Model):
    tournament = models.ForeignKey(Tournament, on_delete=models.CASCADE)
    name = models.CharField(max_length=128)
    date_menu = models.DateField()

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['tournament', 'name', 'date_menu'], name="unique_name_menu")
        ]

我想创建一个表单来添加菜单实例。然而,锦标赛的价值是由页面的 URL 设置的。我不希望用户能够设置它。

为此,我使用了一个 modelForm,不包括锦标赛字段:

class MenuForm(forms.ModelForm):

    date_menu = forms.DateField(initial=datetime.datetime.now())

    class Meta:
        model = Menu
        exclude = ['tournament']

这是我的观点:

def add_menu(request, tournament_slug):
    tournament = get_object_or_404(Tournament, slug=tournament_slug)
    form = MenuForm(request.POST or None)
    
    if form.is_valid():
        menu_id = form.save(commit=False)
        menu_id.tournament = Tournament.objects.get(pk=1)
        menu_id.save()  # I get the integrity error only here

        return HttpResponseRedirect(reverse('admin'))

    return render(request, "view.html", {'form': form, 'formset': formset, "tournament": tournament})

我的问题是,当我.is_valid()在此表单上调用该函数时,由于未设置锦标赛字段,因此无法检查唯一性条件。结果,在视图中调用保存函数时出现完整性错误。

问题是:在检查它是否有效之前,如何链接表单创建的 Menu 实例以添加锦标赛字段?如果这不是正确的做法,我该如何检查模型实例的唯一性并在需要时将相应的错误返回给模板?

我尝试将锦标赛字段作为隐藏字段包含在视图中,它可以工作,但我不知道这是否是最好的方法......

标签: pythondjangodjango-forms

解决方案


您应该使用未保存的实例简单地实例化表单,Menu因此您的视图应该如下所示:

def add_menu(request, tournament_slug):
    tournament = get_object_or_404(Tournament, slug=tournament_slug)
    if request.method == 'POST':
        form = MenuForm(request.POST, instance=Menu(tournament=tournament))
        if form.is_valid():
            menu_id = form.save()
            return HttpResponseRedirect(reverse('admin'))
    else:
        form = MenuForm(instance=Menu(tournament=tournament))
    return render(request, "view.html", {'form': form, "tournament": tournament})

表单还调用_get_validation_exclusions()并排除表单中不存在的字段进行验证。您可以尝试覆盖validate_unique以克服此问题:

class MenuForm(forms.ModelForm):

    date_menu = forms.DateField(initial=datetime.datetime.now())

    class Meta:
        model = Menu
        exclude = ['tournament']
    
    def validate_unique(self):
        exclude = self._get_validation_exclusions()
        if 'tournament' in exclude:
            exclude.remove('tournament') # Make sure `tournament` gets validated
        try:
            self.instance.validate_unique(exclude=exclude)
        except ValidationError as e:
            self._update_errors(e)

注意:我更改了您的视图结构以避免使用MenuForm(request.POST or None)which is an antipattern。(即使 POST 数据中没有发送任何内容,表单也可能有效,而您编写此类表单的方式将被视为无效)。

编辑validate_unique:正如评论中所讨论的,隐藏和禁用字段的选项可能比覆盖表单方法要好得多:

class MenuForm(forms.ModelForm):
    tournament = forms.ModelChoiceField(
        queryset=Tournament.objects.all(),
        widget=forms.HiddenInput(),
        disabled=True
    )
    date_menu = forms.DateField(initial=datetime.datetime.now())

    class Meta:
        model = Menu
        fields = ['tournament', 'name', 'date_menu']

推荐阅读