Django's ModelForm unique_together validation
I have a Django model that looks like this.
class Solution(models.Model):
'''
Represents a solution to a specific problem.
'''
name = models.CharField(max_length=50)
problem = models.ForeignKey(Problem)
description = models.TextField(blank=True)
date = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("name", "problem")
I use a form for adding models that looks like this:
class SolutionForm(forms.ModelForm):
class Meta:
model = Solution
exclude = ['problem']
My problem is that the SolutionForm
does not validate Solution
's unique_together
constraint and thus, it returns an IntegrityError
when trying to save the form. I know that I could use validate_unique
to manually check for this but I was wondering if there's any way to catch this in the form validation and return a form error automatically.
Thanks.
I solved this same problem by overriding the validate_unique()
method of the ModelForm:
def validate_unique(self):
exclude = self._get_validation_exclusions()
exclude.remove('problem') # allow checking against the missing attribute
try:
self.instance.validate_unique(exclude=exclude)
except ValidationError, e:
self._update_errors(e.message_dict)
Now I just always make sure that the attribute not provided on the form is still available, e.g. instance=Solution(problem=some_problem)
on the initializer.
I managed to fix this without modifying the view by adding a clean method to my form:
class SolutionForm(forms.ModelForm):
class Meta:
model = Solution
exclude = ['problem']
def clean(self):
cleaned_data = self.cleaned_data
try:
Solution.objects.get(name=cleaned_data['name'], problem=self.problem)
except Solution.DoesNotExist:
pass
else:
raise ValidationError('Solution with this Name already exists for this problem')
# Always return cleaned_data
return cleaned_data
The only thing I need to do now in the view is to add a problem property to the form before executing is_valid
.
As Felix says, ModelForms are supposed to check the unique_together
constraint in their validation.
However, in your case you are actually excluding one element of that constraint from your form. I imagine this is your problem - how is the form going to check the constraint, if half of it is not even on the form?
the solution from @sttwister is right but can be simplified.
class SolutionForm(forms.ModelForm):
class Meta:
model = Solution
exclude = ['problem']
def clean(self):
cleaned_data = self.cleaned_data
if Solution.objects.filter(name=cleaned_data['name'],
problem=self.problem).exists():
raise ValidationError(
'Solution with this Name already exists for this problem')
# Always return cleaned_data
return cleaned_data
As a bonus you do not retreive the object in case of duplicate but only check if it exists in the database saving a little bit of performances.
With the help of Jarmo's answer, the following seems to work nicely for me (in Django 1.3), but it's possible I've broken some corner case (there are a lot of tickets surrounding _get_validation_exclusions
):
class SolutionForm(forms.ModelForm):
class Meta:
model = Solution
exclude = ['problem']
def _get_validation_exclusions(self):
exclude = super(SolutionForm, self)._get_validation_exclusions()
exclude.remove('problem')
return exclude
I'm not sure, but this seems like a Django bug to me... but I'd have to look around the previously-reported issues.
Edit: I spoke too soon. Maybe what I wrote above will work in some situations, but not in mine; I ended up using Jarmo's answer directly.