Skip to content

Commit

Permalink
Replace LateDaysRemaining with ExtraLateDays model and LateDaysForUser
Browse files Browse the repository at this point in the history
utility class
  • Loading branch information
MattyMay committed Nov 21, 2024
1 parent 3a9045f commit e2fd933
Show file tree
Hide file tree
Showing 14 changed files with 449 additions and 196 deletions.
42 changes: 42 additions & 0 deletions autograder/core/migrations/0109_late_days_change.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generated by Django 3.2.25 on 2024-11-19 03:45

import autograder.core.models.ag_model_base
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0108_internal_admin_notes'),
]

operations = [
migrations.RenameField(
model_name='LateDaysRemaining',
old_name='_extra_late_days_granted',
new_name='extra_late_days',
),
migrations.RemoveField(
model_name='LateDaysRemaining',
name='old_late_days_remaining'
),
migrations.RemoveField(
model_name='LateDaysRemaining',
name='late_days_used'
),
migrations.AlterModelOptions(
name='submission',
options={'ordering': ['-pk', '-timestamp']},
),
migrations.AddIndex(
model_name='submission',
index=models.Index(fields=['group', 'timestamp'], name='core_submis_group_i_160046_idx'),
),
migrations.RenameModel(
old_name='LateDaysRemaining',
new_name='ExtraLateDays'
),
]
3 changes: 2 additions & 1 deletion autograder/core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from .ag_test.ag_test_suite_result import AGTestSuiteResult as AGTestSuiteResult
from .ag_test.feedback_category import FeedbackCategory as FeedbackCategory
from .course import Course as Course
from .course import LateDaysRemaining as LateDaysRemaining
from .course import Semester as Semester
from .group import Group as Group
from .group import GroupInvitation as GroupInvitation
Expand All @@ -43,3 +42,5 @@
from .submission import \
get_submissions_with_results_queryset as get_submissions_with_results_queryset
from .task import Task as Task
from .user_late_days import LateDaysForUser as LateDaysForUser
from .user_late_days import ExtraLateDays as ExtraLateDays
38 changes: 0 additions & 38 deletions autograder/core/models/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,44 +223,6 @@ def save(self, *args: Any, **kwargs: Any) -> None:
)


class LateDaysRemaining(AutograderModel):
objects = AutograderModelManager['LateDaysRemaining']()

class Meta:
unique_together = ('course', 'user')

course = models.ForeignKey(Course, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)

# Remove in version 5.0.0
old_late_days_remaining = models.IntegerField(
validators=[validators.MinValueValidator(0)], blank=True, default=0)

@property
def late_days_remaining(self) -> int:
return max(0, self._true_late_days_remaining)

@late_days_remaining.setter
def late_days_remaining(self, value: int) -> None:
if value < 0:
raise ValidationError({
'late_days_remaining': 'This value cannot be negative.'
})

self._extra_late_days_granted += value - self._true_late_days_remaining

@property
def _true_late_days_remaining(self) -> int:
return (
self.course.num_late_days + self._extra_late_days_granted
- self.late_days_used
)

_extra_late_days_granted = models.IntegerField(blank=True, default=0)
late_days_used = models.IntegerField(
blank=True, default=0, validators=[MinValueValidator(0)])


def clear_cached_user_roles(course_pk: int) -> None:
keys = cache.client.iter_keys(f'course_{course_pk}_user_*', itersize=5000)
cache.delete_many(list(keys))
3 changes: 2 additions & 1 deletion autograder/core/models/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ class Submission(ag_model_base.AutograderModel):
objects = _SubmissionManager()

class Meta:
ordering = ['-pk']
ordering = ['-pk', '-timestamp']
indexes = [models.Index(fields=['group', 'timestamp'])]

class GradingStatus(models.TextChoices):
# The submission has been accepted and saved to the database
Expand Down
140 changes: 140 additions & 0 deletions autograder/core/models/user_late_days.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from typing import TypedDict, overload
from datetime import timedelta
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import models

from .ag_model_base import (
AutograderModel, AutograderModelManager, DictSerializable)
from .group import Group
from .submission import Submission
from .course import Course

from autograder.rest_api.serialize_user import serialize_user

class ExtraLateDays(AutograderModel):
objects = AutograderModelManager['ExtraLateDays']()

class Meta:
unique_together = ('course', 'user')

course = models.ForeignKey(Course, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='late_days')

extra_late_days = models.IntegerField(blank=True, default=0)

SERIALIZABLE_FIELDS = ('course', 'user', 'extra_late_days')
EDITABLE_FIELDS = ('extra_late_days')

def clean(self):
super().clean()
if self.user not in self.course.students.all():
raise ValidationError('The user is not a student in the course')
if self.extra_late_days < 0:
raise ValidationError('extra_late_days must be non-negative')


class LateDaysForUser(DictSerializable):
def __init__(self,
user: User,
course: Course,
extra_late_days: int,
late_days_used: int,
late_days_remaining: int):
self.user = user
self.course = course
self.extra_late_days = extra_late_days
self.late_days_used = late_days_used
self.late_days_remaining = late_days_remaining

@staticmethod
def _days_late(group: Group, submission_timestamp: models.DateTimeField) -> int:
if group.project.closing_time is None:
return 0
elif group.extended_due_date is None:
delta = submission_timestamp - group.project.closing_time
else:
deadline = max(group.project.closing_time, group.extended_due_date)
delta = submission_timestamp - deadline

return delta.days + 1 if delta > timedelta() else 0


def to_dict(self):
return {
'user': serialize_user(self.user),
'course': self.course.to_dict(),
'extra_late_days': self.extra_late_days,
'late_days_used': self.late_days_used,
'late_days_remaining': self.late_days_remaining
}

@staticmethod
def get(user: User, course: Course) -> "LateDaysForUser":
if user not in course.students.all():
raise ValueError("user must be a student in the course")
queryset = User.objects.filter(pk=user.pk)
return LateDaysForUser.get_many(queryset, course)[0]

@staticmethod
def get_many(users_queryset: models.QuerySet, course: Course) -> list["LateDaysForUser"]:
# Fetch all submissions for the course's groups, ordered by descending timestamp
groups_with_submissions = Group.objects.filter(
project__course=course,
project__allow_late_days=True
).prefetch_related(
models.Prefetch(
'submissions',
queryset=Submission.objects.order_by('-timestamp'),
to_attr='all_submissions'
)
)

# Prefetch groups and late days for each user
prefetch_groups = models.Prefetch(
'groups_is_member_of',
queryset=groups_with_submissions,
to_attr='groups_with_submissions'
)
prefetch_late_days = models.Prefetch(
'late_days',
queryset=ExtraLateDays.objects.filter(course=course),
to_attr='late_days_for_course'
)

users_with_groups = users_queryset.prefetch_related(
prefetch_groups,
prefetch_late_days
)

results = []
for user in users_with_groups:
extra = user.late_days_for_course[0].extra_late_days if user.late_days_for_course else 0
used = 0

for group in user.groups_with_submissions:
# Filter submissions that count for the user
user_submissions = [
submission for submission in group.all_submissions
if user.username not in submission.does_not_count_for
]

# Get the first (latest) submission that counts for the user
if user_submissions:
latest_submission = user_submissions[0]
used += LateDaysForUser._days_late(
group,
latest_submission.timestamp
)

remaining = course.num_late_days + extra - used
results.append(
LateDaysForUser(user, course, extra, used, remaining)
)

return results

@staticmethod
def get_all(course: Course) -> list["LateDaysForUser"]:
queryset = User.objects.filter(courses_is_enrolled_in=course)
return LateDaysForUser.get_many(queryset, course)
2 changes: 2 additions & 0 deletions autograder/rest_api/schema/model_schema_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ def api_object_type_name_is_registered(api_class: APIClassType) -> bool:

_API_OBJ_TYPE_NAMES: Dict[APIClassType, str] = {
User: 'User',
ag_models.LateDaysForUser: ag_models.LateDaysForUser.__name__,
ag_models.Course: ag_models.Course.__name__,
ag_models.Semester: ag_models.Semester.__name__,
ag_models.Project: ag_models.Project.__name__,
Expand Down Expand Up @@ -253,6 +254,7 @@ def api_object_type_name_is_registered(api_class: APIClassType) -> bool:
}

_API_UPDATE_OBJ_TYPE_NAMES: Dict[APIClassType, str] = {
# ag_models.LateDays: 'Update' + ag_models.LateDays.__name__,
ag_models.Course: 'Update' + ag_models.Course.__name__,
ag_models.Project: 'Update' + ag_models.Project.__name__,
ag_models.ExpectedStudentFile: 'Update' + ag_models.ExpectedStudentFile.__name__,
Expand Down
Loading

0 comments on commit e2fd933

Please sign in to comment.