diff --git a/api/views.py b/api/views.py index b4c97b45..ec936208 100644 --- a/api/views.py +++ b/api/views.py @@ -14,7 +14,6 @@ Semester, Subject, assignedtask_results, - current_semester_conds, current_semester, submit_assignment_path, ) @@ -202,13 +201,17 @@ def sort_fn(c): @user_passes_test(is_teacher) -def subject_list(request, subject_abbr): +def subject_list(request, subject_abbr: str): + """ + Returns the list of active classes for a given subject. + Used when creating a new task. + """ get_object_or_404( Subject, abbr=subject_abbr ) # The result is not needed, the call is to provide 404 error classes = [] - for clazz in Class.objects.filter(subject__abbr=subject_abbr, **current_semester_conds()): + for clazz in Class.objects.current_semester().filter(subject__abbr=subject_abbr): classes.append( { "id": clazz.pk, @@ -515,9 +518,8 @@ def is_allowed(path): "type": "file", } - classes = Class.objects.filter( + classes = Class.objects.current_semester().filter( subject__abbr=task.subject.abbr, - **current_semester_conds(), ) assigned_count = 0 for clazz in classes: diff --git a/common/migrations/0020_semester_active.py b/common/migrations/0020_semester_active.py new file mode 100644 index 00000000..b4b23aa5 --- /dev/null +++ b/common/migrations/0020_semester_active.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-09-08 07:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0019_submit_ip_address'), + ] + + operations = [ + migrations.AddField( + model_name='semester', + name='active', + field=models.BooleanField(default=False), + ), + ] diff --git a/common/models.py b/common/models.py index 2e5133c3..be26060d 100644 --- a/common/models.py +++ b/common/models.py @@ -2,8 +2,9 @@ import re import logging -from typing import List +from typing import List, Optional +from django.db.models import QuerySet from django.utils import timezone from django.db import models @@ -16,25 +17,34 @@ from jinja2 import Environment, FileSystemLoader -def current_semester_conds(prefix=""): - return { - f"{prefix}semester__begin__lte": timezone.now(), - f"{prefix}semester__end__gte": timezone.now(), - } - - -def current_semester() -> "Semester": - semester = Semester.objects.filter(begin__lte=timezone.now(), end__gte=timezone.now()).first() +def current_semester() -> Optional["Semester"]: + """ + Returns the current active semester, if there is any. + """ + semester = Semester.objects.filter(active=True).order_by("-begin").first() if semester: return semester - return Semester.objects.filter(begin__lte=timezone.now()).order_by("begin").last() + # If no semester is marked as active, return the latest semester that has already begun. + return Semester.objects.filter(begin__lte=timezone.now()).order_by("-begin").first() class ClassManager(models.Manager): - def current_semester(self): - return self.filter(**current_semester_conds()) + def current_semester(self) -> QuerySet: + """ + Return classes for the currently active semester. + Note that the semantics for this call are a bit less strict than for `current_semester`. + Notably, if multiple semesters are active, it will return classes for all of them. + + We could create some query like + WHERE r.active=1 AND NOT + EXISTS (SELECT * FROM semester AS s WHERE s.active=1 AND r.id != s.id AND s.begin > r.begin) + + But it seems like overkill, since we will only ever have exactly one active semester + (we just need to make sure in admin that this property holds). + """ + return self.filter(semester__active=True) class Semester(models.Model): @@ -42,6 +52,9 @@ class Semester(models.Model): end = models.DateField() year = models.IntegerField() winter = models.BooleanField() + # Is the semester currently marked as active? + # Ideally, only one semester should be marked as such. + active = models.BooleanField(default=False) def __str__(self): return f"{self.year}{'W' if self.winter else 'S'}"