50

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!

purefanatic
  • 933
  • 2
  • 8
  • 23
  • 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. – johnklawlor Oct 16 '15 at 17:28
  • 9
    Not everyone uses forms. I consider this a massive flaw in Django. Does anyone have a better answer? – Vincent Buscarello Jan 11 '18 at 23:59

3 Answers3

67

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)
Alasdair
  • 298,606
  • 55
  • 578
  • 516
  • 13
    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! – purefanatic Nov 03 '11 at 11:00
  • 1
    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. – Racing Tadpole May 17 '13 at 12:01
  • @purefanatic `Model.save()` is not expected to raise `ValidationErrors` thus there's no way to validate it directly. – jnns Nov 03 '13 at 19:41
  • 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 prohibited` – Pushpak Dagade Apr 22 '20 at 11:37
  • @PushpakDagade Using `fields = '__all__'` [can lead to security issues](https://docs.djangoproject.com/en/3.0/topics/forms/modelforms/#selecting-the-fields-to-use). I've added an explicit whitelist `fields = ['words', 'language']` to the example above. – Alasdair Apr 22 '20 at 11:52
  • Saved me from implementing pre_save() signal, Thank you. – Faizan Ahmad Jan 12 '23 at 12:48
  • Good Answer! Note that you must use `self.cleaned_data.get('words')` instead of `self.words` in model form. – alitayyeb Mar 14 '23 at 11:34
  • So If I use Forms and API Serializers, I need to do the validation twice? Once for the Form and second for API? – Jan Krupa Aug 03 '23 at 12:35
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.

M.Void
  • 2,764
  • 2
  • 29
  • 44
  • 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()`. – vlz Jan 13 '23 at 13:32
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.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444