ed-api-client 0.1.0__py3-none-any.whl
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/__init__.py +9 -0
- ed_api_client/assignments.py +241 -0
- ed_api_client/canvas.py +67 -0
- ed_api_client/challenges.py +279 -0
- ed_api_client/client.py +272 -0
- ed_api_client/quizzes.py +282 -0
- ed_api_client/slides.py +233 -0
- ed_api_client/users.py +56 -0
- ed_api_client/websockets.py +104 -0
- ed_api_client/workspaces.py +530 -0
- ed_api_client-0.1.0.dist-info/METADATA +118 -0
- ed_api_client-0.1.0.dist-info/RECORD +13 -0
- ed_api_client-0.1.0.dist-info/WHEEL +4 -0
ed_api_client/client.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import operator
|
|
2
|
+
import requests
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from ed_api_client import (
|
|
7
|
+
User,
|
|
8
|
+
Slide,
|
|
9
|
+
Lesson,
|
|
10
|
+
SlideResultSet,
|
|
11
|
+
SlideResult,
|
|
12
|
+
Module,
|
|
13
|
+
Submission,
|
|
14
|
+
Assignment,
|
|
15
|
+
AssignmentResult,
|
|
16
|
+
Question,
|
|
17
|
+
Answer,
|
|
18
|
+
Users,
|
|
19
|
+
QuizResult,
|
|
20
|
+
WorkspaceLog,
|
|
21
|
+
ScaffoldSession, Override,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EdClient:
|
|
26
|
+
"""
|
|
27
|
+
Provides access to the Ed API
|
|
28
|
+
|
|
29
|
+
Methods
|
|
30
|
+
----------
|
|
31
|
+
|
|
32
|
+
get_user () -> User
|
|
33
|
+
Retrieves details of the authenticated user
|
|
34
|
+
get_users (courseId:int) -> List<User>
|
|
35
|
+
Retrieves details of all users involved in the given course
|
|
36
|
+
get_slides (lessonId:int) -> List<Slide>
|
|
37
|
+
Retrieves all slides for the given lesson
|
|
38
|
+
get_module_structure (courseId:int, fetchSlides:bool=False) -> List<Module>
|
|
39
|
+
Retrieves all modules and lessons for the given course, and optionally all slides within each lesson
|
|
40
|
+
get_slide_results (lessonId: int) -> SlideResultSet
|
|
41
|
+
Retrieves records of student interactions with slides within the given lesson
|
|
42
|
+
get_submission (submissionId: int) -> Submission
|
|
43
|
+
Retrieves details of a single submission (i.e. a student hitting Mark).
|
|
44
|
+
Note the returned submission will include details of indiviual test cases that are passed or failed.
|
|
45
|
+
get_submissions (userId: int, challengeId: int) -> List<Submission>
|
|
46
|
+
Retrieves details of all submissions made by the given student towards the given challenge.
|
|
47
|
+
Note the returned submissions will not include details of indiviual test cases that are passed or failed.
|
|
48
|
+
getResultsForStudent (assignment: Assignment, student: Student) -> AssignmentResult
|
|
49
|
+
Retrieves the marks and other details for the given student and given assignment
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, token):
|
|
53
|
+
self.host = "https://edstem.org/api"
|
|
54
|
+
self.session = requests.Session()
|
|
55
|
+
# self.session.headers.update({"x-token": token})
|
|
56
|
+
self.session.headers.update({"Authorization": f"Bearer {token}"})
|
|
57
|
+
self.MAX_ATTEMPTS = 3
|
|
58
|
+
self.DELAY = 60
|
|
59
|
+
|
|
60
|
+
def __get_json(self, url, params=None):
|
|
61
|
+
r = None
|
|
62
|
+
|
|
63
|
+
for attempt in range(0, self.MAX_ATTEMPTS):
|
|
64
|
+
r = self.session.get(url, params=params)
|
|
65
|
+
|
|
66
|
+
if r.status_code >= 200 and r.status_code < 300:
|
|
67
|
+
return r.json()
|
|
68
|
+
|
|
69
|
+
if r.status_code == 429:
|
|
70
|
+
# getting rate limited
|
|
71
|
+
time.sleep(self.DELAY)
|
|
72
|
+
|
|
73
|
+
print(
|
|
74
|
+
"given up on request to {} after {} attempts, due to {}:{}".format(
|
|
75
|
+
url, self.MAX_ATTEMPTS, r.status_code, r.content
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
r.raise_for_status()
|
|
79
|
+
|
|
80
|
+
def get_user(self) -> User:
|
|
81
|
+
r = self.__get_json("{}/user".format(self.host))
|
|
82
|
+
u = r.get("user")
|
|
83
|
+
if u is None:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
return User.from_json(u)
|
|
87
|
+
|
|
88
|
+
def get_users(self, courseId: int, role: str = None) -> Users:
|
|
89
|
+
users = Users()
|
|
90
|
+
|
|
91
|
+
r = self.__get_json("{}/courses/{}/admin".format(self.host, courseId))
|
|
92
|
+
|
|
93
|
+
for u in r.get("users", []):
|
|
94
|
+
user = User.from_json(u)
|
|
95
|
+
if role is None or role == user.role:
|
|
96
|
+
users.add(user)
|
|
97
|
+
|
|
98
|
+
return users
|
|
99
|
+
|
|
100
|
+
def get_slides(self, lessonId):
|
|
101
|
+
slides = []
|
|
102
|
+
|
|
103
|
+
r = self.__get_json("{}/lessons/{}".format(self.host, lessonId))
|
|
104
|
+
|
|
105
|
+
for s in r.get("lesson", {}).get("slides", []):
|
|
106
|
+
slides.append(Slide.from_json(s))
|
|
107
|
+
|
|
108
|
+
return slides
|
|
109
|
+
|
|
110
|
+
def get_slide(self, slideId):
|
|
111
|
+
slides = []
|
|
112
|
+
|
|
113
|
+
r = self.__get_json(f"{self.host}/lessons/slides/{slideId}")
|
|
114
|
+
print(r)
|
|
115
|
+
return Slide.from_json(r["slide"])
|
|
116
|
+
|
|
117
|
+
def get_challenge(self, challengeId):
|
|
118
|
+
slides = []
|
|
119
|
+
|
|
120
|
+
r = self.__get_json(f"{self.host}/challenges/{challengeId}")
|
|
121
|
+
print(r)
|
|
122
|
+
# return Slide.from_json(r['slide'])
|
|
123
|
+
|
|
124
|
+
def get_module_structure(self, courseId, fetch_slides=False):
|
|
125
|
+
r = self.__get_json("{}/courses/{}/lessons".format(self.host, courseId))
|
|
126
|
+
|
|
127
|
+
modules = []
|
|
128
|
+
modulesById = {}
|
|
129
|
+
moduleIndex = 0
|
|
130
|
+
for m in r.get("modules", []):
|
|
131
|
+
module = Module(m["id"], m["name"], moduleIndex)
|
|
132
|
+
modules.append(module)
|
|
133
|
+
modulesById[module.id] = module
|
|
134
|
+
moduleIndex += 1
|
|
135
|
+
|
|
136
|
+
for l in r.get("lessons", []):
|
|
137
|
+
if l["is_hidden"]:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
moduleId = l.get("module_id")
|
|
141
|
+
|
|
142
|
+
if moduleId is None or moduleId not in modulesById:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
lesson = Lesson.from_json(l)
|
|
146
|
+
modulesById[moduleId].add_lesson(lesson)
|
|
147
|
+
|
|
148
|
+
if fetch_slides:
|
|
149
|
+
for slide in self.get_slides(lesson.id):
|
|
150
|
+
lesson.add_slide(slide)
|
|
151
|
+
|
|
152
|
+
modules.sort(key=operator.attrgetter("index"))
|
|
153
|
+
return modules
|
|
154
|
+
|
|
155
|
+
def get_slide_results(self, lessonId):
|
|
156
|
+
r = self.__get_json("{}/lessons/{}/results".format(self.host, lessonId))
|
|
157
|
+
|
|
158
|
+
results = SlideResultSet()
|
|
159
|
+
|
|
160
|
+
for user in r:
|
|
161
|
+
userId = user["user_id"]
|
|
162
|
+
|
|
163
|
+
for slide in user.get("slides", []):
|
|
164
|
+
slideId = slide["slide_id"]
|
|
165
|
+
results.add(userId, slideId, SlideResult(slide))
|
|
166
|
+
|
|
167
|
+
return results
|
|
168
|
+
|
|
169
|
+
def get_submission(self, submissionId):
|
|
170
|
+
r = self.__get_json(
|
|
171
|
+
"{}/challenges/submissions/{}".format(self.host, submissionId)
|
|
172
|
+
)
|
|
173
|
+
s = r.get("submission")
|
|
174
|
+
|
|
175
|
+
if s is None:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
return Submission.from_json(s)
|
|
179
|
+
|
|
180
|
+
def get_submissions(self, userId, challengeId):
|
|
181
|
+
submissions = []
|
|
182
|
+
r = self.__get_json(
|
|
183
|
+
"{}/users/{}/challenges/{}/submissions".format(
|
|
184
|
+
self.host, userId, challengeId
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
for s in r.get("submissions", []):
|
|
189
|
+
submissions.append(Submission.from_json(s))
|
|
190
|
+
|
|
191
|
+
return submissions
|
|
192
|
+
|
|
193
|
+
def get_scaffold(self, challengeId: str):
|
|
194
|
+
|
|
195
|
+
r = self.__get_json(f"{self.host}/challenges/{challengeId}/scaffold")
|
|
196
|
+
print(json.dumps(r))
|
|
197
|
+
|
|
198
|
+
def get_workspace_log(self, workspaceId: str) -> WorkspaceLog:
|
|
199
|
+
|
|
200
|
+
r = self.__get_json(f"{self.host}/workspaces/{workspaceId}/logs")
|
|
201
|
+
# print(r['challenge_attempts'], r['challenge_results'])
|
|
202
|
+
|
|
203
|
+
return WorkspaceLog.from_json(r)
|
|
204
|
+
|
|
205
|
+
def getResultsForStudent(
|
|
206
|
+
self, assignment: Assignment, student: User
|
|
207
|
+
) -> AssignmentResult:
|
|
208
|
+
result = AssignmentResult(student.id)
|
|
209
|
+
|
|
210
|
+
for quiz in assignment.quizzes:
|
|
211
|
+
answers = self.getAnswers(quiz.id, student.id)
|
|
212
|
+
result.add_quiz_result(QuizResult.build(quiz.id, quiz.questions, answers))
|
|
213
|
+
|
|
214
|
+
for challenge in assignment.challenges:
|
|
215
|
+
submissions = self.get_submissions(student.id, challenge.id)
|
|
216
|
+
|
|
217
|
+
submissions.sort(key=lambda x: x.markedAt)
|
|
218
|
+
|
|
219
|
+
for submission in submissions:
|
|
220
|
+
result.add_challenge_submission(submission, student, assignment)
|
|
221
|
+
|
|
222
|
+
result.finalize(assignment, self)
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
def getQuestions(self, slideId):
|
|
226
|
+
|
|
227
|
+
questions = []
|
|
228
|
+
r = self.__get_json(f"{self.host}/lessons/slides/{slideId}/questions")
|
|
229
|
+
|
|
230
|
+
for q in r.get("questions", []):
|
|
231
|
+
questions.append(Question.from_json(q))
|
|
232
|
+
|
|
233
|
+
return questions
|
|
234
|
+
|
|
235
|
+
def getAnswers(self, slideId, userId):
|
|
236
|
+
|
|
237
|
+
answers = []
|
|
238
|
+
r = self.__get_json(
|
|
239
|
+
f"{self.host}/lessons/slides/{slideId}/questions/responses?user_id={userId}"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
for a in r.get("responses", []):
|
|
243
|
+
|
|
244
|
+
if a.get("data") is None:
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
answers.append(Answer.from_json(a))
|
|
248
|
+
|
|
249
|
+
return answers
|
|
250
|
+
|
|
251
|
+
def get_scaffold_files(self, challengeId):
|
|
252
|
+
|
|
253
|
+
r = self.session.post(f"{self.host}/challenges/{challengeId}/connect/scaffold")
|
|
254
|
+
ticket = r.json().get("ticket")
|
|
255
|
+
|
|
256
|
+
session = ScaffoldSession(ticket)
|
|
257
|
+
session.start()
|
|
258
|
+
|
|
259
|
+
return list(session.filesById.values())
|
|
260
|
+
|
|
261
|
+
def get_overrides(self, courseId:int, lessonId:int):
|
|
262
|
+
|
|
263
|
+
r = self.__get_json(
|
|
264
|
+
f"{self.host}/courses/{courseId}/overrides",
|
|
265
|
+
{'lesson_id' : lessonId}
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
overrides = []
|
|
269
|
+
for o in r.get('overrides',[]):
|
|
270
|
+
overrides.append(Override.from_json(o))
|
|
271
|
+
|
|
272
|
+
return overrides
|
ed_api_client/quizzes.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import List, Set, Dict
|
|
3
|
+
import datetime
|
|
4
|
+
import dateutil.parser
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def getParsonsSolutionFromSource(source):
|
|
9
|
+
solution = ""
|
|
10
|
+
for line in re.split("-----\\s*\n", source):
|
|
11
|
+
line = line.rstrip()
|
|
12
|
+
|
|
13
|
+
if len(line) == 0:
|
|
14
|
+
continue
|
|
15
|
+
|
|
16
|
+
if line.endswith("#alt"):
|
|
17
|
+
continue
|
|
18
|
+
|
|
19
|
+
if line.endswith("#distractor"):
|
|
20
|
+
continue
|
|
21
|
+
|
|
22
|
+
solution = solution + line + "\n"
|
|
23
|
+
|
|
24
|
+
return solution
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def getParsonsAnswer(items):
|
|
28
|
+
answer = ""
|
|
29
|
+
for item in items:
|
|
30
|
+
for i in range(0, item.get("indent", 0)):
|
|
31
|
+
answer = answer + " "
|
|
32
|
+
|
|
33
|
+
answer = answer + item.get("text", "") + "\n"
|
|
34
|
+
|
|
35
|
+
return answer
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def doesCodeMatch(codeA: str, codeB: str, strictIndentation: bool = False):
|
|
39
|
+
|
|
40
|
+
codeA = re.sub("-----", "", codeA)
|
|
41
|
+
codeB = re.sub("-----", "", codeB)
|
|
42
|
+
|
|
43
|
+
if strictIndentation:
|
|
44
|
+
return codeA == codeB
|
|
45
|
+
|
|
46
|
+
normA = re.sub("\\s+", " ", codeA).strip()
|
|
47
|
+
normB = re.sub("\\s+", " ", codeB).strip()
|
|
48
|
+
|
|
49
|
+
# print("A", normA)
|
|
50
|
+
# print("B", normB)
|
|
51
|
+
|
|
52
|
+
return normA == normB
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Quiz:
|
|
57
|
+
id: int
|
|
58
|
+
name: str
|
|
59
|
+
questions: list
|
|
60
|
+
|
|
61
|
+
def getMaxScore(self):
|
|
62
|
+
score = 0
|
|
63
|
+
for question in self.questions:
|
|
64
|
+
score = score + question.points
|
|
65
|
+
|
|
66
|
+
return score
|
|
67
|
+
|
|
68
|
+
def add_multichoice_solution(self, questionId: int, solution: Set[int]) -> bool:
|
|
69
|
+
|
|
70
|
+
for question in self.questions:
|
|
71
|
+
if question.type != "multichoice":
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if question.id != questionId:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
question.add_solution(solution)
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def add_parsons_solution(self, questionId: int, solution: str) -> bool:
|
|
83
|
+
|
|
84
|
+
for question in self.questions:
|
|
85
|
+
if question.type != "parsons":
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
if question.id != questionId:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
question.add_solution(solution)
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class Question:
|
|
99
|
+
id: int
|
|
100
|
+
points: int
|
|
101
|
+
content: str
|
|
102
|
+
type: str = field(default=None, init=False)
|
|
103
|
+
|
|
104
|
+
def from_json(json: dict):
|
|
105
|
+
question_type = json.get("data", {}).get("type", "unknown")
|
|
106
|
+
if question_type == "multiple-choice":
|
|
107
|
+
return MultichoiceQuestion.from_json(json)
|
|
108
|
+
if question_type == "parsons":
|
|
109
|
+
return ParsonsQuestion.from_json(json)
|
|
110
|
+
if question_type == "short-answer":
|
|
111
|
+
return ShortAnswerQuestion.from_json(json)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class ShortAnswerQuestion(Question):
|
|
116
|
+
type = "short-answer"
|
|
117
|
+
|
|
118
|
+
def from_json(json: dict):
|
|
119
|
+
return ShortAnswerQuestion(
|
|
120
|
+
json["id"], json["auto_points"], json.get("data", {}).get("content")
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class MultichoiceQuestion(Question):
|
|
126
|
+
type = "multichoice"
|
|
127
|
+
multiple_selection: bool
|
|
128
|
+
options: List[str]
|
|
129
|
+
solutions: List[Set[int]]
|
|
130
|
+
|
|
131
|
+
def from_json(json: dict):
|
|
132
|
+
return MultichoiceQuestion(
|
|
133
|
+
json["id"],
|
|
134
|
+
json["auto_points"],
|
|
135
|
+
json.get("data", {}).get("content"),
|
|
136
|
+
json.get("data", {}).get("multiple_selection", False),
|
|
137
|
+
json.get("data", {}).get("answers", []),
|
|
138
|
+
[json.get("data", {}).get("solution", [])],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def add_solution(self, solution: Set[int]):
|
|
142
|
+
self.solutions.append(solution)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class ParsonsQuestion(Question):
|
|
147
|
+
type = "parsons"
|
|
148
|
+
solutions: List[str] = field(default_factory=list)
|
|
149
|
+
|
|
150
|
+
def from_json(json: dict):
|
|
151
|
+
return ParsonsQuestion(
|
|
152
|
+
json["id"],
|
|
153
|
+
json["auto_points"],
|
|
154
|
+
json.get("data", {}).get("content"),
|
|
155
|
+
[getParsonsSolutionFromSource(json.get("data", {}).get("source"))],
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def add_solution(self, solution: str):
|
|
159
|
+
self.solutions.append(solution)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class Answer:
|
|
164
|
+
question_id: int
|
|
165
|
+
created_at: datetime.datetime
|
|
166
|
+
type: str = field(default=None, init=False)
|
|
167
|
+
|
|
168
|
+
def from_json(j: dict):
|
|
169
|
+
|
|
170
|
+
if type(j["data"]) == list:
|
|
171
|
+
return MultichoiceAnswer.from_json(j)
|
|
172
|
+
elif type(j["data"]) == str:
|
|
173
|
+
return ShortAnswer.from_json(j)
|
|
174
|
+
else:
|
|
175
|
+
return ParsonsAnswer.from_json(j)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class MultichoiceAnswer(Answer):
|
|
180
|
+
type = "multichoice"
|
|
181
|
+
choices: Set[int]
|
|
182
|
+
|
|
183
|
+
def from_json(j: dict):
|
|
184
|
+
return MultichoiceAnswer(
|
|
185
|
+
j["question_id"], dateutil.parser.isoparse(j["created_at"]), j["data"]
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass
|
|
190
|
+
class ParsonsAnswer(Answer):
|
|
191
|
+
type = "parsons"
|
|
192
|
+
code: str
|
|
193
|
+
|
|
194
|
+
def from_json(j: dict):
|
|
195
|
+
|
|
196
|
+
return ParsonsAnswer(
|
|
197
|
+
j["question_id"],
|
|
198
|
+
dateutil.parser.isoparse(j["created_at"]),
|
|
199
|
+
getParsonsAnswer(j["data"]["items"]),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass
|
|
204
|
+
class ShortAnswer(Answer):
|
|
205
|
+
type = "shortanswer"
|
|
206
|
+
text: str
|
|
207
|
+
|
|
208
|
+
def from_json(j: dict):
|
|
209
|
+
|
|
210
|
+
return ShortAnswer(
|
|
211
|
+
j["question_id"], dateutil.parser.isoparse(j["created_at"]), j["data"]
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass
|
|
216
|
+
class QuizResult:
|
|
217
|
+
quizId: int
|
|
218
|
+
score: int = 0
|
|
219
|
+
answersByQuestionId: Dict[int, Answer] = field(default_factory=dict)
|
|
220
|
+
pointsByQuestionId: Dict[int, int] = field(default_factory=dict)
|
|
221
|
+
|
|
222
|
+
def build(quizId: int, questions: List, answers: List):
|
|
223
|
+
|
|
224
|
+
result = QuizResult(quizId)
|
|
225
|
+
|
|
226
|
+
for answer in answers:
|
|
227
|
+
result.answersByQuestionId[answer.question_id] = answer
|
|
228
|
+
|
|
229
|
+
for question in questions:
|
|
230
|
+
|
|
231
|
+
answer = result.answersByQuestionId.get(question.id)
|
|
232
|
+
if answer is None:
|
|
233
|
+
# result.pointsByQuestionId[question.id] = None
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
if question.type == "multichoice":
|
|
237
|
+
correct = False
|
|
238
|
+
for solution in question.solutions:
|
|
239
|
+
|
|
240
|
+
if solution == answer.choices:
|
|
241
|
+
correct = True
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
if correct:
|
|
245
|
+
result.score = result.score + question.points
|
|
246
|
+
result.pointsByQuestionId[question.id] = question.points
|
|
247
|
+
else:
|
|
248
|
+
result.pointsByQuestionId[question.id] = 0
|
|
249
|
+
|
|
250
|
+
if question.type == "parsons":
|
|
251
|
+
correct = False
|
|
252
|
+
for solution in question.solutions:
|
|
253
|
+
if doesCodeMatch(solution, answer.code, False):
|
|
254
|
+
correct = True
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
if correct:
|
|
258
|
+
result.score = result.score + question.points
|
|
259
|
+
result.pointsByQuestionId[question.id] = question.points
|
|
260
|
+
else:
|
|
261
|
+
result.pointsByQuestionId[question.id] = 0
|
|
262
|
+
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
def print_summary(self, quiz: Quiz):
|
|
266
|
+
|
|
267
|
+
print(
|
|
268
|
+
f" {self.score}/{quiz.getMaxScore()} points for "
|
|
269
|
+
+ quiz.name
|
|
270
|
+
+ " from the following questions:"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
for index in range(0, len(quiz.questions)):
|
|
274
|
+
question = quiz.questions[index]
|
|
275
|
+
points = self.pointsByQuestionId.get(question.id)
|
|
276
|
+
|
|
277
|
+
if points is None:
|
|
278
|
+
print(f" Q{index+1}: 0 points (unanswered)")
|
|
279
|
+
elif points == 0:
|
|
280
|
+
print(f" Q{index+1}: 0 points (incorrect)")
|
|
281
|
+
else:
|
|
282
|
+
print(f" Q{index + 1}: {points} points (correct)")
|