ygrader 2.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygrader
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Grading scripts used in BYU's Electrical and Computer Engineering Department
5
5
  Home-page: https://github.com/byu-cpe/ygrader
6
6
  Author: Jeff Goeders
@@ -4,7 +4,7 @@ setup(
4
4
  name="ygrader",
5
5
  packages=["ygrader"],
6
6
  package_data={"ygrader": ["*.ahk"]},
7
- version="2.4.0",
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,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: (late_days, max_score, actual_score) -> new_score
18
- LatePenaltyCallback = Callable[[int, float, float], float]
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 _calculate_late_days(
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 maximum late days across all items.
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, max_late_days).
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 late days from submit_time if we have a due date
101
+ # Calculate effective due date (using most generous exception for group)
127
102
  if due_date is not None:
128
- submit_time_str = deductions_obj.submit_time_by_students.get(
129
- student_key
130
- )
131
- if submit_time_str:
132
- # Calculate effective due date (using most generous exception for group)
133
- effective_due_date = due_date
134
- for member_net_id in student_key:
135
- if member_net_id in due_date_exceptions:
136
- effective_due_date = max(
137
- effective_due_date, due_date_exceptions[member_net_id]
138
- )
139
-
140
- days_late = _calculate_late_days(
141
- submit_time_str, effective_due_date
142
- )
143
- max_late_days = max(max_late_days, days_late)
144
-
145
- return found_student_key, max_late_days
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, int]:
141
+ ) -> Tuple[float, float, Optional[datetime.datetime]]:
158
142
  """Calculate a student's final score.
159
143
 
160
144
  Args:
@@ -162,12 +146,12 @@ 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 days found but no callback.
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, max_late_days).
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_deductions = 0.0
@@ -193,21 +177,26 @@ def _calculate_student_score(
193
177
  # Calculate score before late penalty
194
178
  score = max(0, total_possible - total_deductions)
195
179
 
196
- # Get max late days
197
- _, max_late_days = _get_student_key_and_max_late_days(
180
+ # Get submit info
181
+ _, effective_due_date, submitted_datetime = _get_student_key_and_submit_info(
198
182
  net_id, item_deductions, due_date, due_date_exceptions
199
183
  )
200
184
 
201
- # Apply late penalty if applicable
202
- if max_late_days > 0:
203
- if late_penalty_callback:
204
- score = max(0, late_penalty_callback(max_late_days, total_possible, score))
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
+ )
205
194
  elif warn_on_missing_callback:
206
195
  warning(
207
- f"Student {net_id} has {max_late_days} late day(s) but no late penalty callback provided"
196
+ f"Student {net_id} submitted late but no late penalty callback provided"
208
197
  )
209
198
 
210
- return score, total_possible, max_late_days
199
+ return score, total_possible, submitted_datetime
211
200
 
212
201
 
213
202
  def assemble_grades(
@@ -230,7 +219,8 @@ def assemble_grades(
230
219
  output_zip_path: Path for the output zip file. If None, no zip is generated.
231
220
  output_csv_path: Path for the output CSV file. If None, no CSV is generated.
232
221
  late_penalty_callback: Optional callback function that takes
233
- (late_days, max_score, actual_score) and returns the adjusted score.
222
+ (due_datetime, submitted_datetime, max_score, actual_score) and returns the adjusted score.
223
+ submitted_datetime will be None if on time.
234
224
  due_date: The default due date for the assignment. Required for late penalty.
235
225
  due_date_exceptions_path: Path to YAML file with due date exceptions (net_id: "YYYY-MM-DD HH:MM:SS").
236
226
 
@@ -299,8 +289,13 @@ def assemble_grades(
299
289
  f"but NOT [{', '.join(items_not_graded)}]",
300
290
  )
301
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
+
302
297
  # Calculate score before late penalty
303
- score_before_late, total_possible, max_late_days = _calculate_student_score(
298
+ score_before_late, total_possible, _ = _calculate_student_score(
304
299
  net_id=net_id,
305
300
  ls_column=ls_column,
306
301
  item_deductions=subitem_deductions,
@@ -310,18 +305,25 @@ def assemble_grades(
310
305
  due_date_exceptions=due_date_exceptions,
311
306
  )
312
307
 
313
- # Apply late penalty if applicable
308
+ # Apply late penalty if applicable (submitted_datetime is None if on time)
314
309
  final_score = score_before_late
315
- if max_late_days > 0 and late_penalty_callback:
310
+ if (
311
+ submitted_datetime is not None
312
+ and late_penalty_callback
313
+ and effective_due_date is not None
314
+ ):
316
315
  final_score = max(
317
316
  0,
318
317
  late_penalty_callback(
319
- max_late_days, total_possible, score_before_late
318
+ effective_due_date,
319
+ submitted_datetime,
320
+ total_possible,
321
+ score_before_late,
320
322
  ),
321
323
  )
322
324
  print_color(
323
325
  TermColors.YELLOW,
324
- f"Late: {net_id} ({max_late_days} day{'s' if max_late_days != 1 else ''}): "
326
+ f"Late: {net_id} (submitted {submitted_datetime}): "
325
327
  f"{score_before_late:.1f} -> {final_score:.1f}",
326
328
  )
327
329
 
@@ -469,24 +471,26 @@ def _generate_student_feedback(
469
471
  # Calculate score before late penalty (clamped to 0)
470
472
  score_before_late = max(0, total_points_possible - total_points_deducted)
471
473
 
472
- # Get max late days for this student
473
- _, max_late_days = _get_student_key_and_max_late_days(
474
+ # Get submit info for this student
475
+ _, effective_due_date, submitted_datetime = _get_student_key_and_submit_info(
474
476
  net_id, subitem_deductions, due_date, due_date_exceptions
475
477
  )
476
478
 
477
479
  # Late penalty section
478
480
  lines.append("")
479
481
  lines.append("=" * 60)
480
- if max_late_days > 0 and late_penalty_callback:
482
+ if (
483
+ submitted_datetime is not None
484
+ and late_penalty_callback
485
+ and effective_due_date is not None
486
+ ):
481
487
  final_score = late_penalty_callback(
482
- max_late_days, total_points_possible, score_before_late
488
+ effective_due_date, submitted_datetime, total_points_possible, score_before_late
483
489
  )
484
490
  # Ensure final score is not negative
485
491
  final_score = max(0, final_score)
486
492
  late_penalty_points = score_before_late - final_score
487
- late_label = (
488
- f"Late Penalty ({max_late_days} day{'s' if max_late_days != 1 else ''}):"
489
- )
493
+ late_label = f"Late Penalty (submitted {submitted_datetime.strftime('%Y-%m-%d %H:%M')}):"
490
494
  lines.append(
491
495
  f"{late_label:<{item_col_width}} {-late_penalty_points:>{score_col_width}.1f}"
492
496
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygrader
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Grading scripts used in BYU's Electrical and Computer Engineering Department
5
5
  Home-page: https://github.com/byu-cpe/ygrader
6
6
  Author: Jeff Goeders
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