Search code examples
pythondjangodjango-modelsmany-to-manydjango-2.2

Use ManyToMany field in add object raise needs to have a value for field "id" before this many-to-many relationship can be used


I have two models : Exam and Question. Each question has point that automatically calculate in each exam.

So this is my files:

#Models.py
class Exam(models.Model):
    questions = models.ManyToManyField(Question)
    title = models.CharField()
class Question(models.Model):
    title = models.CharField()
    answer = models.TextField()
    points = models.PositiveSmallIntegerField()

#Forms.py
class ExamForm(ModelForm):
    class Meta:
        model = Exam
        fields = '__all__'

#Views.py
if form.is_valid():
    new_exam = form.save(commit=False)
    # Some modify goes here. 
    new_exam.save()
    form.save_m2m()
    return redirect('view_exam')

I did customize save() method of Exam model to this:

 def save(self, *args, **kwargs):
        self.point = 0
        for question in self.questions:
                self.point += question.point
        super(Exam, self).save(*args, **kwargs)

But I got this error:

"<Exam: NewObject>" needs to have a value for field "id" before this many-to-many relationship can be used.

How can I do this without raising any error?

My goal: For each new exam that created, calculate the points of questions of this exam and put them into the points field of Exam model.


Solution

  • It's never a good idea to save things to your model that can be calculated from other fields/tables in your database, especially if this depends on other models. Too easy to forget to update the value when at some stage you create a new Question for example. You'll just get inconsistency in your db.

    Delete your custom save() method because it doesn't do anything.

    If you want to know the total number of points, add a custom getter on Exam that calculates this on the fly:

    #At the first of models.py -> from django.db.models import Sum 
    @property
    def points(self):
        if self.pk:
           return self.questions.all().aggregate(Sum('points'))['points__sum']
        else:
           return 0
    

    Or with your kind of summation:

    @property
    def points(self):
        if self.pk:
            point = 0
            questions = self.questions.all()
            for question in questions :
                point += question.point
            return point
        else:
            return 0
    

    With this property, you can do exam.points anywhere in your code and it will be up-to-date.