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
« prev ^ index » next coverage.py v7.4.2, created at 2024-03-14 13:22 +0100
1import glob
2import hashlib
3import os
4import re
6from nbgrader.exchange.default import ExchangeList
7from nbgrader.utils import make_unique_key, notebook_hash
9from .exchange import E2xExchange
12def _checksum(path):
13 m = hashlib.md5()
14 m.update(open(path, "rb").read())
15 return m.hexdigest()
18def _get_key(info):
19 return info["course_id"], info["student_id"], info["assignment_id"]
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 )
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 "*"
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 )
67 self.assignments = sorted(glob.glob(pattern))
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>.*)"
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 )
104 return m.groupdict()
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
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
128 if courses is not None and info["course_id"] not in courses:
129 continue
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 )
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"])
153 if self.remove:
154 info["status"] = "removed"
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"]))
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
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
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
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 )
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
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)
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
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
269 assignments.append(info)
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
290 return assignments