ygrader 2.6.11__tar.gz → 2.6.13__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.6.11/ygrader.egg-info → ygrader-2.6.13}/PKG-INFO +1 -1
- {ygrader-2.6.11 → ygrader-2.6.13}/setup.py +1 -1
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/deductions.py +15 -9
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/grader.py +117 -106
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/grading_item.py +6 -8
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/score_input.py +13 -7
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/utils.py +21 -0
- {ygrader-2.6.11 → ygrader-2.6.13/ygrader.egg-info}/PKG-INFO +1 -1
- {ygrader-2.6.11 → ygrader-2.6.13}/LICENSE +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/setup.cfg +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/test/test_interactive.py +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/test/test_unittest.py +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/__init__.py +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/feedback.py +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/grades_csv.py +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/grading_item_config.py +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/remote.py +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/send_ctrl_backtick.ahk +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/student_repos.py +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader/upstream_merger.py +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader.egg-info/SOURCES.txt +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader.egg-info/dependency_links.txt +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/ygrader.egg-info/requires.txt +0 -0
- {ygrader-2.6.11 → ygrader-2.6.13}/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.6.
|
|
7
|
+
version="2.6.13",
|
|
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
|
|
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 (
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
#
|
|
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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
316
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
513
|
-
|
|
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:
|
|
@@ -8,6 +8,7 @@ import subprocess
|
|
|
8
8
|
import hashlib
|
|
9
9
|
import time
|
|
10
10
|
import os
|
|
11
|
+
import shlex
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class TermColors:
|
|
@@ -163,6 +164,26 @@ def is_wsl():
|
|
|
163
164
|
_FOCUS_WARNING_PRINTED = False
|
|
164
165
|
|
|
165
166
|
|
|
167
|
+
def opener(file_path, sleep_time=1.0):
|
|
168
|
+
"""Open a file using VS Code or a custom opener defined in the OPENER environment variable.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
file_path: pathlib.Path or str
|
|
173
|
+
Path to the file to open
|
|
174
|
+
sleep_time: float, optional
|
|
175
|
+
Time in seconds to wait for the editor to open (default: 1.0)
|
|
176
|
+
"""
|
|
177
|
+
if "OPENER" not in os.environ:
|
|
178
|
+
open_file_in_vscode(file_path, sleep_time)
|
|
179
|
+
else:
|
|
180
|
+
subprocess.run(
|
|
181
|
+
f'{os.environ["OPENER"]} {shlex.quote(str(file_path))}',
|
|
182
|
+
shell=True,
|
|
183
|
+
check=False,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
166
187
|
def open_file_in_vscode(file_path, sleep_time=1.0):
|
|
167
188
|
"""Open a file in VS Code and return focus to terminal.
|
|
168
189
|
|
|
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
|