ygrader 1.2.3__tar.gz → 2.0.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.
Files changed (25) hide show
  1. {ygrader-1.2.3/ygrader.egg-info → ygrader-2.0.0}/PKG-INFO +1 -1
  2. {ygrader-1.2.3 → ygrader-2.0.0}/setup.py +1 -1
  3. {ygrader-1.2.3 → ygrader-2.0.0}/test/test_interactive.py +0 -2
  4. ygrader-2.0.0/test/test_unittest.py +139 -0
  5. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader/__init__.py +5 -2
  6. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader/deductions.py +51 -2
  7. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader/feedback.py +16 -21
  8. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader/grader.py +61 -70
  9. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader/grades_csv.py +5 -5
  10. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader/grading_item.py +80 -127
  11. ygrader-2.0.0/ygrader/grading_item_config.py +99 -0
  12. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader/score_input.py +3 -16
  13. {ygrader-1.2.3 → ygrader-2.0.0/ygrader.egg-info}/PKG-INFO +1 -1
  14. ygrader-1.2.3/test/test_unittest.py +0 -105
  15. ygrader-1.2.3/ygrader/grading_item_config.py +0 -129
  16. {ygrader-1.2.3 → ygrader-2.0.0}/LICENSE +0 -0
  17. {ygrader-1.2.3 → ygrader-2.0.0}/setup.cfg +0 -0
  18. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader/send_ctrl_backtick.ahk +0 -0
  19. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader/student_repos.py +0 -0
  20. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader/upstream_merger.py +0 -0
  21. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader/utils.py +0 -0
  22. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader.egg-info/SOURCES.txt +0 -0
  23. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader.egg-info/dependency_links.txt +0 -0
  24. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader.egg-info/requires.txt +0 -0
  25. {ygrader-1.2.3 → ygrader-2.0.0}/ygrader.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygrader
3
- Version: 1.2.3
3
+ Version: 2.0.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="1.2.3",
7
+ version="2.0.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",
@@ -35,12 +35,10 @@ def test_me():
35
35
  "lab1",
36
36
  run_on_milestone,
37
37
  10,
38
- help_msg="This is a long string\nWith lots of advice\nFor TAs",
39
38
  )
40
39
  grader.add_item_to_grade(
41
40
  "lab1m2",
42
41
  run_on_milestone,
43
- help_msg="msg2",
44
42
  )
45
43
  grader.set_submission_system_learning_suite(TEST_RESOURCES_PATH / "submissions.zip")
46
44
  grader.set_other_options(prep_fcn=run_on_lab)
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/python3
2
+
3
+ import unittest
4
+ import pathlib
5
+ import sys
6
+ import filecmp
7
+ import doctest
8
+
9
+ ROOT_PATH = pathlib.Path(__file__).resolve().parent.parent
10
+ sys.path.append(str(ROOT_PATH))
11
+
12
+ import ygrader.student_repos
13
+ from ygrader import Grader, CodeSource
14
+
15
+ TEST_PATH = ROOT_PATH / "test"
16
+ TEST_RESOURCES_PATH = TEST_PATH / "resources"
17
+ TEST_OUTPUT_PATH = TEST_PATH / "grades"
18
+
19
+
20
+ def load_tests(loader, tests, ignore):
21
+ tests.addTests(doctest.DocTestSuite(ygrader.student_repos))
22
+ return tests
23
+
24
+
25
+ class TestGithub(unittest.TestCase):
26
+ def test_me(self):
27
+ class_list_csv_path = TEST_RESOURCES_PATH / "class_list_1.csv"
28
+ deductions1_golden_path = TEST_RESOURCES_PATH / "deductions1_golden.yaml"
29
+ deductions2_golden_path = TEST_RESOURCES_PATH / "deductions2_golden.yaml"
30
+ deductions1_yaml_path = TEST_OUTPUT_PATH / "deductions1.yaml"
31
+ deductions2_yaml_path = TEST_OUTPUT_PATH / "deductions2.yaml"
32
+
33
+ grader = Grader(
34
+ lab_name="github_test",
35
+ class_list_csv_path=class_list_csv_path,
36
+ work_path=TEST_PATH / "temp_github",
37
+ )
38
+ grader.add_item_to_grade(
39
+ "lab1",
40
+ self.runner,
41
+ max_points=10,
42
+ deductions_yaml_path=deductions1_yaml_path,
43
+ )
44
+ grader.add_item_to_grade(
45
+ "lab1m2",
46
+ self.runner,
47
+ max_points=20,
48
+ deductions_yaml_path=deductions2_yaml_path,
49
+ )
50
+ grader.set_submission_system_github(
51
+ "main", TEST_RESOURCES_PATH / "github.csv", use_https=True
52
+ )
53
+ grader.run()
54
+
55
+ self.assertTrue(filecmp.cmp(deductions1_yaml_path, deductions1_golden_path))
56
+ self.assertTrue(filecmp.cmp(deductions2_yaml_path, deductions2_golden_path))
57
+
58
+ def runner(self, **kw):
59
+ if kw["item_name"] == "lab1m2":
60
+ return [("New feedback", 2)]
61
+ return []
62
+
63
+
64
+ class TestLearningSuite(unittest.TestCase):
65
+ def test_me(self):
66
+ class_list_csv_path = TEST_RESOURCES_PATH / "class_list_2.csv"
67
+ deductions_path = TEST_OUTPUT_PATH / "learningsuite_deductions.yaml"
68
+ deductions_golden_path = (
69
+ TEST_RESOURCES_PATH / "learningsuite_deductions_golden.yaml"
70
+ )
71
+
72
+ grader = Grader(
73
+ lab_name="learningsuite_test",
74
+ class_list_csv_path=class_list_csv_path,
75
+ work_path=TEST_PATH / "temp_learningsuite",
76
+ )
77
+ grader.add_item_to_grade(
78
+ item_name="lab1",
79
+ grading_fcn=self.runner,
80
+ deductions_yaml_path=deductions_path,
81
+ max_points=10,
82
+ )
83
+ grader.set_submission_system_learning_suite(
84
+ TEST_RESOURCES_PATH / "submissions.zip"
85
+ )
86
+ grader.run()
87
+
88
+ self.assertTrue(filecmp.cmp(deductions_path, deductions_golden_path))
89
+
90
+ def runner(self, student_code_path, **kw):
91
+ self.assertIn("section", kw)
92
+ self.assertIn("homework_id", kw)
93
+ if (student_code_path / "file_1.txt").is_file() and (
94
+ student_code_path / "file_2.txt"
95
+ ).is_file():
96
+ return []
97
+ else:
98
+ return [("Missing files", 10)]
99
+
100
+ def test_groups(self):
101
+ class_list_csv_path = TEST_RESOURCES_PATH / "class_list_3.csv"
102
+ deductions1_path = TEST_OUTPUT_PATH / "groups_l1.yaml"
103
+ deductions2_path = TEST_OUTPUT_PATH / "groups_l2.yaml"
104
+ deductions3_path = TEST_OUTPUT_PATH / "groups_l3.yaml"
105
+ deductions1_golden_path = TEST_RESOURCES_PATH / "groups_l1_golden.yaml"
106
+ deductions2_golden_path = TEST_RESOURCES_PATH / "groups_l2_golden.yaml"
107
+ deductions3_golden_path = TEST_RESOURCES_PATH / "groups_l3_golden.yaml"
108
+
109
+ grader = Grader(
110
+ "groups_test",
111
+ class_list_csv_path=class_list_csv_path,
112
+ work_path=TEST_PATH / "temp_groups",
113
+ )
114
+ grader.add_item_to_grade(
115
+ item_name="l1",
116
+ grading_fcn=self.group_grader_1,
117
+ deductions_yaml_path=deductions1_path,
118
+ max_points=10,
119
+ )
120
+ grader.add_item_to_grade(
121
+ item_name="l2",
122
+ grading_fcn=self.group_grader_2,
123
+ deductions_yaml_path=deductions2_path,
124
+ max_points=10,
125
+ )
126
+ grader.set_submission_system_learning_suite(
127
+ TEST_RESOURCES_PATH / "submissions2.zip"
128
+ )
129
+ grader.set_learning_suite_groups(TEST_RESOURCES_PATH / "groups3.csv")
130
+ grader.run()
131
+
132
+ self.assertTrue(filecmp.cmp(deductions1_path, deductions1_golden_path))
133
+ self.assertTrue(filecmp.cmp(deductions2_path, deductions2_golden_path))
134
+
135
+ def group_grader_1(self, **kw):
136
+ return []
137
+
138
+ def group_grader_2(self, **kw):
139
+ return [("Did not follow instructions", 5)]
@@ -1,7 +1,10 @@
1
1
  """Default imports for package"""
2
2
 
3
- from .grader import Grader, CodeSource, ScoreMode
3
+ from .grader import Grader, CodeSource
4
4
  from .upstream_merger import UpstreamMerger
5
5
  from .utils import CallbackFailed
6
- from .grading_item_config import generate_subitem_csvs, GradeItemConfig
6
+ from .grading_item_config import (
7
+ LearningSuiteColumn,
8
+ LearningSuiteColumnParseError,
9
+ )
7
10
  from .feedback import generate_feedback_zip
@@ -138,9 +138,11 @@ class StudentDeductions:
138
138
  all_student_keys = set(self.deductions_by_students.keys()) | set(
139
139
  self.days_late_by_students.keys()
140
140
  )
141
- if all_student_keys:
141
+ # Sort student keys for consistent output ordering
142
+ sorted_student_keys = sorted(all_student_keys)
143
+ if sorted_student_keys:
142
144
  student_deduction_list = []
143
- for student_key in all_student_keys:
145
+ for student_key in sorted_student_keys:
144
146
  deduction_items = self.deductions_by_students.get(student_key, [])
145
147
  # Find the deduction IDs for these deduction items
146
148
  deduction_ids = []
@@ -164,6 +166,9 @@ class StudentDeductions:
164
166
  )
165
167
  data["student_deductions"] = student_deduction_list
166
168
 
169
+ # Create parent directory if it doesn't exist
170
+ self.yaml_path.parent.mkdir(parents=True, exist_ok=True)
171
+
167
172
  # Write to file
168
173
  with open(self.yaml_path, "w", encoding="utf-8") as f:
169
174
  yaml.dump(data, f, default_flow_style=False, sort_keys=False)
@@ -191,6 +196,24 @@ class StudentDeductions:
191
196
  self._save()
192
197
  return next_id
193
198
 
199
+ def find_or_create_deduction_type(self, message: str, points: float = 0.0) -> int:
200
+ """Find an existing deduction type by message, or create a new one if not found.
201
+
202
+ Args:
203
+ message: The deduction message/description to find or create
204
+ points: Points to deduct (used only if creating a new type)
205
+
206
+ Returns:
207
+ The ID of the existing or newly created deduction type
208
+ """
209
+ # Search for existing deduction type with matching message
210
+ for deduction_id, deduction_type in self.deduction_types.items():
211
+ if deduction_type.message == message:
212
+ return deduction_id
213
+
214
+ # Not found, create a new one
215
+ return self.add_deduction_type(message, points)
216
+
194
217
  def create_deduction_type_interactive(self) -> int:
195
218
  """Interactively prompt the user to create a new deduction type.
196
219
 
@@ -356,6 +379,32 @@ class StudentDeductions:
356
379
  del self.days_late_by_students[student_key]
357
380
  self._save()
358
381
 
382
+ def ensure_student_in_file(self, net_ids: tuple):
383
+ """Ensure a student is in the deductions file, even with no deductions.
384
+
385
+ This is used to indicate that a student has been graded (with a perfect score)
386
+ rather than being absent from the file (not yet graded).
387
+
388
+ Args:
389
+ net_ids: Tuple of net_ids for the student.
390
+ """
391
+ student_key = tuple(net_ids) if not isinstance(net_ids, tuple) else net_ids
392
+ if student_key not in self.deductions_by_students:
393
+ self.deductions_by_students[student_key] = []
394
+ self._save()
395
+
396
+ def is_student_graded(self, net_ids: tuple) -> bool:
397
+ """Check if a student has been graded (is in the deductions file).
398
+
399
+ Args:
400
+ net_ids: Tuple of net_ids for the student.
401
+
402
+ Returns:
403
+ True if the student is in the deductions file (graded), False otherwise.
404
+ """
405
+ student_key = tuple(net_ids) if not isinstance(net_ids, tuple) else net_ids
406
+ return student_key in self.deductions_by_students
407
+
359
408
  def set_days_late(self, net_ids: tuple, days_late: int):
360
409
  """Set the number of days late for a student.
361
410
 
@@ -7,18 +7,20 @@ from typing import Dict
7
7
  import pandas
8
8
 
9
9
  from .deductions import StudentDeductions
10
- from .grading_item_config import GradeItemConfig
10
+ from .grading_item_config import LearningSuiteColumn
11
11
 
12
12
 
13
13
  def generate_feedback_zip(
14
14
  yaml_path: pathlib.Path,
15
+ class_list_csv_path: pathlib.Path,
15
16
  subitem_feedback_paths: Dict[str, pathlib.Path],
16
17
  output_zip_path: pathlib.Path = None,
17
18
  ) -> pathlib.Path:
18
19
  """Generate a zip file containing feedback files for each student.
19
20
 
20
21
  Args:
21
- yaml_path: Path to the YAML file that can be loaded by GradeItemConfig.
22
+ yaml_path: Path to the YAML file that can be loaded by LearningSuiteColumn.
23
+ class_list_csv_path: Path to CSV file with class list (Net ID, First Name, Last Name).
22
24
  subitem_feedback_paths: Mapping from subitem name to feedback YAML file path.
23
25
  output_zip_path: Path for the output zip file. If None, defaults to
24
26
  <yaml_dir>/feedback.zip.
@@ -27,7 +29,7 @@ def generate_feedback_zip(
27
29
  Path to the generated zip file.
28
30
  """
29
31
  yaml_path = pathlib.Path(yaml_path)
30
- grade_item_config = GradeItemConfig(yaml_path)
32
+ ls_column = LearningSuiteColumn(yaml_path)
31
33
 
32
34
  # Get the lab name from the YAML file's parent directory
33
35
  lab_name = yaml_path.stem
@@ -38,7 +40,7 @@ def generate_feedback_zip(
38
40
 
39
41
  # Load all student deductions for each subitem
40
42
  subitem_deductions: Dict[str, StudentDeductions] = {}
41
- for subitem in grade_item_config.subitems:
43
+ for subitem in ls_column.items:
42
44
  if subitem.name in subitem_feedback_paths:
43
45
  feedback_path = subitem_feedback_paths[subitem.name]
44
46
  if feedback_path.exists():
@@ -48,15 +50,8 @@ def generate_feedback_zip(
48
50
  else:
49
51
  subitem_deductions[subitem.name] = StudentDeductions()
50
52
 
51
- # Load the first subitem's CSV to get the list of students
52
- # (all subitems should have the same students)
53
- first_subitem = grade_item_config.subitems[0]
54
- if not first_subitem.csv_path.exists():
55
- raise FileNotFoundError(
56
- f"CSV file for subitem '{first_subitem.name}' not found at {first_subitem.csv_path}"
57
- )
58
-
59
- students_df = pandas.read_csv(first_subitem.csv_path)
53
+ # Load class list from provided CSV
54
+ students_df = pandas.read_csv(class_list_csv_path)
60
55
 
61
56
  # Create the zip file
62
57
  with zipfile.ZipFile(output_zip_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
@@ -68,7 +63,7 @@ def generate_feedback_zip(
68
63
  # Generate feedback content for this student
69
64
  feedback_content = _generate_student_feedback(
70
65
  student_row=student_row,
71
- grade_item_config=grade_item_config,
66
+ ls_column=ls_column,
72
67
  subitem_deductions=subitem_deductions,
73
68
  )
74
69
 
@@ -92,14 +87,14 @@ def generate_feedback_zip(
92
87
 
93
88
  def _generate_student_feedback(
94
89
  student_row: pandas.Series,
95
- grade_item_config: GradeItemConfig,
90
+ ls_column: LearningSuiteColumn,
96
91
  subitem_deductions: Dict[str, StudentDeductions],
97
92
  ) -> str:
98
93
  """Generate the feedback text content for a single student.
99
94
 
100
95
  Args:
101
96
  student_row: A row from the student DataFrame.
102
- grade_item_config: The GradeItemConfig object.
97
+ ls_column: The LearningSuiteColumn object.
103
98
  subitem_deductions: Mapping from subitem name to StudentDeductions.
104
99
 
105
100
  Returns:
@@ -121,11 +116,11 @@ def _generate_student_feedback(
121
116
  feedback_col_width = 45
122
117
  points_col_width = 15
123
118
 
124
- for subitem in grade_item_config.subitems:
125
- subitem_points_possible = subitem.points
119
+ for item in ls_column.items:
120
+ subitem_points_possible = item.points
126
121
  total_points_possible += subitem_points_possible
127
122
 
128
- lines.append(f"{subitem.name} ({subitem_points_possible} points)")
123
+ lines.append(f"{item.name} ({subitem_points_possible} points)")
129
124
  lines.append("-" * 60)
130
125
 
131
126
  # Header row
@@ -135,8 +130,8 @@ def _generate_student_feedback(
135
130
 
136
131
  subitem_points_deducted = 0
137
132
 
138
- # Get deductions for this student in this subitem
139
- student_deductions_obj = subitem_deductions.get(subitem.name)
133
+ # Get deductions for this student in this item
134
+ student_deductions_obj = subitem_deductions.get(item.name)
140
135
  if student_deductions_obj:
141
136
  # Find the student's deductions (try single net_id first, then tuple)
142
137
  student_key = None
@@ -16,7 +16,7 @@ import pandas
16
16
  import yaml
17
17
 
18
18
  from . import grades_csv, student_repos, utils
19
- from .grading_item import GradeItem, ScoreMode
19
+ from .grading_item import GradeItem
20
20
  from .utils import (
21
21
  CallbackFailed,
22
22
  TermColors,
@@ -40,7 +40,7 @@ class Grader:
40
40
  def __init__(
41
41
  self,
42
42
  lab_name: str,
43
- grades_csv_path: pathlib.Path,
43
+ class_list_csv_path: pathlib.Path,
44
44
  work_path: pathlib.Path = pathlib.Path.cwd() / "temp",
45
45
  ):
46
46
  """
@@ -48,27 +48,31 @@ class Grader:
48
48
  ----------
49
49
  lab_name: str
50
50
  Name of the lab/assignment that you are grading (ie. 'lab3').
51
- This is used for folder naming, logging mesasge, etc., and passed back to your callback functions.
52
- grades_csv_path: pathlib.Path
53
- Path to CSV file with student grades exported from LearningSuite. You need to export netid, first
54
- and last name, and any grade columns you want to populate.
51
+ This is used for logging messages and passed back to your callback functions.
52
+ class_list_csv_path: pathlib.Path
53
+ Path to CSV file with class list exported from LearningSuite. You need to export netid, first
54
+ and last name columns.
55
55
  work_path: pathlib.Path
56
56
  Path to directory where student files will be placed. For example, if you pass in '.', then student
57
57
  code would be placed in './lab3'. By default the working path is a "temp" folder created in your working directory.
58
58
  """
59
59
  self.lab_name = lab_name
60
- self.grades_csv_path = pathlib.Path(grades_csv_path).resolve()
60
+ self.class_list_csv_path = pathlib.Path(class_list_csv_path).resolve()
61
61
 
62
- # Make sure grades csv exists, and that file is writable
63
- if not self.grades_csv_path.is_file():
64
- error("grades_csv_path", "(" + str(grades_csv_path) + ")", "does not exist")
62
+ # Make sure class list csv exists and is readable
63
+ if not self.class_list_csv_path.is_file():
64
+ error(
65
+ "class_list_csv_path",
66
+ "(" + str(class_list_csv_path) + ")",
67
+ "does not exist",
68
+ )
65
69
  try:
66
- with open(grades_csv_path, "a", encoding="utf-8"):
70
+ with open(class_list_csv_path, "r", encoding="utf-8"):
67
71
  pass
68
72
  except PermissionError:
69
73
  error(
70
- "You do not have permissions to modify the grades_csv_path file",
71
- "(" + str(grades_csv_path) + ").",
74
+ "You do not have permissions to read the class_list_csv_path file",
75
+ "(" + str(class_list_csv_path) + ").",
72
76
  "Is this file open and locked?",
73
77
  )
74
78
 
@@ -78,11 +82,11 @@ class Grader:
78
82
 
79
83
  # Read CSV and make sure it isn't empty
80
84
  try:
81
- pandas.read_csv(self.grades_csv_path)
85
+ pandas.read_csv(self.class_list_csv_path)
82
86
  except pandas.errors.EmptyDataError:
83
87
  error(
84
- "Your grades csv",
85
- "(" + str(grades_csv_path) + ")",
88
+ "Your class list csv",
89
+ "(" + str(class_list_csv_path) + ")",
86
90
  "appears to be empty",
87
91
  )
88
92
 
@@ -102,29 +106,24 @@ class Grader:
102
106
 
103
107
  def add_item_to_grade(
104
108
  self,
105
- csv_col_name,
109
+ item_name,
106
110
  grading_fcn,
111
+ deductions_yaml_path,
107
112
  *,
108
113
  grading_fcn_args_dict=None,
109
114
  max_points=None,
110
- score_mode=ScoreMode.MANUAL,
111
- deductions_yaml_path=None,
112
- help_msg=None,
113
115
  ):
114
116
  """Add a new item you want to grade.
115
117
 
116
118
  Parameters
117
119
  ----------
118
- csv_col_name: str
119
- The column name from your grading CSV file that you want to grade.
120
120
  grading_fcn: Callable
121
121
  The callback function that will perform all your grading work. Your callback function will be provided with the following arguments:
122
122
 
123
123
  * lab_name (*str*): This will pass back the lab name you passed to *__init__*.
124
124
  Useful if you use the same callback function to grade multiple different assignments.
125
-
125
+ * item_name (*str*): The name of the item being graded (e.g., "answers.txt").
126
126
  * student_code_path (*pathlib.Path*): The location where the unzipped/cloned student files are stored.
127
- * csv_col_name (*str*): The current CSV column being graded.
128
127
  * points (*int*): The maximum number of points possible for the item being graded, used for validating the
129
128
  grade when prompting the user to input a grade. If your callback function automatically calcuates and
130
129
  returns a grade, this argument is ignored.
@@ -157,38 +156,22 @@ class Grader:
157
156
  def my_callback(**kw):
158
157
  lab_name = kw["lab_name"]
159
158
  first_name = kw["first_names"][0]
159
+ deductions_yaml_path: pathlib.Path
160
+ Path to the YAML file for storing deductions and tracking grading status.
160
161
  grading_fcn_args_dict: dict
161
162
  (Optional) A dictionary of additional arguments that will be passed to your grading function.
162
163
  max_points: int
163
164
  (Optional) Number of max points for the graded column.
164
- deductions_yaml_path: pathlib.Path
165
- (Optional) Path to the YAML file for storing deductions. Required if score_mode is DEDUCTIONS.
166
- help_msg: str
167
- (Optional) When the script asks the user for a grade, it will print this message first. This can be a helpful
168
- reminder to the TAs of a grading rubric, things they should watch out for, etc.
169
165
  """
170
166
  # Check data types
171
167
  if not isinstance(grading_fcn, Callable):
172
168
  error("'grading_fcn' must be a callable function")
173
169
 
174
- df = pandas.read_csv(self.grades_csv_path)
175
- if csv_col_name is not None and csv_col_name not in df:
176
- error(
177
- "Provided grade column name",
178
- "(" + csv_col_name + ")",
179
- "does not exist in grades_csv_path",
180
- "(" + str(self.grades_csv_path) + ").",
181
- "Columns:",
182
- list(df.columns),
183
- )
184
-
185
170
  item = GradeItem(
186
171
  self,
187
- csv_col_name,
188
- grading_fcn,
189
- max_points,
190
- help_msg=help_msg,
191
- score_mode=score_mode,
172
+ item_name,
173
+ fcn=grading_fcn,
174
+ max_points=max_points,
192
175
  deductions_yaml_path=deductions_yaml_path,
193
176
  fcn_args_dict=grading_fcn_args_dict,
194
177
  )
@@ -197,21 +180,32 @@ class Grader:
197
180
  )
198
181
  self.items.append(item)
199
182
 
200
- def add_analysis_item(
183
+ def add_item_to_grade_from_config(
201
184
  self,
202
- analysis_fcn,
185
+ grade_item_config,
186
+ grading_fcn,
203
187
  ):
204
- """Run an analysis function on the student code, without performing any grading.
188
+ """Add a new item to grade using a GradeItemConfig object.
189
+
190
+ This is a convenience method that extracts all necessary information from
191
+ a GradeItemConfig object and calls add_item_to_grade.
205
192
 
206
193
  Parameters
207
194
  ----------
208
- analysis_fcn: Callable
209
- The callback function that will perform the analysis.
210
- The callback will be provided with the same arguments as when you register a grading function.
211
-
195
+ grade_item_config: GradeItemConfig
196
+ The configuration object for the grade item, containing points,
197
+ feedback_path, and other_data.
198
+ grading_fcn: Callable
199
+ The callback function that will perform the grading work.
200
+ See add_item_to_grade for details on the callback function signature.
212
201
  """
213
-
214
- self.add_item_to_grade(None, analysis_fcn)
202
+ self.add_item_to_grade(
203
+ grading_fcn=grading_fcn,
204
+ item_name=grade_item_config.name,
205
+ deductions_yaml_path=grade_item_config.feedback_path,
206
+ grading_fcn_args_dict=grade_item_config.other_data,
207
+ max_points=grade_item_config.points,
208
+ )
215
209
 
216
210
  def set_submission_system_learning_suite(self, zip_path):
217
211
  """
@@ -461,10 +455,11 @@ class Grader:
461
455
  )
462
456
 
463
457
  def _get_all_csv_cols_to_grade(self):
464
- """Collect all columns that will be graded into a single list"""
465
- return [
466
- item.csv_col_name for item in self.items if item.csv_col_name is not None
467
- ]
458
+ """Collect all columns that will be graded into a single list.
459
+
460
+ Returns empty list since grades are stored in deductions files, not CSV columns.
461
+ """
462
+ return []
468
463
 
469
464
  def run(self):
470
465
  """Call this to start (or resume) the grading process"""
@@ -477,7 +472,7 @@ class Grader:
477
472
 
478
473
  # Read in CSV and validate. Print # students who need a grade
479
474
  student_grades_df = grades_csv.parse_and_check(
480
- self.grades_csv_path, self._get_all_csv_cols_to_grade()
475
+ self.class_list_csv_path, self._get_all_csv_cols_to_grade()
481
476
  )
482
477
 
483
478
  # Filter by students who need a grade
@@ -520,14 +515,13 @@ class Grader:
520
515
  concated_names = grades_csv.get_concated_names(row)
521
516
 
522
517
  # Check if student/group needs grading
523
- if not any(item.analysis_only for item in self.items):
524
- num_group_members_need_grade_per_item = [
525
- item.num_grades_needed(row) for item in self.items
526
- ]
518
+ num_group_members_need_grade_per_item = [
519
+ item.num_grades_needed_deductions(net_ids) for item in self.items
520
+ ]
527
521
 
528
- if sum(num_group_members_need_grade_per_item) == 0:
529
- # This student/group is already fully graded
530
- continue
522
+ if sum(num_group_members_need_grade_per_item) == 0:
523
+ # This student/group is already fully graded
524
+ continue
531
525
 
532
526
  # Print name(s) of who we are grading
533
527
  student_work_path = self.work_path / utils.names_to_dir(
@@ -798,6 +792,7 @@ class Grader:
798
792
  def _verify_callback_fcn(fcn, item, fcn_extra_args_dict=None):
799
793
  callback_args = [
800
794
  "lab_name",
795
+ "item_name",
801
796
  "student_code_path",
802
797
  "run",
803
798
  "build",
@@ -809,10 +804,6 @@ def _verify_callback_fcn(fcn, item, fcn_extra_args_dict=None):
809
804
  if item.max_points:
810
805
  callback_args.append("max_points")
811
806
 
812
- # If this is a fcn for a graded item (not a prep-only function), then
813
- # this argument is required.
814
- callback_args.append("csv_col_name")
815
-
816
807
  callback_args_optional = [
817
808
  "section",
818
809
  "homework_id",
@@ -1,4 +1,4 @@
1
- """ Manage the grade CSV file"""
1
+ """Manage the grade CSV file"""
2
2
 
3
3
  import pandas
4
4
 
@@ -6,14 +6,14 @@ from .student_repos import convert_github_url_format
6
6
  from .utils import TermColors, error, print_color, warning
7
7
 
8
8
 
9
- def parse_and_check(grades_csv_path, csv_cols):
10
- """Parse the grades CSV file and check that column names are valid"""
9
+ def parse_and_check(class_list_csv_path, csv_cols):
10
+ """Parse the class list CSV file and check that column names are valid"""
11
11
  try:
12
- grades_df = pandas.read_csv(grades_csv_path)
12
+ grades_df = pandas.read_csv(class_list_csv_path)
13
13
  except pandas.errors.EmptyDataError:
14
14
  error(
15
15
  "Exception: pandas.errors.EmptyDataError. Is your",
16
- grades_csv_path.name,
16
+ class_list_csv_path.name,
17
17
  "file empty?",
18
18
  )
19
19
  check_csv_column_names(grades_df, csv_cols)