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

1import typing 

2from collections import defaultdict 

3from textwrap import dedent 

4 

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 

11 

12 

13class GradeExporter(LoggingConfigurable): 

14 assignments = List( 

15 trait=Unicode(), 

16 default_value=[], 

17 help="The assignments for which grades should be exported", 

18 ) 

19 

20 tasks = Bool( 

21 default_value=False, 

22 help="Whether to include scores per cell or not. Defaults to False.", 

23 ) 

24 

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 ) 

34 

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 ) 

44 

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 ) 

54 

55 def __init__(self, **kwargs): 

56 super().__init__(**kwargs) 

57 self.api = E2xAPI(config=get_nbgrader_config()) 

58 

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 ] 

67 

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)] 

97 

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 

121 

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 

134 

135 def normalize_grade_id(self, grade: Grade) -> str: 

136 return grade.name[5:] if grade.name.startswith("test_") else grade.name