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

Add soft_extended_due_date field to Group model #687

Merged
merged 7 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
37 changes: 37 additions & 0 deletions autograder/core/migrations/0108_group_soft_extended_due_date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 3.2.2 on 2024-08-22 19:23

from django.db import migrations, models

def copy_extended_deadline_to_soft_extended_deadline(apps, schema):
model = apps.get_model('core', 'Group')
model.objects.all().update(soft_extended_due_date=models.F('extended_due_date'))
model.objects.all().update(hard_extended_due_date=models.F('extended_due_date'))

class Migration(migrations.Migration):

dependencies = [
('core', '0107_auto_20240806_1626'),
]

operations = [
migrations.AddField(
model_name='group',
name='soft_extended_due_date',
field=models.DateTimeField(blank=True, default=None, help_text='When this field is set, it indicates the extended due date\n that is visible to members of the group. Members of the group will\n still be able to submit after this date but before the\n extended_due_date.\n Default value: None', null=True),
),
migrations.AddField(
model_name='group',
name='hard_extended_due_date',
field=models.DateTimeField(blank=True, default=None, null=True,
help_text="""When this field is set, it indicates that members
of this submission group can submit until this specified
date, overriding the project closing time.
Default value: None"""),
),
migrations.RunPython(copy_extended_deadline_to_soft_extended_deadline),
migrations.RemoveField(
model_name='group',
name='extended_due_date',
field=models.DateTimeField(blank=True, default=None, null=True),
)
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.2 on 2024-08-26 19:42

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0108_group_soft_extended_due_date'),
]

operations = [
migrations.AlterField(
model_name='group',
name='hard_extended_due_date',
field=models.DateTimeField(blank=True, default=None, help_text='When this field is set, it indicates that members\n of this submission group can submit until this specified\n date, overriding the project closing time.\n Default value: None', null=True),
),
]
65 changes: 61 additions & 4 deletions autograder/core/models/group/group.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

import os
from typing import Any, Dict, Iterable, List, cast
from typing import Any, Dict, Iterable, List, Optional, cast
from datetime import datetime
import zoneinfo

import django.contrib.postgres.fields as pg_fields
Expand Down Expand Up @@ -49,9 +50,9 @@ def validate_and_create( # type: ignore
member_names = [
user.username for user in sorted(members, key=lambda user: user.username)]
group = self.model(_member_names=member_names, **kwargs)
group.full_clean()
group.save()
group.members.add(*members)
group.full_clean()
return group


Expand Down Expand Up @@ -93,13 +94,40 @@ def member_names(self) -> List[str]:
project = models.ForeignKey(Project, related_name="groups",
on_delete=models.CASCADE)

extended_due_date = models.DateTimeField(
@property
def extended_due_date(self) -> Optional[datetime]:
james-perretta marked this conversation as resolved.
Show resolved Hide resolved
"""
DEPRECATED. For API legacy use, you can continue to use this field as usual.
Do NOT mix usage of this field and the two new "hard_extended_due_date" and
"soft_extended_due_date" fields.

When this field is set, it indicates that members of this submission group can
submit until this specified date, overriding the project closing time.

Default value: None
"""
return self.soft_extended_due_date

@extended_due_date.setter
def extended_due_date(self, value: Optional[datetime]) -> None:
self.soft_extended_due_date = value
self.hard_extended_due_date = value

hard_extended_due_date = models.DateTimeField(
null=True, default=None, blank=True,
help_text="""When this field is set, it indicates that members
of this submission group can submit until this specified
date, overriding the project closing time.
Default value: None""")

soft_extended_due_date = models.DateTimeField(
null=True, default=None, blank=True,
help_text="""When this field is set, it indicates the extended due date
that is visible to members of the group. Members of the group will
still be able to submit after this date but before the
extended_due_date.
Default value: None""")

# Remove in version 5.0.0
old_bonus_submissions_remaining = models.IntegerField(
blank=True,
Expand Down Expand Up @@ -179,6 +207,28 @@ def _is_towards_limit(submission: Submission) -> bool:

return utils.count_if(self.submissions.all(), _is_towards_limit)

def clean(self) -> None:
super().clean()

try:
clean_soft, clean_hard = core_ut.clean_and_validate_soft_and_hard_deadlines(
self.soft_extended_due_date, self.hard_extended_due_date)
except core_ut.InvalidSoftDeadlineError:
raise ValidationError(
{'soft_extended_due_date': (
'Soft extended due date must be a valid date')})
except core_ut.InvalidHardDeadlineError:
raise ValidationError(
{'hard_extended_due_date': (
'Hard extended due date must be a valid date')})
except core_ut.HardDeadlineBeforeSoftDeadlineError:
raise ValidationError(
{'hard_extended_due_date': (
'Hard extended due date must not be before soft extended due date')})

self.soft_extended_due_date = clean_soft
self.hard_extended_due_date = clean_hard

def save(self, *args: Any, **kwargs: Any) -> None:
super().save(*args, **kwargs)

Expand Down Expand Up @@ -234,6 +284,8 @@ def validate_and_update( # type: ignore
SERIALIZABLE_FIELDS = (
'pk',
'project',
'hard_extended_due_date',
'soft_extended_due_date',
'extended_due_date',
'member_names',
'members',
Expand All @@ -250,7 +302,12 @@ def validate_and_update( # type: ignore
)
SERIALIZE_RELATED = ('members',)

EDITABLE_FIELDS = ('extended_due_date', 'bonus_submissions_remaining')
EDITABLE_FIELDS = (
'extended_due_date',
'hard_extended_due_date',
'soft_extended_due_date',
'bonus_submissions_remaining'
)

def to_dict(self) -> Dict[str, object]:
result = super().to_dict()
Expand Down
28 changes: 18 additions & 10 deletions autograder/core/models/project/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,16 +238,24 @@ def clean(self) -> None:
{'max_group_size': ('Maximum group size must be greater than '
'or equal to minimum group size')})

if self.closing_time is not None:
self.closing_time = self.closing_time.replace(second=0, microsecond=0)
if self.soft_closing_time is not None:
self.soft_closing_time = self.soft_closing_time.replace(second=0, microsecond=0)

if self.closing_time is not None and self.soft_closing_time is not None:
if self.closing_time < self.soft_closing_time:
raise exceptions.ValidationError(
{'soft_closing_time': (
'Soft closing time must be before hard closing time')})
try:
clean_soft, clean_hard = core_ut.clean_and_validate_soft_and_hard_deadlines(
self.soft_closing_time, self.closing_time)
except core_ut.InvalidSoftDeadlineError:
raise exceptions.ValidationError(
{'soft_closing_time': (
'Soft closing time must be a valid date')})
except core_ut.InvalidHardDeadlineError:
raise exceptions.ValidationError(
{'closing_time': (
'Closing time must be a valid date')})
except core_ut.HardDeadlineBeforeSoftDeadlineError:
raise exceptions.ValidationError(
{'closing_time': (
'Closing time must not be before soft closing time')})

self.soft_closing_time = clean_soft
self.closing_time = clean_hard

@property
def has_handgrading_rubric(self) -> bool:
Expand Down
76 changes: 73 additions & 3 deletions autograder/core/tests/test_models/test_group/test_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ def test_valid_initialization_no_defaults(self):

group.refresh_from_db()

self.assertEqual(group.extended_due_date, extended_due_date)
self.assertEqual(
group.extended_due_date,
extended_due_date.replace(second=0, microsecond=0))
self.assertCountEqual(self.student_users, group.members.all())
self.assertEqual(self.project, group.project)

Expand Down Expand Up @@ -202,6 +204,8 @@ def test_serializable_fields(self):
'members',
'project',
'extended_due_date',
'soft_extended_due_date',
'hard_extended_due_date',

'bonus_submissions_remaining',

Expand All @@ -221,8 +225,10 @@ def test_serializable_fields(self):
self.assertIsInstance(serialized['members'][0], dict)

def test_editable_fields(self):
self.assertCountEqual(['extended_due_date', 'bonus_submissions_remaining'],
ag_models.Group.get_editable_fields())
self.assertCountEqual(
['extended_due_date', 'soft_extended_due_date', 'hard_extended_due_date',
'bonus_submissions_remaining'],
ag_models.Group.get_editable_fields())


class BonusSubmissionTokenCountTestCase(test_ut.UnitTestBase):
Expand Down Expand Up @@ -632,3 +638,67 @@ def test_exception_group_mix_of_student_and_staff(self):
ag_models.Group.objects.validate_and_create(
members=self.staff_users + self.student_users,
project=self.project)


class ExtendedDueDateBackwardsCompatabilityTestCase(_SetUp):
def test_extended_due_date_sets_hard_and_soft_extended_due_dates(self):
extended_due_date = timezone.now() + datetime.timedelta(days=1)
group = ag_models.Group.objects.validate_and_create(
members=self.student_users,
project=self.project,
extended_due_date=extended_due_date)

group.refresh_from_db()
self.assertEqual(
extended_due_date.replace(second=0, microsecond=0),
group.extended_due_date)
self.assertEqual(
extended_due_date.replace(second=0, microsecond=0),
group.soft_extended_due_date)
self.assertEqual(
extended_due_date.replace(second=0, microsecond=0),
group.hard_extended_due_date)

def test_getting_extended_due_date_reads_soft_extended_due_date(self):
soft_extended_due_date = timezone.now() + datetime.timedelta(days=1)
hard_extended_due_date = soft_extended_due_date + datetime.timedelta(days=1)
group = ag_models.Group.objects.validate_and_create(
members=self.student_users,
project=self.project,
soft_extended_due_date=soft_extended_due_date,
hard_extended_due_date=hard_extended_due_date)

self.assertEqual(
soft_extended_due_date.replace(second=0, microsecond=0),
group.extended_due_date)
self.assertEqual(
hard_extended_due_date.replace(second=0, microsecond=0),
group.hard_extended_due_date)


class HardAndSoftExtendedDueDateTestCase(_SetUp):
def test_valid_soft_extended_due_date_None_hard_extended_due_date_not_None(self):
hard_extended_due_date = timezone.now() + datetime.timedelta(days=1)
group = ag_models.Group.objects.validate_and_create(
members=self.student_users,
project=self.project,
hard_extended_due_date=hard_extended_due_date,
soft_extended_due_date=None)

group.refresh_from_db()
self.assertEqual(
hard_extended_due_date.replace(second=0, microsecond=0),
group.hard_extended_due_date)
self.assertIsNone(group.soft_extended_due_date)

def test_error_soft_extended_due_date_after_hard_extended_due_date(self):
hard_extended_due_date = timezone.now() + timezone.timedelta(minutes=5)
soft_extended_due_date = hard_extended_due_date + timezone.timedelta(minutes=5)

with self.assertRaises(exceptions.ValidationError) as cm:
ag_models.Group.objects.validate_and_create(
members=self.student_users,
project=self.project,
hard_extended_due_date=hard_extended_due_date,
soft_extended_due_date=soft_extended_due_date)
self.assertIn('hard_extended_due_date', cm.exception.message_dict)
17 changes: 17 additions & 0 deletions autograder/core/tests/test_models/test_project/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,26 @@ def test_error_soft_closing_time_after_closing_time(self):
name='stove', course=self.course,
closing_time=closing_time,
soft_closing_time=soft_closing_time)
self.assertIn('closing_time', cm.exception.message_dict)

def test_error_soft_closing_time_not_a_date(self):
soft_closing_time = "foobar"
with self.assertRaises(exceptions.ValidationError) as cm:
ag_models.Project.objects.validate_and_create(
name='stove', course=self.course,
soft_closing_time=soft_closing_time)

self.assertIn('soft_closing_time', cm.exception.message_dict)

def test_error_closing_time_not_a_date(self):
closing_time = "foobar"
with self.assertRaises(exceptions.ValidationError) as cm:
ag_models.Project.objects.validate_and_create(
name='stove', course=self.course,
closing_time=closing_time)

self.assertIn('closing_time', cm.exception.message_dict)


class ProjectMiscErrorTestCase(UnitTestBase):
def setUp(self):
Expand Down
Loading
Loading