-
Notifications
You must be signed in to change notification settings - Fork 74
/
Copy pathmodels.py
270 lines (224 loc) · 9.75 KB
/
models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any, Dict, Generic, Iterable, Optional, TypeVar, Union
from django.db import models
from django.utils.translation import gettext_lazy as _
import pytz
from course.models import CourseInstance
from exercise.exercise_models import BaseExercise
from exercise.submission_models import Submission
from userprofile.models import UserProfile
from lib.fields import DefaultForeignKey
from lib.models import UrlMixin
TModel = TypeVar('TModel', bound='SubmissionRuleDeviation')
class SubmissionRuleDeviationManager(models.Manager[TModel], Generic[TModel]):
max_order_by: str
def get_max_deviations(
self,
submitter: UserProfile,
exercises: Iterable[Union[BaseExercise, int]],
) -> Iterable[TModel]:
"""
Returns the maximum deviations for the given submitter in the given
exercises (one deviation per exercise is returned). The deviation may
be granted to the submitter directly, or to some other submitter in
their group.
"""
deviations_self = (
self.filter(
exercise__in=exercises,
submitter=submitter,
)
.select_related('exercise')
)
deviations_group = (
self.filter(
models.Q(exercise__in=exercises)
& ~models.Q(submitter=submitter)
& (
# Check that the owner of the deviation is
# some other user who has submitted the deviation's
# exercise with the user.
models.Exists(
# Note the two 'submitters' filters.
Submission.objects.filter(
exercise=models.OuterRef('exercise'),
submitters=models.OuterRef('submitter'),
).filter(
submitters=submitter,
)
)
)
)
.select_related('exercise')
)
deviations = (
deviations_self
.union(deviations_group)
.order_by('exercise', self.max_order_by)
)
previous_exercise_id = None
for deviation in deviations:
if deviation.exercise.id == previous_exercise_id:
continue
previous_exercise_id = deviation.exercise.id
yield deviation
def get_max_deviation(self, submitter: UserProfile, exercise: Union[BaseExercise, int]) -> Optional[TModel]:
"""
Returns the maximum deviation for the given submitter in the given
exercise. The deviation may be granted to the submitter directly, or to
some other submitter in their group.
"""
deviations = self.get_max_deviations(submitter, [exercise])
for deviation in deviations:
return deviation
class SubmissionRuleDeviation(UrlMixin, models.Model):
"""
An abstract model binding a user to an exercise stating that there is some
kind of deviation from the normal submission boundaries, that is, special
treatment related to the submissions of that particular user to that
particular exercise.
If there are many submitters submitting an exercise out of bounds of the
default bounds, all of the submitters must have an allowing instance of
SubmissionRuleDeviation subclass in order for the submission to be allowed.
"""
exercise = DefaultForeignKey(BaseExercise,
verbose_name=_('LABEL_EXERCISE'),
on_delete=models.CASCADE,
)
submitter = models.ForeignKey(UserProfile,
verbose_name=_('LABEL_SUBMITTER'),
on_delete=models.CASCADE,
)
granter = models.ForeignKey(UserProfile,
verbose_name=_('LABEL_GRANTER'),
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True,
)
grant_time = models.DateTimeField(
verbose_name=_('LABEL_GRANT_TIME'),
auto_now=True,
blank=True,
null=True,
)
if TYPE_CHECKING:
id: models.AutoField
class Meta:
verbose_name = _('MODEL_NAME_SUBMISSION_RULE_DEVIATION')
verbose_name_plural = _('MODEL_NAME_SUBMISSION_RULE_DEVIATION_PLURAL')
abstract = True
unique_together = ["exercise", "submitter"]
def get_url_kwargs(self):
# pylint: disable-next=use-dict-literal
return dict(deviation_id=self.id, **self.exercise.course_instance.get_url_kwargs())
def update_by_form(self, form_data: Dict[str, Any]) -> None:
"""
Update the deviation's attributes based on a provided set of form
values.
"""
raise NotImplementedError()
def is_groupable(self, other: 'SubmissionRuleDeviation') -> bool:
"""
Whether this deviation can be grouped with another deviation in tables.
"""
raise NotImplementedError()
@classmethod
def get_list_url(cls, instance: CourseInstance) -> str:
"""
Get the URL of the deviation list page for deviations of this type.
"""
raise NotImplementedError()
@classmethod
def get_override_url(cls, instance: CourseInstance) -> str:
"""
Get the URL of the deviation override page for deviations of this type.
"""
raise NotImplementedError()
class DeadlineRuleDeviationManager(SubmissionRuleDeviationManager['DeadlineRuleDeviation']):
max_order_by = "-extra_seconds"
class DeadlineRuleDeviation(SubmissionRuleDeviation):
extra_seconds = models.IntegerField(
verbose_name=_('LABEL_EXTRA_SECONDS'),
)
without_late_penalty = models.BooleanField(
verbose_name=_('LABEL_WITHOUT_LATE_PENALTY'),
default=True,
)
objects = DeadlineRuleDeviationManager()
class Meta(SubmissionRuleDeviation.Meta):
verbose_name = _('MODEL_NAME_DEADLINE_RULE_DEVIATION')
verbose_name_plural = _('MODEL_NAME_DEADLINE_RULE_DEVIATION_PLURAL')
def get_extra_time(self):
return timedelta(seconds=self.extra_seconds)
def get_new_deadline(self, normal_deadline: Optional[datetime] = None) -> datetime:
"""
Returns the new deadline after adding the extra time to the normal
deadline.
The `normal_deadline` argument can be provided if it is known by the
caller, to avoid querying it.
"""
if normal_deadline is None:
normal_deadline = self.get_normal_deadline()
return normal_deadline + self.get_extra_time()
def get_normal_deadline(self):
return self.exercise.course_module.closing_time
def update_by_form(self, form_data: Dict[str, Any]) -> None:
seconds = form_data.get('seconds')
new_date = form_data.get('new_date')
if new_date:
seconds = self.exercise.delta_in_seconds_from_closing_to_date(new_date)
else:
seconds = int(seconds)
# Adjust the deadline deviation length by taking daylight savings time into consideration
timezone_string = form_data.get('timezone_string')
try:
tz = pytz.timezone(timezone_string)
module_close = self.get_normal_deadline()
new_deadline = module_close + timedelta(seconds=seconds)
start_date = module_close.replace(tzinfo=pytz.utc).astimezone(tz)
end_date = new_deadline.replace(tzinfo=pytz.utc).astimezone(tz)
# Get the UTC offsets for the start and end dates
start_offset = start_date.utcoffset().total_seconds()
end_offset = end_date.utcoffset().total_seconds()
if start_offset > end_offset:
# Clock was moved backward, add one hour to the deadline deviation
seconds += 60 * 60
elif start_offset < end_offset:
# Clock was moved forward, remove one hour from the deadline deviation
seconds -= 60 * 60
except pytz.exceptions.UnknownTimeZoneError:
pass
self.extra_seconds = seconds
self.without_late_penalty = bool(form_data.get('without_late_penalty'))
def is_groupable(self, other: 'DeadlineRuleDeviation') -> bool:
return (
self.extra_seconds == other.extra_seconds
and self.without_late_penalty == other.without_late_penalty
)
@classmethod
def get_list_url(cls, instance: CourseInstance) -> str:
return instance.get_url('deviations-list-dl')
@classmethod
def get_override_url(cls, instance: CourseInstance) -> str:
return instance.get_url('deviations-override-dl')
class MaxSubmissionsRuleDeviationManager(SubmissionRuleDeviationManager['MaxSubmissionsRuleDeviation']):
max_order_by = "-extra_submissions"
class MaxSubmissionsRuleDeviation(SubmissionRuleDeviation):
extra_submissions = models.IntegerField(
verbose_name=_('LABEL_EXTRA_SUBMISSIONS'),
)
objects = MaxSubmissionsRuleDeviationManager()
class Meta(SubmissionRuleDeviation.Meta):
verbose_name = _('MODEL_NAME_MAX_SUBMISSIONS_RULE_DEVIATION')
verbose_name_plural = _('MODEL_NAME_MAX_SUBMISSIONS_RULE_DEVIATION_PLURAL')
def update_by_form(self, form_data: Dict[str, Any]) -> None:
self.extra_submissions = int(form_data['extra_submissions'])
def is_groupable(self, other: 'MaxSubmissionsRuleDeviation') -> bool:
return self.extra_submissions == other.extra_submissions
@classmethod
def get_list_url(cls, instance: CourseInstance) -> str:
return instance.get_url('deviations-list-submissions')
@classmethod
def get_override_url(cls, instance: CourseInstance) -> str:
return instance.get_url('deviations-override-submissions')