Coverage for e2xgrader/exchange/list.py: 10%

134 statements  

« prev     ^ index     » next       coverage.py v7.4.2, created at 2024-03-14 13:22 +0100

1import glob 

2import hashlib 

3import os 

4import re 

5 

6from nbgrader.exchange.default import ExchangeList 

7from nbgrader.utils import make_unique_key, notebook_hash 

8 

9from .exchange import E2xExchange 

10 

11 

12def _checksum(path): 

13 m = hashlib.md5() 

14 m.update(open(path, "rb").read()) 

15 return m.hexdigest() 

16 

17 

18def _get_key(info): 

19 return info["course_id"], info["student_id"], info["assignment_id"] 

20 

21 

22def _match_key(info, key): 

23 return ( 

24 info["course_id"] == key[0] 

25 and info["student_id"] == key[1] 

26 and info["assignment_id"] == key[2] 

27 ) 

28 

29 

30class E2xExchangeList(E2xExchange, ExchangeList): 

31 def init_dest(self): 

32 course_id = self.coursedir.course_id if self.coursedir.course_id else "*" 

33 assignment_id = ( 

34 self.coursedir.assignment_id if self.coursedir.assignment_id else "*" 

35 ) 

36 student_id = self.coursedir.student_id if self.coursedir.student_id else "*" 

37 

38 if self.inbound: 

39 pattern = os.path.join( 

40 self.root, 

41 course_id, 

42 self.inbound_directory, 

43 "{}+{}+*".format(student_id, assignment_id), 

44 ) 

45 elif self.cached: 

46 pattern = os.path.join( 

47 self.cache, course_id, "{}+{}+*".format(student_id, assignment_id) 

48 ) 

49 else: 

50 if self.personalized_outbound: 

51 # list all assignments here 

52 pattern = os.path.join( 

53 self.root, 

54 course_id, 

55 self.outbound_directory, 

56 student_id, 

57 "{}".format(assignment_id), 

58 ) 

59 else: 

60 pattern = os.path.join( 

61 self.root, 

62 course_id, 

63 self.outbound_directory, 

64 "{}".format(assignment_id), 

65 ) 

66 

67 self.assignments = sorted(glob.glob(pattern)) 

68 

69 def parse_assignment(self, assignment): 

70 course_id = r".*/(?P<course_id>.*)/" 

71 if self.inbound: 

72 regexp = ( 

73 course_id 

74 + self.inbound_directory 

75 + r"/(?P<student_id>[^+]*)\+" 

76 + r"(?P<assignment_id>[^+]*)\+" 

77 + r"(?P<timestamp>[^+]*)" 

78 + r"(?P<random_string>\+.*)?" 

79 ) 

80 elif self.cached: 

81 regexp = ( 

82 course_id 

83 + r"(?P<student_id>.*)\+" 

84 + r"(?P<assignment_id>.*)\+" 

85 + r"(?P<timestamp>.*)" 

86 ) 

87 else: 

88 if self.personalized_outbound: 

89 regexp = ( 

90 course_id 

91 + self.outbound_directory 

92 + r"/(?P<student_id>.*)" 

93 + r"/(?P<assignment_id>.*)" 

94 ) 

95 else: 

96 regexp = course_id + self.outbound_directory + r"/(?P<assignment_id>.*)" 

97 

98 m = re.match(regexp, assignment) 

99 if m is None: 

100 raise RuntimeError( 

101 "Could not match '%s' with regexp '%s'", assignment, regexp 

102 ) 

103 

104 return m.groupdict() 

105 

106 def parse_assignments(self): 

107 if self.coursedir.student_id: 

108 courses = self.authenticator.get_student_courses(self.coursedir.student_id) 

109 else: 

110 courses = None 

111 

112 assignments = [] 

113 released_assignments = [] 

114 for path in self.assignments: 

115 info = self.parse_assignment(path) 

116 # if grader and the assignment is already known as released assignment, skip looking 

117 if ( 

118 self.personalized_outbound 

119 and self.grader 

120 and info["assignment_id"] in released_assignments 

121 ): 

122 self.log.debug( 

123 "Grader role and personalized-outbound are enabled, " 

124 "and the assignment is known to be released already" 

125 ) 

126 continue 

127 

128 if courses is not None and info["course_id"] not in courses: 

129 continue 

130 

131 if self.path_includes_course: 

132 assignment_dir = os.path.join( 

133 self.assignment_dir, info["course_id"], info["assignment_id"] 

134 ) 

135 else: 

136 assignment_dir = os.path.join( 

137 self.assignment_dir, info["assignment_id"] 

138 ) 

139 

140 if self.inbound or self.cached: 

141 info["status"] = "submitted" 

142 info["path"] = path 

143 elif os.path.exists(assignment_dir): 

144 info["status"] = "fetched" 

145 info["path"] = os.path.abspath(assignment_dir) 

146 else: 

147 info["status"] = "released" 

148 info["path"] = path 

149 # update released assignments 

150 if self.personalized_outbound and self.grader: 

151 released_assignments.append(info["assignment_id"]) 

152 

153 if self.remove: 

154 info["status"] = "removed" 

155 

156 notebooks = sorted(glob.glob(os.path.join(info["path"], "*.ipynb"))) 

157 if not notebooks: 

158 self.log.warning("No notebooks found in {}".format(info["path"])) 

159 

160 info["notebooks"] = [] 

161 for notebook in notebooks: 

162 nb_info = { 

163 "notebook_id": os.path.splitext(os.path.split(notebook)[1])[0], 

164 "path": os.path.abspath(notebook), 

165 } 

166 if info["status"] != "submitted": 

167 info["notebooks"].append(nb_info) 

168 continue 

169 

170 nb_info["has_local_feedback"] = False 

171 nb_info["has_exchange_feedback"] = False 

172 nb_info["local_feedback_path"] = None 

173 nb_info["feedback_updated"] = False 

174 

175 # Check whether feedback has been fetched already. 

176 local_feedback_dir = os.path.join( 

177 assignment_dir, "feedback", info["timestamp"] 

178 ) 

179 local_feedback_path = os.path.join( 

180 local_feedback_dir, "{0}.html".format(nb_info["notebook_id"]) 

181 ) 

182 has_local_feedback = os.path.isfile(local_feedback_path) 

183 if has_local_feedback: 

184 local_feedback_checksum = _checksum(local_feedback_path) 

185 else: 

186 local_feedback_checksum = None 

187 

188 # Also look to see if there is feedback available to fetch. 

189 # and check whether personalized-feedback is enabled 

190 if self.personalized_feedback: 

191 exchange_feedback_path = os.path.join( 

192 self.root, 

193 info["course_id"], 

194 self.feedback_directory, 

195 info["student_id"], 

196 info["assignment_id"], 

197 "{0}.html".format(nb_info["notebook_id"]), 

198 ) 

199 else: 

200 unique_key = make_unique_key( 

201 info["course_id"], 

202 info["assignment_id"], 

203 nb_info["notebook_id"], 

204 info["student_id"], 

205 info["timestamp"], 

206 ) 

207 self.log.debug("Unique key is: {}".format(unique_key)) 

208 nb_hash = notebook_hash(notebook, unique_key) 

209 exchange_feedback_path = os.path.join( 

210 self.root, 

211 info["course_id"], 

212 self.feedback_directory, 

213 "{0}.html".format(nb_hash), 

214 ) 

215 

216 has_exchange_feedback = os.path.isfile(exchange_feedback_path) 

217 if not has_exchange_feedback: 

218 # Try looking for legacy feedback. 

219 nb_hash = notebook_hash(notebook) 

220 exchange_feedback_path = os.path.join( 

221 self.root, 

222 info["course_id"], 

223 "feedback", 

224 "{0}.html".format(nb_hash), 

225 ) 

226 has_exchange_feedback = os.path.isfile(exchange_feedback_path) 

227 if has_exchange_feedback: 

228 exchange_feedback_checksum = _checksum(exchange_feedback_path) 

229 else: 

230 exchange_feedback_checksum = None 

231 

232 nb_info["has_local_feedback"] = has_local_feedback 

233 nb_info["has_exchange_feedback"] = has_exchange_feedback 

234 if has_local_feedback: 

235 nb_info["local_feedback_path"] = local_feedback_path 

236 if has_local_feedback and has_exchange_feedback: 

237 nb_info["feedback_updated"] = ( 

238 exchange_feedback_checksum != local_feedback_checksum 

239 ) 

240 info["notebooks"].append(nb_info) 

241 

242 if info["status"] == "submitted": 

243 if info["notebooks"]: 

244 # List feedback if there exists for one of the notebooks files in path 

245 has_local_feedback = any( 

246 [nb["has_local_feedback"] for nb in info["notebooks"]] 

247 ) 

248 has_exchange_feedback = any( 

249 [nb["has_exchange_feedback"] for nb in info["notebooks"]] 

250 ) 

251 feedback_updated = any( 

252 [nb["feedback_updated"] for nb in info["notebooks"]] 

253 ) 

254 else: 

255 has_local_feedback = False 

256 has_exchange_feedback = False 

257 feedback_updated = False 

258 

259 info["has_local_feedback"] = has_local_feedback 

260 info["has_exchange_feedback"] = has_exchange_feedback 

261 info["feedback_updated"] = feedback_updated 

262 if has_local_feedback: 

263 info["local_feedback_path"] = os.path.join( 

264 assignment_dir, "feedback", info["timestamp"] 

265 ) 

266 else: 

267 info["local_feedback_path"] = None 

268 

269 assignments.append(info) 

270 

271 # partition the assignments into groups for course/student/assignment 

272 if self.inbound or self.cached: 

273 assignment_keys = sorted( 

274 list(set([_get_key(info) for info in assignments])) 

275 ) 

276 assignment_submissions = [] 

277 for key in assignment_keys: 

278 submissions = [x for x in assignments if _match_key(x, key)] 

279 submissions = sorted(submissions, key=lambda x: x["timestamp"]) 

280 info = { 

281 "course_id": key[0], 

282 "student_id": key[1], 

283 "assignment_id": key[2], 

284 "status": submissions[0]["status"], 

285 "submissions": submissions, 

286 } 

287 assignment_submissions.append(info) 

288 assignments = assignment_submissions 

289 

290 return assignments