Validating m2m relationship

I have the following models:

class Question(Model):
    # ...

class Choice(Model):
    question = ForeignKey(Question)
    # ...

class AnswerToQuestion(Model):
    user = ForeignKey(User)
    question = ForeignKey(Question)
    selected_choices = ManyToManyField(Choice)

What I would like to do is make sure that the values inside of selected_choices will always be choices belonging to the question stored in the question field.

This is what I tried in the clean method of AnswerToQuestion:

    def clean(self):
        super().clean()
        if self.pk and self.selected_choices.exclude(question=self.question).exists():
            raise ValidationError("Invalid choice(s)")

However, this does not work as expected: since the m2m relationship is updated after the actual model save, if I try and associate an invalid choice, this will work fine, and then every subsequent save will fail because I already have an invalid associated choice.

Is there another way to achieve what I’m trying to do?

One option is to normalize the relationship one level further.

class Choice:
    pass

class Question:
    choices = ManyToManyField(Choice, through='QuestionChoice')

class QuestionChoice
    question = ForeignKey(question)
    choice = ForeignKey(choice)

class AnswerToQuestion:
    question = ForeignKey(Question)
    selected_choices = ManyToManyField(QuestionChoice)

You’d still have issues when changing a question’s choices with existing answers already. You would need to decide what do you do with those selections, do they get deleted, migrated to another selection, etc.

I’ve always found modeling a questionnaire to be really difficult. I keep getting closer and closer to switching a NoSQL approach for this.

I don’t dislike the idea in general, but in my case I really wouldn’t want to create a through model to keep the complexity down: in my application, I really just use selected_choice as an array, conceptually; also, I would have to mess with the way my serializers assign to the field.

I believe there has to be a way to perform validation on the field: for example, if you supply the PK of a model that doesn’t exist at all, Django is able to catch and report a ValidationError, so some additional code might be inserted at that spot, if I can identify it.

Agreed. My whole application is an LMS so most of the data models stuff that is questionnaire-related, and some things have been pretty hard to design.

I ended up restricting the queryset of the PrimaryKeyRelatedField in my serializer for AnswerToQuestion. This doesn’t stop assigning wrong choices “everywhere”, but at least prevents user from doing so when interacting with the API.

In the __init__ method of the serializer, after doing some manipulation of the kwargs passed to the field, I now do this, exploiting the fact that model serializers are (sometimes) passed the model instance as first argument:

try:
    answer_to_question_instance = args[0]
except:
    # DRF sometimes seems to call serializers spuriously without passing instance
    answer_to_question_instance = None
  
selected_choices_kwargs["queryset"] = (
    Choice.objects.all()
    if answer_to_question_instance is None
    else Choice.objects.filter(
        question_id=answer_to_question_instance.question_id
    )
)

This is then passed as an argument to fields['selected_choices'].