gradescopeapi 1.3.1__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.
- gradescopeapi/__init__.py +1 -0
- gradescopeapi/_config/__init__.py +0 -0
- gradescopeapi/_config/config.py +65 -0
- gradescopeapi/api/__init__.py +0 -0
- gradescopeapi/api/api.py +373 -0
- gradescopeapi/api/constants.py +5 -0
- gradescopeapi/classes/__init__.py +0 -0
- gradescopeapi/classes/_helpers/_assignment_helpers.py +178 -0
- gradescopeapi/classes/_helpers/_course_helpers.py +190 -0
- gradescopeapi/classes/_helpers/_login_helpers.py +56 -0
- gradescopeapi/classes/account.py +278 -0
- gradescopeapi/classes/assignments.py +86 -0
- gradescopeapi/classes/connection.py +28 -0
- gradescopeapi/classes/courses.py +11 -0
- gradescopeapi/classes/extensions.py +225 -0
- gradescopeapi/classes/member.py +15 -0
- gradescopeapi/classes/upload.py +79 -0
- gradescopeapi/py.typed +0 -0
- gradescopeapi-1.3.1.dist-info/METADATA +130 -0
- gradescopeapi-1.3.1.dist-info/RECORD +23 -0
- gradescopeapi-1.3.1.dist-info/WHEEL +4 -0
- gradescopeapi-1.3.1.dist-info/entry_points.txt +4 -0
- gradescopeapi-1.3.1.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# empty on purpose
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""config.py
|
|
2
|
+
Configuration file for FastAPI. Specifies the specific objects and data models used in our api
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UserSession(BaseModel):
|
|
12
|
+
user_email: str
|
|
13
|
+
session_token: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LoginRequestModel(BaseModel):
|
|
17
|
+
email: str
|
|
18
|
+
password: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CourseID(BaseModel):
|
|
22
|
+
course_id: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AssignmentID(BaseModel):
|
|
26
|
+
course_id: str
|
|
27
|
+
assignment_id: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StudentSubmission(BaseModel):
|
|
31
|
+
student_email: str
|
|
32
|
+
course_id: str
|
|
33
|
+
assignment_id: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ExtensionData(BaseModel):
|
|
37
|
+
course_id: str
|
|
38
|
+
assignment_id: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UpdateExtensionData(BaseModel):
|
|
42
|
+
course_id: str
|
|
43
|
+
assignment_id: str
|
|
44
|
+
user_id: str
|
|
45
|
+
release_date: datetime | None = None
|
|
46
|
+
due_date: datetime | None = None
|
|
47
|
+
late_due_date: datetime | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AssignmentDates(BaseModel):
|
|
51
|
+
course_id: str
|
|
52
|
+
assignment_id: str
|
|
53
|
+
release_date: datetime | None = None
|
|
54
|
+
due_date: datetime | None = None
|
|
55
|
+
late_due_date: datetime | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FileUploadModel(BaseModel, arbitrary_types_allowed=True):
|
|
59
|
+
file: io.TextIOWrapper
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AssignmentUpload(BaseModel):
|
|
63
|
+
course_id: str
|
|
64
|
+
assignment_id: str
|
|
65
|
+
leaderboard_name: str | None = None
|
|
File without changes
|
gradescopeapi/api/api.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends, FastAPI, HTTPException, status
|
|
4
|
+
|
|
5
|
+
from gradescopeapi._config.config import FileUploadModel, LoginRequestModel
|
|
6
|
+
from gradescopeapi.classes.account import Account
|
|
7
|
+
from gradescopeapi.classes.assignments import Assignment, update_assignment_date
|
|
8
|
+
from gradescopeapi.classes.connection import GSConnection
|
|
9
|
+
from gradescopeapi.classes.courses import Course
|
|
10
|
+
from gradescopeapi.classes.extensions import get_extensions, update_student_extension
|
|
11
|
+
from gradescopeapi.classes.member import Member
|
|
12
|
+
from gradescopeapi.classes.upload import upload_assignment
|
|
13
|
+
|
|
14
|
+
app = FastAPI()
|
|
15
|
+
|
|
16
|
+
# Create instance of GSConnection, to be used where needed
|
|
17
|
+
connection = GSConnection()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_gs_connection():
|
|
21
|
+
"""
|
|
22
|
+
Returns the GSConnection instance
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
connection (GSConnection): an instance of the GSConnection class,
|
|
26
|
+
containing the session object used to make HTTP requests,
|
|
27
|
+
a boolean defining True/False if the user is logged in, and
|
|
28
|
+
the user's Account object.
|
|
29
|
+
"""
|
|
30
|
+
return connection
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_gs_connection_session():
|
|
34
|
+
"""
|
|
35
|
+
Returns session of the the GSConnection instance
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
connection.session (GSConnection.session): an instance of the GSConnection class' session object used to make HTTP requests
|
|
39
|
+
"""
|
|
40
|
+
return connection.session
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_account():
|
|
44
|
+
"""
|
|
45
|
+
Returns the user's Account object
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Account (Account): an instance of the Account class, containing
|
|
49
|
+
methods for interacting with the user's courses and assignments.
|
|
50
|
+
"""
|
|
51
|
+
return Account(session=get_gs_connection_session)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Create instance of GSConnection, to be used where needed
|
|
55
|
+
connection = GSConnection()
|
|
56
|
+
|
|
57
|
+
account = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.get("/")
|
|
61
|
+
def root():
|
|
62
|
+
return {"message": "Hello World"}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.post("/login", name="login")
|
|
66
|
+
def login(
|
|
67
|
+
login_data: LoginRequestModel,
|
|
68
|
+
gs_connection: GSConnection = Depends(get_gs_connection),
|
|
69
|
+
):
|
|
70
|
+
"""Login to Gradescope, with correct credentials
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
username (str): email address of user attempting to log in
|
|
74
|
+
password (str): password of user attempting to log in
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
HTTPException: If the request to login fails, with a 404 Unauthorized Error status code and the error message "Account not found".
|
|
78
|
+
"""
|
|
79
|
+
user_email = login_data.email
|
|
80
|
+
password = login_data.password
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
connection.login(user_email, password)
|
|
84
|
+
global account
|
|
85
|
+
account = connection.account
|
|
86
|
+
return {"message": "Login successful", "status_code": status.HTTP_200_OK}
|
|
87
|
+
except ValueError as e:
|
|
88
|
+
raise HTTPException(status_code=404, detail=f"Account not found. Error {e}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.post("/courses", response_model=dict[str, dict[str, Course]])
|
|
92
|
+
def get_courses():
|
|
93
|
+
"""Get all courses for the user
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
account (Account): Account object containing the user's courses
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
dict: dictionary of dictionaries
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
HTTPException: If the request to get courses fails, with a 500 Internal Server Error status code and the error message.
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
course_list = account.get_courses()
|
|
106
|
+
return course_list
|
|
107
|
+
except RuntimeError as e:
|
|
108
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.post("/course_users", response_model=list[Member])
|
|
112
|
+
def get_course_users(course_id: str):
|
|
113
|
+
"""Get all users for a course. ONLY FOR INSTRUCTORS.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
course_id (str): The ID of the course.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
dict: dictionary of dictionaries
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
HTTPException: If the request to get courses fails, with a 500 Internal Server Error status code and the error message.
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
course_list = connection.account.get_course_users(course_id)
|
|
126
|
+
print(course_list)
|
|
127
|
+
return course_list
|
|
128
|
+
except RuntimeError as e:
|
|
129
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.post("/assignments", response_model=list[Assignment])
|
|
133
|
+
def get_assignments(course_id: str):
|
|
134
|
+
"""Get all assignments for a course. ONLY FOR INSTRUCTORS.
|
|
135
|
+
list: list of user emails
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
HTTPException: If the request to get course users fails, with a 500 Internal Server Error status code and the error message.
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
course_users = connection.account.get_assignments(course_id)
|
|
142
|
+
return course_users
|
|
143
|
+
except RuntimeError as e:
|
|
144
|
+
raise HTTPException(
|
|
145
|
+
status_code=500, detail=f"Failed to get course users. Error {e}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@app.post("/assignment_submissions", response_model=dict[str, list[str]])
|
|
150
|
+
def get_assignment_submissions(
|
|
151
|
+
course_id: str,
|
|
152
|
+
assignment_id: str,
|
|
153
|
+
):
|
|
154
|
+
"""Get all assignment submissions for an assignment. ONLY FOR INSTRUCTORS.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
course_id (str): The ID of the course.
|
|
158
|
+
assignment_id (str): The ID of the assignment.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
list: list of Assignment objects
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
HTTPException: If the request to get assignments fails, with a 500 Internal Server Error status code and the error message.
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
assignment_list = connection.account.get_assignment_submissions(
|
|
168
|
+
course_id=course_id, assignment_id=assignment_id
|
|
169
|
+
)
|
|
170
|
+
return assignment_list
|
|
171
|
+
except RuntimeError as e:
|
|
172
|
+
raise HTTPException(
|
|
173
|
+
status_code=500, detail=f"Failed to get assignments. Error: {e}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.post("/single_assignment_submission", response_model=list[str])
|
|
178
|
+
def get_student_assignment_submission(
|
|
179
|
+
student_email: str, course_id: str, assignment_id: str
|
|
180
|
+
):
|
|
181
|
+
"""Get a student's assignment submission. ONLY FOR INSTRUCTORS.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
student_email (str): The email address of the student.
|
|
185
|
+
course_id (str): The ID of the course.
|
|
186
|
+
assignment_id (str): The ID of the assignment.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
dict: dictionary containing a list of student emails and their corresponding submission IDs
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
HTTPException: If the request to get assignment submissions fails, with a 500 Internal Server Error status code and the error message.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
assignment_submissions = connection.account.get_assignment_submission(
|
|
196
|
+
student_email=student_email,
|
|
197
|
+
course_id=course_id,
|
|
198
|
+
assignment_id=assignment_id,
|
|
199
|
+
)
|
|
200
|
+
return assignment_submissions
|
|
201
|
+
except RuntimeError as e:
|
|
202
|
+
raise HTTPException(
|
|
203
|
+
status_code=500, detail=f"Failed to get assignment submissions. Error: {e}"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@app.post("/assignments/update_dates")
|
|
208
|
+
def update_assignment_dates(
|
|
209
|
+
course_id: str,
|
|
210
|
+
assignment_id: str,
|
|
211
|
+
release_date: datetime,
|
|
212
|
+
due_date: datetime,
|
|
213
|
+
late_due_date: datetime,
|
|
214
|
+
):
|
|
215
|
+
"""
|
|
216
|
+
Update the release and due dates for an assignment. ONLY FOR INSTRUCTORS.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
course_id (str): The ID of the course.
|
|
220
|
+
assignment_id (str): The ID of the assignment.
|
|
221
|
+
release_date (datetime): The release date of the assignment.
|
|
222
|
+
due_date (datetime): The due date of the assignment.
|
|
223
|
+
late_due_date (datetime): The late due date of the assignment.
|
|
224
|
+
|
|
225
|
+
Notes:
|
|
226
|
+
The timezone for dates used in Gradescope is specific to an institution. For example, for NYU, the timezone is America/New_York.
|
|
227
|
+
For datetime objects passed to this function, the timezone should be set to the institution's timezone.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
dict: A dictionary with a "message" key indicating if the assignment dates were updated successfully.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
HTTPException: If the assignment dates update fails, with a 400 Bad Request status code and the error message "Failed to update assignment dates".
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
print(f"late due date {late_due_date}")
|
|
237
|
+
success = update_assignment_date(
|
|
238
|
+
session=connection.session,
|
|
239
|
+
course_id=course_id,
|
|
240
|
+
assignment_id=assignment_id,
|
|
241
|
+
release_date=release_date,
|
|
242
|
+
due_date=due_date,
|
|
243
|
+
late_due_date=late_due_date,
|
|
244
|
+
)
|
|
245
|
+
if success:
|
|
246
|
+
return {
|
|
247
|
+
"message": "Assignment dates updated successfully",
|
|
248
|
+
"status_code": status.HTTP_200_OK,
|
|
249
|
+
}
|
|
250
|
+
else:
|
|
251
|
+
raise HTTPException(
|
|
252
|
+
status_code=400, detail="Failed to update assignment dates"
|
|
253
|
+
)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@app.post("/assignments/extensions", response_model=dict)
|
|
259
|
+
def get_assignment_extensions(course_id: str, assignment_id: str):
|
|
260
|
+
"""
|
|
261
|
+
Get all extensions for an assignment.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
course_id (str): The ID of the course.
|
|
265
|
+
assignment_id (str): The ID of the assignment.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
dict: A dictionary containing the extensions, where the keys are user IDs and the values are Extension objects.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
HTTPException: If the request to get extensions fails, with a 500 Internal Server Error status code and the error message.
|
|
272
|
+
"""
|
|
273
|
+
try:
|
|
274
|
+
extensions = get_extensions(
|
|
275
|
+
session=connection.session,
|
|
276
|
+
course_id=course_id,
|
|
277
|
+
assignment_id=assignment_id,
|
|
278
|
+
)
|
|
279
|
+
return extensions
|
|
280
|
+
except RuntimeError as e:
|
|
281
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@app.post("/assignments/extensions/update")
|
|
285
|
+
def update_extension(
|
|
286
|
+
course_id: str,
|
|
287
|
+
assignment_id: str,
|
|
288
|
+
user_id: str,
|
|
289
|
+
release_date: datetime,
|
|
290
|
+
due_date: datetime,
|
|
291
|
+
late_due_date: datetime,
|
|
292
|
+
):
|
|
293
|
+
"""
|
|
294
|
+
Update the extension for a student on an assignment. ONLY FOR INSTRUCTORS.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
course_id (str): The ID of the course.
|
|
298
|
+
assignment_id (str): The ID of the assignment.
|
|
299
|
+
user_id (str): The ID of the student.
|
|
300
|
+
release_date (datetime): The release date of the extension.
|
|
301
|
+
due_date (datetime): The due date of the extension.
|
|
302
|
+
late_due_date (datetime): The late due date of the extension.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
dict: A dictionary with a "message" key indicating if the extension was updated successfully.
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
HTTPException: If the extension update fails, with a 400 Bad Request status code and the error message.
|
|
309
|
+
HTTPException: If a ValueError is raised (e.g., invalid date order), with a 400 Bad Request status code and the error message.
|
|
310
|
+
HTTPException: If any other exception occurs, with a 500 Internal Server Error status code and the error message.
|
|
311
|
+
"""
|
|
312
|
+
try:
|
|
313
|
+
success = update_student_extension(
|
|
314
|
+
session=connection.session,
|
|
315
|
+
course_id=course_id,
|
|
316
|
+
assignment_id=assignment_id,
|
|
317
|
+
user_id=user_id,
|
|
318
|
+
release_date=release_date,
|
|
319
|
+
due_date=due_date,
|
|
320
|
+
late_due_date=late_due_date,
|
|
321
|
+
)
|
|
322
|
+
if success:
|
|
323
|
+
return {
|
|
324
|
+
"message": "Extension updated successfully",
|
|
325
|
+
"status_code": status.HTTP_200_OK,
|
|
326
|
+
}
|
|
327
|
+
else:
|
|
328
|
+
raise HTTPException(status_code=400, detail="Failed to update extension")
|
|
329
|
+
except ValueError as e:
|
|
330
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
331
|
+
except Exception as e:
|
|
332
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@app.post("/assignments/upload")
|
|
336
|
+
def upload_assignment_files(
|
|
337
|
+
course_id: str, assignment_id: str, leaderboard_name: str, file: FileUploadModel
|
|
338
|
+
):
|
|
339
|
+
"""
|
|
340
|
+
Upload files for an assignment.
|
|
341
|
+
|
|
342
|
+
NOTE: This function within FastAPI is currently nonfunctional, as we did not
|
|
343
|
+
find the datatype for file, which would allow us to upload a file via
|
|
344
|
+
Postman. However, this functionality works correctly if a user
|
|
345
|
+
runs this as a Python package.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
course_id (str): The ID of the course on Gradescope.
|
|
349
|
+
assignment_id (str): The ID of the assignment on Gradescope.
|
|
350
|
+
leaderboard_name (str): The name of the leaderboard.
|
|
351
|
+
file (FileUploadModel): The file object to upload.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
dict: A dictionary containing the submission link for the uploaded files.
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
HTTPException: If the upload fails, with a 400 Bad Request status code and the error message "Upload unsuccessful".
|
|
358
|
+
HTTPException: If any other exception occurs, with a 500 Internal Server Error status code and the error message.
|
|
359
|
+
"""
|
|
360
|
+
try:
|
|
361
|
+
submission_link = upload_assignment(
|
|
362
|
+
session=connection.session,
|
|
363
|
+
course_id=course_id,
|
|
364
|
+
assignment_id=assignment_id,
|
|
365
|
+
files=file,
|
|
366
|
+
leaderboard_name=leaderboard_name,
|
|
367
|
+
)
|
|
368
|
+
if submission_link:
|
|
369
|
+
return {"submission_link": submission_link}
|
|
370
|
+
else:
|
|
371
|
+
raise HTTPException(status_code=400, detail="Upload unsuccessful")
|
|
372
|
+
except Exception as e:
|
|
373
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
File without changes
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import dateutil.parser
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from gradescopeapi.classes.assignments import Assignment
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_page_auth(session, endpoint):
|
|
10
|
+
"""
|
|
11
|
+
raises Exception if user not logged in or doesn't have appropriate authorities
|
|
12
|
+
Returns response if otherwise good
|
|
13
|
+
"""
|
|
14
|
+
submissions_resp = session.get(endpoint)
|
|
15
|
+
# check if page is valid, raise exception if not
|
|
16
|
+
if submissions_resp.status_code == requests.codes.unauthorized:
|
|
17
|
+
# check error type
|
|
18
|
+
# TODO: how should we handle errors so that our API can read them?
|
|
19
|
+
error_msg = [*json.loads(submissions_resp.text).values()][0]
|
|
20
|
+
if error_msg == "You are not authorized to access this page.":
|
|
21
|
+
raise Exception("You are not authorized to access this page.")
|
|
22
|
+
elif error_msg == "You must be logged in to access this page.":
|
|
23
|
+
raise Exception("You must be logged in to access this page.")
|
|
24
|
+
elif submissions_resp.status_code == requests.codes.not_found:
|
|
25
|
+
raise Exception("Page not Found")
|
|
26
|
+
elif submissions_resp.status_code == requests.codes.ok:
|
|
27
|
+
return submissions_resp
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_assignments_instructor_view(coursepage_soup):
|
|
31
|
+
assignments_list = []
|
|
32
|
+
element_with_props = coursepage_soup.find(
|
|
33
|
+
"div", {"data-react-class": "AssignmentsTable"}
|
|
34
|
+
)
|
|
35
|
+
if element_with_props:
|
|
36
|
+
# Extract the value of the data-react-props attribute
|
|
37
|
+
props_str = element_with_props["data-react-props"]
|
|
38
|
+
# Parse the JSON data
|
|
39
|
+
assignment_json = json.loads(props_str)
|
|
40
|
+
|
|
41
|
+
# Extract information for each assignment
|
|
42
|
+
for assignment in assignment_json["table_data"]:
|
|
43
|
+
assignment_obj = Assignment(
|
|
44
|
+
assignment_id=assignment["url"].split("/")[-1],
|
|
45
|
+
name=assignment["title"],
|
|
46
|
+
release_date=assignment["submission_window"]["release_date"],
|
|
47
|
+
due_date=assignment["submission_window"]["due_date"],
|
|
48
|
+
late_due_date=assignment["submission_window"].get("hard_due_date"),
|
|
49
|
+
submissions_status=None,
|
|
50
|
+
grade=None,
|
|
51
|
+
max_grade=str(float(assignment["total_points"])),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# convert to datetime objects
|
|
55
|
+
assignment_obj.release_date = (
|
|
56
|
+
dateutil.parser.parse(assignment_obj.release_date)
|
|
57
|
+
if assignment_obj.release_date
|
|
58
|
+
else assignment_obj.release_date
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
assignment_obj.due_date = (
|
|
62
|
+
dateutil.parser.parse(assignment_obj.due_date)
|
|
63
|
+
if assignment_obj.due_date
|
|
64
|
+
else assignment_obj.due_date
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assignment_obj.late_due_date = (
|
|
68
|
+
dateutil.parser.parse(assignment_obj.late_due_date)
|
|
69
|
+
if assignment_obj.late_due_date
|
|
70
|
+
else assignment_obj.late_due_date
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Add the assignment dictionary to the list
|
|
74
|
+
assignments_list.append(assignment_obj)
|
|
75
|
+
return assignments_list
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_assignments_student_view(coursepage_soup):
|
|
79
|
+
# parse into list of lists: Assignments[row_elements[]]
|
|
80
|
+
assignment_table = []
|
|
81
|
+
for assignment_row in coursepage_soup.findAll("tr", role="row")[
|
|
82
|
+
1:-1
|
|
83
|
+
]: # Skip header row and tail row (dropzonePreview--fileNameHeader)
|
|
84
|
+
row = []
|
|
85
|
+
for th in assignment_row.findAll("th"):
|
|
86
|
+
row.append(th)
|
|
87
|
+
for td in assignment_row.findAll("td"):
|
|
88
|
+
row.append(td)
|
|
89
|
+
assignment_table.append(row)
|
|
90
|
+
assignment_info_list = []
|
|
91
|
+
|
|
92
|
+
# Iterate over the list of Tag objects
|
|
93
|
+
for assignment in assignment_table:
|
|
94
|
+
# Extract assignment ID and name
|
|
95
|
+
name = assignment[0].text
|
|
96
|
+
# 3 cases: 1. submitted -> href element, 2. not submitted, submittable -> button element, 3. not submitted, cant submit -> only text
|
|
97
|
+
assignment_a_href = assignment[0].find("a", href=True)
|
|
98
|
+
assignment_button = assignment[0].find("button", class_="js-submitAssignment")
|
|
99
|
+
if assignment_a_href:
|
|
100
|
+
assignment_id = assignment_a_href["href"].split("/")[4]
|
|
101
|
+
elif assignment_button:
|
|
102
|
+
assignment_id = assignment_button["data-assignment-id"]
|
|
103
|
+
else:
|
|
104
|
+
assignment_id = None
|
|
105
|
+
|
|
106
|
+
# Extract submission status, grade, max_grade
|
|
107
|
+
try: # Points not guaranteed
|
|
108
|
+
points = assignment[1].text.split(" / ")
|
|
109
|
+
grade = float(points[0])
|
|
110
|
+
max_grade = float(points[1])
|
|
111
|
+
submission_status = "Submitted"
|
|
112
|
+
except (IndexError, ValueError):
|
|
113
|
+
grade = None
|
|
114
|
+
max_grade = None
|
|
115
|
+
submission_status = assignment[1].text
|
|
116
|
+
|
|
117
|
+
# Extract release date, due date, and late due date
|
|
118
|
+
release_date = due_date = late_due_date = None
|
|
119
|
+
try: # release date, due date, and late due date not guaranteed to be available
|
|
120
|
+
release_obj = assignment[2].find(class_="submissionTimeChart--releaseDate")
|
|
121
|
+
release_date = release_obj["datetime"] if release_obj else None
|
|
122
|
+
# both due data and late due date have the same class
|
|
123
|
+
due_dates_obj = assignment[2].find_all(
|
|
124
|
+
class_="submissionTimeChart--dueDate"
|
|
125
|
+
)
|
|
126
|
+
if due_dates_obj:
|
|
127
|
+
due_date = due_dates_obj[0]["datetime"] if due_dates_obj else None
|
|
128
|
+
if len(due_dates_obj) > 1:
|
|
129
|
+
late_due_date = (
|
|
130
|
+
due_dates_obj[1]["datetime"] if due_dates_obj else None
|
|
131
|
+
)
|
|
132
|
+
except IndexError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
# convert to datetime objects
|
|
136
|
+
release_date = (
|
|
137
|
+
dateutil.parser.parse(release_date) if release_date else release_date
|
|
138
|
+
)
|
|
139
|
+
due_date = dateutil.parser.parse(due_date) if due_date else due_date
|
|
140
|
+
late_due_date = (
|
|
141
|
+
dateutil.parser.parse(late_due_date) if late_due_date else late_due_date
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Store the extracted information in a dictionary
|
|
145
|
+
assignment_obj = Assignment(
|
|
146
|
+
assignment_id=assignment_id,
|
|
147
|
+
name=name,
|
|
148
|
+
release_date=release_date,
|
|
149
|
+
due_date=due_date,
|
|
150
|
+
late_due_date=late_due_date,
|
|
151
|
+
submissions_status=submission_status,
|
|
152
|
+
grade=grade,
|
|
153
|
+
max_grade=max_grade,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Append the dictionary to the list
|
|
157
|
+
assignment_info_list.append(assignment_obj)
|
|
158
|
+
|
|
159
|
+
return assignment_info_list
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_submission_files(session, course_id, assignment_id, submission_id):
|
|
163
|
+
ASSIGNMENT_ENDPOINT = (
|
|
164
|
+
f"https://www.gradescope.com/courses/{course_id}/assignments/{assignment_id}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
file_info_link = f"{ASSIGNMENT_ENDPOINT}/submissions/{submission_id}.json?content=react&only_keys[]=text_files&only_keys[]=file_comments"
|
|
168
|
+
file_info_resp = session.get(file_info_link)
|
|
169
|
+
if file_info_resp.status_code == requests.codes.ok:
|
|
170
|
+
file_info_json = json.loads(file_info_resp.text)
|
|
171
|
+
if file_info_json.get("text_files"):
|
|
172
|
+
aws_links = []
|
|
173
|
+
for file_data in file_info_json["text_files"]:
|
|
174
|
+
aws_links.append(file_data["file"]["url"])
|
|
175
|
+
else:
|
|
176
|
+
raise NotImplementedError("Image only submissions not yet supported")
|
|
177
|
+
# TODO add support for image questions
|
|
178
|
+
return aws_links
|