Coverage for e2xgrader/exporters/gradeexporter.py: 31%
67 statements
« prev ^ index » next coverage.py v7.4.2, created at 2024-03-14 13:22 +0100
« prev ^ index » next coverage.py v7.4.2, created at 2024-03-14 13:22 +0100
1import typing
2from collections import defaultdict
3from textwrap import dedent
5import pandas as pd
6from e2xcore.api import E2xAPI
7from e2xcore.utils.utils import get_nbgrader_config
8from nbgrader.api import Grade
9from traitlets import Bool, List, Unicode
10from traitlets.config import LoggingConfigurable
13class GradeExporter(LoggingConfigurable):
14 assignments = List(
15 trait=Unicode(),
16 default_value=[],
17 help="The assignments for which grades should be exported",
18 )
20 tasks = Bool(
21 default_value=False,
22 help="Whether to include scores per cell or not. Defaults to False.",
23 )
25 notebooks = Bool(
26 default_value=True,
27 help=dedent(
28 """
29 Whether to include scores per notebook or not.
30 Can only be False if tasks is False. Defaults to True.
31 """
32 ),
33 )
35 include_max_score = Bool(
36 default_value=False,
37 help=dedent(
38 """
39 Whether to include a row with the maximum score or not.
40 If True a row named "max_score" will be added. Defaults to False.
41 """
42 ),
43 )
45 normalize = Bool(
46 default_value=False,
47 help=dedent(
48 """
49 Whether to divide all scores by the maximum score or not.
50 Defaults to False.
51 """
52 ),
53 )
55 def __init__(self, **kwargs):
56 super().__init__(**kwargs)
57 self.api = E2xAPI(config=get_nbgrader_config())
59 def get_assignment_ids(self) -> typing.List[str]:
60 if len(self.assignments) > 0:
61 return self.assignments
62 else:
63 return [
64 assignment["name"]
65 for assignment in self.api.get_assignments(include_score=False)
66 ]
68 def get_grades(self) -> pd.DataFrame:
69 data = defaultdict(lambda: defaultdict(float))
70 assignments = self.get_assignment_ids()
71 for assignment_id in assignments:
72 for notebook in self.api.get_notebooks(assignment_id):
73 notebook_id = notebook["name"]
74 # Get the maximum score
75 if self.include_max_score or self.normalize:
76 for key, value in self.max_score(assignment_id, notebook).items():
77 data[key].update(value)
78 for submission in self.api.get_notebook_submissions(
79 assignment_id=assignment_id, notebook_id=notebook_id
80 ):
81 if self.tasks:
82 grades = self.task_grades(assignment_id, submission)
83 for key, value in grades.items():
84 data[key].update(value)
85 elif self.notebooks:
86 data[(assignment_id, notebook_id)][submission["student"]] = (
87 submission["score"]
88 )
89 else:
90 data[assignment_id][submission["student"]] += submission[
91 "score"
92 ]
93 grades = pd.DataFrame.from_dict(data, orient="index").T
94 if self.normalize:
95 grades = grades.div(grades.loc["max_score"]).drop("max_score")
96 return grades[sorted(grades.columns)]
98 def max_score(
99 self, assignment_id: str, notebook: typing.Dict[str, typing.Any]
100 ) -> typing.Dict[str, typing.Dict[str, float]]:
101 data = defaultdict(dict)
102 assignment = self.api.get_assignment(assignment_id, include_score=False)
103 if assignment["num_submissions"] < 1 or notebook["max_score"] == 0:
104 return data
105 if self.tasks:
106 with self.api.gradebook as gb:
107 for grade in gb.find_notebook(
108 name=notebook["name"], assignment=assignment_id
109 ).grade_cells:
110 grade_id = self.normalize_grade_id(grade)
111 data[(assignment_id, notebook["name"], grade_id)][
112 "max_score"
113 ] = grade.max_score
114 elif self.notebooks:
115 data[(assignment_id, notebook["name"])]["max_score"] = notebook["max_score"]
116 else:
117 data[assignment_id]["max_score"] = self.api.get_assignment(
118 assignment_id, include_score=False
119 )["max_score"]
120 return data
122 def task_grades(
123 self, assignment_id: str, submission: typing.Dict[str, typing.Any]
124 ) -> typing.Dict[str, typing.Dict[str, float]]:
125 data = defaultdict(dict)
126 with self.api.gradebook as gb:
127 nb = gb.find_submission_notebook_by_id(submission["id"])
128 for grade in nb.grades:
129 grade_id = self.normalize_grade_id(grade)
130 data[(assignment_id, submission["name"], grade_id)][
131 submission["student"]
132 ] = grade.score
133 return data
135 def normalize_grade_id(self, grade: Grade) -> str:
136 return grade.name[5:] if grade.name.startswith("test_") else grade.name