scratchattach 2.1.15b0__py3-none-any.whl → 3.0.0b0__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.
- scratchattach/__init__.py +14 -6
- scratchattach/__main__.py +93 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b0.dist-info}/METADATA +7 -11
- scratchattach-3.0.0b0.dist-info/RECORD +8 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b0.dist-info}/WHEEL +1 -1
- scratchattach-3.0.0b0.dist-info/entry_points.txt +2 -0
- scratchattach/cloud/__init__.py +0 -2
- scratchattach/cloud/_base.py +0 -458
- scratchattach/cloud/cloud.py +0 -183
- scratchattach/editor/__init__.py +0 -21
- scratchattach/editor/asset.py +0 -253
- scratchattach/editor/backpack_json.py +0 -117
- scratchattach/editor/base.py +0 -193
- scratchattach/editor/block.py +0 -579
- scratchattach/editor/blockshape.py +0 -357
- scratchattach/editor/build_defaulting.py +0 -51
- scratchattach/editor/code_translation/__init__.py +0 -0
- scratchattach/editor/code_translation/parse.py +0 -177
- scratchattach/editor/comment.py +0 -80
- scratchattach/editor/commons.py +0 -273
- scratchattach/editor/extension.py +0 -50
- scratchattach/editor/field.py +0 -99
- scratchattach/editor/inputs.py +0 -135
- scratchattach/editor/meta.py +0 -114
- scratchattach/editor/monitor.py +0 -183
- scratchattach/editor/mutation.py +0 -324
- scratchattach/editor/pallete.py +0 -90
- scratchattach/editor/prim.py +0 -170
- scratchattach/editor/project.py +0 -279
- scratchattach/editor/sprite.py +0 -599
- scratchattach/editor/twconfig.py +0 -114
- scratchattach/editor/vlb.py +0 -134
- scratchattach/eventhandlers/__init__.py +0 -0
- scratchattach/eventhandlers/_base.py +0 -100
- scratchattach/eventhandlers/cloud_events.py +0 -110
- scratchattach/eventhandlers/cloud_recorder.py +0 -26
- scratchattach/eventhandlers/cloud_requests.py +0 -459
- scratchattach/eventhandlers/cloud_server.py +0 -246
- scratchattach/eventhandlers/cloud_storage.py +0 -136
- scratchattach/eventhandlers/combine.py +0 -30
- scratchattach/eventhandlers/filterbot.py +0 -161
- scratchattach/eventhandlers/message_events.py +0 -42
- scratchattach/other/__init__.py +0 -0
- scratchattach/other/other_apis.py +0 -284
- scratchattach/other/project_json_capabilities.py +0 -475
- scratchattach/site/__init__.py +0 -0
- scratchattach/site/_base.py +0 -66
- scratchattach/site/activity.py +0 -382
- scratchattach/site/alert.py +0 -227
- scratchattach/site/backpack_asset.py +0 -118
- scratchattach/site/browser_cookie3_stub.py +0 -17
- scratchattach/site/browser_cookies.py +0 -61
- scratchattach/site/classroom.py +0 -447
- scratchattach/site/cloud_activity.py +0 -107
- scratchattach/site/comment.py +0 -242
- scratchattach/site/forum.py +0 -432
- scratchattach/site/project.py +0 -826
- scratchattach/site/session.py +0 -1238
- scratchattach/site/studio.py +0 -611
- scratchattach/site/user.py +0 -956
- scratchattach/utils/__init__.py +0 -0
- scratchattach/utils/commons.py +0 -255
- scratchattach/utils/encoder.py +0 -158
- scratchattach/utils/enums.py +0 -236
- scratchattach/utils/exceptions.py +0 -243
- scratchattach/utils/requests.py +0 -93
- scratchattach-2.1.15b0.dist-info/RECORD +0 -66
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b0.dist-info}/licenses/LICENSE +0 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b0.dist-info}/top_level.txt +0 -0
scratchattach/site/activity.py
DELETED
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
"""Activity and CloudActivity class"""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
from bs4 import PageElement
|
|
5
|
-
|
|
6
|
-
from . import user, project, studio
|
|
7
|
-
from ._base import BaseSiteComponent
|
|
8
|
-
from scratchattach.utils import exceptions
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Activity(BaseSiteComponent):
|
|
12
|
-
"""
|
|
13
|
-
Represents a Scratch activity (message or other user page activity)
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
def __repr__(self):
|
|
17
|
-
return f"Activity({repr(self.raw)})"
|
|
18
|
-
|
|
19
|
-
def __str__(self):
|
|
20
|
-
return str(self.raw)
|
|
21
|
-
|
|
22
|
-
def __init__(self, **entries):
|
|
23
|
-
# Set attributes every Activity object needs to have:
|
|
24
|
-
self._session = None
|
|
25
|
-
self.raw = None
|
|
26
|
-
|
|
27
|
-
# Possible attributes
|
|
28
|
-
self.project_id = None
|
|
29
|
-
self.gallery_id = None
|
|
30
|
-
|
|
31
|
-
self.username = None
|
|
32
|
-
self.followed_username = None
|
|
33
|
-
self.recipient_username = None
|
|
34
|
-
|
|
35
|
-
self.comment_type = None
|
|
36
|
-
self.comment_obj_id = None
|
|
37
|
-
self.comment_obj_title = None
|
|
38
|
-
self.comment_id = None
|
|
39
|
-
|
|
40
|
-
self.datetime_created = None
|
|
41
|
-
self.time = None
|
|
42
|
-
self.type = None
|
|
43
|
-
|
|
44
|
-
# Update attributes from entries dict:
|
|
45
|
-
self.__dict__.update(entries)
|
|
46
|
-
|
|
47
|
-
def update(self):
|
|
48
|
-
print("Warning: Activity objects can't be updated")
|
|
49
|
-
return False # Objects of this type cannot be updated
|
|
50
|
-
|
|
51
|
-
def _update_from_dict(self, data):
|
|
52
|
-
self.raw = data
|
|
53
|
-
self.__dict__.update(data)
|
|
54
|
-
return True
|
|
55
|
-
|
|
56
|
-
def _update_from_json(self, data: dict):
|
|
57
|
-
"""
|
|
58
|
-
Update using JSON, used in the classroom API.
|
|
59
|
-
"""
|
|
60
|
-
activity_type = data["type"]
|
|
61
|
-
|
|
62
|
-
_time = data["datetime_created"] if "datetime_created" in data else None
|
|
63
|
-
|
|
64
|
-
if "actor" in data:
|
|
65
|
-
username = data["actor"]["username"]
|
|
66
|
-
elif "actor_username" in data:
|
|
67
|
-
username = data["actor_username"]
|
|
68
|
-
else:
|
|
69
|
-
username = None
|
|
70
|
-
|
|
71
|
-
if data.get("recipient") is not None:
|
|
72
|
-
recipient_username = data["recipient"]["username"]
|
|
73
|
-
|
|
74
|
-
elif data.get("recipient_username") is not None:
|
|
75
|
-
recipient_username = data["recipient_username"]
|
|
76
|
-
|
|
77
|
-
elif data.get("project_creator") is not None:
|
|
78
|
-
recipient_username = data["project_creator"]["username"]
|
|
79
|
-
else:
|
|
80
|
-
recipient_username = None
|
|
81
|
-
|
|
82
|
-
default_case = False
|
|
83
|
-
# Even if `activity_type` is an invalid value; it will default to 'user performed an action'
|
|
84
|
-
|
|
85
|
-
if activity_type == 0:
|
|
86
|
-
# follow
|
|
87
|
-
followed_username = data["followed_username"]
|
|
88
|
-
|
|
89
|
-
self.raw = f"{username} followed user {followed_username}"
|
|
90
|
-
|
|
91
|
-
self.datetime_created = _time
|
|
92
|
-
self.type = "followuser"
|
|
93
|
-
self.username = username
|
|
94
|
-
self.followed_username = followed_username
|
|
95
|
-
|
|
96
|
-
elif activity_type == 1:
|
|
97
|
-
# follow studio
|
|
98
|
-
studio_id = data["gallery"]
|
|
99
|
-
|
|
100
|
-
raw = f"{username} followed studio https://scratch.mit.edu/studios/{studio_id}"
|
|
101
|
-
|
|
102
|
-
self.raw = raw
|
|
103
|
-
self.datetime_created = _time
|
|
104
|
-
self.type = "followstudio"
|
|
105
|
-
|
|
106
|
-
self.username = username
|
|
107
|
-
self.gallery_id = studio_id
|
|
108
|
-
|
|
109
|
-
elif activity_type == 2:
|
|
110
|
-
# love project
|
|
111
|
-
project_id = data["project"]
|
|
112
|
-
|
|
113
|
-
raw = f"{username} loved project https://scratch.mit.edu/projects/{project_id}"
|
|
114
|
-
|
|
115
|
-
self.raw = raw
|
|
116
|
-
self.datetime_created = _time,
|
|
117
|
-
self.type = "loveproject"
|
|
118
|
-
|
|
119
|
-
self.username = username
|
|
120
|
-
self.project_id = project_id
|
|
121
|
-
self.recipient_username = recipient_username
|
|
122
|
-
|
|
123
|
-
elif activity_type == 3:
|
|
124
|
-
# Favorite project
|
|
125
|
-
project_id = data["project"]
|
|
126
|
-
|
|
127
|
-
raw = f"{username} favorited project https://scratch.mit.edu/projects/{project_id}"
|
|
128
|
-
|
|
129
|
-
self.raw = raw
|
|
130
|
-
self.datetime_created = _time
|
|
131
|
-
self.type = "favoriteproject"
|
|
132
|
-
|
|
133
|
-
self.username = username
|
|
134
|
-
self.project_id = project_id
|
|
135
|
-
self.recipient_username = recipient_username
|
|
136
|
-
|
|
137
|
-
elif activity_type == 7:
|
|
138
|
-
# Add project to studio
|
|
139
|
-
|
|
140
|
-
project_id = data["project"]
|
|
141
|
-
studio_id = data["gallery"]
|
|
142
|
-
|
|
143
|
-
raw = f"{username} added the project https://scratch.mit.edu/projects/{project_id} to studio https://scratch.mit.edu/studios/{studio_id}"
|
|
144
|
-
|
|
145
|
-
self.raw = raw
|
|
146
|
-
self.datetime_created = _time
|
|
147
|
-
self.type = "addprojecttostudio"
|
|
148
|
-
|
|
149
|
-
self.username = username
|
|
150
|
-
self.project_id = project_id
|
|
151
|
-
self.recipient_username = recipient_username
|
|
152
|
-
|
|
153
|
-
elif activity_type in (8, 9, 10):
|
|
154
|
-
# Share/Reshare project
|
|
155
|
-
project_id = data["project"]
|
|
156
|
-
is_reshare = data["is_reshare"]
|
|
157
|
-
|
|
158
|
-
raw_reshare = "reshared" if is_reshare else "shared"
|
|
159
|
-
|
|
160
|
-
raw = f"{username} {raw_reshare} the project https://scratch.mit.edu/projects/{project_id}"
|
|
161
|
-
|
|
162
|
-
self.raw = raw
|
|
163
|
-
self.datetime_created = _time
|
|
164
|
-
self.type = "shareproject"
|
|
165
|
-
|
|
166
|
-
self.username = username
|
|
167
|
-
self.project_id = project_id
|
|
168
|
-
self.recipient_username = recipient_username
|
|
169
|
-
|
|
170
|
-
elif activity_type == 11:
|
|
171
|
-
# Remix
|
|
172
|
-
parent_id = data["parent"]
|
|
173
|
-
|
|
174
|
-
raw = f"{username} remixed the project https://scratch.mit.edu/projects/{parent_id}"
|
|
175
|
-
|
|
176
|
-
self.raw = raw
|
|
177
|
-
self.datetime_created = _time
|
|
178
|
-
self.type = "remixproject"
|
|
179
|
-
|
|
180
|
-
self.username = username
|
|
181
|
-
self.project_id = parent_id
|
|
182
|
-
self.recipient_username = recipient_username
|
|
183
|
-
|
|
184
|
-
# type 12 does not exist in the HTML. That's why it was removed, not merged with type 13.
|
|
185
|
-
|
|
186
|
-
elif activity_type == 13:
|
|
187
|
-
# Create ('add') studio
|
|
188
|
-
studio_id = data["gallery"]
|
|
189
|
-
|
|
190
|
-
raw = f"{username} created the studio https://scratch.mit.edu/studios/{studio_id}"
|
|
191
|
-
|
|
192
|
-
self.raw = raw
|
|
193
|
-
self.datetime_created = _time
|
|
194
|
-
self.type = "createstudio"
|
|
195
|
-
|
|
196
|
-
self.username = username
|
|
197
|
-
self.gallery_id = studio_id
|
|
198
|
-
|
|
199
|
-
elif activity_type == 15:
|
|
200
|
-
# Update studio
|
|
201
|
-
studio_id = data["gallery"]
|
|
202
|
-
|
|
203
|
-
raw = f"{username} updated the studio https://scratch.mit.edu/studios/{studio_id}"
|
|
204
|
-
|
|
205
|
-
self.raw = raw
|
|
206
|
-
self.datetime_created = _time
|
|
207
|
-
self.type = "updatestudio"
|
|
208
|
-
|
|
209
|
-
self.username = username
|
|
210
|
-
self.gallery_id = studio_id
|
|
211
|
-
|
|
212
|
-
elif activity_type in (16, 17, 18, 19):
|
|
213
|
-
# Remove project from studio
|
|
214
|
-
|
|
215
|
-
project_id = data["project"]
|
|
216
|
-
studio_id = data["gallery"]
|
|
217
|
-
|
|
218
|
-
raw = f"{username} removed the project https://scratch.mit.edu/projects/{project_id} from studio https://scratch.mit.edu/studios/{studio_id}"
|
|
219
|
-
|
|
220
|
-
self.raw = raw
|
|
221
|
-
self.datetime_created = _time
|
|
222
|
-
self.type = "removeprojectfromstudio"
|
|
223
|
-
|
|
224
|
-
self.username = username
|
|
225
|
-
self.project_id = project_id
|
|
226
|
-
|
|
227
|
-
elif activity_type in (20, 21, 22):
|
|
228
|
-
# Was promoted to manager for studio
|
|
229
|
-
studio_id = data["gallery"]
|
|
230
|
-
|
|
231
|
-
raw = f"{recipient_username} was promoted to manager by {username} for studio https://scratch.mit.edu/studios/{studio_id}"
|
|
232
|
-
|
|
233
|
-
self.raw = raw
|
|
234
|
-
self.datetime_created = _time
|
|
235
|
-
self.type = "promotetomanager"
|
|
236
|
-
|
|
237
|
-
self.username = username
|
|
238
|
-
self.recipient_username = recipient_username
|
|
239
|
-
self.gallery_id = studio_id
|
|
240
|
-
|
|
241
|
-
elif activity_type in (23, 24, 25):
|
|
242
|
-
# Update profile
|
|
243
|
-
raw = f"{username} made a profile update"
|
|
244
|
-
|
|
245
|
-
self.raw = raw
|
|
246
|
-
self.datetime_created = _time
|
|
247
|
-
self.type = "updateprofile"
|
|
248
|
-
|
|
249
|
-
self.username = username
|
|
250
|
-
|
|
251
|
-
elif activity_type in (26, 27):
|
|
252
|
-
# Comment (quite complicated)
|
|
253
|
-
comment_type: int = data["comment_type"]
|
|
254
|
-
fragment = data["comment_fragment"]
|
|
255
|
-
comment_id = data["comment_id"]
|
|
256
|
-
comment_obj_id = data["comment_obj_id"]
|
|
257
|
-
comment_obj_title = data["comment_obj_title"]
|
|
258
|
-
|
|
259
|
-
if comment_type == 0:
|
|
260
|
-
# Project comment
|
|
261
|
-
raw = f"{username} commented on project https://scratch.mit.edu/projects/{comment_obj_id}/#comments-{comment_id} {fragment!r}"
|
|
262
|
-
|
|
263
|
-
elif comment_type == 1:
|
|
264
|
-
# Profile comment
|
|
265
|
-
raw = f"{username} commented on user https://scratch.mit.edu/users/{comment_obj_title}/#comments-{comment_id} {fragment!r}"
|
|
266
|
-
|
|
267
|
-
elif comment_type == 2:
|
|
268
|
-
# Studio comment
|
|
269
|
-
# Scratch actually provides an incorrect link, but it is fixed here:
|
|
270
|
-
raw = f"{username} commented on studio https://scratch.mit.edu/studios/{comment_obj_id}/comments/#comments-{comment_id} {fragment!r}"
|
|
271
|
-
|
|
272
|
-
else:
|
|
273
|
-
raw = f"{username} commented {fragment!r}" # This should never happen
|
|
274
|
-
|
|
275
|
-
self.raw = raw
|
|
276
|
-
self.datetime_created = _time
|
|
277
|
-
self.type = "addcomment"
|
|
278
|
-
|
|
279
|
-
self.username = username
|
|
280
|
-
|
|
281
|
-
self.comment_type = comment_type
|
|
282
|
-
self.comment_obj_id = comment_obj_id
|
|
283
|
-
self.comment_obj_title = comment_obj_title
|
|
284
|
-
self.comment_id = comment_id
|
|
285
|
-
else:
|
|
286
|
-
default_case = True
|
|
287
|
-
|
|
288
|
-
if default_case:
|
|
289
|
-
# This is coded in the scratch HTML, haven't found an example of it though
|
|
290
|
-
raw = f"{username} performed an action."
|
|
291
|
-
|
|
292
|
-
self.raw = raw
|
|
293
|
-
self.datetime_created = _time
|
|
294
|
-
self.type = "performaction"
|
|
295
|
-
|
|
296
|
-
self.username = username
|
|
297
|
-
|
|
298
|
-
def _update_from_html(self, data: PageElement):
|
|
299
|
-
|
|
300
|
-
self.raw = data
|
|
301
|
-
|
|
302
|
-
_time = data.find('div').find('span').findNext().findNext().text.strip()
|
|
303
|
-
|
|
304
|
-
if '\xa0' in _time:
|
|
305
|
-
while '\xa0' in _time:
|
|
306
|
-
_time = _time.replace('\xa0', ' ')
|
|
307
|
-
|
|
308
|
-
self.datetime_created = _time
|
|
309
|
-
self.actor_username = data.find('div').find('span').text
|
|
310
|
-
|
|
311
|
-
self.target_name = data.find('div').find('span').findNext().text
|
|
312
|
-
self.target_link = data.find('div').find('span').findNext()["href"]
|
|
313
|
-
self.target_id = data.find('div').find('span').findNext()["href"].split("/")[-2]
|
|
314
|
-
|
|
315
|
-
self.type = data.find('div').find_all('span')[0].next_sibling.strip()
|
|
316
|
-
if self.type == "loved":
|
|
317
|
-
self.type = "loveproject"
|
|
318
|
-
|
|
319
|
-
elif self.type == "favorited":
|
|
320
|
-
self.type = "favoriteproject"
|
|
321
|
-
|
|
322
|
-
elif "curator" in self.type:
|
|
323
|
-
self.type = "becomecurator"
|
|
324
|
-
|
|
325
|
-
elif "shared" in self.type:
|
|
326
|
-
self.type = "shareproject"
|
|
327
|
-
|
|
328
|
-
elif "is now following" in self.type:
|
|
329
|
-
if "users" in self.target_link:
|
|
330
|
-
self.type = "followuser"
|
|
331
|
-
else:
|
|
332
|
-
self.type = "followstudio"
|
|
333
|
-
|
|
334
|
-
return True
|
|
335
|
-
|
|
336
|
-
def actor(self):
|
|
337
|
-
"""
|
|
338
|
-
Returns the user that performed the activity as User object
|
|
339
|
-
"""
|
|
340
|
-
return self._make_linked_object("username", self.actor_username, user.User, exceptions.UserNotFound)
|
|
341
|
-
|
|
342
|
-
def target(self):
|
|
343
|
-
"""
|
|
344
|
-
Returns the activity's target (depending on the activity, this is either a User, Project, Studio or Comment object).
|
|
345
|
-
May also return None if the activity type is unknown.
|
|
346
|
-
"""
|
|
347
|
-
|
|
348
|
-
if "project" in self.type: # target is a project
|
|
349
|
-
if "target_id" in self.__dict__:
|
|
350
|
-
return self._make_linked_object("id", self.target_id, project.Project, exceptions.ProjectNotFound)
|
|
351
|
-
if "project_id" in self.__dict__:
|
|
352
|
-
return self._make_linked_object("id", self.project_id, project.Project, exceptions.ProjectNotFound)
|
|
353
|
-
|
|
354
|
-
if self.type == "becomecurator" or self.type == "followstudio": # target is a studio
|
|
355
|
-
if "target_id" in self.__dict__:
|
|
356
|
-
return self._make_linked_object("id", self.target_id, studio.Studio, exceptions.StudioNotFound)
|
|
357
|
-
if "gallery_id" in self.__dict__:
|
|
358
|
-
return self._make_linked_object("id", self.gallery_id, studio.Studio, exceptions.StudioNotFound)
|
|
359
|
-
# NOTE: the "becomecurator" type is ambigous - if it is inside the studio activity tab, the target is the user who joined
|
|
360
|
-
if "username" in self.__dict__:
|
|
361
|
-
return self._make_linked_object("username", self.username, user.User, exceptions.UserNotFound)
|
|
362
|
-
|
|
363
|
-
if self.type == "followuser" or "curator" in self.type: # target is a user
|
|
364
|
-
if "target_name" in self.__dict__:
|
|
365
|
-
return self._make_linked_object("username", self.target_name, user.User, exceptions.UserNotFound)
|
|
366
|
-
if "followed_username" in self.__dict__:
|
|
367
|
-
return self._make_linked_object("username", self.followed_username, user.User, exceptions.UserNotFound)
|
|
368
|
-
if "recipient_username" in self.__dict__: # the recipient_username field always indicates the target is a user
|
|
369
|
-
return self._make_linked_object("username", self.recipient_username, user.User, exceptions.UserNotFound)
|
|
370
|
-
|
|
371
|
-
if self.type == "addcomment": # target is a comment
|
|
372
|
-
if self.comment_type == 0:
|
|
373
|
-
_c = project.Project(id=self.comment_obj_id, author_name=self._session.username,
|
|
374
|
-
_session=self._session).comment_by_id(self.comment_id)
|
|
375
|
-
if self.comment_type == 1:
|
|
376
|
-
_c = user.User(username=self.comment_obj_title, _session=self._session).comment_by_id(self.comment_id)
|
|
377
|
-
if self.comment_type == 2:
|
|
378
|
-
_c = user.User(id=self.comment_obj_id, _session=self._session).comment_by_id(self.comment_id)
|
|
379
|
-
else:
|
|
380
|
-
raise ValueError(f"{self.comment_type} is an invalid comment type")
|
|
381
|
-
|
|
382
|
-
return _c
|
scratchattach/site/alert.py
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
# classroom alerts (& normal alerts in the future)
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import pprint
|
|
7
|
-
import warnings
|
|
8
|
-
from dataclasses import dataclass, field, KW_ONLY
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
11
|
-
from typing_extensions import Self
|
|
12
|
-
|
|
13
|
-
from . import user, project, studio, comment, session
|
|
14
|
-
from scratchattach.utils import enums
|
|
15
|
-
|
|
16
|
-
if TYPE_CHECKING:
|
|
17
|
-
...
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# todo: implement regular alerts
|
|
21
|
-
# If you implement regular alerts, it may be applicable to make EducatorAlert a subclass.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass
|
|
25
|
-
class EducatorAlert:
|
|
26
|
-
"""
|
|
27
|
-
Represents an alert for student activity, viewable at https://scratch.mit.edu/site-api/classrooms/alerts/
|
|
28
|
-
|
|
29
|
-
Attributes:
|
|
30
|
-
model: The type of alert (presumably); should always equal "educators.educatoralert" in this class
|
|
31
|
-
type: An integer that identifies the type of alert, differentiating e.g. against bans or autoban or censored comments etc
|
|
32
|
-
raw: The raw JSON data from the API
|
|
33
|
-
id: The ID of the alert (internally called 'pk' by scratch, not sure what this is for)
|
|
34
|
-
time_read: The time the alert was read
|
|
35
|
-
time_created: The time the alert was created
|
|
36
|
-
target: The user that the alert is about (the student)
|
|
37
|
-
actor: The user that created the alert (the admin)
|
|
38
|
-
target_object: The object that the alert is about (e.g. a project, studio, or comment)
|
|
39
|
-
notification_type: not sure what this is for, but inferred from the scratch HTML reference
|
|
40
|
-
"""
|
|
41
|
-
_: KW_ONLY
|
|
42
|
-
# required attrs
|
|
43
|
-
target: user.User
|
|
44
|
-
actor: user.User
|
|
45
|
-
target_object: Optional[Union[project.Project, studio.Studio, comment.Comment, studio.Studio]]
|
|
46
|
-
notification_type: str
|
|
47
|
-
_session: Optional[session.Session]
|
|
48
|
-
|
|
49
|
-
# defaulted attrs
|
|
50
|
-
model: str = "educators.educatoralert"
|
|
51
|
-
type: int = -1
|
|
52
|
-
raw: dict = field(repr=False, default_factory=dict)
|
|
53
|
-
id: int = -1
|
|
54
|
-
time_read: datetime = datetime.fromtimestamp(0.0)
|
|
55
|
-
time_created: datetime = datetime.fromtimestamp(0.0)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@classmethod
|
|
59
|
-
def from_json(cls, data: dict[str, Any], _session: Optional[session.Session] = None) -> Self:
|
|
60
|
-
"""
|
|
61
|
-
Load an EducatorAlert from a JSON object.
|
|
62
|
-
|
|
63
|
-
Arguments:
|
|
64
|
-
data (dict): The JSON object
|
|
65
|
-
_session (session.Session): The session object used to load this data, to 'connect' to the alerts rather than just 'get' them
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
EducatorAlert: The loaded EducatorAlert object
|
|
69
|
-
"""
|
|
70
|
-
model = data.get("model") # With this class, should be equal to educators.educatoralert
|
|
71
|
-
assert isinstance(model, str)
|
|
72
|
-
alert_id = data.get("pk") # not sure what kind of pk/id this is. Doesn't seem to be a user or class id.
|
|
73
|
-
assert isinstance(alert_id, int)
|
|
74
|
-
|
|
75
|
-
fields = data.get("fields")
|
|
76
|
-
assert isinstance(fields, dict)
|
|
77
|
-
|
|
78
|
-
time_read_raw = fields.get("educator_datetime_read")
|
|
79
|
-
assert isinstance(time_read_raw, str)
|
|
80
|
-
time_read: datetime = datetime.fromisoformat(time_read_raw)
|
|
81
|
-
|
|
82
|
-
admin_action = fields.get("admin_action")
|
|
83
|
-
assert isinstance(admin_action, dict)
|
|
84
|
-
|
|
85
|
-
time_created_raw = admin_action.get("datetime_created")
|
|
86
|
-
assert isinstance(time_created_raw, str)
|
|
87
|
-
time_created: datetime = datetime.fromisoformat(time_created_raw)
|
|
88
|
-
|
|
89
|
-
alert_type = admin_action.get("type")
|
|
90
|
-
assert isinstance(alert_type, int)
|
|
91
|
-
|
|
92
|
-
target_data = admin_action.get("target_user")
|
|
93
|
-
assert isinstance(target_data, dict)
|
|
94
|
-
target = user.User(username=target_data.get("username"),
|
|
95
|
-
id=target_data.get("pk"),
|
|
96
|
-
icon_url=target_data.get("thumbnail_url"),
|
|
97
|
-
admin=target_data.get("admin", False),
|
|
98
|
-
_session=_session)
|
|
99
|
-
|
|
100
|
-
actor_data = admin_action.get("actor")
|
|
101
|
-
assert isinstance(actor_data, dict)
|
|
102
|
-
actor = user.User(username=actor_data.get("username"),
|
|
103
|
-
id=actor_data.get("pk"),
|
|
104
|
-
icon_url=actor_data.get("thumbnail_url"),
|
|
105
|
-
admin=actor_data.get("admin", False),
|
|
106
|
-
_session=_session)
|
|
107
|
-
|
|
108
|
-
object_id = admin_action.get("object_id") # this could be a comment id, a project id, etc.
|
|
109
|
-
assert isinstance(object_id, int)
|
|
110
|
-
target_object: project.Project | studio.Studio | comment.Comment | None = None
|
|
111
|
-
|
|
112
|
-
extra_data: dict[str, Any] = json.loads(admin_action.get("extra_data", "{}"))
|
|
113
|
-
# todo: if possible, properly implement the incomplete parts of this parser (look for warning.warn())
|
|
114
|
-
notification_type: str = ""
|
|
115
|
-
|
|
116
|
-
if "project_title" in extra_data:
|
|
117
|
-
# project
|
|
118
|
-
target_object = project.Project(id=object_id,
|
|
119
|
-
title=extra_data["project_title"],
|
|
120
|
-
_session=_session)
|
|
121
|
-
elif "comment_content" in extra_data:
|
|
122
|
-
# comment
|
|
123
|
-
comment_data: dict[str, Any] = extra_data["comment_content"]
|
|
124
|
-
content: str | None = comment_data.get("content")
|
|
125
|
-
|
|
126
|
-
comment_obj_id: int | None = comment_data.get("comment_obj_id")
|
|
127
|
-
|
|
128
|
-
comment_type: int | None = comment_data.get("comment_type")
|
|
129
|
-
|
|
130
|
-
if comment_type == 0:
|
|
131
|
-
# project
|
|
132
|
-
comment_source_type = "project"
|
|
133
|
-
elif comment_type == 1:
|
|
134
|
-
# profile
|
|
135
|
-
comment_source_type = "profile"
|
|
136
|
-
else:
|
|
137
|
-
# probably a studio
|
|
138
|
-
comment_source_type = "studio"
|
|
139
|
-
warnings.warn(
|
|
140
|
-
f"The parser was not able to recognise the \"comment_type\" of {comment_type} in the alert JSON response.\n"
|
|
141
|
-
f"Full response: \n{pprint.pformat(data)}.\n\n"
|
|
142
|
-
f"Please draft an issue on github: https://github.com/TimMcCool/scratchattach/issues, providing this "
|
|
143
|
-
f"whole error message. This will allow us to implement an incomplete part of this parser")
|
|
144
|
-
|
|
145
|
-
# the comment_obj's corresponding attribute of comment.Comment is the place() method. As it has no cache, the title data is wasted.
|
|
146
|
-
# if the comment_obj is deleted, this is still a valid way of working out the title/username
|
|
147
|
-
|
|
148
|
-
target_object = comment.Comment(
|
|
149
|
-
id=object_id,
|
|
150
|
-
content=content,
|
|
151
|
-
source=comment_source_type,
|
|
152
|
-
source_id=comment_obj_id,
|
|
153
|
-
_session=_session
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
elif "gallery_title" in extra_data:
|
|
157
|
-
# studio
|
|
158
|
-
# possible implemented incorrectly
|
|
159
|
-
target_object = studio.Studio(
|
|
160
|
-
id=object_id,
|
|
161
|
-
title=extra_data["gallery_title"],
|
|
162
|
-
_session=_session
|
|
163
|
-
)
|
|
164
|
-
elif "notification_type" in extra_data:
|
|
165
|
-
# possible implemented incorrectly
|
|
166
|
-
notification_type = extra_data["notification_type"]
|
|
167
|
-
else:
|
|
168
|
-
warnings.warn(
|
|
169
|
-
f"The parser was not able to recognise the \"extra_data\" in the alert JSON response.\n"
|
|
170
|
-
f"Full response: \n{pprint.pformat(data)}.\n\n"
|
|
171
|
-
f"Please draft an issue on github: https://github.com/TimMcCool/scratchattach/issues, providing this "
|
|
172
|
-
f"whole error message. This will allow us to implement an incomplete part of this parser")
|
|
173
|
-
|
|
174
|
-
return cls(
|
|
175
|
-
id=alert_id,
|
|
176
|
-
model=model,
|
|
177
|
-
type=alert_type,
|
|
178
|
-
raw=data,
|
|
179
|
-
time_read=time_read,
|
|
180
|
-
time_created=time_created,
|
|
181
|
-
target=target,
|
|
182
|
-
actor=actor,
|
|
183
|
-
target_object=target_object,
|
|
184
|
-
notification_type=notification_type,
|
|
185
|
-
_session=_session
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
def __str__(self):
|
|
189
|
-
return f"EducatorAlert: {self.message}"
|
|
190
|
-
|
|
191
|
-
@property
|
|
192
|
-
def alert_type(self) -> enums.AlertType:
|
|
193
|
-
"""
|
|
194
|
-
Get an associated AlertType object for this alert (based on the type index)
|
|
195
|
-
"""
|
|
196
|
-
alert_type = enums.AlertTypes.find(self.type)
|
|
197
|
-
if not alert_type:
|
|
198
|
-
alert_type = enums.AlertTypes.default.value
|
|
199
|
-
|
|
200
|
-
return alert_type
|
|
201
|
-
|
|
202
|
-
@property
|
|
203
|
-
def message(self):
|
|
204
|
-
"""
|
|
205
|
-
Format the alert message using the alert type's message template, as it would be on the website.
|
|
206
|
-
"""
|
|
207
|
-
raw_message = self.alert_type.message
|
|
208
|
-
comment_content = ""
|
|
209
|
-
if isinstance(self.target_object, comment.Comment):
|
|
210
|
-
comment_content = self.target_object.content
|
|
211
|
-
|
|
212
|
-
return raw_message.format(username=self.target.username,
|
|
213
|
-
project=self.target_object_title,
|
|
214
|
-
studio=self.target_object_title,
|
|
215
|
-
notification_type=self.notification_type,
|
|
216
|
-
comment=comment_content)
|
|
217
|
-
|
|
218
|
-
@property
|
|
219
|
-
def target_object_title(self):
|
|
220
|
-
"""
|
|
221
|
-
Get the title of the target object (if applicable)
|
|
222
|
-
"""
|
|
223
|
-
if isinstance(self.target_object, project.Project):
|
|
224
|
-
return self.target_object.title
|
|
225
|
-
if isinstance(self.target_object, studio.Studio):
|
|
226
|
-
return self.target_object.title
|
|
227
|
-
return None # explicit
|