I have the following two models:
class Exercise(models.Model):
MULTIPLE_CHOICE_SINGLE_POSSIBLE = 0
MULTIPLE_CHOICE_MULTIPLE_POSSIBLE = 1
EXERCISE_TYPES = (
(MULTIPLE_CHOICE_SINGLE_POSSIBLE, "Multiple choice, single possible"),
(MULTIPLE_CHOICE_MULTIPLE_POSSIBLE, "Multiple choice, multiple possible"),
)
parent = models.ForeignKey(
"Exercise",
null=True,
blank=True,
related_name="sub_exercises",
on_delete=models.CASCADE,
)
exercise_type = models.PositiveSmallIntegerField(choices=EXERCISE_TYPES)
text = models.TextField(blank=True)
class Choice(models.Model):
exercise = models.ForeignKey(
Exercise, related_name="choices", on_delete=models.CASCADE
)
text = models.TextField()
correct = models.BooleanField()
MULTIPLE_CHOICE_SINGLE_POSSIBLE
represents a multiple choice question where the choices are selectable via a radio button, i.e. only one is correct; MULTIPLE_CHOICE_MULTIPLE_POSSIBLE
pretty intuitively represents a question where more than one choice can be selected, via a checkbox.
Under the hood, for a MULTIPLE_CHOICE_MULTIPLE_POSSIBLE
exercise, my custom manager creates a sub-exercise that has type MULTIPLE_CHOICE_SINGLE_POSSIBLE
and a single choice. This helps me in other areas of the application.
This test case should clear up what I’m saying,
def test_multiple_choice_multiple_possible_exercise_creation(self):
choices = [
{"text": "c1", "correct": True},
{"text": "c2", "correct": False},
{"text": "c3", "correct": False},
]
e1 = Exercise.objects.create(
text=self.e1_text,
choices=choices,
exercise_type=Exercise.MULTIPLE_CHOICE_MULTIPLE_POSSIBLE,
)
self.assertEqual(e1.text, self.e1_text)
self.assertEqual(e1.exercise_type, Exercise.MULTIPLE_CHOICE_MULTIPLE_POSSIBLE)
self.assertEqual( # one sub-exercise is created for each choice
e1.sub_exercises.count(),
len(choices),
)
self.assertEqual( # no choices are directly related to the parent exercise
e1.choices.count(), 0
)
i = 0
for sub_exercise in e1.sub_exercises.all():
# the automatically created sub-exercises have empty text
self.assertEqual(sub_exercise.text, "")
# the automatically created sub-exercises have a single choice
self.assertEqual(sub_exercise.choices.count(), 1)
choice = sub_exercise.choices.first()
# the created choices are the same ones supplied for the parent exercise
self.assertEqual(
{"text": choice.text, "correct": choice.correct}, choices[i]
)
i += 1
This works fine for me, but I have one issues: I want to be able to access an exercise’s choices with a uniform API, regardless of whether it’s of the first or second type.
With a single-possible choice exercise, I can just do my_exercise.choices.all()
, whereas in the other case I need to build a list by iterating over the sub-exercises like this: [e.choices.first() for e in my_exercise.sub_exercises.all()]
.
I could just declare a property that returns the correct set of choices depending on the exercise type, but then I would still have the issue of having two different calls to add/delete choices depending on the type of exercise–In the first case, I can just call my_exercise.choices.add(**new_choice)
, whereas in the second case I need to manually create a new exercise with a choice and make it a sub-exercise of the one I’m trying to add a choice to.
Is there a place where I can put this logic into so that I can do stuff like this?
single_possible_choice_exercise.choices.add(**my_choice) # creates a Choice object
multiple_possible_choice_exercise.choices.add(**my_choice) # creates an Exercise object with a Choice object
I have read that accessing the reverse side of a one-to-many relationship returns a RelatedManager, but I haven’t found much on how to customize it. I feel like that would be the most appropriate place to put this logic.