From bef2a7036789895e3cdea1b6179d265c747587aa Mon Sep 17 00:00:00 2001 From: Harrison Katz Date: Tue, 2 Dec 2025 03:16:25 -0800 Subject: [PATCH 1/2] feat: add models to handle storing course tutor info --- hknweb/tutoring/admin.py | 8 +++++++- hknweb/tutoring/apps.py | 3 +++ hknweb/tutoring/models.py | 33 ++++++++++++++++++++++++++++++--- hknweb/tutoring/signals.py | 16 ++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 hknweb/tutoring/signals.py diff --git a/hknweb/tutoring/admin.py b/hknweb/tutoring/admin.py index 8cb8cdd8..fbba68e0 100644 --- a/hknweb/tutoring/admin.py +++ b/hknweb/tutoring/admin.py @@ -1,6 +1,12 @@ from django.contrib import admin -from hknweb.tutoring.models import Room, TutoringLogistics, Slot +from hknweb.tutoring.models import Tutor, Room, TutoringLogistics, Slot + + +@admin.register(Tutor) +class TutorAdmin(admin.ModelAdmin): + search_fields = ("user__username", "user__first_name", "user__last_name", "user__email") + autocomplete_fields = ('user',) @admin.register(TutoringLogistics) diff --git a/hknweb/tutoring/apps.py b/hknweb/tutoring/apps.py index d7c32180..93c189bf 100644 --- a/hknweb/tutoring/apps.py +++ b/hknweb/tutoring/apps.py @@ -3,3 +3,6 @@ class TutoringConfig(AppConfig): name = "hknweb.tutoring" + + def ready(self): + from . import signals diff --git a/hknweb/tutoring/models.py b/hknweb/tutoring/models.py index 3a534850..00e2b8b9 100644 --- a/hknweb/tutoring/models.py +++ b/hknweb/tutoring/models.py @@ -6,8 +6,35 @@ from django.db.models import Value from django.db.models.functions import Concat from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from hknweb import settings from hknweb.coursesemester.models import Semester +from hknweb.models import Course + + +class Tutor(models.Model): + user = models.OneToOneField( + User, on_delete=models.CASCADE, related_name="tutoring_info" + ) + completed_courses = models.ManyToManyField( + Course, blank=True, related_name="completed_tutors" + ) + in_progress_courses = models.ManyToManyField( + Course, blank=True, related_name="in_progress_tutors" + ) + preferred_courses = models.ManyToManyField( + Course, blank=True, related_name="preferred_tutors" + ) + + def save(self, *args, **kwargs): + if not self.user.cmemberships.filter( + committee=settings.TUTORING_GROUP + ).exists(): + raise ValidationError( + f"User {self.user.username} is not in the tutoring committee." + ) + super().save(*args, **kwargs) class Room(models.Model): @@ -26,10 +53,10 @@ class Meta: Semester, on_delete=models.CASCADE, null=True, default=None ) one_hour_tutors = models.ManyToManyField( - User, blank=True, related_name="one_hour_tutoring" + Tutor, blank=True, related_name="one_hour_tutoring" ) two_hour_tutors = models.ManyToManyField( - User, blank=True, related_name="two_hour_tutoring" + Tutor, blank=True, related_name="two_hour_tutoring" ) def __str__(self) -> str: # pragma: no cover @@ -50,7 +77,7 @@ class Slot(models.Model): ) room = models.ForeignKey(Room, on_delete=models.CASCADE, null=True, default=None) num_tutors = models.IntegerField(default=0) - tutors = models.ManyToManyField(User, blank=True, related_name="tutoring_slots") + tutors = models.ManyToManyField(Tutor, blank=True, related_name="tutoring_slots") WEEKDAY_STRS = "Mon Tue Wed Thu Fri Sat Sun".split() WEEKDAY_CHOICES = list(zip(range(len(WEEKDAY_STRS)), WEEKDAY_STRS)) diff --git a/hknweb/tutoring/signals.py b/hknweb/tutoring/signals.py new file mode 100644 index 00000000..62dce039 --- /dev/null +++ b/hknweb/tutoring/signals.py @@ -0,0 +1,16 @@ +from django.db.models.signals import m2m_changed +from django.dispatch import receiver + +from hknweb import settings +from hknweb.models import Committeeship, User +from hknweb.tutoring.models import Tutor + + +@receiver(m2m_changed, sender=Committeeship.committee_members.through) +def deleteTutorIfRemovedFromCommittee(sender, instance, action, pk_set, **_): + if action == "post_remove": + Tutor.objects.filter(user__pk__in=pk_set).exclude( + user__pk__in=User.objects.filter( + pk__in=pk_set, cmembership__committee=settings.TUTORING_GROUP + ).values_list("pk", flat=True) + ).delete() From 356b3b37fcea1d3f44b257ce9652dd4f03c7f897 Mon Sep 17 00:00:00 2001 From: Harrison Katz Date: Wed, 21 Jan 2026 15:18:18 -0800 Subject: [PATCH 2/2] Add migration change model to use user as pk in order to make migration extremely smooth, no real downside to this --- .../0004_tutor_alter_slot_tutors_and_more.py | 91 +++++++++++++++++++ hknweb/tutoring/models.py | 2 +- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 hknweb/tutoring/migrations/0004_tutor_alter_slot_tutors_and_more.py diff --git a/hknweb/tutoring/migrations/0004_tutor_alter_slot_tutors_and_more.py b/hknweb/tutoring/migrations/0004_tutor_alter_slot_tutors_and_more.py new file mode 100644 index 00000000..f1b0c51b --- /dev/null +++ b/hknweb/tutoring/migrations/0004_tutor_alter_slot_tutors_and_more.py @@ -0,0 +1,91 @@ +# Mostly Generated by Django 4.2.17, create_tutors manually created + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +def create_tutors(apps, schema_editor): + User = apps.get_model(settings.AUTH_USER_MODEL) + Tutor = apps.get_model('tutoring', 'Tutor') + tutoring_users = User.objects.exclude( + one_hour_tutoring__isnull=True, + two_hour_tutoring__isnull=True, + tutoring_slots__isnull=True + ).distinct().iterator() + Tutor.objects.bulk_create( + ( Tutor(user=user) for user in tutoring_users ), + ignore_conflicts=True + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ("coursesemester", "0002_auto_20210202_0225"), + ("tutoring", "0003_auto_20221128_1703"), + ] + + operations = [ + migrations.CreateModel( + name="Tutor", + fields=[ + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + related_name="tutoring_info", + serialize=False, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "completed_courses", + models.ManyToManyField( + blank=True, + related_name="completed_tutors", + to="coursesemester.course", + ), + ), + ( + "in_progress_courses", + models.ManyToManyField( + blank=True, + related_name="in_progress_tutors", + to="coursesemester.course", + ), + ), + ( + "preferred_courses", + models.ManyToManyField( + blank=True, + related_name="preferred_tutors", + to="coursesemester.course", + ), + ), + ], + ), + migrations.RunPython(create_tutors, elidable=True), + migrations.AlterField( + model_name="slot", + name="tutors", + field=models.ManyToManyField( + blank=True, related_name="tutoring_slots", to="tutoring.tutor" + ), + ), + migrations.AlterField( + model_name="tutoringlogistics", + name="one_hour_tutors", + field=models.ManyToManyField( + blank=True, related_name="one_hour_tutoring", to="tutoring.tutor" + ), + ), + migrations.AlterField( + model_name="tutoringlogistics", + name="two_hour_tutors", + field=models.ManyToManyField( + blank=True, related_name="two_hour_tutoring", to="tutoring.tutor" + ), + ), + ] diff --git a/hknweb/tutoring/models.py b/hknweb/tutoring/models.py index 00e2b8b9..ed47ed44 100644 --- a/hknweb/tutoring/models.py +++ b/hknweb/tutoring/models.py @@ -15,7 +15,7 @@ class Tutor(models.Model): user = models.OneToOneField( - User, on_delete=models.CASCADE, related_name="tutoring_info" + User, on_delete=models.CASCADE, related_name="tutoring_info", primary_key=True ) completed_courses = models.ManyToManyField( Course, blank=True, related_name="completed_tutors"