This repository has been archived by the owner on Aug 6, 2021. It is now read-only.
/
grader.py
318 lines (245 loc) · 11.5 KB
/
grader.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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
"""Grader: Menus and popups for grading and pair programming"""
import curses
import getpass
from zygrader.config import preferences
from zygrader.config.shared import SharedData
from zygrader import data
from zygrader import utils
from zygrader.ui import components, UI_GO_BACK
from zygrader.ui.window import Event, WinContext, Window
from zygrader.zybooks import Zybooks
def color_student_lines(lab, student):
"""Color the student names in the grader based on locked, flagged, or normal status"""
if data.lock.is_lab_locked(student, lab) and not isinstance(student, str):
return curses.color_pair(2)
if data.flags.is_submission_flagged(student, lab) and not isinstance(student, str):
return curses.color_pair(7)
return curses.color_pair(1)
def fill_student_list(lab, students):
"""Given a list of students, fill the list sorting locked and
flagged students to the top; also color the lines"""
lines = []
num_locked = 0
for i, student in enumerate(students):
line = components.FilteredList.ListLine(i + 1, student)
line.color = color_student_lines(lab, student)
if line.color == curses.color_pair(2):
lines.insert(0, line)
num_locked += 1
elif line.color == curses.color_pair(7):
lines.insert(num_locked, line)
else:
lines.append(line)
return lines
def update_student_list(window: Window, student_list: components.FilteredList):
"""Update the student list when the locks or flags change"""
student_list.refresh()
window.push_refresh_event()
def get_submission(lab, student, use_locks=True):
"""Get a submission from zyBooks given the lab and student"""
window = Window.get_window()
zy_api = Zybooks()
# Lock student
if use_locks:
data.lock.lock_lab(student, lab)
submission_response = zy_api.download_assignment(student, lab)
submission = data.model.Submission(student, lab, submission_response)
# Report missing files
if submission.flag & data.model.SubmissionFlag.BAD_ZIP_URL:
msg = [f"One or more URLs for {student.full_name}'s code submission are bad.",
"Some files could not be downloaded. Please",
"View the most recent submission on zyBooks."]
window.create_popup("Warning", msg)
# Return None if student has not submitted
if submission.flag == data.model.SubmissionFlag.NO_SUBMISSION:
msg = [f"{student.full_name} has not submitted"]
window.create_popup("No Submissions", msg)
return None
return submission
def pick_submission(lab: data.model.Lab, student: data.model.Student,
submission: data.model.Submission):
"""Allow the user to pick a submission to view"""
window = Window.get_window()
zy_api = Zybooks()
# If the lab has multiple parts, prompt to pick a part
part_index = 0
if len(lab.parts) > 1:
part_index = window.create_list_popup("Select Part",
input_data=[name["name"] for name in lab.parts])
if part_index is UI_GO_BACK:
return
# Get list of all submissions for that part
part = lab.parts[part_index]
all_submissions = zy_api.get_submissions_list(part["id"], student.id)
if not all_submissions:
window.create_popup("No Submissions", ["The student did not submit this part"])
return
# Reverse to display most recent submission first
all_submissions.reverse()
submission_index = window.create_list_popup("Select Submission", all_submissions)
if submission_index is UI_GO_BACK:
return
# Modify submission index to un-reverse the index
submission_index = abs(submission_index - (len(all_submissions) - 1))
# Fetch that submission
part_response = zy_api.download_assignment_part(lab, student.id, part, submission_index)
submission.update_part(part_response, part_index)
def view_diff(first, second):
"""View a diff of the two submissions"""
use_browser = preferences.is_preference_set("browser_diff")
paths_a = utils.get_source_file_paths(first.files_directory)
paths_b = utils.get_source_file_paths(second.files_directory)
paths_a.sort()
paths_b.sort()
diff = utils.make_diff_string(paths_a, paths_b, first.student.full_name,
second.student.full_name, use_browser)
utils.view_string(diff, "submissions.diff", use_browser)
def run_code_fn(window, context: WinContext, submission):
"""Callback to compile and run a submission's code"""
use_gdb = context.event.modifier == Event.MOD_ALT
if not submission.compile_and_run_code(use_gdb):
window.create_popup("Error", ["Could not compile and run code"])
def pair_programming_submission_callback(submission):
"""Show both pair programming students for viewing a diff"""
window = Window.get_window()
options = {
"Run": lambda context: run_code_fn(window, context, submission),
"View": lambda _: submission.show_files()
}
window.create_options_popup("Pair Programming Submission",
submission.msg, options, components.Popup.ALIGN_LEFT)
SharedData.running_process = None
def can_get_through_locks(use_locks, student, lab):
if not use_locks:
return True
window = Window.get_window()
if data.lock.is_lab_locked(student, lab):
netid = data.lock.get_locked_netid(student, lab)
# If being graded by the user who locked it, allow grading
if netid != getpass.getuser():
msg = [f"This student is already being graded by {netid}"]
window.create_popup("Student Locked", msg)
return False
if data.flags.is_submission_flagged(student, lab):
msg = ["This submission has been flagged", "",
f"Note: {data.flags.get_flag_message(student, lab)}", "",
"Would you like to unflag it?"]
remove = window.create_bool_popup("Submission Flagged", msg)
if remove:
data.flags.unflag_submission(student, lab)
else:
return False
return True
def grade_pair_programming(student_list, first_submission, use_locks):
"""Pick a second student to grade pair programming with"""
# Get second student
window = Window.get_window()
students = data.get_students()
lab = first_submission.lab
# Get student
student_index = window.create_filtered_list("Student",
list_fill=lambda: fill_student_list(lab, students),
filter_function=data.Student.find)
if student_index is UI_GO_BACK:
return
student = students[student_index]
if not can_get_through_locks(use_locks, student, lab):
return
try:
second_submission = get_submission(lab, student, use_locks)
if second_submission is None:
return
# Redraw the original list
update_student_list(window, student_list)
first_submission_fn = lambda _: pair_programming_submission_callback(first_submission)
second_submission_fn = lambda _: pair_programming_submission_callback(second_submission)
options = {
first_submission.student.full_name: first_submission_fn,
second_submission.student.full_name: second_submission_fn,
"View Diff": lambda _: view_diff(first_submission, second_submission)
}
msg = [f"{first_submission.student.full_name} {first_submission.latest_submission}",
f"{second_submission.student.full_name} {second_submission.latest_submission}",
"", "Pick a student's submission to view or view the diff"]
window.create_options_popup("Pair Programming", msg, options)
finally:
if use_locks:
data.lock.unlock_lab(student, lab)
def flag_submission(lab, student):
"""Flag a submission with a note"""
window = Window.get_window()
note = window.create_text_input("Flag Note", "Enter a flag note")
if note == UI_GO_BACK:
return
data.flags.flag_submission(student, lab, note)
def diff_parts_fn(window, submission):
"""Callback for text diffing parts of a submission"""
error = submission.diff_parts()
if error:
window.create_popup("Error", [error])
def student_callback(context: WinContext, lab, use_locks=True):
"""Show the submission for the selected lab and student"""
window = context.window
student_list = context.component
student = data.get_students()[context.data]
# Wait for student's assignment to be available
if not can_get_through_locks(use_locks, student, lab):
return
try:
# Get the student's submission
submission = get_submission(lab, student, use_locks)
# Exit if student has not submitted
if submission is None:
return
update_student_list(window, student_list)
options = {
"Flag": lambda _: flag_submission(lab, student),
"Pick Submission": lambda _: pick_submission(lab, student, submission),
"Pair Programming": lambda _: grade_pair_programming(student_list, submission, use_locks),
"Diff Parts": lambda _: diff_parts_fn(window, submission),
"Run": lambda context: run_code_fn(window, context, submission),
"View": lambda _: submission.show_files()
}
# Add option to diff parts if this lab requires it
if not (use_locks and submission.flag & data.model.SubmissionFlag.DIFF_PARTS):
del options["Diff Parts"]
window.create_options_popup("Submission", submission,
options, components.Popup.ALIGN_LEFT)
SharedData.running_process = None
finally:
# Always unlock the lab when no longer grading
if use_locks:
data.lock.unlock_lab(student, lab)
def watch_students(window: Window, student_list: components.FilteredList):
"""Register paths when the filtered list is created"""
paths = [SharedData.get_locks_directory(), SharedData.get_flags_directory()]
update_list = lambda _: update_student_list(window, student_list)
data.fs_watch.fs_watch_register(paths, "student_list_watch", update_list)
def lab_callback(context: WinContext, use_locks=True):
"""Create the list of labs to pick a student to grade"""
window = context.window
lab = data.get_labs()[context.data]
window.set_header(lab.name)
students = data.get_students()
student_select_fn = (lambda context: student_callback(context, lab, use_locks))
# Get student
window.create_filtered_list("Student", list_fill=lambda: fill_student_list(lab, students),
callback=student_select_fn, filter_function=data.Student.find,
create_fn=lambda student_list: watch_students(window, student_list))
# Remove the file watch handler when done choosing students
data.fs_watch.fs_watch_unregister("student_list_watch")
def grade(use_locks=True):
"""Create the list of labs to pick one to grade"""
window = Window.get_window()
labs = data.get_labs()
if use_locks:
window.set_header("Grader")
else:
window.set_header("Run for Fun")
if not labs:
window.create_popup("Error", ["No labs have been created yet"])
return
# Pick a lab
lab_select_fn = lambda context: lab_callback(context, use_locks)
window.create_filtered_list("Assignment", input_data=labs, callback=lab_select_fn,
filter_function=data.Lab.find)