ygrader 2.4.0__tar.gz → 2.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ygrader-2.4.0/ygrader.egg-info → ygrader-2.5.1}/PKG-INFO +1 -1
- {ygrader-2.4.0 → ygrader-2.5.1}/setup.py +1 -1
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader/feedback.py +115 -88
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader/grading_item.py +15 -3
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader/score_input.py +16 -7
- {ygrader-2.4.0 → ygrader-2.5.1/ygrader.egg-info}/PKG-INFO +1 -1
- {ygrader-2.4.0 → ygrader-2.5.1}/LICENSE +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/setup.cfg +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/test/test_interactive.py +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/test/test_unittest.py +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader/__init__.py +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader/deductions.py +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader/grader.py +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader/grades_csv.py +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader/grading_item_config.py +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader/send_ctrl_backtick.ahk +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader/student_repos.py +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader/upstream_merger.py +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader/utils.py +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader.egg-info/SOURCES.txt +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader.egg-info/dependency_links.txt +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader.egg-info/requires.txt +0 -0
- {ygrader-2.4.0 → ygrader-2.5.1}/ygrader.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ setup(
|
|
|
4
4
|
name="ygrader",
|
|
5
5
|
packages=["ygrader"],
|
|
6
6
|
package_data={"ygrader": ["*.ahk"]},
|
|
7
|
-
version="2.
|
|
7
|
+
version="2.5.1",
|
|
8
8
|
description="Grading scripts used in BYU's Electrical and Computer Engineering Department",
|
|
9
9
|
author="Jeff Goeders",
|
|
10
10
|
author_email="jeff.goeders@gmail.com",
|
|
@@ -5,7 +5,6 @@ import pathlib
|
|
|
5
5
|
import zipfile
|
|
6
6
|
from typing import Callable, Dict, Optional, Tuple
|
|
7
7
|
|
|
8
|
-
import numpy as np
|
|
9
8
|
import pandas
|
|
10
9
|
import yaml
|
|
11
10
|
|
|
@@ -14,8 +13,12 @@ from .grading_item_config import LearningSuiteColumn
|
|
|
14
13
|
from .utils import warning, print_color, TermColors
|
|
15
14
|
|
|
16
15
|
|
|
17
|
-
# Type alias for late penalty callback:
|
|
18
|
-
|
|
16
|
+
# Type alias for late penalty callback:
|
|
17
|
+
# (due_datetime, submitted_datetime, max_score, actual_score) -> new_score
|
|
18
|
+
# If submitted_datetime is None, the student submitted on time or no submit time was recorded.
|
|
19
|
+
LatePenaltyCallback = Callable[
|
|
20
|
+
[datetime.datetime, Optional[datetime.datetime], float, float], float
|
|
21
|
+
]
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def _load_due_date_exceptions(
|
|
@@ -48,43 +51,13 @@ def _load_due_date_exceptions(
|
|
|
48
51
|
return exceptions
|
|
49
52
|
|
|
50
53
|
|
|
51
|
-
def
|
|
52
|
-
submit_time_str: Optional[str],
|
|
53
|
-
due_date: datetime.datetime,
|
|
54
|
-
) -> int:
|
|
55
|
-
"""Calculate the number of business days late from a submit time.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
submit_time_str: ISO format timestamp string of submission.
|
|
59
|
-
due_date: The effective due date for the student.
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
Number of business days late (0 if on time or no submit time).
|
|
63
|
-
"""
|
|
64
|
-
if not submit_time_str:
|
|
65
|
-
return 0
|
|
66
|
-
|
|
67
|
-
try:
|
|
68
|
-
submit_time = datetime.datetime.fromisoformat(submit_time_str)
|
|
69
|
-
except ValueError:
|
|
70
|
-
return 0
|
|
71
|
-
|
|
72
|
-
if submit_time <= due_date:
|
|
73
|
-
return 0
|
|
74
|
-
|
|
75
|
-
days_late = np.busday_count(due_date.date(), submit_time.date())
|
|
76
|
-
if days_late == 0:
|
|
77
|
-
days_late = 1 # Same day but after deadline
|
|
78
|
-
return int(days_late)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _get_student_key_and_max_late_days(
|
|
54
|
+
def _get_student_key_and_submit_info(
|
|
82
55
|
net_id: str,
|
|
83
56
|
item_deductions: Dict[str, StudentDeductions],
|
|
84
57
|
due_date: Optional[datetime.datetime] = None,
|
|
85
58
|
due_date_exceptions: Optional[Dict[str, datetime.datetime]] = None,
|
|
86
|
-
) -> tuple:
|
|
87
|
-
"""Find the student key and
|
|
59
|
+
) -> Tuple[Optional[tuple], Optional[datetime.datetime], Optional[datetime.datetime]]:
|
|
60
|
+
"""Find the student key and submission timing info across all items.
|
|
88
61
|
|
|
89
62
|
Args:
|
|
90
63
|
net_id: The student's net ID.
|
|
@@ -93,10 +66,12 @@ def _get_student_key_and_max_late_days(
|
|
|
93
66
|
due_date_exceptions: Mapping from net_id to exception due date.
|
|
94
67
|
|
|
95
68
|
Returns:
|
|
96
|
-
Tuple of (student_key or None,
|
|
69
|
+
Tuple of (student_key or None, effective_due_date or None, latest_submit_time or None).
|
|
70
|
+
latest_submit_time is None if on time or no submit time recorded.
|
|
97
71
|
"""
|
|
98
|
-
max_late_days = 0
|
|
99
72
|
found_student_key = None
|
|
73
|
+
latest_submit_time: Optional[datetime.datetime] = None
|
|
74
|
+
effective_due_date: Optional[datetime.datetime] = due_date
|
|
100
75
|
|
|
101
76
|
if due_date_exceptions is None:
|
|
102
77
|
due_date_exceptions = {}
|
|
@@ -123,26 +98,35 @@ def _get_student_key_and_max_late_days(
|
|
|
123
98
|
if student_key:
|
|
124
99
|
found_student_key = student_key
|
|
125
100
|
|
|
126
|
-
# Calculate
|
|
101
|
+
# Calculate effective due date (using most generous exception for group)
|
|
127
102
|
if due_date is not None:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
103
|
+
effective_due_date = due_date
|
|
104
|
+
for member_net_id in student_key:
|
|
105
|
+
if member_net_id in due_date_exceptions:
|
|
106
|
+
effective_due_date = max(
|
|
107
|
+
effective_due_date, due_date_exceptions[member_net_id]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Get submit time
|
|
111
|
+
submit_time_str = deductions_obj.submit_time_by_students.get(student_key)
|
|
112
|
+
if submit_time_str:
|
|
113
|
+
try:
|
|
114
|
+
submit_time = datetime.datetime.fromisoformat(submit_time_str)
|
|
115
|
+
# Track latest submit time across all items
|
|
116
|
+
if latest_submit_time is None or submit_time > latest_submit_time:
|
|
117
|
+
latest_submit_time = submit_time
|
|
118
|
+
except ValueError:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
# Return None for submit_time if on time
|
|
122
|
+
if (
|
|
123
|
+
latest_submit_time is not None
|
|
124
|
+
and effective_due_date is not None
|
|
125
|
+
and latest_submit_time <= effective_due_date
|
|
126
|
+
):
|
|
127
|
+
latest_submit_time = None
|
|
128
|
+
|
|
129
|
+
return found_student_key, effective_due_date, latest_submit_time
|
|
146
130
|
|
|
147
131
|
|
|
148
132
|
def _calculate_student_score(
|
|
@@ -154,7 +138,7 @@ def _calculate_student_score(
|
|
|
154
138
|
warn_on_missing_callback: bool = True,
|
|
155
139
|
due_date: Optional[datetime.datetime] = None,
|
|
156
140
|
due_date_exceptions: Optional[Dict[str, datetime.datetime]] = None,
|
|
157
|
-
) -> Tuple[float, float,
|
|
141
|
+
) -> Tuple[float, float, Optional[datetime.datetime]]:
|
|
158
142
|
"""Calculate a student's final score.
|
|
159
143
|
|
|
160
144
|
Args:
|
|
@@ -162,18 +146,21 @@ def _calculate_student_score(
|
|
|
162
146
|
ls_column: The LearningSuiteColumn configuration.
|
|
163
147
|
item_deductions: Mapping from item name to StudentDeductions.
|
|
164
148
|
late_penalty_callback: Optional callback for late penalty.
|
|
165
|
-
warn_on_missing_callback: Whether to warn if late
|
|
149
|
+
warn_on_missing_callback: Whether to warn if late but no callback.
|
|
166
150
|
due_date: The default due date for the assignment.
|
|
167
151
|
due_date_exceptions: Mapping from net_id to exception due date.
|
|
168
152
|
|
|
169
153
|
Returns:
|
|
170
|
-
Tuple of (final_score, total_possible,
|
|
154
|
+
Tuple of (final_score, total_possible, submitted_datetime or None if on time).
|
|
171
155
|
"""
|
|
172
156
|
total_possible = sum(item.points for item in ls_column.items)
|
|
173
|
-
|
|
157
|
+
total_score = 0.0
|
|
174
158
|
|
|
175
159
|
for item in ls_column.items:
|
|
176
160
|
deductions_obj = item_deductions.get(item.name)
|
|
161
|
+
student_graded = False
|
|
162
|
+
item_deduction_total = 0.0
|
|
163
|
+
|
|
177
164
|
if deductions_obj:
|
|
178
165
|
# Find the student's deductions
|
|
179
166
|
student_key = None
|
|
@@ -186,28 +173,39 @@ def _calculate_student_score(
|
|
|
186
173
|
break
|
|
187
174
|
|
|
188
175
|
if student_key:
|
|
176
|
+
student_graded = True
|
|
189
177
|
deductions = deductions_obj.deductions_by_students[student_key]
|
|
190
178
|
for deduction in deductions:
|
|
191
|
-
|
|
179
|
+
item_deduction_total += deduction.points
|
|
192
180
|
|
|
193
|
-
|
|
194
|
-
|
|
181
|
+
# Only award points if the student was graded for this item
|
|
182
|
+
if student_graded:
|
|
183
|
+
total_score += max(0, item.points - item_deduction_total)
|
|
184
|
+
# else: student gets 0 for this item (not graded)
|
|
195
185
|
|
|
196
|
-
#
|
|
197
|
-
|
|
186
|
+
# Score is already calculated
|
|
187
|
+
score = total_score
|
|
188
|
+
|
|
189
|
+
# Get submit info
|
|
190
|
+
_, effective_due_date, submitted_datetime = _get_student_key_and_submit_info(
|
|
198
191
|
net_id, item_deductions, due_date, due_date_exceptions
|
|
199
192
|
)
|
|
200
193
|
|
|
201
|
-
# Apply late penalty if applicable
|
|
202
|
-
if
|
|
203
|
-
if late_penalty_callback:
|
|
204
|
-
score = max(
|
|
194
|
+
# Apply late penalty if applicable (submitted_datetime is None if on time)
|
|
195
|
+
if submitted_datetime is not None:
|
|
196
|
+
if late_penalty_callback and effective_due_date is not None:
|
|
197
|
+
score = max(
|
|
198
|
+
0,
|
|
199
|
+
late_penalty_callback(
|
|
200
|
+
effective_due_date, submitted_datetime, total_possible, score
|
|
201
|
+
),
|
|
202
|
+
)
|
|
205
203
|
elif warn_on_missing_callback:
|
|
206
204
|
warning(
|
|
207
|
-
f"Student {net_id}
|
|
205
|
+
f"Student {net_id} submitted late but no late penalty callback provided"
|
|
208
206
|
)
|
|
209
207
|
|
|
210
|
-
return score, total_possible,
|
|
208
|
+
return score, total_possible, submitted_datetime
|
|
211
209
|
|
|
212
210
|
|
|
213
211
|
def assemble_grades(
|
|
@@ -230,7 +228,8 @@ def assemble_grades(
|
|
|
230
228
|
output_zip_path: Path for the output zip file. If None, no zip is generated.
|
|
231
229
|
output_csv_path: Path for the output CSV file. If None, no CSV is generated.
|
|
232
230
|
late_penalty_callback: Optional callback function that takes
|
|
233
|
-
(
|
|
231
|
+
(due_datetime, submitted_datetime, max_score, actual_score) and returns the adjusted score.
|
|
232
|
+
submitted_datetime will be None if on time.
|
|
234
233
|
due_date: The default due date for the assignment. Required for late penalty.
|
|
235
234
|
due_date_exceptions_path: Path to YAML file with due date exceptions (net_id: "YYYY-MM-DD HH:MM:SS").
|
|
236
235
|
|
|
@@ -299,8 +298,15 @@ def assemble_grades(
|
|
|
299
298
|
f"but NOT [{', '.join(items_not_graded)}]",
|
|
300
299
|
)
|
|
301
300
|
|
|
301
|
+
# Get submit info for this student
|
|
302
|
+
_, effective_due_date, submitted_datetime = (
|
|
303
|
+
_get_student_key_and_submit_info(
|
|
304
|
+
net_id, subitem_deductions, due_date, due_date_exceptions
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
302
308
|
# Calculate score before late penalty
|
|
303
|
-
score_before_late, total_possible,
|
|
309
|
+
score_before_late, total_possible, _ = _calculate_student_score(
|
|
304
310
|
net_id=net_id,
|
|
305
311
|
ls_column=ls_column,
|
|
306
312
|
item_deductions=subitem_deductions,
|
|
@@ -310,18 +316,25 @@ def assemble_grades(
|
|
|
310
316
|
due_date_exceptions=due_date_exceptions,
|
|
311
317
|
)
|
|
312
318
|
|
|
313
|
-
# Apply late penalty if applicable
|
|
319
|
+
# Apply late penalty if applicable (submitted_datetime is None if on time)
|
|
314
320
|
final_score = score_before_late
|
|
315
|
-
if
|
|
321
|
+
if (
|
|
322
|
+
submitted_datetime is not None
|
|
323
|
+
and late_penalty_callback
|
|
324
|
+
and effective_due_date is not None
|
|
325
|
+
):
|
|
316
326
|
final_score = max(
|
|
317
327
|
0,
|
|
318
328
|
late_penalty_callback(
|
|
319
|
-
|
|
329
|
+
effective_due_date,
|
|
330
|
+
submitted_datetime,
|
|
331
|
+
total_possible,
|
|
332
|
+
score_before_late,
|
|
320
333
|
),
|
|
321
334
|
)
|
|
322
335
|
print_color(
|
|
323
336
|
TermColors.YELLOW,
|
|
324
|
-
f"Late: {net_id} (
|
|
337
|
+
f"Late: {net_id} (submitted {submitted_datetime}): "
|
|
325
338
|
f"{score_before_late:.1f} -> {final_score:.1f}",
|
|
326
339
|
)
|
|
327
340
|
|
|
@@ -419,7 +432,8 @@ def _generate_student_feedback(
|
|
|
419
432
|
total_points_possible += subitem_points_possible
|
|
420
433
|
|
|
421
434
|
subitem_points_deducted = 0
|
|
422
|
-
|
|
435
|
+
item_deduction_list = []
|
|
436
|
+
student_graded = False
|
|
423
437
|
|
|
424
438
|
# Get deductions for this student in this item
|
|
425
439
|
student_deductions_obj = subitem_deductions.get(item.name)
|
|
@@ -436,13 +450,19 @@ def _generate_student_feedback(
|
|
|
436
450
|
break
|
|
437
451
|
|
|
438
452
|
if student_key:
|
|
453
|
+
student_graded = True
|
|
439
454
|
deductions = student_deductions_obj.deductions_by_students[student_key]
|
|
440
455
|
for deduction in deductions:
|
|
441
|
-
|
|
456
|
+
item_deduction_list.append((deduction.message, deduction.points))
|
|
442
457
|
subitem_points_deducted += deduction.points
|
|
443
458
|
|
|
444
|
-
# Calculate item score
|
|
445
|
-
|
|
459
|
+
# Calculate item score (0 if not graded)
|
|
460
|
+
if student_graded:
|
|
461
|
+
subitem_score = max(0, subitem_points_possible - subitem_points_deducted)
|
|
462
|
+
else:
|
|
463
|
+
subitem_score = 0
|
|
464
|
+
item_deduction_list.append(("Not graded", subitem_points_possible))
|
|
465
|
+
subitem_points_deducted = subitem_points_possible
|
|
446
466
|
score_str = f"{subitem_score:.1f} / {subitem_points_possible:.1f}"
|
|
447
467
|
|
|
448
468
|
# Item line with score
|
|
@@ -452,7 +472,7 @@ def _generate_student_feedback(
|
|
|
452
472
|
)
|
|
453
473
|
|
|
454
474
|
# Deduction lines (indented)
|
|
455
|
-
for msg, pts in
|
|
475
|
+
for msg, pts in item_deduction_list:
|
|
456
476
|
# Wrap long messages
|
|
457
477
|
wrapped = _wrap_text(msg, deduction_msg_width)
|
|
458
478
|
for i, line_text in enumerate(wrapped):
|
|
@@ -469,23 +489,30 @@ def _generate_student_feedback(
|
|
|
469
489
|
# Calculate score before late penalty (clamped to 0)
|
|
470
490
|
score_before_late = max(0, total_points_possible - total_points_deducted)
|
|
471
491
|
|
|
472
|
-
# Get
|
|
473
|
-
_,
|
|
492
|
+
# Get submit info for this student
|
|
493
|
+
_, effective_due_date, submitted_datetime = _get_student_key_and_submit_info(
|
|
474
494
|
net_id, subitem_deductions, due_date, due_date_exceptions
|
|
475
495
|
)
|
|
476
496
|
|
|
477
497
|
# Late penalty section
|
|
478
498
|
lines.append("")
|
|
479
499
|
lines.append("=" * 60)
|
|
480
|
-
if
|
|
500
|
+
if (
|
|
501
|
+
submitted_datetime is not None
|
|
502
|
+
and late_penalty_callback
|
|
503
|
+
and effective_due_date is not None
|
|
504
|
+
):
|
|
481
505
|
final_score = late_penalty_callback(
|
|
482
|
-
|
|
506
|
+
effective_due_date,
|
|
507
|
+
submitted_datetime,
|
|
508
|
+
total_points_possible,
|
|
509
|
+
score_before_late,
|
|
483
510
|
)
|
|
484
511
|
# Ensure final score is not negative
|
|
485
512
|
final_score = max(0, final_score)
|
|
486
513
|
late_penalty_points = score_before_late - final_score
|
|
487
514
|
late_label = (
|
|
488
|
-
f"Late Penalty (
|
|
515
|
+
f"Late Penalty (submitted {submitted_datetime.strftime('%Y-%m-%d %H:%M')}):"
|
|
489
516
|
)
|
|
490
517
|
lines.append(
|
|
491
518
|
f"{late_label:<{item_col_width}} {-late_penalty_points:>{score_col_width}.1f}"
|
|
@@ -37,12 +37,15 @@ class GradeItem:
|
|
|
37
37
|
self.fcn_args_dict = fcn_args_dict if fcn_args_dict is not None else {}
|
|
38
38
|
self.student_deductions = StudentDeductions(deductions_yaml_path)
|
|
39
39
|
self.last_graded_net_ids = None # Track last graded student for undo
|
|
40
|
-
self.names_by_netid =
|
|
40
|
+
self.names_by_netid = (
|
|
41
|
+
self._build_names_lookup()
|
|
42
|
+
) # net_id -> (first_name, last_name)
|
|
41
43
|
|
|
42
44
|
def _build_names_lookup(self):
|
|
43
45
|
"""Build a lookup dictionary from net_id to (first_name, last_name) from the class list CSV."""
|
|
44
46
|
# Import pandas here to avoid circular import and since it's already imported in grader.py
|
|
45
47
|
import pandas # pylint: disable=import-outside-toplevel
|
|
48
|
+
|
|
46
49
|
names_by_netid = {}
|
|
47
50
|
try:
|
|
48
51
|
df = pandas.read_csv(self.grader.class_list_csv_path)
|
|
@@ -51,7 +54,11 @@ class GradeItem:
|
|
|
51
54
|
net_id = row["Net ID"]
|
|
52
55
|
first_name = row["First Name"]
|
|
53
56
|
last_name = row["Last Name"]
|
|
54
|
-
if
|
|
57
|
+
if (
|
|
58
|
+
pandas.notna(net_id)
|
|
59
|
+
and pandas.notna(first_name)
|
|
60
|
+
and pandas.notna(last_name)
|
|
61
|
+
):
|
|
55
62
|
names_by_netid[net_id] = (first_name, last_name)
|
|
56
63
|
except (FileNotFoundError, pandas.errors.EmptyDataError, KeyError):
|
|
57
64
|
pass # If we can't read the CSV, just use an empty dict
|
|
@@ -259,6 +266,9 @@ class GradeItem:
|
|
|
259
266
|
# run again, but don't build
|
|
260
267
|
build = False
|
|
261
268
|
continue
|
|
269
|
+
if score == ScoreResult.EXIT:
|
|
270
|
+
print_color(TermColors.BLUE, "Exiting grader")
|
|
271
|
+
sys.exit(0)
|
|
262
272
|
if score == ScoreResult.UNDO_LAST:
|
|
263
273
|
# Undo the last graded student and signal to go back
|
|
264
274
|
if self.last_graded_net_ids is not None:
|
|
@@ -276,7 +286,9 @@ class GradeItem:
|
|
|
276
286
|
# Record score - save submit_time and ensure the student is in the deductions file
|
|
277
287
|
# (even if they have no deductions, to indicate they were graded)
|
|
278
288
|
if pending_submit_time is not None:
|
|
279
|
-
self.student_deductions.set_submit_time(
|
|
289
|
+
self.student_deductions.set_submit_time(
|
|
290
|
+
tuple(net_ids), pending_submit_time
|
|
291
|
+
)
|
|
280
292
|
self.student_deductions.ensure_student_in_file(tuple(net_ids))
|
|
281
293
|
# Track this student as last graded for undo functionality
|
|
282
294
|
self.last_graded_net_ids = tuple(net_ids)
|
|
@@ -13,6 +13,7 @@ class ScoreResult(Enum):
|
|
|
13
13
|
RERUN = auto()
|
|
14
14
|
CREATE_DEDUCTION = auto()
|
|
15
15
|
UNDO_LAST = auto()
|
|
16
|
+
EXIT = auto()
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def get_score(
|
|
@@ -55,8 +56,7 @@ def get_score(
|
|
|
55
56
|
# Show current deductions for this student
|
|
56
57
|
current_deductions = student_deductions.get_student_deductions(tuple(net_ids))
|
|
57
58
|
print(
|
|
58
|
-
fpad2
|
|
59
|
-
+ f"Current score: {TermColors.GREEN}{computed_score}{TermColors.END}"
|
|
59
|
+
fpad2 + f"Current score: {TermColors.GREEN}{computed_score}{TermColors.END}"
|
|
60
60
|
)
|
|
61
61
|
print(fpad2 + "Current deductions:")
|
|
62
62
|
if current_deductions:
|
|
@@ -99,11 +99,15 @@ def get_score(
|
|
|
99
99
|
left_items.append(("[g]", "Manage grades"))
|
|
100
100
|
allowed_cmds["g"] = "manage"
|
|
101
101
|
|
|
102
|
-
# Add undo option
|
|
102
|
+
# Add undo option if there's a last graded student
|
|
103
103
|
if last_graded_net_ids is not None:
|
|
104
104
|
left_items.append(("[u]", f"Undo last ({last_graded_net_ids[0]})"))
|
|
105
105
|
allowed_cmds["u"] = ScoreResult.UNDO_LAST
|
|
106
106
|
|
|
107
|
+
# Add exit option at bottom of left column
|
|
108
|
+
left_items.append(("[e]", "Exit grader"))
|
|
109
|
+
allowed_cmds["e"] = ScoreResult.EXIT
|
|
110
|
+
|
|
107
111
|
# Format menu items in two columns
|
|
108
112
|
col_width = 38 # Each column width (2 columns * 38 = 76 < 80)
|
|
109
113
|
input_txt = (
|
|
@@ -269,7 +273,10 @@ def _manage_grades_interactive(student_deductions, names_by_netid=None):
|
|
|
269
273
|
# Check if search matches first/last name
|
|
270
274
|
if names_by_netid and net_id in names_by_netid:
|
|
271
275
|
first_name, last_name = names_by_netid[net_id]
|
|
272
|
-
if not list_all and (
|
|
276
|
+
if not list_all and (
|
|
277
|
+
search_lower in first_name.lower()
|
|
278
|
+
or search_lower in last_name.lower()
|
|
279
|
+
):
|
|
273
280
|
match_found = True
|
|
274
281
|
display_parts.append(f"{first_name} {last_name} ({net_id})")
|
|
275
282
|
else:
|
|
@@ -309,9 +316,11 @@ def _manage_grades_interactive(student_deductions, names_by_netid=None):
|
|
|
309
316
|
if 0 <= idx < len(matches):
|
|
310
317
|
student_key, display = matches[idx]
|
|
311
318
|
# Confirm deletion
|
|
312
|
-
confirm =
|
|
313
|
-
f"Delete grade for {display}? This cannot be undone. [y/N]: "
|
|
314
|
-
|
|
319
|
+
confirm = (
|
|
320
|
+
input(f"Delete grade for {display}? This cannot be undone. [y/N]: ")
|
|
321
|
+
.strip()
|
|
322
|
+
.lower()
|
|
323
|
+
)
|
|
315
324
|
|
|
316
325
|
if confirm == "y":
|
|
317
326
|
student_deductions.clear_student_deductions(student_key)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|