web-dev-qa-db-fra.com

Django's ModelForm validation unique_together

J'ai un modèle Django qui ressemble à ceci.

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

J'utilise un formulaire pour ajouter des modèles qui ressemble à ceci:

class SolutionForm(forms.ModelForm):
    class Meta:
        model = Solution
        exclude = ['problem']

Mon problème est que le SolutionForm ne valide pas le Solutionunique_together contrainte et donc, elle renvoie un IntegrityError lors de la tentative d'enregistrement du formulaire. Je sais que je pourrais utiliser validate_unique pour vérifier manuellement cela, mais je me demandais s'il y avait un moyen d'attraper cela dans la validation du formulaire et de renvoyer automatiquement une erreur de formulaire.

Merci.

60
sttwister

J'ai réussi à résoudre ce problème sans modifier la vue en ajoutant une méthode propre à mon formulaire:

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

La seule chose que je dois faire maintenant dans la vue est d'ajouter une propriété de problème au formulaire avant d'exécuter is_valid.

18
sttwister

J'ai résolu ce même problème en remplaçant la méthode validate_unique() du 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)

Maintenant, je m'assure toujours que l'attribut non fourni sur le formulaire est toujours disponible, par exemple instance=Solution(problem=some_problem) sur l'initialiseur.

36
Jarmo Jaakkola

Comme le dit Felix, les ModelForms sont censés vérifier le unique_together contrainte dans leur validation.

Cependant, dans votre cas, vous excluez en fait un élément de cette contrainte de votre formulaire. J'imagine que c'est votre problème - comment le formulaire va-t-il vérifier la contrainte, si la moitié n'est même pas sur le formulaire?

23
Daniel Roseman

la solution de @sttwister est correcte mais peut être simplifiée.

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

En bonus, vous ne récupérez pas l'objet en cas de doublon mais vérifiez seulement s'il existe dans la base de données en économisant un peu de performances.

7
boblefrag

Avec l'aide de la réponse de Jarmo, ce qui suit semble bien fonctionner pour moi (dans Django 1.3), mais il est possible que j'ai cassé un cas de coin (il y a beaucoup de tickets entourant _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

Je ne suis pas sûr, mais cela semble être un bug Django pour moi ... mais je devrais regarder autour des problèmes signalés précédemment.


Edit: j'ai parlé trop tôt. Peut-être que ce que j'ai écrit ci-dessus fonctionnera dans certaines situations, mais pas dans le mien; J'ai fini par utiliser directement la réponse de Jarmo.

1
Sam Hartsfield

Ma solution est basée sur Django 2.1

Laissez SolutionForm tranquille, ayez une méthode save () dans 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()

L'appel de full_clean () dans save () ne fonctionne pas car la ValidationError ne sera pas gérée

0
mb_atx

Vous devrez faire quelque chose comme ceci:

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
0
douglaz

Si vous souhaitez que le message d'erreur soit associé au champ name (et apparaisse à côté):

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
0
Risadinha