Why doesn't Django apply the unique_together constraint as form.ValidationError instead of throwing an exception?

Edit: Although this post is a duplicate of the Django ModelForm unique_together authentication , the accepted answer here about removing "exclude" from ModelForm is a much cleaner solution than the accepted answer in another question.

This is a continuation of this issue .

If I did not 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 journal_journal_owner_id_key constraint

Request Method: POST

Request URL: http: // localhost: 8000 / journal / journal / 4

Exception Type: IntegrityError

Exception value: a duplicate key value violates the unique journal_journal_owner_id_key constraint

Exception location: /Library/Python/2.6/site-packages/django/db/backends/util.py in execution, line 19

However, I got the impression that Django would effectively apply this restriction by raising a ValidationError, not the exception that I need to catch.

Below is my code with the optional clean_title () method, which I use to work. But I want to know what I'm doing wrong, so django does not apply the restriction in expected mode.

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

UPDATED WORK CODE: Thanks to Daniel Roseman.

The model code remains the same as above.

The code of the form is to remove the exclude statement and the 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 a unique 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>') 
+7
django validation django-forms modelform
source share
2 answers

The problem is that you specifically exclude one of the fields involved in the unique verification, and Django will not verify this circumstance - see the _get_unique_checks method on line 722 of django.db.models.base .

Instead of excluding the owner field, I would just leave it outside the template and explicitly set the value of the data that you pass when creating the instance:

  data = request.POST.copy() data['owner'] = request.user.id form = JournalForm(data, instance=journal) 

Please note that you are not using the model features here. You do not need to explicitly set the data dictionary in the original GET - and, in fact, you should not pass the data parameter there, since it causes a check: if you need to pass values ​​other than instance, you should use initial . But most of the time, just skipping instance .

And in POST again, you do not need to explicitly set values: you can just do:

 journal = form.save() 

which will correctly update the instance and return it.

+9
source share

I think the philosophy here is that unique_together is an ORM concept, not a form property. If you want to force use unique_together for a specific form, you can write your own clean method, which is simple, simple 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 wrote.

+2
source share

All Articles