/
staff.py
233 lines (193 loc) · 8.36 KB
/
staff.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
"""
Models for managing staff assessments.
"""
from datetime import timedelta
import logging
from django.db import DatabaseError, models
from django.utils.timezone import now
from openassessment.assessment.errors import StaffAssessmentInternalError
logger = logging.getLogger("openassessment.assessment.models") # pylint: disable=invalid-name
class StaffWorkflow(models.Model):
"""
Internal Model for tracking Staff Assessment Workflow
This model can be used to determine the following information required
throughout the Staff Assessment Workflow:
1) Get next submission that requires assessment.
2) Does a submission have a staff assessment?
3) Does this staff member already have a submission open for assessment?
4) Close open assessments when completed.
"""
# Amount of time before a lease on a submission expires
TIME_LIMIT = timedelta(hours=8)
scorer_id = models.CharField(max_length=40, db_index=True, blank=True)
course_id = models.CharField(max_length=255, db_index=True)
item_id = models.CharField(max_length=128, db_index=True)
submission_uuid = models.CharField(max_length=128, db_index=True, unique=True)
created_at = models.DateTimeField(default=now, db_index=True)
grading_completed_at = models.DateTimeField(null=True, db_index=True, blank=True)
grading_started_at = models.DateTimeField(null=True, db_index=True, blank=True)
cancelled_at = models.DateTimeField(null=True, db_index=True, blank=True)
assessment = models.CharField(max_length=128, db_index=True, null=True, blank=True)
class Meta:
ordering = ["created_at", "id"]
app_label = "assessment"
@property
def is_cancelled(self):
"""
Check if the workflow is cancelled.
Returns:
True/False
"""
return bool(self.cancelled_at)
@property
def identifying_uuid(self):
"""
Return the 'primary' identifying UUID for the staff workflow.
(submission_uuid for StaffWorkflow, team_submission_uuid for TeamStaffWorkflow)
"""
return self.submission_uuid
@classmethod
def get_workflow_statistics(cls, course_id, item_id):
"""
Returns the number of graded, ungraded, and in-progress submissions for staff grading.
Args:
course_id (str): The course that this problem belongs to
item_id (str): The student_item (problem) that we want to know statistics about.
Returns:
dict: a dictionary that contains the following keys: 'graded', 'ungraded', and 'in-progress'
"""
# pylint: disable=unicode-format-string
timeout = (now() - cls.TIME_LIMIT).strftime("%Y-%m-%d %H:%M:%S")
ungraded = cls.objects.filter(
models.Q(grading_started_at=None) | models.Q(grading_started_at__lte=timeout),
course_id=course_id, item_id=item_id, grading_completed_at=None, cancelled_at=None
).count()
in_progress = cls.objects.filter(
course_id=course_id, item_id=item_id, grading_completed_at=None, cancelled_at=None,
grading_started_at__gt=timeout
).count()
graded = cls.objects.filter(
course_id=course_id, item_id=item_id, cancelled_at=None
).exclude(grading_completed_at=None).count()
return {'ungraded': ungraded, 'in-progress': in_progress, 'graded': graded}
@classmethod
def get_submission_for_review(cls, course_id, item_id, scorer_id):
"""
Find a submission for staff assessment. This function will find the next
submission that requires assessment, excluding any submission that has been
completely graded, or is actively being reviewed by other staff members.
Args:
course_id (str): The course that we would like to retrieve submissions for,
item_id (str): The student_item that we would like to retrieve submissions for.
scorer_id (str): The user id of the staff member scoring this submission
Returns:
identifying_uuid (str): The identifying_uuid for the (team or individual) submission to review.
Raises:
StaffAssessmentInternalError: Raised when there is an error retrieving
the workflows for this request.
"""
# pylint: disable=unicode-format-string
timeout = (now() - cls.TIME_LIMIT).strftime("%Y-%m-%d %H:%M:%S")
try:
# Search for existing submissions that the scorer has worked on.
staff_workflows = cls.objects.filter(
course_id=course_id,
item_id=item_id,
scorer_id=scorer_id,
grading_completed_at=None,
cancelled_at=None,
)
# If no existing submissions exist, then get any other
# available workflows.
if not staff_workflows:
staff_workflows = cls.objects.filter(
models.Q(scorer_id='') | models.Q(grading_started_at__lte=timeout),
course_id=course_id,
item_id=item_id,
grading_completed_at=None,
cancelled_at=None,
)
if not staff_workflows:
return None
workflow = staff_workflows[0]
workflow.scorer_id = scorer_id
workflow.grading_started_at = now()
workflow.save()
return workflow.identifying_uuid
except DatabaseError as ex:
error_message = (
"An internal error occurred while retrieving a submission for staff grading"
)
logger.exception(error_message)
raise StaffAssessmentInternalError(error_message) from ex
@classmethod
def bulk_retrieve_workflow_status(cls, course_id, item_id, submission_uuids):
"""
Retrieves a dictionary with the requested submission UUIDs statuses.
Args:
course_id (str): The course that this problem belongs to
item_ids (list of strings): The student_item (problem) that we want to know statistics about.
Returns:
dict: a dictionary with the submission uuids as keys and their statuses as values.
Example:
{
"uuid_1": "submitted",
"uuid_2": "not_submitted
}
"""
# Retrieve queryed submissions
steps = cls.objects.filter(
course_id=course_id,
item_id=item_id,
submission_uuid__in=submission_uuids,
)
# Parse them to a dict readable format
assessments_list = {}
for assessment in steps:
status = None
if assessment.grading_completed_at:
status = 'submitted'
else:
status = 'not_submitted'
assessments_list[assessment.submission_uuid] = status
return assessments_list
@classmethod
def get_staff_workflows_for_course(cls, course_id):
"""
Retrieve all staff workflows for a certain course
"""
return cls.objects.filter(course_id=course_id)
@classmethod
def get_staff_workflow(cls, course_id, item_id, submission_uuid):
return cls.objects.get(
course_id=course_id,
item_id=item_id,
submission_uuid=submission_uuid
)
def close_active_assessment(self, assessment, scorer_id):
"""
Assign assessment to workflow, and mark the grading as complete.
"""
self.assessment = assessment.id
self.scorer_id = scorer_id
self.grading_completed_at = now()
self.save()
class TeamStaffWorkflow(StaffWorkflow):
"""
Extends the StafWorkflow to be used for team based assessments.
"""
team_submission_uuid = models.CharField(max_length=128, unique=True, null=False)
@property
def identifying_uuid(self):
"""
Return the 'primary' identifying UUID for the staff workflow.
(submission_uuid for StaffWorkflow, team_submission_uuid for TeamStaffWorkflow)
"""
return self.team_submission_uuid
@classmethod
def get_team_staff_workflow(cls, course_id, item_id, team_submission_uuid):
return cls.objects.get(
course_id=course_id,
item_id=item_id,
team_submission_uuid=team_submission_uuid
)