为什么 Django 不将我的 unique_together 约束强制作为 form.ValidationError 而不是抛出异常?
编辑:虽然这篇文章是Django的ModelForm unique_together验证的重复,此处接受的从 ModelForm 中删除“排除”的答案是比其他问题中接受的答案更干净的解决方案。
这是 这个问题。
如果我没有明确检查 clean_title() 函数中的 unique_together 约束,django 会抛出异常:
/journal/journal/4 处出现完整性错误
重复的键值违反了唯一约束“journal_journal_owner_id_key”
请求方式:POST
请求网址:http://localhost:8000/journal/journal/4< /p>
异常类型: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。
模型代码与上面相同。
表单代码 - 删除排除语句和 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>')
Edit: While this post is a duplicate of Django's ModelForm unique_together validation, the accepted answer here of removing the 'exclude' from the ModelForm is a much cleaner solution than the accepted answer in the other question.
This is a follow-up to this question.
If I don't explicitly check the unique_together constraint in the clean_title() function, django throws an exception:
IntegrityError at /journal/journal/4
duplicate key value violates unique constraint "journal_journal_owner_id_key"
Request Method: POST
Request URL: http://localhost:8000/journal/journal/4
Exception Type: IntegrityError
Exception Value: duplicate key value violates unique constraint "journal_journal_owner_id_key"
Exception Location: /Library/Python/2.6/site-packages/django/db/backends/util.py in execute, line 19
However I was under the impression that Django would enforce this constraint nicely by raising a ValidationError, not with an exception I need to catch.
Below is my code with an additional clean_title() method I use as a work-around. But I want to know what I'm doing wrong such that django is not enforcing the constraint in the expected manner.
Thanks.
Model code:
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
Form code:
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
View Code:
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>')
UPDATE WORKING CODE:
Thanks to Daniel Roseman.
Model code stays the same as above.
Form code - remove exclude statement and clean_title function:
class JournalForm (ModelForm):
class Meta:
model = models.Journal
html_input = forms.CharField(label=u'Journal Content:', widget=TinyMCE(attrs={'cols':'85', 'rows':'40'},),)
View Code - add custom uniqueness error message:
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>')
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
问题在于,您专门排除了唯一检查中涉及的字段之一,并且 Django 在这种情况下不会运行检查 - 请参阅 django 第 722 行中的 _get_unique_checks 方法.db.models.base。
我不会排除
owner
字段,而是考虑将其排除在模板之外,并在实例化时传入的数据上显式设置值:请注意,您并没有真正使用该功能此处的模型形式。您不需要在初始 GET 上显式设置数据字典 - 事实上,您不应该在那里传递
data
参数,因为它会触发验证:如果您需要传入与实例不同的值,则应使用initial
代替。但大多数时候,只需传递instance
就足够了。并且,在 POST 上,您不需要显式设置值:您只需执行以下操作即可:
这将正确更新实例并返回它。
The trouble is that you're specifically excluding one of the fields involved in the unique check, and Django won't run the check in this circumstance - see the
_get_unique_checks
method in line 722 ofdjango.db.models.base
.Instead of excluding the
owner
field, I would consider just leaving it out of the template and setting the value explicitly on the data you're passing in on instantiation:Note that you're not really using the power of the modelform here. You don't need to explicitly set the data dictionary on the initial GET - and, in fact, you shouldn't pass a
data
parameter there, as it triggers validation: if you need to pass in values that are different to the instance's, you should useinitial
instead. But most of the time, just passinginstance
is enough.And, on POST, again you don't need to set the values explicitly: you can just do:
which will update the instance correctly and return it.
我认为这里的哲学是 unique_together 是一个 ORM 概念,而不是表单的属性。如果您想对特定表单强制执行 unique_together,您可以编写自己的干净方法,该方法简单、直接且非常灵活:
http://docs.djangoproject.com/en/dev/ref/forms/validation/# clean-and-validating-fields-that-depend-on-each-other
这将替换您编写的 clean_title 方法。
I think the philosophy here is that unique_together is an ORM concept, not a property of a form. If you want to enforce unique_together for a particular form, you can write your own clean method, which is easy, straightforward, and very flexible:
http://docs.djangoproject.com/en/dev/ref/forms/validation/#cleaning-and-validating-fields-that-depend-on-each-other
This will replace the clean_title method you have written.