ygrader 2.5.0__tar.gz → 2.5.2__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.5.0/ygrader.egg-info → ygrader-2.5.2}/PKG-INFO +1 -1
- {ygrader-2.5.0 → ygrader-2.5.2}/setup.py +1 -1
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader/deductions.py +107 -1
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader/feedback.py +36 -13
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader/grading_item.py +15 -3
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader/score_input.py +27 -8
- {ygrader-2.5.0 → ygrader-2.5.2/ygrader.egg-info}/PKG-INFO +1 -1
- {ygrader-2.5.0 → ygrader-2.5.2}/LICENSE +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/setup.cfg +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/test/test_interactive.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/test/test_unittest.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader/__init__.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader/grader.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader/grades_csv.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader/grading_item_config.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader/send_ctrl_backtick.ahk +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader/student_repos.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader/upstream_merger.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader/utils.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader.egg-info/SOURCES.txt +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader.egg-info/dependency_links.txt +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/ygrader.egg-info/requires.txt +0 -0
- {ygrader-2.5.0 → ygrader-2.5.2}/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.5.
|
|
7
|
+
version="2.5.2",
|
|
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",
|
|
@@ -214,9 +214,14 @@ class StudentDeductions:
|
|
|
214
214
|
# Not found, create a new one
|
|
215
215
|
return self.add_deduction_type(message, points)
|
|
216
216
|
|
|
217
|
-
def create_deduction_type_interactive(
|
|
217
|
+
def create_deduction_type_interactive(
|
|
218
|
+
self, max_points: Optional[float] = None
|
|
219
|
+
) -> int:
|
|
218
220
|
"""Interactively prompt the user to create a new deduction type.
|
|
219
221
|
|
|
222
|
+
Args:
|
|
223
|
+
max_points: Optional maximum points for validation.
|
|
224
|
+
|
|
220
225
|
Returns:
|
|
221
226
|
The ID of the created deduction type, or -1 if cancelled.
|
|
222
227
|
"""
|
|
@@ -236,6 +241,18 @@ class StudentDeductions:
|
|
|
236
241
|
return -1
|
|
237
242
|
try:
|
|
238
243
|
points = float(points_str)
|
|
244
|
+
if points < 0:
|
|
245
|
+
print_color(
|
|
246
|
+
TermColors.YELLOW,
|
|
247
|
+
"Deduction cannot be negative. Try again.",
|
|
248
|
+
)
|
|
249
|
+
continue
|
|
250
|
+
if max_points is not None and points > max_points:
|
|
251
|
+
print_color(
|
|
252
|
+
TermColors.YELLOW,
|
|
253
|
+
f"Deduction ({points}) cannot exceed max points ({max_points}). Try again.",
|
|
254
|
+
)
|
|
255
|
+
continue
|
|
239
256
|
break
|
|
240
257
|
except ValueError:
|
|
241
258
|
print("Invalid number. Try again.")
|
|
@@ -330,6 +347,95 @@ class StudentDeductions:
|
|
|
330
347
|
print(f"Deleted deduction type [{deduction_id}]: {deduction_type.message}")
|
|
331
348
|
return True
|
|
332
349
|
|
|
350
|
+
def change_deduction_value(self, deduction_id: int, new_points: float) -> bool:
|
|
351
|
+
"""Change the point value of an existing deduction type.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
deduction_id: The ID of the deduction type to modify.
|
|
355
|
+
new_points: The new point value.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
True if successful, False if deduction_id not found.
|
|
359
|
+
"""
|
|
360
|
+
if deduction_id not in self.deduction_types:
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
self.deduction_types[deduction_id].points = new_points
|
|
364
|
+
self._save()
|
|
365
|
+
return True
|
|
366
|
+
|
|
367
|
+
def change_deduction_value_interactive(
|
|
368
|
+
self, max_points: Optional[float] = None
|
|
369
|
+
) -> bool:
|
|
370
|
+
"""Interactively prompt the user to change a deduction type's point value.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
max_points: Optional maximum points for validation.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
True if a deduction value was changed, False otherwise.
|
|
377
|
+
"""
|
|
378
|
+
if not self.deduction_types:
|
|
379
|
+
print("No deduction types to modify.")
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
print("\nChange deduction value (empty input to cancel):")
|
|
383
|
+
print("Available deduction types:")
|
|
384
|
+
for deduction_id, deduction_type in self.deduction_types.items():
|
|
385
|
+
in_use = " (IN USE)" if self.is_deduction_in_use(deduction_id) else ""
|
|
386
|
+
print(
|
|
387
|
+
f" [{deduction_id}] -{deduction_type.points}: {deduction_type.message}{in_use}"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
id_str = input(" Enter ID to modify: ").strip()
|
|
391
|
+
if not id_str:
|
|
392
|
+
print("Cancelled.")
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
deduction_id = int(id_str)
|
|
397
|
+
except ValueError:
|
|
398
|
+
print("Invalid ID.")
|
|
399
|
+
return False
|
|
400
|
+
|
|
401
|
+
if deduction_id not in self.deduction_types:
|
|
402
|
+
print("Deduction type not found.")
|
|
403
|
+
return False
|
|
404
|
+
|
|
405
|
+
deduction_type = self.deduction_types[deduction_id]
|
|
406
|
+
print(f" Current value: {deduction_type.points} points")
|
|
407
|
+
|
|
408
|
+
while True:
|
|
409
|
+
points_str = input(" Enter new points value: ").strip()
|
|
410
|
+
if not points_str:
|
|
411
|
+
print("Cancelled.")
|
|
412
|
+
return False
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
new_points = float(points_str)
|
|
416
|
+
if new_points < 0:
|
|
417
|
+
print_color(
|
|
418
|
+
TermColors.YELLOW,
|
|
419
|
+
"Deduction cannot be negative. Try again.",
|
|
420
|
+
)
|
|
421
|
+
continue
|
|
422
|
+
if max_points is not None and new_points > max_points:
|
|
423
|
+
print_color(
|
|
424
|
+
TermColors.YELLOW,
|
|
425
|
+
f"Deduction ({new_points}) cannot exceed max points ({max_points}). Try again.",
|
|
426
|
+
)
|
|
427
|
+
continue
|
|
428
|
+
break
|
|
429
|
+
except ValueError:
|
|
430
|
+
print("Invalid number. Try again.")
|
|
431
|
+
|
|
432
|
+
old_points = deduction_type.points
|
|
433
|
+
self.change_deduction_value(deduction_id, new_points)
|
|
434
|
+
print(
|
|
435
|
+
f"Changed deduction [{deduction_id}] from {old_points} to {new_points} points"
|
|
436
|
+
)
|
|
437
|
+
return True
|
|
438
|
+
|
|
333
439
|
def get_student_deductions(self, net_ids: tuple) -> List[DeductionType]:
|
|
334
440
|
"""Get the list of deductions applied to a student.
|
|
335
441
|
|
|
@@ -154,10 +154,13 @@ def _calculate_student_score(
|
|
|
154
154
|
Tuple of (final_score, total_possible, submitted_datetime or None if on time).
|
|
155
155
|
"""
|
|
156
156
|
total_possible = sum(item.points for item in ls_column.items)
|
|
157
|
-
|
|
157
|
+
total_score = 0.0
|
|
158
158
|
|
|
159
159
|
for item in ls_column.items:
|
|
160
160
|
deductions_obj = item_deductions.get(item.name)
|
|
161
|
+
student_graded = False
|
|
162
|
+
item_deduction_total = 0.0
|
|
163
|
+
|
|
161
164
|
if deductions_obj:
|
|
162
165
|
# Find the student's deductions
|
|
163
166
|
student_key = None
|
|
@@ -170,12 +173,18 @@ def _calculate_student_score(
|
|
|
170
173
|
break
|
|
171
174
|
|
|
172
175
|
if student_key:
|
|
176
|
+
student_graded = True
|
|
173
177
|
deductions = deductions_obj.deductions_by_students[student_key]
|
|
174
178
|
for deduction in deductions:
|
|
175
|
-
|
|
179
|
+
item_deduction_total += deduction.points
|
|
180
|
+
|
|
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)
|
|
176
185
|
|
|
177
|
-
#
|
|
178
|
-
score =
|
|
186
|
+
# Score is already calculated
|
|
187
|
+
score = total_score
|
|
179
188
|
|
|
180
189
|
# Get submit info
|
|
181
190
|
_, effective_due_date, submitted_datetime = _get_student_key_and_submit_info(
|
|
@@ -290,8 +299,10 @@ def assemble_grades(
|
|
|
290
299
|
)
|
|
291
300
|
|
|
292
301
|
# Get submit info for this student
|
|
293
|
-
_, effective_due_date, submitted_datetime =
|
|
294
|
-
|
|
302
|
+
_, effective_due_date, submitted_datetime = (
|
|
303
|
+
_get_student_key_and_submit_info(
|
|
304
|
+
net_id, subitem_deductions, due_date, due_date_exceptions
|
|
305
|
+
)
|
|
295
306
|
)
|
|
296
307
|
|
|
297
308
|
# Calculate score before late penalty
|
|
@@ -421,7 +432,8 @@ def _generate_student_feedback(
|
|
|
421
432
|
total_points_possible += subitem_points_possible
|
|
422
433
|
|
|
423
434
|
subitem_points_deducted = 0
|
|
424
|
-
|
|
435
|
+
item_deduction_list = []
|
|
436
|
+
student_graded = False
|
|
425
437
|
|
|
426
438
|
# Get deductions for this student in this item
|
|
427
439
|
student_deductions_obj = subitem_deductions.get(item.name)
|
|
@@ -438,13 +450,19 @@ def _generate_student_feedback(
|
|
|
438
450
|
break
|
|
439
451
|
|
|
440
452
|
if student_key:
|
|
453
|
+
student_graded = True
|
|
441
454
|
deductions = student_deductions_obj.deductions_by_students[student_key]
|
|
442
455
|
for deduction in deductions:
|
|
443
|
-
|
|
456
|
+
item_deduction_list.append((deduction.message, deduction.points))
|
|
444
457
|
subitem_points_deducted += deduction.points
|
|
445
458
|
|
|
446
|
-
# Calculate item score
|
|
447
|
-
|
|
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
|
|
448
466
|
score_str = f"{subitem_score:.1f} / {subitem_points_possible:.1f}"
|
|
449
467
|
|
|
450
468
|
# Item line with score
|
|
@@ -454,7 +472,7 @@ def _generate_student_feedback(
|
|
|
454
472
|
)
|
|
455
473
|
|
|
456
474
|
# Deduction lines (indented)
|
|
457
|
-
for msg, pts in
|
|
475
|
+
for msg, pts in item_deduction_list:
|
|
458
476
|
# Wrap long messages
|
|
459
477
|
wrapped = _wrap_text(msg, deduction_msg_width)
|
|
460
478
|
for i, line_text in enumerate(wrapped):
|
|
@@ -485,12 +503,17 @@ def _generate_student_feedback(
|
|
|
485
503
|
and effective_due_date is not None
|
|
486
504
|
):
|
|
487
505
|
final_score = late_penalty_callback(
|
|
488
|
-
effective_due_date,
|
|
506
|
+
effective_due_date,
|
|
507
|
+
submitted_datetime,
|
|
508
|
+
total_points_possible,
|
|
509
|
+
score_before_late,
|
|
489
510
|
)
|
|
490
511
|
# Ensure final score is not negative
|
|
491
512
|
final_score = max(0, final_score)
|
|
492
513
|
late_penalty_points = score_before_late - final_score
|
|
493
|
-
late_label =
|
|
514
|
+
late_label = (
|
|
515
|
+
f"Late Penalty (submitted {submitted_datetime.strftime('%Y-%m-%d %H:%M')}):"
|
|
516
|
+
)
|
|
494
517
|
lines.append(
|
|
495
518
|
f"{late_label:<{item_col_width}} {-late_penalty_points:>{score_col_width}.1f}"
|
|
496
519
|
)
|
|
@@ -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:
|
|
@@ -92,6 +92,9 @@ def get_score(
|
|
|
92
92
|
|
|
93
93
|
right_items.append(("[0]", "Clear deductions"))
|
|
94
94
|
|
|
95
|
+
right_items.append(("[v]", "Change deduction value"))
|
|
96
|
+
allowed_cmds["v"] = "change_value"
|
|
97
|
+
|
|
95
98
|
# Accept score at the bottom of right column
|
|
96
99
|
right_items.append(("[Enter]", "Accept score"))
|
|
97
100
|
|
|
@@ -99,11 +102,15 @@ def get_score(
|
|
|
99
102
|
left_items.append(("[g]", "Manage grades"))
|
|
100
103
|
allowed_cmds["g"] = "manage"
|
|
101
104
|
|
|
102
|
-
# Add undo option
|
|
105
|
+
# Add undo option if there's a last graded student
|
|
103
106
|
if last_graded_net_ids is not None:
|
|
104
107
|
left_items.append(("[u]", f"Undo last ({last_graded_net_ids[0]})"))
|
|
105
108
|
allowed_cmds["u"] = ScoreResult.UNDO_LAST
|
|
106
109
|
|
|
110
|
+
# Add exit option at bottom of left column
|
|
111
|
+
left_items.append(("[e]", "Exit grader"))
|
|
112
|
+
allowed_cmds["e"] = ScoreResult.EXIT
|
|
113
|
+
|
|
107
114
|
# Format menu items in two columns
|
|
108
115
|
col_width = 38 # Each column width (2 columns * 38 = 76 < 80)
|
|
109
116
|
input_txt = (
|
|
@@ -163,7 +170,9 @@ def get_score(
|
|
|
163
170
|
|
|
164
171
|
# Handle special cases that need to loop back
|
|
165
172
|
if result == "create":
|
|
166
|
-
deduction_id = student_deductions.create_deduction_type_interactive(
|
|
173
|
+
deduction_id = student_deductions.create_deduction_type_interactive(
|
|
174
|
+
max_points=max_points
|
|
175
|
+
)
|
|
167
176
|
if deduction_id >= 0:
|
|
168
177
|
# Auto-apply the new deduction to this student
|
|
169
178
|
student_deductions.apply_deduction_to_student(
|
|
@@ -173,6 +182,11 @@ def get_score(
|
|
|
173
182
|
if result == "delete":
|
|
174
183
|
student_deductions.delete_deduction_type_interactive()
|
|
175
184
|
continue
|
|
185
|
+
if result == "change_value":
|
|
186
|
+
student_deductions.change_deduction_value_interactive(
|
|
187
|
+
max_points=max_points
|
|
188
|
+
)
|
|
189
|
+
continue
|
|
176
190
|
if result == "manage":
|
|
177
191
|
_manage_grades_interactive(student_deductions, names_by_netid)
|
|
178
192
|
continue
|
|
@@ -269,7 +283,10 @@ def _manage_grades_interactive(student_deductions, names_by_netid=None):
|
|
|
269
283
|
# Check if search matches first/last name
|
|
270
284
|
if names_by_netid and net_id in names_by_netid:
|
|
271
285
|
first_name, last_name = names_by_netid[net_id]
|
|
272
|
-
if not list_all and (
|
|
286
|
+
if not list_all and (
|
|
287
|
+
search_lower in first_name.lower()
|
|
288
|
+
or search_lower in last_name.lower()
|
|
289
|
+
):
|
|
273
290
|
match_found = True
|
|
274
291
|
display_parts.append(f"{first_name} {last_name} ({net_id})")
|
|
275
292
|
else:
|
|
@@ -309,9 +326,11 @@ def _manage_grades_interactive(student_deductions, names_by_netid=None):
|
|
|
309
326
|
if 0 <= idx < len(matches):
|
|
310
327
|
student_key, display = matches[idx]
|
|
311
328
|
# Confirm deletion
|
|
312
|
-
confirm =
|
|
313
|
-
f"Delete grade for {display}? This cannot be undone. [y/N]: "
|
|
314
|
-
|
|
329
|
+
confirm = (
|
|
330
|
+
input(f"Delete grade for {display}? This cannot be undone. [y/N]: ")
|
|
331
|
+
.strip()
|
|
332
|
+
.lower()
|
|
333
|
+
)
|
|
315
334
|
|
|
316
335
|
if confirm == "y":
|
|
317
336
|
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
|