ed-api-client 0.1.0__tar.gz → 0.1.2__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.
- {ed_api_client-0.1.0 → ed_api_client-0.1.2}/PKG-INFO +5 -8
- {ed_api_client-0.1.0 → ed_api_client-0.1.2}/ed_api_client/__init__.py +2 -1
- ed_api_client-0.1.2/ed_api_client/assignments.py +382 -0
- ed_api_client-0.1.2/ed_api_client/canvas.py +124 -0
- ed_api_client-0.1.2/ed_api_client/challenges.py +363 -0
- ed_api_client-0.1.2/ed_api_client/client.py +542 -0
- {ed_api_client-0.1.0 → ed_api_client-0.1.2}/ed_api_client/quizzes.py +181 -22
- ed_api_client-0.1.2/ed_api_client/slides.py +326 -0
- ed_api_client-0.1.2/ed_api_client/threads.py +350 -0
- ed_api_client-0.1.2/ed_api_client/users.py +122 -0
- {ed_api_client-0.1.0 → ed_api_client-0.1.2}/ed_api_client/websockets.py +39 -4
- ed_api_client-0.1.2/ed_api_client/workspaces.py +647 -0
- {ed_api_client-0.1.0 → ed_api_client-0.1.2}/pyproject.toml +8 -3
- ed_api_client-0.1.0/ed_api_client/assignments.py +0 -241
- ed_api_client-0.1.0/ed_api_client/canvas.py +0 -67
- ed_api_client-0.1.0/ed_api_client/challenges.py +0 -279
- ed_api_client-0.1.0/ed_api_client/client.py +0 -272
- ed_api_client-0.1.0/ed_api_client/slides.py +0 -233
- ed_api_client-0.1.0/ed_api_client/users.py +0 -56
- ed_api_client-0.1.0/ed_api_client/workspaces.py +0 -530
- {ed_api_client-0.1.0 → ed_api_client-0.1.2}/README.md +0 -0
|
@@ -1,26 +1,23 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: ed-api-client
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A client for interacting with the Ed LMS
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: David Milne
|
|
7
7
|
Author-email: d.n.milne@gmail.com
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
16
|
Requires-Dist: DateTime (>=5.1,<6.0)
|
|
20
|
-
Requires-Dist: dataclasses (==0.6)
|
|
21
17
|
Requires-Dist: pylcs (>=0.1.1,<0.2.0)
|
|
22
18
|
Requires-Dist: python-dateutil (>=2.9.0,<3.0.0)
|
|
23
19
|
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
20
|
+
Requires-Dist: websocket-client (>=1.6,<2.0)
|
|
24
21
|
Description-Content-Type: text/markdown
|
|
25
22
|
|
|
26
23
|
# ed-api-client
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from ed_api_client.slides import *
|
|
2
2
|
from ed_api_client.users import *
|
|
3
|
+
from ed_api_client.threads import *
|
|
3
4
|
from ed_api_client.challenges import *
|
|
4
5
|
from ed_api_client.quizzes import *
|
|
5
6
|
from ed_api_client.assignments import *
|
|
6
7
|
from ed_api_client.workspaces import *
|
|
7
8
|
from ed_api_client.websockets import *
|
|
8
9
|
from ed_api_client.client import *
|
|
9
|
-
from ed_api_client.canvas import *
|
|
10
|
+
from ed_api_client.canvas import *
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import List, Dict, Set
|
|
3
|
+
import datetime
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
from ed_api_client import Slide, ChallengeResult, User, QuizResult, Quiz
|
|
7
|
+
from ed_api_client.challenges import ChallengeMarker, MarkType, Challenge
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Assignment:
|
|
12
|
+
"""
|
|
13
|
+
An assignment against which students are graded
|
|
14
|
+
|
|
15
|
+
Attributes
|
|
16
|
+
----------
|
|
17
|
+
name : str
|
|
18
|
+
The name of the assignment.
|
|
19
|
+
max_marks : int
|
|
20
|
+
The total available marks for this assignment
|
|
21
|
+
marker : ChallengeMarker
|
|
22
|
+
The approach used for calculating grades
|
|
23
|
+
deadline : datetime
|
|
24
|
+
The default deadline beyond which submissions will be penalised.
|
|
25
|
+
challenges : List[Challenge]
|
|
26
|
+
The challenges involved in this assignment
|
|
27
|
+
quizzes : List[Quiz]
|
|
28
|
+
The quizzes involved in this assignment
|
|
29
|
+
extensions_by_student_id : Dict[int, datetime]
|
|
30
|
+
Associates ids of students with deadlines that have been given to them (i.e. as extensions)
|
|
31
|
+
max_days_late : int
|
|
32
|
+
The maximum number of days late that will be considered for marking
|
|
33
|
+
daily_penalty : float
|
|
34
|
+
The fraction of max_marks deducted per day late
|
|
35
|
+
|
|
36
|
+
Methods
|
|
37
|
+
----------
|
|
38
|
+
add_challenge_slide(slide: Slide)
|
|
39
|
+
Adds a slide (usually a code challenge) to this assignment
|
|
40
|
+
add_extension(student: User, deadline: datetime)
|
|
41
|
+
Provides the given student with an extension, until the given deadline
|
|
42
|
+
get_deadline(student: User) -> datetime
|
|
43
|
+
Returns the deadline for the given student
|
|
44
|
+
get_max_score() -> int
|
|
45
|
+
Returns the maximum possible score across all challenges and quizzes
|
|
46
|
+
get_canvas_col() -> str
|
|
47
|
+
Returns the name of the column that would match this assignment in a gradebook exported from Canvas.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
name: str
|
|
52
|
+
max_marks: int
|
|
53
|
+
marker: ChallengeMarker
|
|
54
|
+
deadline: datetime.datetime
|
|
55
|
+
challenges: List[Challenge] = field(default_factory=list)
|
|
56
|
+
quizzes: List[Quiz] = field(default_factory=list)
|
|
57
|
+
extensions_by_student_id: Dict[int, datetime.datetime] = field(default_factory=dict)
|
|
58
|
+
max_days_late: int = 7
|
|
59
|
+
daily_penalty: float = 0.05
|
|
60
|
+
|
|
61
|
+
def add_challenge_slide(self, slide: Slide):
|
|
62
|
+
"""
|
|
63
|
+
Add a code challenge slide to this assignment.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
slide : Slide
|
|
68
|
+
The slide to add. Must be a code challenge (``slide.challenge_id`` must not be None).
|
|
69
|
+
|
|
70
|
+
Raises
|
|
71
|
+
------
|
|
72
|
+
ValueError
|
|
73
|
+
If the slide is not a code challenge.
|
|
74
|
+
"""
|
|
75
|
+
if slide.challenge_id is None:
|
|
76
|
+
raise ValueError("This slide is not a code challenge")
|
|
77
|
+
|
|
78
|
+
self.challenges.append(Challenge(slide.challenge_id, slide.id, slide.title))
|
|
79
|
+
|
|
80
|
+
def add_quiz_slide(self, slide: Slide, questions: list):
|
|
81
|
+
"""
|
|
82
|
+
Add a quiz slide to this assignment.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
slide : Slide
|
|
87
|
+
The quiz slide to add.
|
|
88
|
+
questions : list
|
|
89
|
+
The questions for this quiz, typically fetched via ``EdClient.get_questions``.
|
|
90
|
+
"""
|
|
91
|
+
self.quizzes.append(Quiz(slide.id, slide.title, questions))
|
|
92
|
+
|
|
93
|
+
def add_multichoice_solution(self, question_id: int, solution: Set[int]):
|
|
94
|
+
"""
|
|
95
|
+
Add an accepted answer to a multiple-choice question across all quizzes in this assignment.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
question_id : int
|
|
100
|
+
The id of the question to update.
|
|
101
|
+
solution : Set[int]
|
|
102
|
+
A set of option indices that constitute a correct answer.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
for quiz in self.quizzes:
|
|
106
|
+
|
|
107
|
+
if quiz.add_multichoice_solution(question_id, solution):
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
print(f"could not locate multichoice question {question_id}")
|
|
111
|
+
|
|
112
|
+
def add_parsons_solution(self, question_id: int, solution: str):
|
|
113
|
+
"""
|
|
114
|
+
Add an accepted arrangement to a Parsons question across all quizzes in this assignment.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
question_id : int
|
|
119
|
+
The id of the question to update.
|
|
120
|
+
solution : str
|
|
121
|
+
The accepted code arrangement as a string.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
for quiz in self.quizzes:
|
|
125
|
+
|
|
126
|
+
if quiz.add_parsons_solution(question_id, solution):
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
print(f"could not locate parsons question {question_id}")
|
|
130
|
+
|
|
131
|
+
def add_extension(self, student, deadline):
|
|
132
|
+
"""
|
|
133
|
+
Grant a deadline extension to a single student.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
student : User
|
|
138
|
+
The student receiving the extension.
|
|
139
|
+
deadline : datetime
|
|
140
|
+
The new deadline for this student.
|
|
141
|
+
"""
|
|
142
|
+
self.extensions_by_student_id[student.id] = deadline
|
|
143
|
+
|
|
144
|
+
def add_extensions(self, students, deadline):
|
|
145
|
+
"""
|
|
146
|
+
Grant the same deadline extension to multiple students.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
students : List[User]
|
|
151
|
+
The students receiving the extension.
|
|
152
|
+
deadline : datetime
|
|
153
|
+
The new deadline for all listed students.
|
|
154
|
+
"""
|
|
155
|
+
for student in students:
|
|
156
|
+
self.extensions_by_student_id[student.id] = deadline
|
|
157
|
+
|
|
158
|
+
def get_deadline(self, student):
|
|
159
|
+
"""
|
|
160
|
+
Return the effective deadline for a student, accounting for any extension.
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
student : User
|
|
165
|
+
The student to look up.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
datetime
|
|
170
|
+
The student's personal extension deadline if one exists, otherwise
|
|
171
|
+
the assignment's default deadline.
|
|
172
|
+
"""
|
|
173
|
+
if student.id in self.extensions_by_student_id:
|
|
174
|
+
return self.extensions_by_student_id[student.id]
|
|
175
|
+
|
|
176
|
+
return self.deadline
|
|
177
|
+
|
|
178
|
+
def get_max_score(self):
|
|
179
|
+
"""
|
|
180
|
+
Return the maximum possible score across all challenges and quizzes.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
int
|
|
185
|
+
The total points available in this assignment.
|
|
186
|
+
"""
|
|
187
|
+
max_score = 0
|
|
188
|
+
for quiz in self.quizzes:
|
|
189
|
+
max_score = max_score + quiz.get_max_score()
|
|
190
|
+
|
|
191
|
+
if self.marker.mark_type == MarkType.BY_CHALLENGE:
|
|
192
|
+
max_score = max_score + (
|
|
193
|
+
len(self.challenges) * self.marker.points_per_challenge
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
max_score = max_score + self.marker.max_test_case_score
|
|
197
|
+
|
|
198
|
+
return max_score
|
|
199
|
+
|
|
200
|
+
def get_canvas_col(self):
|
|
201
|
+
"""
|
|
202
|
+
Return the column header that matches this assignment in a Canvas gradebook export.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
str
|
|
207
|
+
"""
|
|
208
|
+
return "{} ({})".format(self.name, self.id)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass
|
|
212
|
+
class AssignmentResult:
|
|
213
|
+
"""
|
|
214
|
+
Records the results of a student for an assignment.
|
|
215
|
+
|
|
216
|
+
Marks for the assignment include a 10% penalty per day if work is received late.
|
|
217
|
+
Late submissions are only considered if they improve the student's marks after the late penalty is applied.
|
|
218
|
+
|
|
219
|
+
Attributes
|
|
220
|
+
----------
|
|
221
|
+
student_id : int
|
|
222
|
+
The id of the student
|
|
223
|
+
completed : bool
|
|
224
|
+
True if the student successfully completed this assignment, otherwise false.
|
|
225
|
+
score : int
|
|
226
|
+
The score received by this student
|
|
227
|
+
mark : float
|
|
228
|
+
The mark received by this student.
|
|
229
|
+
days_late : int
|
|
230
|
+
The number of days past the deadline that work was received and considered.
|
|
231
|
+
challenge_results : Dict[int, ChallengeResult]
|
|
232
|
+
Associates ids of challenges with their individual results
|
|
233
|
+
|
|
234
|
+
Methods
|
|
235
|
+
----------
|
|
236
|
+
add_challenge_submission(submission, student, assignment)
|
|
237
|
+
Updates this result based on a single challenge submission
|
|
238
|
+
add_quiz_result(quiz_result)
|
|
239
|
+
Records the result for a quiz
|
|
240
|
+
finalize(assignment, crawler)
|
|
241
|
+
To be called after all submissions have been added.
|
|
242
|
+
print_summary(student, assignment)
|
|
243
|
+
Print a human-readable summary of the student's mark
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
student_id: int
|
|
247
|
+
score: int = None
|
|
248
|
+
mark: float = None
|
|
249
|
+
days_late: int = None
|
|
250
|
+
penalty: float = None
|
|
251
|
+
challenge_results: Dict[int, ChallengeResult] = field(default_factory=dict)
|
|
252
|
+
quiz_results: Dict[int, QuizResult] = field(default_factory=dict)
|
|
253
|
+
|
|
254
|
+
def add_challenge_submission(self, submission, student, assignment):
|
|
255
|
+
"""
|
|
256
|
+
Record a challenge submission for this student.
|
|
257
|
+
|
|
258
|
+
Calculates days late relative to the student's effective deadline and
|
|
259
|
+
updates the per-challenge result. Call this for every submission before
|
|
260
|
+
calling ``finalize``.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
submission : Submission
|
|
265
|
+
The submission to record.
|
|
266
|
+
student : User
|
|
267
|
+
The student who made the submission.
|
|
268
|
+
assignment : Assignment
|
|
269
|
+
The assignment being graded (used to look up the student's deadline).
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
deadline = assignment.get_deadline(student)
|
|
273
|
+
|
|
274
|
+
days_late = 0
|
|
275
|
+
if submission.marked_at > deadline:
|
|
276
|
+
diff = submission.marked_at - deadline
|
|
277
|
+
days_late = math.ceil(diff.total_seconds() / (60 * 60 * 24))
|
|
278
|
+
|
|
279
|
+
if submission.challenge_id not in self.challenge_results:
|
|
280
|
+
self.challenge_results[submission.challenge_id] = ChallengeResult()
|
|
281
|
+
|
|
282
|
+
self.challenge_results[submission.challenge_id].add(submission, days_late)
|
|
283
|
+
|
|
284
|
+
def add_quiz_result(self, quiz_result: QuizResult):
|
|
285
|
+
"""
|
|
286
|
+
Record the result for a quiz.
|
|
287
|
+
|
|
288
|
+
Parameters
|
|
289
|
+
----------
|
|
290
|
+
quiz_result : QuizResult
|
|
291
|
+
The graded quiz result to store.
|
|
292
|
+
"""
|
|
293
|
+
self.quiz_results[quiz_result.quiz_id] = quiz_result
|
|
294
|
+
|
|
295
|
+
def finalize(self, assignment: Assignment, crawler):
|
|
296
|
+
"""
|
|
297
|
+
Calculate the student's final mark. Call after all submissions and quiz results have been added.
|
|
298
|
+
|
|
299
|
+
Finds the number of days late that maximises the student's mark after
|
|
300
|
+
applying the late penalty, and stores the result in ``mark``, ``score``,
|
|
301
|
+
``days_late``, and ``penalty``.
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
assignment : Assignment
|
|
306
|
+
The assignment being graded.
|
|
307
|
+
crawler : EdClient
|
|
308
|
+
Used to fetch full submission detail when the marker type is
|
|
309
|
+
``BY_TESTCASE_SCORE``. May be None for other mark types.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
self.score = 0
|
|
313
|
+
self.mark = 0
|
|
314
|
+
self.days_late = 0
|
|
315
|
+
self.penalty = 0
|
|
316
|
+
|
|
317
|
+
quiz_score = 0
|
|
318
|
+
for quiz_result in self.quiz_results.values():
|
|
319
|
+
quiz_score = quiz_score + quiz_result.score
|
|
320
|
+
|
|
321
|
+
if self.challenge_results is not None:
|
|
322
|
+
for challenge in self.challenge_results.values():
|
|
323
|
+
challenge.finalize(assignment.marker, crawler)
|
|
324
|
+
|
|
325
|
+
daily_mark_penalty = assignment.daily_penalty * assignment.max_marks
|
|
326
|
+
|
|
327
|
+
for days_late in range(0, assignment.max_days_late + 1):
|
|
328
|
+
score = quiz_score
|
|
329
|
+
|
|
330
|
+
for challenge in self.challenge_results.values():
|
|
331
|
+
score = score + challenge.scores_by_calendar_days_late[days_late]
|
|
332
|
+
|
|
333
|
+
mark = assignment.max_marks * (score / assignment.get_max_score())
|
|
334
|
+
penalty = days_late * daily_mark_penalty
|
|
335
|
+
mark = mark - penalty
|
|
336
|
+
if mark < 0:
|
|
337
|
+
mark = 0
|
|
338
|
+
|
|
339
|
+
if mark > self.mark:
|
|
340
|
+
self.score = score
|
|
341
|
+
self.mark = mark
|
|
342
|
+
self.days_late = days_late
|
|
343
|
+
self.penalty = penalty
|
|
344
|
+
|
|
345
|
+
def print_summary(self, student: User, assignment: Assignment):
|
|
346
|
+
"""
|
|
347
|
+
Print a human-readable summary of the student's mark and per-challenge breakdown.
|
|
348
|
+
|
|
349
|
+
Parameters
|
|
350
|
+
----------
|
|
351
|
+
student : User
|
|
352
|
+
The student whose result is being summarised.
|
|
353
|
+
assignment : Assignment
|
|
354
|
+
The assignment being summarised.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
msg = f"{student.name} got {self.mark:.1f}/{assignment.max_marks} marks from {self.score}/{assignment.get_max_score()} points"
|
|
358
|
+
|
|
359
|
+
if self.days_late > 0:
|
|
360
|
+
msg = f"{msg} and a penalty of {self.penalty:.1f} marks for submitting {self.days_late} days late"
|
|
361
|
+
|
|
362
|
+
print(msg)
|
|
363
|
+
|
|
364
|
+
for quiz in assignment.quizzes:
|
|
365
|
+
quiz_result = self.quiz_results.get(quiz.id)
|
|
366
|
+
|
|
367
|
+
if quiz.id is None:
|
|
368
|
+
print(
|
|
369
|
+
f" 0/{quiz.get_max_score()} points for "
|
|
370
|
+
+ quiz.name
|
|
371
|
+
+ " due to no attempt"
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
quiz_result.print_summary(quiz)
|
|
375
|
+
|
|
376
|
+
for challenge in assignment.challenges:
|
|
377
|
+
|
|
378
|
+
challenge_result = self.challenge_results.get(challenge.id)
|
|
379
|
+
if challenge_result is None:
|
|
380
|
+
print(f" 0 points for " + challenge.name + " due to no attempt")
|
|
381
|
+
else:
|
|
382
|
+
challenge_result.print_summary(challenge, self.days_late)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from ed_api_client import Assignment, Users, AssignmentResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CanvasExporter:
|
|
8
|
+
"""
|
|
9
|
+
Builds a Canvas-compatible gradebook CSV from Ed assignment results.
|
|
10
|
+
|
|
11
|
+
Takes an exported Canvas gradebook as a template and fills in marks from Ed,
|
|
12
|
+
matching students by their SIS Login ID (email address).
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
students : Users
|
|
17
|
+
The full collection of Ed users for the course, used to match Canvas
|
|
18
|
+
rows to Ed student ids.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, students: Users):
|
|
22
|
+
self.__students = students
|
|
23
|
+
self.__assignments_by_col = {}
|
|
24
|
+
self.__results_by_col = {}
|
|
25
|
+
self.__field_names = ["Student", "ID", "SIS User ID", "SIS Login ID", "Section"]
|
|
26
|
+
|
|
27
|
+
def add_assignment(
|
|
28
|
+
self,
|
|
29
|
+
assignment: Assignment,
|
|
30
|
+
column_name: str,
|
|
31
|
+
results: Dict[int, AssignmentResult],
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Register an assignment and its results to be included in the export.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
assignment : Assignment
|
|
39
|
+
The assignment to include.
|
|
40
|
+
column_name : str
|
|
41
|
+
The column header to use in the output CSV. Should match the
|
|
42
|
+
corresponding column in the source Canvas gradebook export.
|
|
43
|
+
results : Dict[int, AssignmentResult]
|
|
44
|
+
Maps Ed student ids to their graded results for this assignment.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
CanvasExporter
|
|
49
|
+
Returns self to allow method chaining.
|
|
50
|
+
"""
|
|
51
|
+
self.__assignments_by_col[column_name] = assignment
|
|
52
|
+
self.__results_by_col[column_name] = results
|
|
53
|
+
self.__field_names.append(column_name)
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def generate_csv(self, source_csv, target_csv):
|
|
57
|
+
"""
|
|
58
|
+
Generate the output gradebook CSV.
|
|
59
|
+
|
|
60
|
+
Reads the Canvas gradebook export at ``source_csv``, looks up each
|
|
61
|
+
student's Ed mark, and writes the result to ``target_csv``. Students
|
|
62
|
+
not found in the Ed user collection are skipped with a warning.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
source_csv : str
|
|
67
|
+
Path to the Canvas gradebook export CSV.
|
|
68
|
+
target_csv : str
|
|
69
|
+
Path to write the output CSV to.
|
|
70
|
+
"""
|
|
71
|
+
with open(source_csv) as input:
|
|
72
|
+
reader = csv.DictReader(input)
|
|
73
|
+
|
|
74
|
+
with open(target_csv, "w") as output:
|
|
75
|
+
writer = csv.DictWriter(output, fieldnames=self.__field_names)
|
|
76
|
+
writer.writeheader()
|
|
77
|
+
|
|
78
|
+
for input_row in reader:
|
|
79
|
+
|
|
80
|
+
output_row = self.handle_row(input_row)
|
|
81
|
+
|
|
82
|
+
if output_row is None:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
writer.writerow(output_row)
|
|
86
|
+
|
|
87
|
+
def handle_row(self, input_row: dict):
|
|
88
|
+
"""
|
|
89
|
+
Convert a single Canvas gradebook row into an output row with Ed marks.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
input_row : dict
|
|
94
|
+
A row from the Canvas gradebook CSV, keyed by column header.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
dict or None
|
|
99
|
+
The output row with Ed marks filled in, or None if the student
|
|
100
|
+
could not be matched (row is skipped).
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
student_email = input_row.get("SIS Login ID")
|
|
104
|
+
if student_email is None:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
student = self.__students.get_by_email(student_email.lower())
|
|
108
|
+
if student is None:
|
|
109
|
+
print(f"Could not locate student {student_email}")
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
output_row = {}
|
|
113
|
+
for field in ["Student", "ID", "SIS User ID", "SIS Login ID", "Section"]:
|
|
114
|
+
output_row[field] = input_row[field]
|
|
115
|
+
|
|
116
|
+
for col, assignment in self.__assignments_by_col.items():
|
|
117
|
+
|
|
118
|
+
results = self.__results_by_col.get(col, {}).get(student.id)
|
|
119
|
+
if results is None:
|
|
120
|
+
output_row[col] = 0
|
|
121
|
+
else:
|
|
122
|
+
output_row[col] = round(results.mark, 3)
|
|
123
|
+
|
|
124
|
+
return output_row
|