Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Waitlisting Feature Dev #506

Merged
merged 13 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions anonymized_data.json
gabrielhan23 marked this conversation as resolved.
Show resolved Hide resolved

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions csm_web/scheduler/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Spacetime,
Student,
User,
WaitlistedStudent,
)

# Helper methods
Expand Down Expand Up @@ -239,6 +240,21 @@ def has_delete_permission(self, request, obj=None):
return request.user.is_superuser


@admin.register(WaitlistedStudent)
class WaitlistedStudentAdmin(BasePermissionModelAdmin):
autocomplete_fields = (
"user",
"section",
"course",
)
list_display = (
"id",
"user",
"section",
"course",
)


@admin.register(Student)
class StudentAdmin(BasePermissionModelAdmin):
fieldsets = (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Generated by Django 4.2.7 on 2024-09-24 01:18

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("scheduler", "0032_word_of_the_day"),
]

operations = [
migrations.AddField(
model_name="section",
name="waitlist_capacity",
field=models.PositiveSmallIntegerField(default=3),
),
migrations.CreateModel(
name="WaitlistedStudent",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"active",
models.BooleanField(
default=True,
help_text="An inactive student is a dropped student.",
),
),
("timestamp", models.DateTimeField(auto_now_add=True)),
(
"course",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="scheduler.course",
),
),
(
"section",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="scheduler.section",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
]
17 changes: 17 additions & 0 deletions csm_web/scheduler/migrations/0034_course_max_waitlist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2024-09-24 08:32

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("scheduler", "0033_section_waitlist_capacity_waitlistedstudent"),
]

operations = [
migrations.AddField(
model_name="course",
name="max_waitlist",
field=models.SmallIntegerField(default=3),
),
]
63 changes: 48 additions & 15 deletions csm_web/scheduler/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
from django.db import models
from django.db.models.fields.related_descriptors import ReverseOneToOneDescriptor
from django.dispatch import receiver
from django.utils import functional, timezone
from django.utils import timezone
from rest_framework.serializers import ValidationError

logger = logging.getLogger(__name__)

logger.info = logger.warning

DEFAULT_WAITLIST_CAP = 3


class DayOfWeekField(models.Field):
DAYS = (
Expand Down Expand Up @@ -167,10 +169,10 @@ class Course(ValidatingModel):
enrollment_start = models.DateTimeField()
enrollment_end = models.DateTimeField()
permitted_absences = models.PositiveSmallIntegerField()
# max_waitlist = models.SmallIntegerField(default=3)
# time limit for wotd submission;
# section occurrence date + day limit, rounded to EOD
word_of_the_day_limit = models.DurationField(null=True, blank=True)

is_restricted = models.BooleanField(default=False)
whitelist = models.ManyToManyField("User", blank=True, related_name="whitelist")

Expand Down Expand Up @@ -198,6 +200,14 @@ def is_open(self):
now = timezone.now().astimezone(timezone.get_default_timezone())
return self.enrollment_start < now < self.enrollment_end

def is_coordinator(self, user):
"""
Returns boolean
- True if is coord
- False if is not coord
"""
return self.coordinator_set.filter(user=user).exists()


class Profile(ValidatingModel):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
Expand All @@ -217,6 +227,21 @@ class Meta:
abstract = True


class WaitlistedStudent(Profile):
"""
Represents a given "instance" of a waitlisted student. Every section in which a student enrolls
on the waitlist should have a new WaitlistedStudent profile.
"""

section = models.ForeignKey(
"Section", on_delete=models.CASCADE, related_name="waitlist"
)
active = models.BooleanField(
default=True, help_text="An inactive student is a dropped student."
)
timestamp = models.DateTimeField(auto_now_add=True)


class Student(Profile):
"""
Represents a given "instance" of a student. Every section in which a student enrolls should
Expand Down Expand Up @@ -260,19 +285,15 @@ def save(self, *args, **kwargs):
):
if settings.DJANGO_ENV != settings.DEVELOPMENT:
logger.info(
(
"<SectionOccurrence> SO automatically created for student"
" %s in course %s for date %s"
),
"<SectionOccurrence> SO automatically created for student"
" %s in course %s for date %s",
self.user.email,
course.name,
now.date(),
)
logger.info(
(
"<Attendance> Attendance automatically created for student"
" %s in course %s for date %s"
),
"<Attendance> Attendance automatically created for student"
" %s in course %s for date %s",
self.user.email,
course.name,
now.date(),
Expand Down Expand Up @@ -331,6 +352,7 @@ class Meta:
class Section(ValidatingModel):
# course = models.ForeignKey(Course, on_delete=models.CASCADE)
capacity = models.PositiveSmallIntegerField()
waitlist_capacity = models.PositiveSmallIntegerField(default=DEFAULT_WAITLIST_CAP)
mentor = OneToOneOrNoneField(
Mentor, on_delete=models.CASCADE, blank=True, null=True
)
Expand All @@ -343,15 +365,26 @@ class Section(ValidatingModel):
),
)

# @functional.cached_property
# def course(self):
# return self.mentor.course

@functional.cached_property
@property
def current_student_count(self):
"""Query the number of students currently enrolled in this section."""
return self.students.filter(active=True).count()

@property
def current_waitlist_count(self):
"""Query the number of waitlisted students currently enrolled in this section."""
return WaitlistedStudent.objects.filter(active=True, section=self).count()

@property
def is_waitlist_full(self):
"""Returns whether waitlist is open"""
return self.current_waitlist_count >= self.waitlist_capacity

@property
def is_section_full(self):
"""Returns whether section capacity is open"""
return self.current_student_count >= self.capacity

def delete(self, *args, **kwargs):
if self.current_student_count and not kwargs.get("force"):
raise models.ProtectedError(
Expand Down
10 changes: 10 additions & 0 deletions csm_web/scheduler/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Spacetime,
Student,
User,
WaitlistedStudent,
Worksheet,
day_to_number,
)
Expand Down Expand Up @@ -249,6 +250,14 @@ class Meta:
fields = ("id", "name", "email", "attendances", "section")


class WaitlistedStudentSerializer(serializers.ModelSerializer):
email = serializers.EmailField(source="user.email")

class Meta:
model = WaitlistedStudent
fields = ("id", "name", "email", "section")


class SectionSerializer(serializers.ModelSerializer):
spacetimes = SpacetimeSerializer(many=True)
num_students_enrolled = serializers.SerializerMethodField()
Expand Down Expand Up @@ -309,6 +318,7 @@ class Meta:
"user_role",
"course_title",
"course_restricted",
"waitlist_capacity",
)


Expand Down
18 changes: 18 additions & 0 deletions csm_web/scheduler/tests/models/test_waitlisted_student.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# import pytest
# from django.core.exceptions import ValidationError
# from scheduler.factories import (
# CourseFactory,
# MentorFactory,
# SectionFactory,
# StudentFactory,
# UserFactory,
# )
# from scheduler.models import Student, User, WaitlistedStudent

# @pytest.mark.django_db
# def test_add_waitlist():
# mentor_user, student_user, waitlist_user = UserFactory.create_batch(3)
# course = CourseFactory.create()
# mentor = MentorFactory.create(course=course, user=mentor_user)
# section = SectionFactory.create(mentor=mentor, capacity=1, waitlist_capacity=1)
# student = Student.objects.create(user=student_user, course=course, section=section)
2 changes: 2 additions & 0 deletions csm_web/scheduler/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@
path("matcher/<int:pk>/mentors/", views.matcher.mentors),
path("matcher/<int:pk>/configure/", views.matcher.configure),
path("matcher/<int:pk>/create/", views.matcher.create),
path("waitlist/<int:pk>/add/", views.waitlistedStudent.add),
path("waitlist/<int:pk>/drop/", views.waitlistedStudent.drop),
path("export/", views.export_data),
]
2 changes: 1 addition & 1 deletion csm_web/scheduler/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from . import matcher
from . import matcher, waitlistedStudent
from .course import CourseViewSet
from .export import export_data
from .profile import ProfileViewSet
Expand Down
8 changes: 6 additions & 2 deletions csm_web/scheduler/views/profile.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
from .utils import viewset_with

from django.db.models.query import EmptyQuerySet
from rest_framework.response import Response

from ..serializers import ProfileSerializer
from .utils import viewset_with


class ProfileViewSet(*viewset_with("list")):
serializer_class = None
queryset = EmptyQuerySet

def list(self, request):
"""
Lists out the profiles created by students, waitlisted students,
mentors, and coords.
"""
return Response(
ProfileSerializer(
[
*request.user.student_set.filter(active=True, banned=False),
*request.user.waitlistedstudent_set.filter(active=True),
*request.user.mentor_set.all(), # .exclude(section=None),
*request.user.coordinator_set.all(),
],
Expand Down
Loading
Loading