Django ManyToMany model validation
Asked Answered
W

3

52

I have a model with a ManyToManyField similar to this one (the model Word has a language, too):

class Sentence(models.Model):
    words = models.ManyToManyField(Word)
    language = models.ForeignKey(Language)
    def clean(self):
        for word in self.words.all():
            if word.language_id != self.language_id:
                raise ValidationError('One of the words has a false language')

When trying to add a new sentence (e.g. through django admin) I get 'Sentence' instance needs to have a primary key value before a many-to-many relationship can be used. This means I can't access self.words before saving it, but this is exactly what I'm trying to do. Is there any way to work around this so you can validate this model nevertheless? I really want to directly validate the model's fields.

I found many questions concerning this exception, but I couldn't find help for my problem. I would appreciate any suggestions!

Witha answered 2/11, 2011 at 19:52 Comment(2)
If you wanted to create a Word, how would you validate that it was associated with a Sentence? It does not have a 'sentence' field in its model definition.Elkins
Not everyone uses forms. I consider this a massive flaw in Django. Does anyone have a better answer?Belay
H
72

It is not possible to do this validation in the model's clean method, but you can create a model form which can validate the choice of words.

from django import forms

class SentenceForm(forms.ModelForm):
    class Meta:
        model = Sentence
        fields = ['words', 'language']

    def clean(self):
        """
        Checks that all the words belong to the sentence's language.
        """
        words = self.cleaned_data.get('words')
        language = self.cleaned_data.get('language')
        if language and words:
            # only check the words if the language is valid
            for word in words:
                if words.language != language:
                    raise ValidationError("The word %s has a different language" % word)
        return self.cleaned_data

You can then customise your Sentence model admin class, to use your form in the Django admin.

class SentenceAdmin(admin.ModelAdmin):
    form = SentenceForm

admin.register(Sentence, SentenceAdmin)
Heretofore answered 2/11, 2011 at 20:30 Comment(8)
It is regrettable to see there is no possibility to validate it directly in the model. But so far a custom ModelForm is enough for me. Thank you for your answer!Witha
Also, if your Sentence form can also be seen as an inline in another model (e.g. a Paragraph), you will want to add the line form = SentenceForm to the SentenceInline class as well.Declassify
@Witha Model.save() is not expected to raise ValidationErrors thus there's no way to validate it directly.Striped
With django 2.2.4, I had to also add fields = '__all__' in the meta class, else it throws the following error - django.core.exceptions.ImproperlyConfigured: Creating a ModelForm without either the 'fields' attribute or the 'exclude' attribute is prohibitedVhf
@PushpakDagade Using fields = '__all__' can lead to security issues. I've added an explicit whitelist fields = ['words', 'language'] to the example above.Heretofore
Saved me from implementing pre_save() signal, Thank you.Blessing
Good Answer! Note that you must use self.cleaned_data.get('words') instead of self.words in model form.Serai
So If I use Forms and API Serializers, I need to do the validation twice? Once for the Form and second for API?Issi
P
7

According to Django docs you can listen to the m2m_changed signal, which will trigger pre_add and post_add actions.

Using add() with a many-to-many relationship, however, will not call any save() methods (the bulk argument doesn’t exist), but rather create the relationships using QuerySet.bulk_create(). If you need to execute some custom logic when a relationship is created, listen to the m2m_changed signal, which will trigger pre_add and post_add actions.

Pneumonoultramicroscopicsilicovolcanoconiosis answered 29/5, 2019 at 21:17 Comment(1)
This certainly helps, but notice that a ValidationError raised in the signal handler will not be neatly shown in the admin as if it was thrown from a models or forms clean().Travel
D
1

You can't do it from the clean method on the model. It's simply not possible with the way M2M relationships work in Django. However, you can do this sort of validation on forms used to create a Sentence such as in the admin or a form on your site.

Dolph answered 2/11, 2011 at 20:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.