saving object then saving m2m relationship

I have a view which creates and then saves a new object. Then I want to save the object’s m2m relationship. However, this gives an error. If I remove the m2m save in the view, I can manually save m2m relationship later.

Playing around with this, and shown in the code below, I now have the view create and save Student object working because the m2m save classblock.students.add(new_student) is commented out. I then run this view again, and since the student objects are already created, the view executes the classblock.students.add(new_student) in the else case, and this works. The existing students get added to the classblock m2m relationship.

Is there some kind of timing issue with when you first save an object, and then add it to a m2m relationship? How do I work around this?

models

class Student(models.Model):
	user = models.OneToOneField(CustomUser, on_delete=models.CASCADE, primary_key=True)
    student_first = models.CharField(max_length=30)
    student_last = models.CharField(max_length=30)
    nickname = models.CharField(max_length=31)
    fullname = models.CharField(max_length=60)
    attend = models.BooleanField(default=True)
    do_not_pick = models.BooleanField(default=False)
    student_number = models.IntegerField()
    email = models.EmailField(max_length=50)

class Classroom(models.Model):
    """The gradebook is split into courses, classes and students"""
    classroom_name = models.CharField(max_length=10)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    students = models.ManyToManyField(Student)
	user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)

view

def addmultistudent(request, classroom_id):
    """Add multiple students at once."""
    classblock = get_object_or_404(Classroom, pk=classroom_id)
    context = {'classblock': classblock}
    
    if request.method == 'POST':
        form = StudentInputForm(request.POST)

        if form.is_valid():
            s = form.save(commit=False)
            input_list = []
            input_list = s.name_list.split('\n')

            # the for returns a list of students with student number, fisrt_name, last_name in each line
            for line in input_list:
                # remove the carriage return
                line = line.strip('\r')
                # removes double quotes
                line = line.translate({ord(c): None for c in '"'})
                # ignore the first line which is a header starting with "Pupil"
                if not line.startswith("Pupil"):
                    # get rid of the commas from the csv file
                    all_names = line.split(",")
                    # pop the studnet number
                    sn = all_names.pop(0)

                    # make sure the student not already been added
                    if not Student.objects.filter(student_number=sn).exists():
                        first = all_names.pop(0)
                        last = ""
                        for name in all_names:
                            last = last + name + " "
                        last = last[:-1]
                        # create the object and then add the attributes
                        email_add = sn + "@bc.ca"
                        nick = first + last[0]
                        full = first + " " + last
                        new_student = Student(student_last=last, student_first=first,
                                            student_number=sn, nickname=nick, fullname=full, email=email_add)
                        print(new_student)
                        print(classblock)
                        new_student.save()
                        # classblock.students.add(new_student)

# attach student to a user
                        if not User.objects.filter(username=sn):
                            user = User.objects.create_user(sn, email_add)
                            user.last_name = last
                            user.first_name = first
                            user.save()

                    # if the student has been added, we just need to add them to the new classroom
                    else:
                        new_student = Student.objects.get(student_number=sn)
                        classblock.students.add(new_student)


            form = StudentInputForm(None)
            context['form'] = form
            return render(request, "gradebook/addmultistudent.html", context)

        else:
            context['form'] = form
            return render(request, "gradebook/addmultistudent.html", context)

Hi shmish,

What is the error you encountered?

Cannot add “<Student: Doug Smith>”: the value for field “student” is None

That’s the error if I run this view with classblock.students.add(new_student) (where it is currently commented out in the code above).

How does the user field get set for the new students? You have it defined as a non-nullable relationship field that’s the primary key, but in that for loop it’s not being set. I’m surprised new_student.save() saves at all.

I’m really sorry, I didn’t correctly copy/paste part of the view. I’ve edited the original code, you can see how the user is being added (just after the classblock.students.add(new_student)).

From your response, I tried re-arranging the saves and this is now working.

# attach student to a user
                        if not User.objects.filter(username=sn):
                            user = User.objects.create_user(sn, email_add)
                            user.last_name = last
                            user.first_name = first
                            user.save()
                            new_student.user = user
                        
                        print(new_student)
                        print(classblock)
                        new_student.save()

                        classblock.students.add(new_student)

I create the User and now I can add the user field to the student before saving.

For the Student model, the line user = models.OneToOneField(CustomUser, on_delete=models.CASCADE, primary_key=True) is also fairly new to my code. Prior to adding the many2many relationship and user field, the view worked very well. I need users to access objects with user = current user, which is why I added the user OneToOne field. My next question was going to be about the primary_key=True because I’ve read the docs on it but I’m still confused about what it does. In other views, my (old) working code use to refer to student.id, but now that gives an error. I’ve had to replace student.id with student.pk. I’m not sure why.

Not a problem, it happens. I would expect the code to look like the following though:


                        # Attempt to fetch the user
                        user = User.objects.filter(username=sn).first()
                        if not user:
                            # The user doesn't exist, so create one.
                            user = User.objects.create_user(sn, email_add, last_name=last, first_name=first)
                        new_student = Student(
                                            # attach student to a user here
                                            user=user,
                                            student_last=last, student_first=first,
                                            student_number=sn, nickname=nick, fullname=full, email=email_add)
                        new_student.save()
                        classblock.students.add(new_student)

This is because defining a field with primary_key=True will result in the id AutoField not being added to the model (docs reference). As you found, using the pk property will expose the user id. I believe you could also use student.user_id

1 Like

Thanks. What are the advantages of setting primary_key=True?

It really depends on the types of queries you’d be using, the column type and size of data. It’s really a database design question rather than a Django question. Because of that unless I have an explicit reason, I typically will always use the AutoField for the primary key for tables/models.

That said, looking through my repositories I’ve used it in the following cases:

  • When I know the relationship will never change.
  • When I want to use a UUID field as the primary key (and I probably regret doing it)
1 Like