为什么Django不强制我的unique_together约束作为form.ValidationError而不是抛出异常?

时间:2010-10-27 15:28:13

标签: django validation django-forms modelform

编辑:虽然这篇文章是Django's ModelForm unique_together validation的副本,但是从模型框架中删除'排除'的接受答案是比其他问题中接受的答案更清晰的解决方案。

这是this question的后续行动。

如果我没有显式检查clean_title()函数中的unique_together约束,则django会抛出异常:

  

/ journal / journal / 4

中的IntegrityError      

重复键值违反了唯一约束“journal_journal_owner_id_key”

     

请求方法:POST

     

请求网址:http://localhost:8000/journal/journal/4

     

异常类型:IntegrityError

     

异常值:重复键值违反唯一约束“journal_journal_owner_id_key”

     

异常位置:/Library/Python/2.6/site-packages/django/db/backends/util.py执行中,第19行

然而,我的印象是Django会通过引发ValidationError来很好地强制执行此约束,而不是我需要捕获的异常。

下面是我的代码,其中包含一个额外的clean_title()方法,我将其用作解决方法。但我想知道我做错了什么,以致django没有以预期的方式强制执行约束。

感谢。

型号代码:

class Journal (models.Model):
    owner = models.ForeignKey(User, related_name='journals')
    title = models.CharField(null=False, max_length=256)
    published = models.BooleanField(default=False)

    class Meta:
        unique_together = ("owner", "title")

    def __unicode__(self):
        return self.title 

表格代码:

class JournalForm (ModelForm):
    class Meta:
        model = models.Journal
        exclude = ('owner',)

    html_input = forms.CharField(label=u'Journal Content:', widget=TinyMCE(attrs={'cols':'85', 'rows':'40'}, ), )

    def clean_title(self):
        title = self.cleaned_data['title']
        if self.instance.id:
            if models.Journal.objects.filter(owner=self.instance.owner, title=title).exclude(id=self.instance.id).count() > 0:
               raise forms.ValidationError(u'You already have a Journal with that title. Please change your title so it is unique.')
        else:
            if models.Journal.objects.filter(owner=self.instance.owner, title=title).count() > 0:
               raise forms.ValidationError(u'You already have a Journal with that title. Please change your title so it is unique.')
        return title

查看代码:

def journal (request, id=''):
    if not request.user.is_active:
        return _handle_login(request)
    owner = request.user
    try:
        if request.method == 'GET':
            if '' == id:
                form = forms.JournalForm(instance=owner)
                return shortcuts.render_to_response('journal/Journal.html', { 'form':form, })
            journal = models.Journal.objects.get(id=id)
            if request.user.id != journal.owner.id:
                return http.HttpResponseForbidden('<h1>Access denied</h1>')
            data = {
                'title' : journal.title,
                'html_input' : _journal_fields_to_HTML(journal.id),
                'published' : journal.published
            }
            form = forms.JournalForm(data, instance=journal)
            return shortcuts.render_to_response('journal/Journal.html', { 'form':form, })
        elif request.method == 'POST':
            if LOGIN_FORM_KEY in request.POST:
                return _handle_login(request)
            else:
                if '' == id:
                    journal = models.Journal()
                    journal.owner = owner
                else:
                    journal = models.Journal.objects.get(id=id)
                form = forms.JournalForm(data=request.POST, instance=journal)
                if form.is_valid():
                    journal.owner = owner
                    journal.title = form.cleaned_data['title']
                    journal.published = form.cleaned_data['published']
                    journal.save()
                    if _HTML_to_journal_fields(journal, form.cleaned_data['html_input']):
                        html_memo = "Save successful."
                    else:
                        html_memo = "Unable to save Journal."
                    return shortcuts.render_to_response('journal/Journal.html', { 'form':form, 'saved':html_memo})
                else:
                    return shortcuts.render_to_response('journal/Journal.html', { 'form':form })
        return http.HttpResponseNotAllowed(['GET', 'POST'])
    except models.Journal.DoesNotExist:
        return http.HttpResponseNotFound('<h1>Requested journal not found</h1>')

更新工作代码: 感谢Daniel Roseman。

模型代码保持与上面相同。

表单代码 - 删除exclude语句和clean_title函数:

class JournalForm (ModelForm):
    class Meta:
        model = models.Journal

    html_input = forms.CharField(label=u'Journal Content:', widget=TinyMCE(attrs={'cols':'85', 'rows':'40'},),)

查看代码 - 添加自定义唯一性错误消息:

def journal (request, id=''):
    if not request.user.is_active:
        return _handle_login(request)
    try:
        if '' != id:
            journal = models.Journal.objects.get(id=id)
            if request.user.id != journal.owner.id:
                return http.HttpResponseForbidden('<h1>Access denied</h1>')
        if request.method == 'GET':
            if '' == id:
                form = forms.JournalForm()
            else:
                form = forms.JournalForm(initial={'html_input':_journal_fields_to_HTML(journal.id)},instance=journal)
            return shortcuts.render_to_response('journal/Journal.html', { 'form':form, })
        elif request.method == 'POST':
            if LOGIN_FORM_KEY in request.POST:
                return _handle_login(request)
            data = request.POST.copy()
            data['owner'] = request.user.id
            if '' == id:
                form = forms.JournalForm(data)
            else:
                form = forms.JournalForm(data, instance=journal)
            if form.is_valid():
                journal = form.save()
                if _HTML_to_journal_fields(journal, form.cleaned_data['html_input']):
                    html_memo = "Save successful."
                else:
                    html_memo = "Unable to save Journal."
                return shortcuts.render_to_response('journal/Journal.html', { 'form':form, 'saved':html_memo})
            else:
                if form.unique_error_message:
                    err_message = u'You already have a Lab Journal with that title. Please change your title so it is unique.'
                else:
                    err_message = form.errors
                return shortcuts.render_to_response('journal/Journal.html', { 'form':form, 'error_message':err_message})
        return http.HttpResponseNotAllowed(['GET', 'POST'])
    except models.Journal.DoesNotExist:
        return http.HttpResponseNotFound('<h1>Requested journal not found</h1>')

2 个答案:

答案 0 :(得分:9)

麻烦的是你专门排除了唯一检查中涉及的一个字段,并且Django在这种情况下不会运行检查 - 请参阅{{1}的第722行中的_get_unique_checks方法}。

我不会排除django.db.models.base字段,而是考虑将其从模板中删除,并在实例化时传递的数据上明确设置值:

owner

请注意,您并未真正使用此模型的强大功能。您不需要在初始GET上显式设置数据字典 - 实际上,您不应该在那里传递 data = request.POST.copy() data['owner'] = request.user.id form = JournalForm(data, instance=journal) 参数,因为它会触发验证:如果您需要要传入与实例不同的值,您应该使用data代替。但大部分时间,仅仅通过initial就足够了。

而且,在POST上,您再次无需明确设置值:您可以这样做:

instance

将正确更新实例并将其返回。

答案 1 :(得分:2)

我认为这里的哲学是unique_together是一个ORM概念,而不是形式的属性。如果您想为特定表单强制执行unique_together,您可以编写自己的clean方法,该方法简单,直接且非常灵活:

http://docs.djangoproject.com/en/dev/ref/forms/validation/#cleaning-and-validating-fields-that-depend-on-each-other

这将替换您编写的clean_title方法。