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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygrader
3
- Version: 2.5.0
3
+ Version: 2.5.2
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.5.0",
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(self) -> int:
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
- total_deductions = 0.0
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
- total_deductions += deduction.points
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
- # Calculate score before late penalty
178
- score = max(0, total_possible - total_deductions)
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 = _get_student_key_and_submit_info(
294
- net_id, subitem_deductions, due_date, due_date_exceptions
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
- item_deductions = []
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
- item_deductions.append((deduction.message, deduction.points))
456
+ item_deduction_list.append((deduction.message, deduction.points))
444
457
  subitem_points_deducted += deduction.points
445
458
 
446
- # Calculate item score
447
- subitem_score = max(0, subitem_points_possible - subitem_points_deducted)
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 item_deductions:
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, submitted_datetime, total_points_possible, score_before_late
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 = f"Late Penalty (submitted {submitted_datetime.strftime('%Y-%m-%d %H:%M')}):"
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 = self._build_names_lookup() # net_id -> (first_name, last_name)
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 pandas.notna(net_id) and pandas.notna(first_name) and pandas.notna(last_name):
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(tuple(net_ids), pending_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 last if there's a last graded student
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 (search_lower in first_name.lower() or search_lower in last_name.lower()):
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 = input(
313
- f"Delete grade for {display}? This cannot be undone. [y/N]: "
314
- ).strip().lower()
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygrader
3
- Version: 2.5.0
3
+ Version: 2.5.2
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