Django's ModelForm unique_together validation

DjangoValidationModelform

Django Problem Overview


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.

Django Solutions


Solution 1 - Django

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.

Solution 2 - Django

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.

Solution 3 - Django

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?

Solution 4 - Django

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.

Solution 5 - Django

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.

Solution 6 - Django

You will need to do something like this:

def your_view(request):
    if request.method == 'GET':
        form = SolutionForm()
    elif request.method == 'POST':
        problem = ... # logic to find the problem instance
        solution = Solution(problem=problem) # or solution.problem = problem
        form = SolutionForm(request.POST, instance=solution)
        # the form will validate because the problem has been provided on solution instance
        if form.is_valid():
            solution = form.save()
            # redirect or return other response
    # show the form

Solution 7 - Django

If you want the error message to be a associated with the name field (and appear next to it):

def clean(self):
    cleaned_data = super().clean()
    name_field = 'name'
    name = cleaned_data.get(name_field)

    if name:
        if Solution.objects.filter(name=name, problem=self.problem).exists():
            cleaned_data.pop(name_field)  # is also done by add_error
            self.add_error(name_field, _('There is already a solution with this name.'))

    return cleaned_data

Solution 8 - Django

My solution is based off Django 2.1

Leave SolutionForm alone, have a save() method in Solution

class Solution(models.Model):
...
   def save(self, *args, **kwargs):
      self.clean()
      return super(Solution, self).save(*args, **kwargs)


  def clean():
      # have your custom model field checks here
      # They can raise Validation Error

      # Now this is the key to enforcing unique constraint
      self.validate_unique()
  

Calling full_clean() in save() does not work as the ValidationError will be unhandled

Solution 9 - Django

I needed to exclude the company field in my case and add it in the view's form_valid function. I ended up doing the following (taking inspiration from different answers). In my CreateView

    def form_valid(self, form):
        cleaned_data = form.cleaned_data
        user_company = self.request.user.profile.company
        if UnitCategory.objects.filter(code=cleaned_data['code'],
                                    company=user_company).exists():
            form.add_error('code',                           _(
                'A UnitCategory with this Code already exists for this company.'))
            return super(UnitCategoryCreateView, self).form_invalid(form)
        if UnitCategory.objects.filter(color=cleaned_data['color'],
                                    company=user_company).exists():
            form.add_error('color',                           _(
                'A UnitCategory with this Color already exists for this company.'))
            return super(UnitCategoryCreateView, self).form_invalid(form)
        form.instance.company = user_company
        return super(UnitCategoryCreateView, self).form_valid(form)

In my UpdateView I had to exclude the current instance of the object in checking if the query exist using exclude(pk=self.kwargs['pk'])

    def form_valid(self, form):
        cleaned_data = form.cleaned_data
        user_company = self.request.user.profile.company
        if UnitCategory.objects.filter(code=cleaned_data['code'],
                                       company=user_company).exclude(pk=self.kwargs['pk']).exists():
            form.add_error(
                'code', _('A UnitCategory with this Code already exists for this company.'))
            return super(UnitCategoryUpdateView, self).form_invalid(form)
        if UnitCategory.objects.filter(color=cleaned_data['color'],
                                       company=user_company).exclude(pk=self.kwargs['pk']).exists():
            form.add_error('color', _(
                'A UnitCategory with this Color already exists for this company.'))
            return super(UnitCategoryUpdateView, self).form_invalid(form)
        # Return form_valid if no errors raised
        # Add logged-in user's company as form's company field
        form.instance.company = user_company
        return super(UnitCategoryUpdateView, self).form_valid(form)

Not the cleanest solution I was hoping for, but thought it might benefit someone.

Solution 10 - Django

If you always want to check the uniqueness constraint (i.e. on every ModelForm you create), you can make sure the uniqueness constraint is always validated on the model:

# Always check name-problem uniqueness
def validate_unique(self, exclude=None):
    if exclude is None:
        exclude = []

    try:
        exclude.remove("problem")
    except ValueError:
        pass

    return super().validate_unique(exclude)

This works because the model's validate_unique method is called during form validation.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionsttwisterView Question on Stackoverflow
Solution 1 - DjangoJarmo JaakkolaView Answer on Stackoverflow
Solution 2 - DjangosttwisterView Answer on Stackoverflow
Solution 3 - DjangoDaniel RosemanView Answer on Stackoverflow
Solution 4 - DjangoboblefragView Answer on Stackoverflow
Solution 5 - DjangoSam HartsfieldView Answer on Stackoverflow
Solution 6 - DjangodouglazView Answer on Stackoverflow
Solution 7 - DjangoRisadinhaView Answer on Stackoverflow
Solution 8 - Djangomb_atxView Answer on Stackoverflow
Solution 9 - DjangoRami AlloushView Answer on Stackoverflow
Solution 10 - DjangogokubiView Answer on Stackoverflow