Saturday, January 13, 2024

Registration app and model

After creating a basic student registration end-point, now expanding on this registration feature as registration will not just be the user but also time of registration, price paid etc. For now, let us just have the time of registration and handle the discount options later.

Since the registration may be a fairly complex logic at a later stage, I created another app called registration. Within this app, I created a model CourseStudentRegistration:

class CourseStudentRegistration(models.Model):
    '''Registration of a student in a course'''
    user = models.ForeignKey(
        'user_auth.User',
        null=True,
        on_delete=models.SET_NULL
    )
    course = models.ForeignKey(
        'courses.Course',
        null=True,
        on_delete=models.SET_NULL
    )
    registered_at = models.DateTimeField(auto_now_add=True)

Back to the Course model, the students field is changed to point to this model as a through table, but for now the instructors field is kept the same:

instructors = models.ManyToManyField(
    'user_auth.User', related_name='courses_taught')
students = models.ManyToManyField(
    'user_auth.User',
    blank=True,
    through='registration.CourseStudentRegistration'
)

At this point, there was a quite a lot of confusion. Before, all the models in the ForeignKey and ManyToMany fields were specified as Python objects rather than strings with the format '<app_label>.<model_name>'. So before, there was just User instead of 'user_auth.User' and CourseStudentRegistration rather than 'registration.CourseStudentRegistration'. But specifying Python objects results in circular dependency errors as we have a model that references another model which in turn references the original model. So the conflict is understandable.

But, quite strangely, getting the migrations right even with the above relative strings was a bit tricky and not much in the documentation to help. In the documentation, all models are in a single file and so running migrations works. For that matter, if I put the CourseStudentRegistration in the same file as Course, migrations worked. The problem comes with having them in different apps and files.

When there are multiple apps, one would think that migrations have to be run successively:

python manage.py makemigrations app1
python manage.py makemigrations app2
...

If I ran makemigrations on the three apps - user_auth, courses and registration - one after the other, I was getting circular dependency errors. Turns out when there are such interconnections between models, one should run all the migrations in a single command and let Django figure out how they are connected:

python manage.py makemigrations app1 app2 ...

After this, the circular dependencies were resolved and the server can be launched. The tests related to course registration failed because I had commented out the part related to adding students:

def add_students(self, user):
    '''Add students to the course'''
    # TODO - registering students logic will 
    # need to be separated from Course model
    # CourseStudentRegistration.objects.create(
    #     course=self,
    #     user=user
    # )
    pass
    # if user not in self.students.all():
    #     self.students.add(user)
    # else:
    #     raise CourseGenericError('User is already registered')

With a through table now being defined in the ManyToMany field, one cannot simply add the user, but rather a new CourseStudentRegistration model instance has to be created pointing to the Course and the User. This now implies that this method will go, as it makes little sense accessing another model manager method - CourseStudentRegistration.objects.create - from another model method. The sensible thing to do is to create a model Manager for the CourseStudentRegistration that can be called directly from the view class.

No comments:

Post a Comment