From 401da8a85bad9f4c14214685677c227ab0b8bd5e Mon Sep 17 00:00:00 2001 From: UserPlayer1234 Date: Wed, 4 Mar 2026 11:04:28 -0800 Subject: [PATCH 1/2] feat: implement remove_student_extension --- src/gradescopeapi/classes/extensions.py | 53 ++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/gradescopeapi/classes/extensions.py b/src/gradescopeapi/classes/extensions.py index d14518d..c4f6eae 100644 --- a/src/gradescopeapi/classes/extensions.py +++ b/src/gradescopeapi/classes/extensions.py @@ -18,6 +18,7 @@ import dateutil.parser import requests from bs4 import BeautifulSoup +from requests_toolbelt.multipart.encoder import MultipartEncoder from gradescopeapi import DEFAULT_GRADESCOPE_BASE_URL @@ -226,6 +227,56 @@ def add_to_body(extension_name: str, extension_datetime: datetime.datetime): def remove_student_extension( session: requests.Session, + course_id: str, + assignment_id: str, delete_path: str, + gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL, ) -> bool: - raise NotImplementedError("Not implemented yet") + """Removes the extension for a student on an assignment. + + If the user currently has an extension, this will remove their + current extension. If the user does not have an extension, this + will return a ValueError. + + Args: + session (requests.Session): The session to use for the request + course_id (str): The ID of the course on Gradescope. + assignment_id (str): The ID of the assignment on Gradescope. + delete_path (str): The delete path for the extension + + Returns: + bool: True if the extension was successfully deleted, False otherwise + + Raises: + ValueError: If the delete_path is empty + """ + + # Check if the delete path is set + if delete_path is None or delete_path == "": + raise ValueError("The delete path cannot be empty") + + GS_EXTENSIONS_ENDPOINT = f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}/extensions" + + # Get auth token + response = session.get(GS_EXTENSIONS_ENDPOINT) + soup = BeautifulSoup(response.text, "html.parser") + auth_token = soup.find("meta", {"name": "csrf-token"})["content"] + + # Setup multipart form data + fields = [ + ("authenticity_token", auth_token), + ("_method", "delete"), + ] + + multipart = MultipartEncoder(fields=fields) + + headers = { + "Content-Type": multipart.content_type, + "Referer": GS_EXTENSIONS_ENDPOINT, + } + try: + response = session.post(gradescope_base_url + delete_path, data=multipart, headers=headers) + except Exception: + raise ValueError("The delete path is invalid") + + return response.status_code == 200 From e1696a9a4db879f3aa38c352fa3f63e861c73235 Mon Sep 17 00:00:00 2001 From: UserPlayer1234 Date: Wed, 4 Mar 2026 11:13:58 -0800 Subject: [PATCH 2/2] style: format --- src/gradescopeapi/classes/extensions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/gradescopeapi/classes/extensions.py b/src/gradescopeapi/classes/extensions.py index c4f6eae..b765c20 100644 --- a/src/gradescopeapi/classes/extensions.py +++ b/src/gradescopeapi/classes/extensions.py @@ -254,7 +254,7 @@ def remove_student_extension( # Check if the delete path is set if delete_path is None or delete_path == "": raise ValueError("The delete path cannot be empty") - + GS_EXTENSIONS_ENDPOINT = f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}/extensions" # Get auth token @@ -275,8 +275,10 @@ def remove_student_extension( "Referer": GS_EXTENSIONS_ENDPOINT, } try: - response = session.post(gradescope_base_url + delete_path, data=multipart, headers=headers) + response = session.post( + gradescope_base_url + delete_path, data=multipart, headers=headers + ) except Exception: raise ValueError("The delete path is invalid") - + return response.status_code == 200