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
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from ed_api_client.slides import *
|
|
2
|
+
from ed_api_client.users import *
|
|
3
|
+
from ed_api_client.challenges import *
|
|
4
|
+
from ed_api_client.quizzes import *
|
|
5
|
+
from ed_api_client.assignments import *
|
|
6
|
+
from ed_api_client.workspaces import *
|
|
7
|
+
from ed_api_client.websockets import *
|
|
8
|
+
from ed_api_client.client import *
|
|
9
|
+
from ed_api_client.canvas import *
|
|
@@ -0,0 +1,241 @@
|
|
|
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 : int
|
|
18
|
+
The name of the assignment.
|
|
19
|
+
marks : int
|
|
20
|
+
The total available marks for this assignment
|
|
21
|
+
maxScore : int
|
|
22
|
+
The maximum score that users might achieve on this assignment. This depends on the gradeType:
|
|
23
|
+
For BY_CHALLENGE this should be the number of challenges.
|
|
24
|
+
For BY_TESTCASE this should be the number of test cases accross all challenges.
|
|
25
|
+
For BY_TESTCASE_SCORE this should be the sum of all scores for all test cases across all challenges
|
|
26
|
+
gradeType: GradeType
|
|
27
|
+
The approach used for calculating grades
|
|
28
|
+
deadline: datetime
|
|
29
|
+
The default deadline beyond which submissions will be penalised.
|
|
30
|
+
lessonIds: Set[int]
|
|
31
|
+
The ids of all lessons involved in this assignment (note this is populated automatically as slides are added)
|
|
32
|
+
slideIds: Set[int]
|
|
33
|
+
The ids of all slides involved in this assignment (note this is populated automatically as slides are added)
|
|
34
|
+
challengeIds: List[int]
|
|
35
|
+
The ids of all challenges involved in this assignment (note this is populated automatically as slides are added)
|
|
36
|
+
challengeNames: List[str]
|
|
37
|
+
The names of all challenges involved in this assignment (note this is populated automatically as slides are added)
|
|
38
|
+
extensionsByStudentId: Dict[int,datetime]
|
|
39
|
+
Associates ids of students with deadlines that have been given to them (i.e. as extensions)
|
|
40
|
+
|
|
41
|
+
Methods
|
|
42
|
+
----------
|
|
43
|
+
add_slide (slide:Slide)
|
|
44
|
+
Adds a slide (usually a code challenge) to this assignment
|
|
45
|
+
add_extension (student: User, deadline:datetime)
|
|
46
|
+
Provides the given student with an extension, until the given deadline
|
|
47
|
+
get_deadline (student: Student) -> datetime
|
|
48
|
+
Returns the deadline for the given student
|
|
49
|
+
get_canvas_col ( ) -> str
|
|
50
|
+
Returns the name of the column that would match this assignment in an gradebook exported from canvas.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
name: str
|
|
55
|
+
maxMarks: int
|
|
56
|
+
marker: ChallengeMarker
|
|
57
|
+
deadline: datetime.datetime
|
|
58
|
+
challenges: List[Challenge] = field(default_factory=list)
|
|
59
|
+
quizzes: List[Quiz] = field(default_factory=list)
|
|
60
|
+
extensionsByStudentId: Dict[int, datetime.datetime] = field(default_factory=dict)
|
|
61
|
+
dailyPenalty: float = 0.05
|
|
62
|
+
|
|
63
|
+
def add_challenge_slide(self, slide: Slide):
|
|
64
|
+
|
|
65
|
+
if slide.challengeId is None:
|
|
66
|
+
raise ValueError("This slide is not a code challenge")
|
|
67
|
+
|
|
68
|
+
self.challenges.append(Challenge(slide.challengeId, slide.id, slide.title))
|
|
69
|
+
|
|
70
|
+
def add_quiz_slide(self, slide: Slide, questions: list):
|
|
71
|
+
|
|
72
|
+
self.quizzes.append(Quiz(slide.id, slide.title, questions))
|
|
73
|
+
|
|
74
|
+
def add_multichoice_solution(self, questionId: int, solution: Set[int]):
|
|
75
|
+
|
|
76
|
+
for quiz in self.quizzes:
|
|
77
|
+
|
|
78
|
+
if quiz.add_multichoice_solution(questionId, solution):
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
print(f"could not locate multichoice question {questionId}")
|
|
82
|
+
|
|
83
|
+
def add_parsons_solution(self, questionId: int, solution: str):
|
|
84
|
+
|
|
85
|
+
for quiz in self.quizzes:
|
|
86
|
+
|
|
87
|
+
if quiz.add_parsons_solution(questionId, solution):
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
print(f"could not locate parsons question {questionId}")
|
|
91
|
+
|
|
92
|
+
def add_extension(self, student, deadline):
|
|
93
|
+
self.extensionsByStudentId[student.id] = deadline
|
|
94
|
+
|
|
95
|
+
def add_extensions(self, students, deadline):
|
|
96
|
+
for student in students:
|
|
97
|
+
self.extensionsByStudentId[student.id] = deadline
|
|
98
|
+
|
|
99
|
+
def get_deadline(self, student):
|
|
100
|
+
if student.id in self.extensionsByStudentId:
|
|
101
|
+
return self.extensionsByStudentId[student.id]
|
|
102
|
+
|
|
103
|
+
return self.deadline
|
|
104
|
+
|
|
105
|
+
def get_max_score(self):
|
|
106
|
+
maxScore = 0
|
|
107
|
+
for quiz in self.quizzes:
|
|
108
|
+
maxScore = maxScore + quiz.getMaxScore()
|
|
109
|
+
|
|
110
|
+
if self.marker.markType == MarkType.BY_CHALLENGE:
|
|
111
|
+
maxScore = maxScore + (
|
|
112
|
+
len(self.challenges) * self.marker.pointsPerChallenge
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
maxScore = maxScore + self.marker.maxTestCaseScore
|
|
116
|
+
|
|
117
|
+
return maxScore
|
|
118
|
+
|
|
119
|
+
def get_canvas_col(self):
|
|
120
|
+
return "{} ({})".format(self.name, self.id)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class AssignmentResult:
|
|
125
|
+
"""
|
|
126
|
+
Records the results of a student for an assignment.
|
|
127
|
+
|
|
128
|
+
Marks for the assignment include a 10% penalty per day if work is received late.
|
|
129
|
+
Late submissions are only considered if they improve the student's marks after the late penalty is applied.
|
|
130
|
+
|
|
131
|
+
Attributes
|
|
132
|
+
----------
|
|
133
|
+
studentId : int
|
|
134
|
+
The id of the student
|
|
135
|
+
completed : bool
|
|
136
|
+
True if the student successfully completed this assignment, otherwise false.
|
|
137
|
+
score : int
|
|
138
|
+
The score recieved by this student
|
|
139
|
+
mark : float
|
|
140
|
+
The mark recieved by this student.
|
|
141
|
+
daysLate: int
|
|
142
|
+
The number of days past the deadline that work was received and considered.
|
|
143
|
+
challengeResults: Dict[int, ChallengeResult]
|
|
144
|
+
Associates ids of challenges with their individual results
|
|
145
|
+
|
|
146
|
+
Methods
|
|
147
|
+
----------
|
|
148
|
+
add (submission:Submission, student:Student, assignment:Assignment)
|
|
149
|
+
Updates this result based on a single submission
|
|
150
|
+
finalize (assignment:Assignment, crawler: EdCrawler)
|
|
151
|
+
To be called after all submissions have been added.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
studentId: int
|
|
155
|
+
score: int = None
|
|
156
|
+
mark: float = None
|
|
157
|
+
daysLate: int = None
|
|
158
|
+
penalty: float = None
|
|
159
|
+
challengeResults: Dict[int, ChallengeResult] = field(default_factory=dict)
|
|
160
|
+
quizResults: Dict[int, QuizResult] = field(default_factory=dict)
|
|
161
|
+
|
|
162
|
+
def add_challenge_submission(self, submission, student, assignment):
|
|
163
|
+
|
|
164
|
+
deadline = assignment.get_deadline(student)
|
|
165
|
+
|
|
166
|
+
daysLate = 0
|
|
167
|
+
if submission.markedAt > deadline:
|
|
168
|
+
diff = submission.markedAt - deadline
|
|
169
|
+
daysLate = math.ceil(diff.total_seconds() / (60 * 60 * 24))
|
|
170
|
+
|
|
171
|
+
if submission.challengeId not in self.challengeResults:
|
|
172
|
+
self.challengeResults[submission.challengeId] = ChallengeResult()
|
|
173
|
+
|
|
174
|
+
self.challengeResults[submission.challengeId].add(submission, daysLate)
|
|
175
|
+
|
|
176
|
+
def add_quiz_result(self, quizResult: QuizResult):
|
|
177
|
+
self.quizResults[quizResult.quizId] = quizResult
|
|
178
|
+
|
|
179
|
+
def finalize(self, assignment: Assignment, crawler):
|
|
180
|
+
|
|
181
|
+
self.score = 0
|
|
182
|
+
self.mark = 0
|
|
183
|
+
self.daysLate = 0
|
|
184
|
+
self.penalty = 0
|
|
185
|
+
|
|
186
|
+
quizScore = 0
|
|
187
|
+
for quizResult in self.quizResults.values():
|
|
188
|
+
quizScore = quizScore + quizResult.score
|
|
189
|
+
|
|
190
|
+
if self.challengeResults is not None:
|
|
191
|
+
for challengeId, challenge in self.challengeResults.items():
|
|
192
|
+
challenge.finalize(assignment.marker, crawler)
|
|
193
|
+
|
|
194
|
+
dailyMarkPenalty = assignment.dailyPenalty * assignment.maxMarks
|
|
195
|
+
|
|
196
|
+
for daysLate in range(0, 6):
|
|
197
|
+
score = quizScore
|
|
198
|
+
|
|
199
|
+
for challengeId, challenge in self.challengeResults.items():
|
|
200
|
+
score = score + challenge.scoresByCalendarDaysLate[daysLate]
|
|
201
|
+
|
|
202
|
+
mark = assignment.maxMarks * (score / assignment.get_max_score())
|
|
203
|
+
penalty = daysLate * dailyMarkPenalty
|
|
204
|
+
mark = mark - penalty
|
|
205
|
+
if mark < 0:
|
|
206
|
+
mark = 0
|
|
207
|
+
|
|
208
|
+
if mark > self.mark:
|
|
209
|
+
self.score = score
|
|
210
|
+
self.mark = mark
|
|
211
|
+
self.daysLate = daysLate
|
|
212
|
+
self.penalty = penalty
|
|
213
|
+
|
|
214
|
+
def printSummary(self, student: User, assignment: Assignment):
|
|
215
|
+
|
|
216
|
+
msg = f"{student.name} got {self.mark:.1f}/{assignment.maxMarks} marks from {self.score}/{assignment.get_max_score()} points"
|
|
217
|
+
|
|
218
|
+
if self.daysLate > 0:
|
|
219
|
+
msg = f"{msg} and a penalty of {self.penalty:.1f} marks for submitting {self.daysLate} days late"
|
|
220
|
+
|
|
221
|
+
print(msg)
|
|
222
|
+
|
|
223
|
+
for quiz in assignment.quizzes:
|
|
224
|
+
quizResult = self.quizResults.get(quiz.id)
|
|
225
|
+
|
|
226
|
+
if quiz.id is None:
|
|
227
|
+
print(
|
|
228
|
+
f" 0/{quiz.getMaxScore()} points for "
|
|
229
|
+
+ quiz.name
|
|
230
|
+
+ " due to no attempt"
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
quizResult.print_summary(quiz)
|
|
234
|
+
|
|
235
|
+
for challenge in assignment.challenges:
|
|
236
|
+
|
|
237
|
+
challengeResult = self.challengeResults.get(challenge.id)
|
|
238
|
+
if challengeResult is None:
|
|
239
|
+
print(f" 0 points for " + challenge.name + " due to no attempt")
|
|
240
|
+
else:
|
|
241
|
+
challengeResult.print_summary(challenge, self.daysLate)
|
ed_api_client/canvas.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from ed_api_client import Assignment, Users, AssignmentResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CanvasExporter:
|
|
8
|
+
def __init__(self, students: Users):
|
|
9
|
+
self.__students = students
|
|
10
|
+
self.__assignmentsByCol = {}
|
|
11
|
+
self.__resultsByCol = {}
|
|
12
|
+
self.__fieldNames = ["Student", "ID", "SIS User ID", "SIS Login ID", "Section"]
|
|
13
|
+
|
|
14
|
+
def addAssignment(
|
|
15
|
+
self,
|
|
16
|
+
assignment: Assignment,
|
|
17
|
+
columnName: str,
|
|
18
|
+
results: Dict[int, AssignmentResult],
|
|
19
|
+
):
|
|
20
|
+
|
|
21
|
+
self.__assignmentsByCol[columnName] = assignment
|
|
22
|
+
self.__resultsByCol[columnName] = results
|
|
23
|
+
self.__fieldNames.append(columnName)
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
def generateCsv(self, sourceCsv, targetCsv):
|
|
27
|
+
|
|
28
|
+
with open(sourceCsv) as input:
|
|
29
|
+
reader = csv.DictReader(input)
|
|
30
|
+
|
|
31
|
+
with open(targetCsv, "w") as output:
|
|
32
|
+
writer = csv.DictWriter(output, fieldnames=self.__fieldNames)
|
|
33
|
+
writer.writeheader()
|
|
34
|
+
|
|
35
|
+
for inputRow in reader:
|
|
36
|
+
|
|
37
|
+
outputRow = self.handleRow(inputRow)
|
|
38
|
+
|
|
39
|
+
if outputRow is None:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
writer.writerow(outputRow)
|
|
43
|
+
|
|
44
|
+
def handleRow(self, inputRow: dict):
|
|
45
|
+
|
|
46
|
+
studentEmail = inputRow.get("SIS Login ID")
|
|
47
|
+
if studentEmail is None:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
student = self.__students.getByEmail(studentEmail.lower())
|
|
51
|
+
if student is None:
|
|
52
|
+
print(f"Could not locate student {studentEmail}")
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
outputRow = {}
|
|
56
|
+
for field in ["Student", "ID", "SIS User ID", "SIS Login ID", "Section"]:
|
|
57
|
+
outputRow[field] = inputRow[field]
|
|
58
|
+
|
|
59
|
+
for col, assignment in self.__assignmentsByCol.items():
|
|
60
|
+
|
|
61
|
+
results = self.__resultsByCol.get(col, {}).get(student.id)
|
|
62
|
+
if results is None:
|
|
63
|
+
outputRow[col] = 0
|
|
64
|
+
else:
|
|
65
|
+
outputRow[col] = round(results.mark, 3)
|
|
66
|
+
|
|
67
|
+
return outputRow
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import List
|
|
3
|
+
from enum import Enum
|
|
4
|
+
import datetime
|
|
5
|
+
import dateutil.parser
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Challenge:
|
|
10
|
+
id: int
|
|
11
|
+
slideId: int
|
|
12
|
+
name: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MarkType(Enum):
|
|
16
|
+
"""
|
|
17
|
+
Different approaches used for grading assignments
|
|
18
|
+
|
|
19
|
+
BY_CHALLENGE
|
|
20
|
+
user recieves a point for each challenge they complete
|
|
21
|
+
BY_TESTCASE
|
|
22
|
+
user recieves a point for each testcase they pass, across all challenges
|
|
23
|
+
BY_TESTCASE_SCORE
|
|
24
|
+
user recieves points for each testcase they pass, across all challenges.
|
|
25
|
+
The points recieved for each testcase is whatever score value was assigned
|
|
26
|
+
to the testcase
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
BY_CHALLENGE = 1
|
|
30
|
+
BY_TESTCASE = 2
|
|
31
|
+
BY_TESTCASE_SCORE = 3
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ChallengeMarker:
|
|
35
|
+
def __init__(self, markType: MarkType):
|
|
36
|
+
self.markType = markType
|
|
37
|
+
|
|
38
|
+
def passFail(pointsPerChallenge: int = 1):
|
|
39
|
+
marker = ChallengeMarker(MarkType.BY_CHALLENGE)
|
|
40
|
+
marker.pointsPerChallenge = pointsPerChallenge
|
|
41
|
+
return marker
|
|
42
|
+
|
|
43
|
+
def perTestCase(maxTestCaseScore: int):
|
|
44
|
+
marker = ChallengeMarker(MarkType.BY_TESTCASE)
|
|
45
|
+
marker.maxTestCaseScore = maxTestCaseScore
|
|
46
|
+
return marker
|
|
47
|
+
|
|
48
|
+
def perScoredTestCase(maxTestCaseScore: int):
|
|
49
|
+
marker = ChallengeMarker(MarkType.BY_TESTCASE_SCORE)
|
|
50
|
+
marker.maxTestCaseScore = maxTestCaseScore
|
|
51
|
+
return marker
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class TestCase:
|
|
56
|
+
"""
|
|
57
|
+
A test case, that a user may or may not have passed
|
|
58
|
+
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
Attributes
|
|
62
|
+
----------
|
|
63
|
+
name : str
|
|
64
|
+
The name of the test case
|
|
65
|
+
score : int
|
|
66
|
+
The points assigned to this test case
|
|
67
|
+
passed : bool
|
|
68
|
+
True if the user has passed the test case, otherwise false
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
name: str
|
|
73
|
+
score: int
|
|
74
|
+
passed: bool
|
|
75
|
+
|
|
76
|
+
def from_json(j: dict):
|
|
77
|
+
return TestCase(j["name"], j["score"], j["passed"])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class Submission:
|
|
82
|
+
"""
|
|
83
|
+
A submission that a user has made towards a challenge
|
|
84
|
+
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
Attributes
|
|
88
|
+
----------
|
|
89
|
+
id : int
|
|
90
|
+
The unique id of the submission
|
|
91
|
+
userId : int
|
|
92
|
+
The id of the user who made the submission
|
|
93
|
+
challengeId : int
|
|
94
|
+
The id of the challenge this submission was made toward
|
|
95
|
+
status : bool
|
|
96
|
+
One of "passed", "failed", etc.
|
|
97
|
+
testCasesPassed: int
|
|
98
|
+
The number of test cases that this submission has passed
|
|
99
|
+
testCasesTotal: int
|
|
100
|
+
The number of test cases that this submission was tested against
|
|
101
|
+
(Note this may be smaller than the number of test cases provided by the
|
|
102
|
+
challenge, if the submission has compilation errors)
|
|
103
|
+
markedAt: datetime
|
|
104
|
+
A timestamp of when this submission was recieved and assessed
|
|
105
|
+
testcases: List[TestCase]
|
|
106
|
+
A list of details for each test case (Note this is only populated if you request
|
|
107
|
+
a specific submission from the api. It will be left unpopulated by any requests that
|
|
108
|
+
return multiple submissions)
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
id: int
|
|
113
|
+
userId: int
|
|
114
|
+
challengeId: int
|
|
115
|
+
workspaceId: str
|
|
116
|
+
status: str
|
|
117
|
+
testCasesPassed: int
|
|
118
|
+
testCasesTotal: int
|
|
119
|
+
markedAt: datetime.datetime
|
|
120
|
+
testcases: List[TestCase] = field(default_factory=list)
|
|
121
|
+
|
|
122
|
+
def from_json(j: dict):
|
|
123
|
+
submission = Submission(
|
|
124
|
+
j["id"],
|
|
125
|
+
j["user_id"],
|
|
126
|
+
j["challenge_id"],
|
|
127
|
+
j["workspace_id"],
|
|
128
|
+
j["status"],
|
|
129
|
+
j["testcase_pass_count"],
|
|
130
|
+
j["testcase_total_count"],
|
|
131
|
+
dateutil.parser.isoparse(j["marked_at"]),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
for tc in j.get("result", {}).get("testcases", []):
|
|
135
|
+
submission.testcases.append(TestCase.from_json(tc))
|
|
136
|
+
|
|
137
|
+
return submission
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class ChallengeResult:
|
|
142
|
+
"""
|
|
143
|
+
Records the results of a student for a challenge within an assignment
|
|
144
|
+
|
|
145
|
+
Attributes
|
|
146
|
+
----------
|
|
147
|
+
attempts : int
|
|
148
|
+
The number of attempts that the student submitted for this assignment.
|
|
149
|
+
completed : bool
|
|
150
|
+
True if the student successfully completed this challenge, otherwise false.
|
|
151
|
+
bestSubmissionByCalendarDaysLate : List[Submission]
|
|
152
|
+
The best submissions recieved by this student before and after the deadline.
|
|
153
|
+
The first element contains the best submission recieved before the deadline.
|
|
154
|
+
The second element contains the best submission recieved within one day after the deadline.
|
|
155
|
+
The third element contains the best submission recieved between one and two days after the deadline. etc.
|
|
156
|
+
scoresByCalendarDaysLate : List[int]
|
|
157
|
+
The best scores recieved by this student before and after the deadline.
|
|
158
|
+
The first element contains the best score recieved before the deadline.
|
|
159
|
+
The second element contains the best score recieved within one day after the deadline.
|
|
160
|
+
The third element contains the best score recieved between one and two days after the deadline. etc.
|
|
161
|
+
firstAttempt: datetime
|
|
162
|
+
A timestamp of this students first submission towards the assignment.
|
|
163
|
+
lastAttempt: datetime
|
|
164
|
+
A timestamp of this student's last submission towards the assignment.
|
|
165
|
+
|
|
166
|
+
Methods
|
|
167
|
+
----------
|
|
168
|
+
add (submission:Submission, daysLate: int)
|
|
169
|
+
Updates this result based on a single submission
|
|
170
|
+
finalize (assignment:Assignment, crawler: EdCrawler)
|
|
171
|
+
To be called after all submissions have been added.
|
|
172
|
+
"""
|
|
173
|
+
maxDaysLate: int = 7
|
|
174
|
+
attempts: int = 0
|
|
175
|
+
completed: bool = False
|
|
176
|
+
bestSubmissionByCalendarDaysLate: List[Submission] = None
|
|
177
|
+
scoresByCalendarDaysLate: List[int] = None
|
|
178
|
+
firstAttempt: datetime.datetime = None
|
|
179
|
+
lastAttempt: datetime.datetime = None
|
|
180
|
+
|
|
181
|
+
def add(self, submission: Submission, daysLate: int):
|
|
182
|
+
|
|
183
|
+
if self.bestSubmissionByCalendarDaysLate is None:
|
|
184
|
+
self.bestSubmissionByCalendarDaysLate = [None] * (self.maxDaysLate + 1)
|
|
185
|
+
self.scoresByCalendarDaysLate = [0] * (self.maxDaysLate + 1)
|
|
186
|
+
|
|
187
|
+
self.attempts += 1
|
|
188
|
+
if (
|
|
189
|
+
submission.status == "passed"
|
|
190
|
+
and submission.testCasesPassed == submission.testCasesTotal
|
|
191
|
+
):
|
|
192
|
+
self.completed = True
|
|
193
|
+
|
|
194
|
+
if self.firstAttempt is None or self.firstAttempt > submission.markedAt:
|
|
195
|
+
self.firstAttempt = submission.markedAt
|
|
196
|
+
if self.lastAttempt is None or self.lastAttempt < submission.markedAt:
|
|
197
|
+
self.lastAttempt = submission.markedAt
|
|
198
|
+
|
|
199
|
+
if daysLate <= self.maxDaysLate:
|
|
200
|
+
if (
|
|
201
|
+
self.bestSubmissionByCalendarDaysLate[daysLate] is None
|
|
202
|
+
or self.bestSubmissionByCalendarDaysLate[daysLate].testCasesPassed
|
|
203
|
+
< submission.testCasesPassed
|
|
204
|
+
):
|
|
205
|
+
self.bestSubmissionByCalendarDaysLate[daysLate] = submission
|
|
206
|
+
|
|
207
|
+
def finalize(self, marker: ChallengeMarker, crawler):
|
|
208
|
+
for daysLate in range(0, self.maxDaysLate+1):
|
|
209
|
+
submission = self.bestSubmissionByCalendarDaysLate[daysLate]
|
|
210
|
+
if submission is None:
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
if marker.markType is MarkType.BY_CHALLENGE:
|
|
214
|
+
if (
|
|
215
|
+
submission.status == "passed"
|
|
216
|
+
and submission.testCasesPassed == submission.testCasesTotal
|
|
217
|
+
):
|
|
218
|
+
self.scoresByCalendarDaysLate[daysLate] = marker.pointsPerChallenge
|
|
219
|
+
elif marker.markType is MarkType.BY_TESTCASE:
|
|
220
|
+
self.scoresByCalendarDaysLate[daysLate] += submission.testCasesPassed
|
|
221
|
+
elif marker.markType is MarkType.BY_TESTCASE_SCORE:
|
|
222
|
+
# we have to fetch full submission details
|
|
223
|
+
s = crawler.get_submission(submission.id)
|
|
224
|
+
for test in s.testcases:
|
|
225
|
+
# print(test)
|
|
226
|
+
if test.passed:
|
|
227
|
+
self.scoresByCalendarDaysLate[daysLate] = (
|
|
228
|
+
self.scoresByCalendarDaysLate[daysLate] + test.score
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
raise Exception(f"Invalid marker type {marker.markType}")
|
|
232
|
+
|
|
233
|
+
# make sure scores can only go up
|
|
234
|
+
for daysLate in range(1, self.maxDaysLate+1):
|
|
235
|
+
if (
|
|
236
|
+
self.scoresByCalendarDaysLate[daysLate]
|
|
237
|
+
< self.scoresByCalendarDaysLate[daysLate - 1]
|
|
238
|
+
):
|
|
239
|
+
self.scoresByCalendarDaysLate[daysLate] = self.scoresByCalendarDaysLate[
|
|
240
|
+
daysLate - 1
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
def __get_best_day(self, daysLate: int):
|
|
244
|
+
|
|
245
|
+
bestScore = 0
|
|
246
|
+
bestDay = 0
|
|
247
|
+
|
|
248
|
+
for day in range(0, daysLate + 1):
|
|
249
|
+
if self.scoresByCalendarDaysLate[day] > bestScore:
|
|
250
|
+
bestScore = self.scoresByCalendarDaysLate[day]
|
|
251
|
+
bestDay = day
|
|
252
|
+
|
|
253
|
+
return bestDay
|
|
254
|
+
|
|
255
|
+
def print_summary(self, challenge: Challenge, daysLate: int):
|
|
256
|
+
|
|
257
|
+
bestDay = self.__get_best_day(daysLate)
|
|
258
|
+
bestScore = self.scoresByCalendarDaysLate[bestDay]
|
|
259
|
+
|
|
260
|
+
print(
|
|
261
|
+
f" {bestScore} points for {challenge.name} from the following submissions:"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
for day in range(0, len(self.scoresByCalendarDaysLate)):
|
|
265
|
+
score = self.scoresByCalendarDaysLate[day]
|
|
266
|
+
submission = self.bestSubmissionByCalendarDaysLate[day]
|
|
267
|
+
if submission is None:
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
submittedAt = submission.markedAt.strftime("%m/%d/%Y, %H:%M:%S")
|
|
271
|
+
|
|
272
|
+
msg = f" {score} points from {submission.testCasesPassed}/{submission.testCasesTotal} test cases @ {day} days overdue ({submittedAt})"
|
|
273
|
+
|
|
274
|
+
if day < bestDay:
|
|
275
|
+
msg = f"{msg} - superseded by later submission"
|
|
276
|
+
elif day > bestDay:
|
|
277
|
+
msg = f"{msg} - ignored for not overcoming late penalty"
|
|
278
|
+
|
|
279
|
+
print(msg)
|