ygrader 2.5.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.5.0/ygrader.egg-info → ygrader-2.5.1}/PKG-INFO +1 -1
- {ygrader-2.5.0 → ygrader-2.5.1}/setup.py +1 -1
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader/feedback.py +36 -13
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader/grading_item.py +15 -3
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader/score_input.py +16 -7
- {ygrader-2.5.0 → ygrader-2.5.1/ygrader.egg-info}/PKG-INFO +1 -1
- {ygrader-2.5.0 → ygrader-2.5.1}/LICENSE +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/setup.cfg +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/test/test_interactive.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/test/test_unittest.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader/__init__.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader/deductions.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader/grader.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader/grades_csv.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader/grading_item_config.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader/send_ctrl_backtick.ahk +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader/student_repos.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader/upstream_merger.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader/utils.py +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader.egg-info/SOURCES.txt +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader.egg-info/dependency_links.txt +0 -0
- {ygrader-2.5.0 → ygrader-2.5.1}/ygrader.egg-info/requires.txt +0 -0
- {ygrader-2.5.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.5.
|
|
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",
|
|
@@ -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:
|
|
@@ -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
|