From aead4359e7ecad5741c605a7a13c408c33b0754e Mon Sep 17 00:00:00 2001 From: r Date: Sat, 7 Jun 2025 16:56:58 +0100 Subject: [PATCH 1/3] edit: classroom dataclass --- scratchattach/site/_base.py | 7 ++-- scratchattach/site/classroom.py | 60 ++++++++++++++++++--------------- scratchattach/site/session.py | 4 +++ 3 files changed, 40 insertions(+), 31 deletions(-) diff --git a/scratchattach/site/_base.py b/scratchattach/site/_base.py index 30df01b7..41214cb5 100644 --- a/scratchattach/site/_base.py +++ b/scratchattach/site/_base.py @@ -13,9 +13,10 @@ class BaseSiteComponent(ABC): update_api: str _headers: dict[str, str] _cookies: dict[str, str] - @abstractmethod - def __init__(self): - pass + + # @abstractmethod + # def __init__(self): # dataclasses do not implement __init__ directly + # pass def update(self): """ diff --git a/scratchattach/site/classroom.py b/scratchattach/site/classroom.py index f04d96b3..c92f6be3 100644 --- a/scratchattach/site/classroom.py +++ b/scratchattach/site/classroom.py @@ -2,9 +2,12 @@ import datetime import warnings -from typing import Optional, TYPE_CHECKING, Any +from dataclasses import dataclass, field +from datetime import datetime +from typing import Optional, TYPE_CHECKING, Any, Callable import bs4 +from bs4 import BeautifulSoup if TYPE_CHECKING: from scratchattach.site.session import Session @@ -13,46 +16,45 @@ from . import user, activity from ._base import BaseSiteComponent from scratchattach.utils import exceptions, commons -from scratchattach.utils.commons import headers - -from bs4 import BeautifulSoup +@dataclass class Classroom(BaseSiteComponent): - def __init__(self, **entries): + title: str = None + id: int = None + classtoken: str = None + is_closed: bool = False + datetime: datetime = None + author: user.User = None + update_function: Callable = requests.get + + _session: Optional[Session] = field(repr=False, default=None) + + def __post_init__(self): # Info on how the .update method has to fetch the data: # NOTE: THIS DOESN'T WORK WITH CLOSED CLASSES! - self.update_function = requests.get - if "id" in entries: - self.update_api = f"https://api.scratch.mit.edu/classrooms/{entries['id']}" - elif "classtoken" in entries: - self.update_api = f"https://api.scratch.mit.edu/classtoken/{entries['classtoken']}" + if self.id: + self.update_api = f"https://api.scratch.mit.edu/classrooms/{self.id}" + elif self.classtoken: + self.update_api = f"https://api.scratch.mit.edu/classtoken/{self.classtoken}" else: - raise KeyError(f"No class id or token provided! Entries: {entries}") - - # Set attributes every Classroom object needs to have: - self._session: Session = None - self.id = None - self.classtoken = None - self.is_closed = False - - self.__dict__.update(entries) + raise KeyError(f"No class id or token provided! {self.__dict__ = }") # Headers and cookies: if self._session is None: - self._headers = headers + self._headers = commons.headers self._cookies = {} else: self._headers = self._session._headers self._cookies = self._session._cookies # Headers for operations that require accept and Content-Type fields: - self._json_headers = dict(self._headers) - self._json_headers["accept"] = "application/json" - self._json_headers["Content-Type"] = "application/json" + self._json_headers = {**self._headers, + "accept": "application/json", + "Content-Type": "application/json"} - def __repr__(self) -> str: - return f"classroom called {self.title!r}" + def __str__(self) -> str: + return f"" def update(self): try: @@ -305,7 +307,8 @@ def close(self) -> None: warnings.warn(f"{self._session} may not be authenticated to edit {self}") raise e - def register_student(self, username: str, password: str = '', birth_month: Optional[int] = None, birth_year: Optional[int] = None, + def register_student(self, username: str, password: str = '', birth_month: Optional[int] = None, + birth_year: Optional[int] = None, gender: Optional[str] = None, country: Optional[str] = None, is_robot: bool = False) -> None: return register_by_token(self.id, self.classtoken, username, password, birth_month, birth_year, gender, country, is_robot) @@ -346,7 +349,8 @@ def public_activity(self, *, limit=20): return activities - def activity(self, student: str = "all", mode: str = "Last created", page: Optional[int] = None) -> list[dict[str, Any]]: + def activity(self, student: str = "all", mode: str = "Last created", page: Optional[int] = None) -> list[ + dict[str, Any]]: """ Get a list of private activity, only available to the class owner. Returns: @@ -421,7 +425,7 @@ def register_by_token(class_id: int, class_token: str, username: str, password: "is_robot": is_robot} response = requests.post("https://scratch.mit.edu/classes/register_new_student/", - data=data, headers=headers, cookies={"scratchcsrftoken": 'a'}) + data=data, headers=commons.headers, cookies={"scratchcsrftoken": 'a'}) ret = response.json()[0] if "username" in ret: diff --git a/scratchattach/site/session.py b/scratchattach/site/session.py index e2935ea1..c0ae9d37 100644 --- a/scratchattach/site/session.py +++ b/scratchattach/site/session.py @@ -98,6 +98,7 @@ def __init__(self, **entries): self.username = None self.xtoken = None self.new_scratcher = None + self.is_teacher = None # Set attributes that Session object may get self._user: user.User = None @@ -704,6 +705,9 @@ def mystuff_studios(self, filter_arg: str = "all", *, page: int = 1, sort_by: st raise exceptions.FetchError() def mystuff_classes(self, mode: str = "Last created", page: Optional[int] = None) -> list[classroom.Classroom]: + if self.is_teacher is None: + self.update() + if not self.is_teacher: raise exceptions.Unauthorized(f"{self.username} is not a teacher; can't have classes") ascsort, descsort = get_class_sort_mode(mode) From a9c6cf740aeeb061f75126a0a184be6710b4cd3e Mon Sep 17 00:00:00 2001 From: r Date: Sat, 7 Jun 2025 16:58:21 +0100 Subject: [PATCH 2/3] edit: add more attrs --- scratchattach/site/classroom.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scratchattach/site/classroom.py b/scratchattach/site/classroom.py index c92f6be3..82fec6f3 100644 --- a/scratchattach/site/classroom.py +++ b/scratchattach/site/classroom.py @@ -23,11 +23,16 @@ class Classroom(BaseSiteComponent): title: str = None id: int = None classtoken: str = None + + author: user.User = None + about_class: str = None + working_on: str = None + is_closed: bool = False datetime: datetime = None - author: user.User = None - update_function: Callable = requests.get + + update_function: Callable = field(repr=False, default=requests.get) _session: Optional[Session] = field(repr=False, default=None) def __post_init__(self): From 72495efc1d21b953ae5515f035c5151b5afa0318 Mon Sep 17 00:00:00 2001 From: r Date: Sat, 7 Jun 2025 17:12:56 +0100 Subject: [PATCH 3/3] fix: activity._update_from_json --- scratchattach/site/activity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scratchattach/site/activity.py b/scratchattach/site/activity.py index d7cdd702..15555b8f 100644 --- a/scratchattach/site/activity.py +++ b/scratchattach/site/activity.py @@ -20,7 +20,6 @@ def __str__(self): return str(self.raw) def __init__(self, **entries): - # Set attributes every Activity object needs to have: self._session = None self.raw = None @@ -80,7 +79,7 @@ def _update_from_json(self, data: dict): else: recipient_username = None - default_case = True + default_case = False # Even if `activity_type` is an invalid value; it will default to 'user performed an action' if activity_type == 0: @@ -283,7 +282,8 @@ def _update_from_json(self, data: dict): self.comment_obj_id = comment_obj_id self.comment_obj_title = comment_obj_title self.comment_id = comment_id - + else: + default_case = True if default_case: # This is coded in the scratch HTML, haven't found an example of it though