ygrader 2.6.10__tar.gz → 2.6.12__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.
Files changed (24) hide show
  1. {ygrader-2.6.10/ygrader.egg-info → ygrader-2.6.12}/PKG-INFO +1 -1
  2. {ygrader-2.6.10 → ygrader-2.6.12}/setup.py +1 -1
  3. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/deductions.py +15 -9
  4. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/grader.py +117 -106
  5. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/grading_item.py +6 -8
  6. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/score_input.py +13 -7
  7. {ygrader-2.6.10 → ygrader-2.6.12/ygrader.egg-info}/PKG-INFO +1 -1
  8. {ygrader-2.6.10 → ygrader-2.6.12}/LICENSE +0 -0
  9. {ygrader-2.6.10 → ygrader-2.6.12}/setup.cfg +0 -0
  10. {ygrader-2.6.10 → ygrader-2.6.12}/test/test_interactive.py +0 -0
  11. {ygrader-2.6.10 → ygrader-2.6.12}/test/test_unittest.py +0 -0
  12. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/__init__.py +0 -0
  13. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/feedback.py +0 -0
  14. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/grades_csv.py +0 -0
  15. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/grading_item_config.py +0 -0
  16. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/remote.py +0 -0
  17. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/send_ctrl_backtick.ahk +0 -0
  18. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/student_repos.py +0 -0
  19. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/upstream_merger.py +0 -0
  20. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader/utils.py +0 -0
  21. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader.egg-info/SOURCES.txt +0 -0
  22. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader.egg-info/dependency_links.txt +0 -0
  23. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader.egg-info/requires.txt +0 -0
  24. {ygrader-2.6.10 → ygrader-2.6.12}/ygrader.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygrader
3
- Version: 2.6.10
3
+ Version: 2.6.12
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.6.10",
7
+ version="2.6.12",
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",
@@ -158,7 +158,7 @@ class StudentDeductions:
158
158
  "net_ids": FlowList(student_key),
159
159
  "deductions": FlowList(deduction_ids),
160
160
  **(
161
- {"submit_time": self.submit_time_by_students[student_key]}
161
+ {"submit_time": self.submit_time_by_students.get(student_key)}
162
162
  if student_key in self.submit_time_by_students
163
163
  else {}
164
164
  ),
@@ -502,29 +502,35 @@ class StudentDeductions:
502
502
  self._save()
503
503
 
504
504
  def is_student_graded(self, net_ids: tuple) -> bool:
505
- """Check if a student has been graded (is in the deductions file).
505
+ """Check if a student has been graded (grading was finalized).
506
+
507
+ A student is considered graded only if they have a submit_time set,
508
+ which indicates grading was finalized (Enter was pressed).
509
+
510
+ This prevents students with partial/pending deductions (from a crash before
511
+ pressing Enter) from being considered "graded".
506
512
 
507
513
  Args:
508
514
  net_ids: Tuple of net_ids for the student.
509
515
 
510
516
  Returns:
511
- True if the student is in the deductions file (graded), False otherwise.
517
+ True if the student's grading was finalized, False otherwise.
512
518
  """
513
519
  student_key = tuple(net_ids) if not isinstance(net_ids, tuple) else net_ids
514
- return student_key in self.deductions_by_students
520
+ return student_key in self.submit_time_by_students
515
521
 
516
522
  def set_submit_time(self, net_ids: tuple, submit_time: Optional[str]):
517
523
  """Set the submission time for a student.
518
524
 
525
+ This also marks the student as graded (grading finalized).
526
+ The submit_time can be None if no submission time is available.
527
+
519
528
  Args:
520
529
  net_ids: Tuple of net_ids for the student.
521
- submit_time: ISO format timestamp string, or None to remove.
530
+ submit_time: ISO format timestamp string, or None if not available.
522
531
  """
523
532
  student_key = tuple(net_ids) if not isinstance(net_ids, tuple) else net_ids
524
- if submit_time:
525
- self.submit_time_by_students[student_key] = submit_time
526
- elif student_key in self.submit_time_by_students:
527
- del self.submit_time_by_students[student_key]
533
+ self.submit_time_by_students[student_key] = submit_time
528
534
  self._save()
529
535
 
530
536
  def get_submit_time(self, net_ids: tuple) -> Optional[str]:
@@ -512,124 +512,135 @@ class Grader:
512
512
 
513
513
  # Convert to list for index-based iteration (needed for going back)
514
514
  rows_list = list(sorted_df.iterrows())
515
- idx = 0
516
- prev_idx = None # Track previous student index for undo
517
-
518
- # Loop through all of the students/groups and perform grading
519
- while idx < len(rows_list):
520
- _, row = rows_list[idx]
521
- first_names = grades_csv.get_first_names(row)
522
- last_names = grades_csv.get_last_names(row)
523
- net_ids = grades_csv.get_net_ids(row)
524
- concated_names = grades_csv.get_concated_names(row)
525
-
526
- # Check if student/group needs grading
527
- num_group_members_need_grade_per_item = [
528
- item.num_grades_needed_deductions(net_ids) for item in self.items
529
- ]
530
515
 
531
- if sum(num_group_members_need_grade_per_item) == 0:
532
- # This student/group is already fully graded
533
- idx += 1
534
- continue
516
+ # Outer loop to allow re-grading after deleting from completion menu
517
+ while True:
518
+ idx = 0
519
+ prev_idx = None # Track previous student index for undo
520
+
521
+ # Loop through all of the students/groups and perform grading
522
+ while idx < len(rows_list):
523
+ _, row = rows_list[idx]
524
+ first_names = grades_csv.get_first_names(row)
525
+ last_names = grades_csv.get_last_names(row)
526
+ net_ids = grades_csv.get_net_ids(row)
527
+ concated_names = grades_csv.get_concated_names(row)
528
+
529
+ # Check if student/group needs grading
530
+ num_group_members_need_grade_per_item = [
531
+ item.num_grades_needed_deductions(net_ids) for item in self.items
532
+ ]
533
+
534
+ if sum(num_group_members_need_grade_per_item) == 0:
535
+ # This student/group is already fully graded
536
+ idx += 1
537
+ continue
535
538
 
536
- # Print name(s) of who we are grading
537
- student_work_path = self.work_path / utils.names_to_dir(
538
- first_names, last_names, net_ids
539
- )
540
- print_color(
541
- TermColors.PURPLE,
542
- "\nGrading: ",
543
- concated_names,
544
- "-",
545
- student_work_path.relative_to(self.work_path.parent),
546
- )
539
+ # Print name(s) of who we are grading
540
+ student_work_path = self.work_path / utils.names_to_dir(
541
+ first_names, last_names, net_ids
542
+ )
543
+ print_color(
544
+ TermColors.PURPLE,
545
+ "\nGrading: ",
546
+ concated_names,
547
+ "-",
548
+ student_work_path.relative_to(self.work_path.parent),
549
+ )
547
550
 
548
- # Get student code from zip or github. If this fails it returns False.
549
- # Code from zip will return modified time (epoch, float). Code from github will return True.
550
- success = self._get_student_code(row, student_work_path)
551
- if not success:
552
- idx += 1
553
- continue
551
+ # Get student code from zip or github. If this fails it returns False.
552
+ # Code from zip will return modified time (epoch, float). Code from github will return True.
553
+ success = self._get_student_code(row, student_work_path)
554
+ if not success:
555
+ idx += 1
556
+ continue
554
557
 
555
- # Format student code
556
- if self.format_code:
557
- print_color(TermColors.BLUE, "Formatting code")
558
- utils.clang_format_code(student_work_path)
558
+ # Format student code
559
+ if self.format_code:
560
+ print_color(TermColors.BLUE, "Formatting code")
561
+ utils.clang_format_code(student_work_path)
562
+
563
+ callback_args = {}
564
+ callback_args["lab_name"] = self.lab_name
565
+ callback_args["student_code_path"] = student_work_path
566
+ callback_args["run"] = not self.build_only
567
+ callback_args["first_names"] = first_names
568
+ callback_args["last_names"] = last_names
569
+ callback_args["net_ids"] = net_ids
570
+ callback_args["output"] = sys.stdout # Default to stdout in sequential mode
571
+ if self.code_source == CodeSource.GITHUB:
572
+ callback_args["repo_url"] = row["github_url"]
573
+ callback_args["tag"] = self.github_tag
574
+ if "Section Number" in row:
575
+ callback_args["section"] = row["Section Number"]
576
+ if "Course Homework ID" in row:
577
+ callback_args["homework_id"] = row["Course Homework ID"]
578
+
579
+ if self.prep_fcn is not None:
580
+ try:
581
+ self.prep_fcn(
582
+ **callback_args,
583
+ build=not self.run_only,
584
+ )
585
+ except CallbackFailed as e:
586
+ print_color(TermColors.RED, repr(e))
587
+ idx += 1
588
+ continue
589
+ except KeyboardInterrupt:
590
+ pass
591
+
592
+ # Loop through all items that are to be graded
593
+ go_back = False
594
+ for item in self.items:
595
+ go_back = item.run_grading(student_grades_df, row, callback_args)
596
+ if go_back:
597
+ break # Stop grading items for this student if going back
598
+
599
+ if go_back and prev_idx is not None:
600
+ # Go back to previous student
601
+ idx = prev_idx
602
+ prev_idx = None # Clear so we can't go back twice in a row
603
+ self.last_graded_net_ids = None # Clear since we're undoing
604
+ continue
559
605
 
560
- callback_args = {}
561
- callback_args["lab_name"] = self.lab_name
562
- callback_args["student_code_path"] = student_work_path
563
- callback_args["run"] = not self.build_only
564
- callback_args["first_names"] = first_names
565
- callback_args["last_names"] = last_names
566
- callback_args["net_ids"] = net_ids
567
- callback_args["output"] = sys.stdout # Default to stdout in sequential mode
568
- if self.code_source == CodeSource.GITHUB:
569
- callback_args["repo_url"] = row["github_url"]
570
- callback_args["tag"] = self.github_tag
571
- if "Section Number" in row:
572
- callback_args["section"] = row["Section Number"]
573
- if "Course Homework ID" in row:
574
- callback_args["homework_id"] = row["Course Homework ID"]
575
-
576
- if self.prep_fcn is not None:
577
- try:
578
- self.prep_fcn(
579
- **callback_args,
580
- build=not self.run_only,
606
+ # Track this student as last graded (only if all items completed normally)
607
+ if (
608
+ not go_back
609
+ and not self.build_only
610
+ and not self.dry_run_first
611
+ and not self.dry_run_all
612
+ ):
613
+ self.last_graded_net_ids = tuple(net_ids)
614
+
615
+ if self.dry_run_first:
616
+ print_color(
617
+ TermColors.YELLOW,
618
+ "'dry_run_first' is set, so exiting after first student.",
581
619
  )
582
- except CallbackFailed as e:
583
- print_color(TermColors.RED, repr(e))
584
- idx += 1
585
- continue
586
- except KeyboardInterrupt:
587
- pass
588
-
589
- # Loop through all items that are to be graded
590
- go_back = False
591
- for item in self.items:
592
- go_back = item.run_grading(student_grades_df, row, callback_args)
593
- if go_back:
594
- break # Stop grading items for this student if going back
595
-
596
- if go_back and prev_idx is not None:
597
- # Go back to previous student
598
- idx = prev_idx
599
- prev_idx = None # Clear so we can't go back twice in a row
600
- self.last_graded_net_ids = None # Clear since we're undoing
601
- continue
620
+ break
621
+
622
+ # Move to next student and remember this one for potential undo
623
+ prev_idx = idx
624
+ idx += 1
602
625
 
603
- # Track this student as last graded (only if all items completed normally)
626
+ # Show completion menu when all students are done (unless disabled or in special modes)
627
+ # If a grade is deleted from the menu, re-run the grading loop
604
628
  if (
605
- not go_back
606
- and not self.build_only
607
- and not self.dry_run_first
608
- and not self.dry_run_all
629
+ not self.show_completion_menu
630
+ or self.build_only
631
+ or self.dry_run_first
632
+ or self.dry_run_all
609
633
  ):
610
- self.last_graded_net_ids = tuple(net_ids)
611
-
612
- if self.dry_run_first:
613
- print_color(
614
- TermColors.YELLOW,
615
- "'dry_run_first' is set, so exiting after first student.",
616
- )
617
- break
634
+ break # Exit outer loop - no completion menu
618
635
 
619
- # Move to next student and remember this one for potential undo
620
- prev_idx = idx
621
- idx += 1
622
-
623
- # Show completion menu when all students are done (unless disabled or in special modes)
624
- if (
625
- self.show_completion_menu
626
- and not self.build_only
627
- and not self.dry_run_first
628
- and not self.dry_run_all
629
- ):
630
636
  # Get names_by_netid from first item for display purposes
631
637
  names_by_netid = self.items[0].names_by_netid if self.items else None
632
- display_completion_menu(self.items, names_by_netid)
638
+ if display_completion_menu(self.items, names_by_netid):
639
+ # A grade was deleted, continue outer loop to re-run grading
640
+ self.last_graded_net_ids = None
641
+ continue
642
+ # User exited normally
643
+ break
633
644
 
634
645
  def _process_single_student_build(self, row):
635
646
  """Process a single student for parallel build mode. Returns (net_ids, success, message, log_path)."""
@@ -243,10 +243,10 @@ class GradeItem:
243
243
  f"Applied deduction: {deduction_desc} (-{deduction_points})",
244
244
  )
245
245
  # Save submit_time now that grading succeeded
246
- if pending_submit_time is not None:
247
- self.student_deductions.set_submit_time(
248
- tuple(net_ids), pending_submit_time
249
- )
246
+ # (may be None if no .commitdate file exists)
247
+ self.student_deductions.set_submit_time(
248
+ tuple(net_ids), pending_submit_time
249
+ )
250
250
  # Ensure student is in the deductions file
251
251
  self.student_deductions.ensure_student_in_file(tuple(net_ids))
252
252
  break
@@ -312,10 +312,8 @@ class GradeItem:
312
312
 
313
313
  # Record score - save submit_time and ensure the student is in the deductions file
314
314
  # (even if they have no deductions, to indicate they were graded)
315
- if pending_submit_time is not None:
316
- self.student_deductions.set_submit_time(
317
- tuple(net_ids), pending_submit_time
318
- )
315
+ # submit_time may be None if no .commitdate file exists
316
+ self.student_deductions.set_submit_time(tuple(net_ids), pending_submit_time)
319
317
  self.student_deductions.ensure_student_in_file(tuple(net_ids))
320
318
  return False # Normal completion
321
319
 
@@ -50,7 +50,8 @@ def display_completion_menu(items, names_by_netid=None):
50
50
  names_by_netid: Dict mapping net_id -> (first_name, last_name) for search
51
51
 
52
52
  Returns:
53
- True if user wants to exit, False to continue showing the menu
53
+ True if a grade was deleted (caller should re-run grading loop),
54
+ False otherwise (user exited normally)
54
55
  """
55
56
  if not items:
56
57
  print_color(TermColors.YELLOW, "No grade items configured.")
@@ -90,7 +91,9 @@ def display_completion_menu(items, names_by_netid=None):
90
91
  txt = input(TermColors.BLUE + ">>> " + TermColors.END).strip().lower()
91
92
 
92
93
  if txt == MenuCommand.MANAGE_GRADES.value:
93
- _manage_grades_interactive(items, names_by_netid)
94
+ if _manage_grades_interactive(items, names_by_netid):
95
+ # A grade was deleted, signal caller to re-run grading
96
+ return True
94
97
  elif txt == MenuCommand.DELETE_DEDUCTION.value:
95
98
  selected_item = _select_item_interactive(
96
99
  items, "delete deduction type from"
@@ -387,6 +390,9 @@ def _manage_grades_interactive(all_items, names_by_netid=None):
387
390
  Args:
388
391
  all_items: List of GradeItem objects (to access their student_deductions)
389
392
  names_by_netid: Dict mapping net_id -> (first_name, last_name) for search
393
+
394
+ Returns:
395
+ True if a grade was deleted, False otherwise
390
396
  """
391
397
  while True:
392
398
  # Collect graded students from ALL items
@@ -402,7 +408,7 @@ def _manage_grades_interactive(all_items, names_by_netid=None):
402
408
  if not all_graded_students:
403
409
  print("No students have been graded yet.")
404
410
  input("Press Enter to continue...")
405
- return
411
+ return False
406
412
 
407
413
  print(f"{len(all_graded_students)} student(s) graded.")
408
414
  print("\nOptions:")
@@ -413,7 +419,7 @@ def _manage_grades_interactive(all_items, names_by_netid=None):
413
419
  txt = input("\nSearch: ").strip()
414
420
 
415
421
  if txt == "":
416
- return
422
+ return False
417
423
 
418
424
  # Check for wildcard to list all
419
425
  list_all = txt == "*"
@@ -487,7 +493,7 @@ def _manage_grades_interactive(all_items, names_by_netid=None):
487
493
  choice = input("\n>>> ").strip()
488
494
 
489
495
  if choice == "":
490
- continue
496
+ return False
491
497
 
492
498
  try:
493
499
  idx = int(choice) - 1
@@ -509,8 +515,8 @@ def _manage_grades_interactive(all_items, names_by_netid=None):
509
515
  print_color(
510
516
  TermColors.GREEN, f"Deleted grade for {display} from all items"
511
517
  )
512
- else:
513
- print("Cancelled.")
518
+ return True # Signal that a grade was deleted
519
+ print("Cancelled.")
514
520
  else:
515
521
  print_color(TermColors.YELLOW, "Invalid selection.")
516
522
  except ValueError:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygrader
3
- Version: 2.6.10
3
+ Version: 2.6.12
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