ygrader 2.3.0__tar.gz → 2.5.0__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.3.0/ygrader.egg-info → ygrader-2.5.0}/PKG-INFO +1 -1
- {ygrader-2.3.0 → ygrader-2.5.0}/setup.py +1 -1
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader/feedback.py +113 -73
- {ygrader-2.3.0 → ygrader-2.5.0/ygrader.egg-info}/PKG-INFO +1 -1
- {ygrader-2.3.0 → ygrader-2.5.0}/LICENSE +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/setup.cfg +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/test/test_interactive.py +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/test/test_unittest.py +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader/__init__.py +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader/deductions.py +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader/grader.py +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader/grades_csv.py +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader/grading_item.py +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader/grading_item_config.py +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader/score_input.py +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader/send_ctrl_backtick.ahk +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader/student_repos.py +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader/upstream_merger.py +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader/utils.py +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader.egg-info/SOURCES.txt +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader.egg-info/dependency_links.txt +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/ygrader.egg-info/requires.txt +0 -0
- {ygrader-2.3.0 → ygrader-2.5.0}/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.0",
|
|
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,55 +5,59 @@ 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
|
|
9
|
+
import yaml
|
|
10
10
|
|
|
11
11
|
from .deductions import StudentDeductions
|
|
12
12
|
from .grading_item_config import LearningSuiteColumn
|
|
13
13
|
from .utils import warning, print_color, TermColors
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
# Type alias for late penalty callback:
|
|
17
|
-
|
|
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
|
+
]
|
|
18
22
|
|
|
19
23
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"""Calculate the number of business days late from a submit time.
|
|
24
|
+
def _load_due_date_exceptions(
|
|
25
|
+
exceptions_path: pathlib.Path,
|
|
26
|
+
) -> Dict[str, datetime.datetime]:
|
|
27
|
+
"""Load due date exceptions from YAML file.
|
|
25
28
|
|
|
26
29
|
Args:
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
exceptions_path: Path to the deadline_exceptions.yaml file.
|
|
31
|
+
Expected format is: net_id: "YYYY-MM-DD HH:MM:SS"
|
|
29
32
|
|
|
30
33
|
Returns:
|
|
31
|
-
|
|
34
|
+
Mapping from net_id to exception datetime.
|
|
32
35
|
"""
|
|
33
|
-
if not
|
|
34
|
-
return
|
|
36
|
+
if not exceptions_path.exists():
|
|
37
|
+
return {}
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
except ValueError:
|
|
39
|
-
return 0
|
|
39
|
+
with open(exceptions_path, "r", encoding="utf-8") as f:
|
|
40
|
+
data = yaml.safe_load(f)
|
|
40
41
|
|
|
41
|
-
if
|
|
42
|
-
return
|
|
42
|
+
if not data:
|
|
43
|
+
return {}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
exceptions = {}
|
|
46
|
+
for net_id, exception_date in data.items():
|
|
47
|
+
if net_id and exception_date:
|
|
48
|
+
exceptions[net_id] = datetime.datetime.strptime(
|
|
49
|
+
exception_date, "%Y-%m-%d %H:%M:%S"
|
|
50
|
+
)
|
|
51
|
+
return exceptions
|
|
48
52
|
|
|
49
53
|
|
|
50
|
-
def
|
|
54
|
+
def _get_student_key_and_submit_info(
|
|
51
55
|
net_id: str,
|
|
52
56
|
item_deductions: Dict[str, StudentDeductions],
|
|
53
57
|
due_date: Optional[datetime.datetime] = None,
|
|
54
58
|
due_date_exceptions: Optional[Dict[str, datetime.datetime]] = None,
|
|
55
|
-
) -> tuple:
|
|
56
|
-
"""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.
|
|
57
61
|
|
|
58
62
|
Args:
|
|
59
63
|
net_id: The student's net ID.
|
|
@@ -62,10 +66,12 @@ def _get_student_key_and_max_late_days(
|
|
|
62
66
|
due_date_exceptions: Mapping from net_id to exception due date.
|
|
63
67
|
|
|
64
68
|
Returns:
|
|
65
|
-
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.
|
|
66
71
|
"""
|
|
67
|
-
max_late_days = 0
|
|
68
72
|
found_student_key = None
|
|
73
|
+
latest_submit_time: Optional[datetime.datetime] = None
|
|
74
|
+
effective_due_date: Optional[datetime.datetime] = due_date
|
|
69
75
|
|
|
70
76
|
if due_date_exceptions is None:
|
|
71
77
|
due_date_exceptions = {}
|
|
@@ -92,26 +98,35 @@ def _get_student_key_and_max_late_days(
|
|
|
92
98
|
if student_key:
|
|
93
99
|
found_student_key = student_key
|
|
94
100
|
|
|
95
|
-
# Calculate
|
|
101
|
+
# Calculate effective due date (using most generous exception for group)
|
|
96
102
|
if due_date is not None:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
115
130
|
|
|
116
131
|
|
|
117
132
|
def _calculate_student_score(
|
|
@@ -123,7 +138,7 @@ def _calculate_student_score(
|
|
|
123
138
|
warn_on_missing_callback: bool = True,
|
|
124
139
|
due_date: Optional[datetime.datetime] = None,
|
|
125
140
|
due_date_exceptions: Optional[Dict[str, datetime.datetime]] = None,
|
|
126
|
-
) -> Tuple[float, float,
|
|
141
|
+
) -> Tuple[float, float, Optional[datetime.datetime]]:
|
|
127
142
|
"""Calculate a student's final score.
|
|
128
143
|
|
|
129
144
|
Args:
|
|
@@ -131,12 +146,12 @@ def _calculate_student_score(
|
|
|
131
146
|
ls_column: The LearningSuiteColumn configuration.
|
|
132
147
|
item_deductions: Mapping from item name to StudentDeductions.
|
|
133
148
|
late_penalty_callback: Optional callback for late penalty.
|
|
134
|
-
warn_on_missing_callback: Whether to warn if late
|
|
149
|
+
warn_on_missing_callback: Whether to warn if late but no callback.
|
|
135
150
|
due_date: The default due date for the assignment.
|
|
136
151
|
due_date_exceptions: Mapping from net_id to exception due date.
|
|
137
152
|
|
|
138
153
|
Returns:
|
|
139
|
-
Tuple of (final_score, total_possible,
|
|
154
|
+
Tuple of (final_score, total_possible, submitted_datetime or None if on time).
|
|
140
155
|
"""
|
|
141
156
|
total_possible = sum(item.points for item in ls_column.items)
|
|
142
157
|
total_deductions = 0.0
|
|
@@ -162,21 +177,26 @@ def _calculate_student_score(
|
|
|
162
177
|
# Calculate score before late penalty
|
|
163
178
|
score = max(0, total_possible - total_deductions)
|
|
164
179
|
|
|
165
|
-
# Get
|
|
166
|
-
_,
|
|
180
|
+
# Get submit info
|
|
181
|
+
_, effective_due_date, submitted_datetime = _get_student_key_and_submit_info(
|
|
167
182
|
net_id, item_deductions, due_date, due_date_exceptions
|
|
168
183
|
)
|
|
169
184
|
|
|
170
|
-
# Apply late penalty if applicable
|
|
171
|
-
if
|
|
172
|
-
if late_penalty_callback:
|
|
173
|
-
score = max(
|
|
185
|
+
# Apply late penalty if applicable (submitted_datetime is None if on time)
|
|
186
|
+
if submitted_datetime is not None:
|
|
187
|
+
if late_penalty_callback and effective_due_date is not None:
|
|
188
|
+
score = max(
|
|
189
|
+
0,
|
|
190
|
+
late_penalty_callback(
|
|
191
|
+
effective_due_date, submitted_datetime, total_possible, score
|
|
192
|
+
),
|
|
193
|
+
)
|
|
174
194
|
elif warn_on_missing_callback:
|
|
175
195
|
warning(
|
|
176
|
-
f"Student {net_id}
|
|
196
|
+
f"Student {net_id} submitted late but no late penalty callback provided"
|
|
177
197
|
)
|
|
178
198
|
|
|
179
|
-
return score, total_possible,
|
|
199
|
+
return score, total_possible, submitted_datetime
|
|
180
200
|
|
|
181
201
|
|
|
182
202
|
def assemble_grades(
|
|
@@ -188,7 +208,7 @@ def assemble_grades(
|
|
|
188
208
|
output_csv_path: Optional[pathlib.Path] = None,
|
|
189
209
|
late_penalty_callback: Optional[LatePenaltyCallback] = None,
|
|
190
210
|
due_date: Optional[datetime.datetime] = None,
|
|
191
|
-
|
|
211
|
+
due_date_exceptions_path: Optional[pathlib.Path] = None,
|
|
192
212
|
) -> Tuple[Optional[pathlib.Path], Optional[pathlib.Path]]:
|
|
193
213
|
"""Generate feedback zip and/or grades CSV from deductions.
|
|
194
214
|
|
|
@@ -199,14 +219,20 @@ def assemble_grades(
|
|
|
199
219
|
output_zip_path: Path for the output zip file. If None, no zip is generated.
|
|
200
220
|
output_csv_path: Path for the output CSV file. If None, no CSV is generated.
|
|
201
221
|
late_penalty_callback: Optional callback function that takes
|
|
202
|
-
(
|
|
222
|
+
(due_datetime, submitted_datetime, max_score, actual_score) and returns the adjusted score.
|
|
223
|
+
submitted_datetime will be None if on time.
|
|
203
224
|
due_date: The default due date for the assignment. Required for late penalty.
|
|
204
|
-
|
|
225
|
+
due_date_exceptions_path: Path to YAML file with due date exceptions (net_id: "YYYY-MM-DD HH:MM:SS").
|
|
205
226
|
|
|
206
227
|
Returns:
|
|
207
228
|
Tuple of (feedback_zip_path or None, grades_csv_path or None).
|
|
208
229
|
"""
|
|
209
230
|
yaml_path = pathlib.Path(yaml_path)
|
|
231
|
+
|
|
232
|
+
# Load due date exceptions if path provided
|
|
233
|
+
due_date_exceptions: Dict[str, datetime.datetime] = {}
|
|
234
|
+
if due_date_exceptions_path:
|
|
235
|
+
due_date_exceptions = _load_due_date_exceptions(due_date_exceptions_path)
|
|
210
236
|
ls_column = LearningSuiteColumn(yaml_path)
|
|
211
237
|
|
|
212
238
|
# Get the lab name from the YAML file's parent directory
|
|
@@ -263,8 +289,13 @@ def assemble_grades(
|
|
|
263
289
|
f"but NOT [{', '.join(items_not_graded)}]",
|
|
264
290
|
)
|
|
265
291
|
|
|
292
|
+
# Get submit info for this student
|
|
293
|
+
_, effective_due_date, submitted_datetime = _get_student_key_and_submit_info(
|
|
294
|
+
net_id, subitem_deductions, due_date, due_date_exceptions
|
|
295
|
+
)
|
|
296
|
+
|
|
266
297
|
# Calculate score before late penalty
|
|
267
|
-
score_before_late, total_possible,
|
|
298
|
+
score_before_late, total_possible, _ = _calculate_student_score(
|
|
268
299
|
net_id=net_id,
|
|
269
300
|
ls_column=ls_column,
|
|
270
301
|
item_deductions=subitem_deductions,
|
|
@@ -274,18 +305,25 @@ def assemble_grades(
|
|
|
274
305
|
due_date_exceptions=due_date_exceptions,
|
|
275
306
|
)
|
|
276
307
|
|
|
277
|
-
# Apply late penalty if applicable
|
|
308
|
+
# Apply late penalty if applicable (submitted_datetime is None if on time)
|
|
278
309
|
final_score = score_before_late
|
|
279
|
-
if
|
|
310
|
+
if (
|
|
311
|
+
submitted_datetime is not None
|
|
312
|
+
and late_penalty_callback
|
|
313
|
+
and effective_due_date is not None
|
|
314
|
+
):
|
|
280
315
|
final_score = max(
|
|
281
316
|
0,
|
|
282
317
|
late_penalty_callback(
|
|
283
|
-
|
|
318
|
+
effective_due_date,
|
|
319
|
+
submitted_datetime,
|
|
320
|
+
total_possible,
|
|
321
|
+
score_before_late,
|
|
284
322
|
),
|
|
285
323
|
)
|
|
286
324
|
print_color(
|
|
287
325
|
TermColors.YELLOW,
|
|
288
|
-
f"Late: {net_id} (
|
|
326
|
+
f"Late: {net_id} (submitted {submitted_datetime}): "
|
|
289
327
|
f"{score_before_late:.1f} -> {final_score:.1f}",
|
|
290
328
|
)
|
|
291
329
|
|
|
@@ -433,24 +471,26 @@ def _generate_student_feedback(
|
|
|
433
471
|
# Calculate score before late penalty (clamped to 0)
|
|
434
472
|
score_before_late = max(0, total_points_possible - total_points_deducted)
|
|
435
473
|
|
|
436
|
-
# Get
|
|
437
|
-
_,
|
|
474
|
+
# Get submit info for this student
|
|
475
|
+
_, effective_due_date, submitted_datetime = _get_student_key_and_submit_info(
|
|
438
476
|
net_id, subitem_deductions, due_date, due_date_exceptions
|
|
439
477
|
)
|
|
440
478
|
|
|
441
479
|
# Late penalty section
|
|
442
480
|
lines.append("")
|
|
443
481
|
lines.append("=" * 60)
|
|
444
|
-
if
|
|
482
|
+
if (
|
|
483
|
+
submitted_datetime is not None
|
|
484
|
+
and late_penalty_callback
|
|
485
|
+
and effective_due_date is not None
|
|
486
|
+
):
|
|
445
487
|
final_score = late_penalty_callback(
|
|
446
|
-
|
|
488
|
+
effective_due_date, submitted_datetime, total_points_possible, score_before_late
|
|
447
489
|
)
|
|
448
490
|
# Ensure final score is not negative
|
|
449
491
|
final_score = max(0, final_score)
|
|
450
492
|
late_penalty_points = score_before_late - final_score
|
|
451
|
-
late_label = (
|
|
452
|
-
f"Late Penalty ({max_late_days} day{'s' if max_late_days != 1 else ''}):"
|
|
453
|
-
)
|
|
493
|
+
late_label = f"Late Penalty (submitted {submitted_datetime.strftime('%Y-%m-%d %H:%M')}):"
|
|
454
494
|
lines.append(
|
|
455
495
|
f"{late_label:<{item_col_width}} {-late_penalty_points:>{score_col_width}.1f}"
|
|
456
496
|
)
|
|
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
|
|
File without changes
|
|
File without changes
|