djangodjango-modelsdjango-validation

Django: how to access ManyToManyField assignments before .save() method (during model .clean() method)


Background

Goal

I have a Model that needs to be associated with different objects depending on its type, so I'm validating this in the Model's .clean() method. If it's a "Dialogue" type, then it should only have "dialogue" objects (ForeignKeyField) associated with it. If it's a "Mixed" type, then it should only have "mixed_chunk" (ManyToManyField with a through model) objects associated with it.

Problem

However, I'm running into issues validating the ManyToManyField ("mixed_chunks") because the relationship cannot be accessed before the object is saved and receives an id.

With the code below, when I test in the Admin and try to save a "D" type Evaluation object without attaching any mixed_chunks objects (this is done through an inline in the Admin), the conditional if self.mixed_chunks in my custom .clean() method for the Evaluation model evaluates as true even though I haven't selected any mixed_chunks objects, therefore raising my validation error raise ValidationError(_("Only dialogues allowed to be attached for a 'Dialogue'-type eval")).

Things I've tried

I've tried to print some things out to see what is going on:

This tells me that the object is not yet associated with the ManyToManyField objects that I'm assigning to it in the admin at the time I hit "submit" on the form. How can I perform validation if they aren't accessible when the .clean() method is called? If the answer to this question is correct, then it looks like there isn't a way.

I tried calling self.save() in my .clean() method but the print results above were exactly the same

I also don't think it has something to do with the admin itself because this happens no matter how I try to create a new Evaluation object.

Code

models.py

from scenario.models import Dialogue

class MixedEvalChunk(models.Model):
    difficulty = models.CharField(max_length=15)
    # bunch of other fields...

class EvalMixedEvalChunkCombo(models.Model):
    eval = models.ForeignKey(
        'Evaluation',
        on_delete=models.CASCADE,
        related_name="e_eval_mixed_chunk_combos"
    )
    mixed_eval_chunk = models.ForeignKey(
        MixedEvalChunk,
        on_delete=models.CASCADE,
        related_name="m_eval_mixed_chunk_combos"
    )

class Evaluation(models.Model):
    class EVAL_TYPES(models.TextChoices):
        D = 'D', _('Dialogue')
        S = 'S', _('Simultaneous')
        T = 'T', _('Translation')
        M = 'M', _('Mixed')
    type = models.CharField(max_length=1, choices=EVAL_TYPES.choices, default='D')

    dialogue = models.ForeignKey(
        Dialogue,
        blank=True,
        null=True,
        on_delete=models.SET_NULL
    )

    mixed_chunks = models.ManyToManyField(
        MixedEvalChunk,
        blank=True,
        through=EvalMixedEvalChunkCombo
    )

    def clean(self):
        if self.type == 'D':
            if self.mixed_chunks:
                raise ValidationError(_("Only dialogues allowed to be attached for a 'Dialogue'-type eval"))
        elif self.type == 'M':
            if self.dialogue:
                raise ValidationError(
                    {"dialogue": _("Only MIXED EVAL CHUNKS allowed to be attached for a 'Mixed'-type eval"),}
                )

admin.py

class MixedEvalChunkInlineAdmin(admin.TabularInline):
    model = Evaluation.mixed_chunks.through
    extra = 0

@admin.register(MixedEvalChunk)
class MixedEvalChunkAdmin(admin.ModelAdmin):
    pass

@admin.register(EvalMixedEvalChunkCombo)
class EvalMixedEvalChunkComboAdmin(admin.ModelAdmin):
    list_display = ["""bunch of stuff.."""]
    fieldsets = ["""bunch of stuff.."""]

@admin.register(Evaluation)
class EvaluationAdmin(admin.ModelAdmin):
    fieldsets = ["""bunch of stuff.."""]
    inlines = (MixedEvalChunkInlineAdmin,)

Solution

  • First: if self.mixed_chunks: doesn't evaluate to False, because mixed_chunks returns a related manager, not a queryset. You still have to ask it to query something, like .exists() or .all(), for your conditional check to work against the data as intended.

    Try if self.mixed_chunks.exists(): and see if that resolves your other issues with validation.

    Update:

    Given your error message:

    <object> needs to have a value for field “id” before this many-to-many relationship can be used

    ...the issue you're running into is a bit clearer. Reverse relationships emit additional queries when accessed (or when prefetched explicitly). They do need a primary key to exist before any foreign keys can reference them - including those in many-to-many relationships.

    By definition, your Evaluation records are allowed to exist independently of their mixed_chunk relationships. It's not the Evaluation model that needs to be responsible for the validation of the many-to-many data - it's the EvalMixedEvalChunkCombo model.

    Try putting the clean() method on the EvalMixedEvalChunkCombo instead. Note that I've renamed the field to evaluation, because eval is a Python built-in and should be avoided as a variable name:

    class EvalMixedEvalChunkCombo(models.Model):
        def clean(self):
            if self.evaluation.type == 'D':
                raise ValidationError
            ...
    

    Other comments: