From cff77bcc95328785c7cf50c09b065b96ef2cb890 Mon Sep 17 00:00:00 2001 From: Boyan Date: Sat, 6 Apr 2024 15:38:52 +0200 Subject: [PATCH] Working. Testing out doc writing. --- Folder.py | 30 ------- docs/index.rst | 18 ++-- src/Assignment.py | 31 ------- src/Base.py | 72 --------------- src/Course.py | 19 ++-- src/Downloadable.py | 45 ---------- src/Exercise.py | 73 ---------------- src/ExerciseGroup.py | 131 +++++++++++++++++++++++----- src/exceptions/CourseUnavailable.py | 4 +- src/exceptions/IllegalAction.py | 4 + 10 files changed, 143 insertions(+), 284 deletions(-) delete mode 100644 Folder.py delete mode 100644 src/Assignment.py delete mode 100644 src/Base.py delete mode 100644 src/Downloadable.py delete mode 100644 src/Exercise.py create mode 100644 src/exceptions/IllegalAction.py diff --git a/Folder.py b/Folder.py deleted file mode 100644 index 11a0fdc..0000000 --- a/Folder.py +++ /dev/null @@ -1,30 +0,0 @@ -# Module to handle each assignment (most difficult part) - -from Base import Base -from Exercise import Exercise -from requests import Session - - -class Assignment(Base): - def __init__(self, url:str, name:str, session:Session, parent): - super().__init__(url, name, session, parent) - self.download = Downloadable(name, session, self) - - def __str__(self): - return f"Assignment {self.name} in course {self.parent.name}" - - def getExercises(self) -> list[Exercise]: - # Find li large - ul = self.soup.find('ul', class_='round') - - # Turn each li to an exercise instance - return self.liLargeToExercises(ul, self.session, self) - - def getExercise(self, name:str) -> Exercise: - # Get the exercise - r = self.session.get(self.url) - soup = BeautifulSoup(r.text, 'lxml') - # Search by name - exercise = soup.find('a', text=name) - # Get the url and transform it into an exercise object - return Exercise(url=exercise['href'], name=name, session=self.session, assignment=self) \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 8f4b9a0..1e42761 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,9 +13,17 @@ Temmies! -Indices and tables -================== +Contents +-------- -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +.. toctree:: + + Home + quickstart + install + usage + api + Themis + Year + Course + ExerciseGroup \ No newline at end of file diff --git a/src/Assignment.py b/src/Assignment.py deleted file mode 100644 index 9141530..0000000 --- a/src/Assignment.py +++ /dev/null @@ -1,31 +0,0 @@ -# Module to handle each assignment (most difficult part) - -from Downloadable import Downloadable -from Base import Base -from Exercise import Exercise -from requests import Session - - -class Assignment(Base): - def __init__(self, url:str, name:str, session:Session, parent): - super().__init__(url, name, session, parent) - self.download = Downloadable(name, session, self) - - def __str__(self): - return f"Assignment {self.name} in course {self.parent.name}" - - def getExercises(self) -> list[Exercise]: - # Find li large - ul = self.soup.find('ul', class_='round') - - # Turn each li to an exercise instance - return self.liLargeToExercises(ul, self.session, self) - - def getExercise(self, name:str) -> Exercise: - # Get the exercise - r = self.session.get(self.url) - soup = BeautifulSoup(r.text, 'lxml') - # Search by name - exercise = soup.find('a', text=name) - # Get the url and transform it into an exercise object - return Exercise(url=exercise['href'], name=name, session=self.session, assignment=self) \ No newline at end of file diff --git a/src/Base.py b/src/Base.py deleted file mode 100644 index a10dcf0..0000000 --- a/src/Base.py +++ /dev/null @@ -1,72 +0,0 @@ -# Noticed there's a similar pattern in the classes, so I'm going to create a base class for them - -# classes that inherit from Base: -# - Course -# - Assignment -# - Exercise -from requests import Session -from bs4 import BeautifulSoup - -class Base: - def __init__(self, url:str, name:str, session:Session, parent): - self.url = url - self.name = name - self.session = session - self.parent = parent - - def __parseCfgBlock(self, div:BeautifulSoup) -> dict: - # We assume that the div is a submission with class "cfg-container round" - # Put each key and value in a dictionary - # The key is a span with a class "cfg-key" - # The value is a span with a class "cfg-val" - - # Get the key and value spans - keys = div.find_all('span', class_="cfg-key") - values = div.find_all('span', class_="cfg-val") - - # Create a dictionary - submission = {} - - # Put each key and value in the dictionary - for i in range(len(keys)): - submission[keys[i].text] = values[i].text - - return submission - - - # TODO: Fix - def getDownloadable(self, soup) -> list: - # Make sure we only get the ones that have a link - # We parse the cfg and check for the key "Downloads" - # Check if downloads are available - print(soup) - cfg = soup.find('div', class_='cfg-container round') - print(cfg) - cfg = self.__parseCfgBlock(cfg) - # Get the downloads - downloads = cfg.get("Downloads", None) - if downloads == None: - return [] - # Get the links - links = downloads.find_all('a') - files = [] - for link in links: - files.append(Base(link['href'], link.text, self.session, self)) - - return files - - def getSubmissions(self): - # We change the url where course becomes stats - url = self.url.replace("course", "stats") - r = self.session.get(url) - - # Get each div with class "cfg-container round" - soup = BeautifulSoup(r.text, 'lxml') - divs = soup.find_all('div', class_="cfg-container round") - - # The first one is an overview, the next ones are the submissions - submissions = [] - for div in divs[1:]: - submissions.append(self.__parseCfgBlock(div)) - return self.__parseCfgBlock(divs[0]), submissions - \ No newline at end of file diff --git a/src/Course.py b/src/Course.py index 3492c7e..a8f74a8 100644 --- a/src/Course.py +++ b/src/Course.py @@ -3,7 +3,6 @@ from bs4 import BeautifulSoup from requests import Session from ExerciseGroup import ExerciseGroup import re -from Base import Base from exceptions.CourseUnavailable import CourseUnavailable # PROBLEM: This implementation is bad due to inconsistencies in the website @@ -12,10 +11,13 @@ from exceptions.CourseUnavailable import CourseUnavailable # Therefore, we should take that into consideration and spawn the corresponding Exercise or Assignment class # Naming becomes a bit inconsistent like that as well, as Assignments could be Exercises. Might opt to call the "assignments" "exerciseGroups" or some shit. -class Course(Base): +class Course: # Extend the Base class init def __init__(self, url:str, name:str, session:Session, parent): - super().__init__(url, name, session, parent) + self.url = url + self.name = name + self.session = session + self.parent = parent self.assignments = [] self.__courseAvailable(self.session.get(self.url)) @@ -26,7 +28,7 @@ class Course(Base): # Check if we got an error # print(self.url) if "Something went wrong" in r.text: - raise CourseUnavailable() + raise CourseUnavailable(message="'Something went wrong'. Course most likely not found. ") @property def info(self): @@ -42,4 +44,11 @@ class Course(Base): soup = BeautifulSoup(r.text, 'lxml') section = soup.find('div', class_="ass-children") entries = section.find_all('a', href=True) - return [ExerciseGroup(f"https://themis.housing.rug.nl{x['href']}", x.text, self.session, self) for x in entries] \ No newline at end of file + return [ + ExerciseGroup( + f"https://themis.housing.rug.nl{x['href']}", + x, + self.session, + self, + ) + for x in entries] \ No newline at end of file diff --git a/src/Downloadable.py b/src/Downloadable.py deleted file mode 100644 index c586e66..0000000 --- a/src/Downloadable.py +++ /dev/null @@ -1,45 +0,0 @@ -# Since we can download files both from the assignment itself and its exercises, this class will handle both - -from requests import Session -from bs4 import BeautifulSoup -from Base import Base - -class Downloadable(Base): - def __init__(self, name, session:Session, parent): - self.name = name - self.session = session - self.parent = parent - - # File handling - def __findFile(self, name:str): - # Get the file by name - for file in self.files: - if file.name == name: - return file - return None - - @property - def files(self) -> list: - # Create a list of files - # They are all links in a span with class "cfg-val" - r = self.session.get("https://themis.housing.rug.nl" + self.parent.url) - soup = BeautifulSoup(r.text, 'lxml') - return self.getDownloadable(soup) - - def download(self, filename:str) -> str: - # Download the file - if filename == None: - raise NameError("No filename provided") - - file = self.__findFile(filename) - r = self.session.get(file.url, stream=True) - with open(file.name, 'wb') as f: - for chunk in r.iter_content(chunk_size=1024): - if chunk: - f.write(chunk) - return file.name - - def downloadAll(self) -> list[str]: - # Download all files - return [self.download(file.name) for file in self.files] - diff --git a/src/Exercise.py b/src/Exercise.py deleted file mode 100644 index fee557f..0000000 --- a/src/Exercise.py +++ /dev/null @@ -1,73 +0,0 @@ -from Base import Base -from Downloadable import Downloadable -from requests import Session - -from time import sleep - - -class Exercise(Base): - def __init__(self, url:str, name:str, session:Session, parent): - super().__init__() - self.download = Downloadable(url, name, session, self) - - def __str__(self): - return f"Exercise {self.name} in assignment {self.parent.name}" - - def getTests(self) -> list[str]: - pass - - def submit(self, file:str, comment:str) -> str: - # Submit a file - # The form is in the page with class "cfg-container round" - # The form is a POST request to the url with the file and the comment - # The url looks like this: https://themis.housing.rug.nl/submit/{year}/{course}/{assignment}/{exercise}?_csrf={session_csrf}&sudo={username} - # The current url looks like: https://themis.housing.rug.nl/course/{year}/{course}/{assignment}/{exercise} - # The request should contain the contents of the file - - # Get the url - url = self.url.replace("course", "submit") - # Get the csrf token - csrf = self.session.cookies['_csrf'] - # Get the username - username = self.session.cookies['username'] - - # Open the file - with open(file, 'rb') as f: - # Submit the file - # After submission it will 302 to the current submission page - r = self.session.post(url, files={'file': f}, data={'comment': comment, '_csrf': csrf, 'sudo': username}) - - # Follow the redirect and repeatedly send get requests to the page - - # We have a table which represents the test cases. The program should wait until all the test cases are done - # The test case is done when all of the elements in the table are not none - # The element which showcases this for each - # is the class in there. if it is "queued" it is still running. - - # Get the url - url = r.url - # Get the page - r = self.session.get(url) - # Get the soup - soup = BeautifulSoup(r.text, 'lxml') - # Get the table - table = soup.find('table') - # Get the rows - rows = table.find_all('tr', class_='sub-casetop') - # Get the status - status = [row.find('td', class_='status').text for row in rows] - # Wait until all the status are not queued - while "queued" in status: - # Wait a bit - sleep(1) - # Get the page - r = self.session.get(url) - # Get the soup - soup = BeautifulSoup(r.text, 'lxml') - # Get the table - table = soup.find('table') - # Get the rows - rows = table.find_all('tr', class_='sub-casetop') - - - pass \ No newline at end of file diff --git a/src/ExerciseGroup.py b/src/ExerciseGroup.py index 4bb23f8..31e3e7c 100644 --- a/src/ExerciseGroup.py +++ b/src/ExerciseGroup.py @@ -1,39 +1,128 @@ -from Base import Base -from bs4 import BeautifulSoup\ +from bs4 import BeautifulSoup +from exceptions.IllegalAction import IllegalAction +import re -class ExerciseGroup(Base): - # I can't tell if I'm already an exercise :C - - def __init__(self, url:str, name:str, session, parent): - super().__init__(url, name, session, parent) - self.exercises = self.getExercises() - self.folders = self.getFolders() +class ExerciseGroup(): + def __init__(self, url:str, soup, session, parent): + self.url = url + self.name = soup.text + self.__raw = soup + self.session = session + self.parent = parent # This is unnecessary, but I'll keep it for now + self.request = self.session.get(self.url) + self.soup = BeautifulSoup(self.request.text, 'lxml') def __str__(self): - return f"ExerciseGroup {self.name} in course {self.parent.name}" + return f"ExerciseGroup {self.name} in folder {self.parent.name}" - def getExercises(self) -> list: - r = self.session.get(self.url) - soup = BeautifulSoup(r.text, 'lxml') - section = soup.find('div', class_="ass-children") + @property + def amExercise(self): + return "ass-submitable" in self.__raw['class'] + + def submit(self): + if not self.amExercise: + raise IllegalAction(message="You are submitting to a folder.") + + # Logic for submitting + + # Test cases + @property + def testCases(self): + section = self.soup.find_all('div', class_="subsec round shade") + tcs = [] + for div in section: + res = div.find("h4", class_="info") + if not res: + continue + + if "Test cases" in res.text: + for case in div.find_all("div", class_="cfg-line"): + if link := case.find("a"): + tcs.append(link) + return tcs + return None + + def downloadTCs(self, path="."): + # Logic for downloading test cases(if any) + # In a div with class "subsec round shade", where there is an h4 with text "Test cases" + if not self.amExercise: + raise IllegalAction(message="You are downloading test cases from a folder.") + + for tc in self.testCases: + url= f"https://themis.housing.rug.nl{tc['href']}" + + print(f"Downloading {tc.text}") + # download the files + with open(f"{path}/{tc.text}", "wb") as f: + f.write(self.session.get(url).content) + + return self.testCases + + # Files + @property + def files(self): + details = self.soup.find('div', id=lambda x: x and x.startswith('details')) + + cfg_lines = details.find_all('div', class_='cfg-line') + + link_list = [] + + for line in cfg_lines: + key = line.find('span', class_='cfg-key') + + if key and "Downloads" in key.text.strip(): + # Extract all links in the cfg-val span + links = line.find_all('span', class_='cfg-val') + for link in links: + a = link.find_all('a') + for a in a: + link_list.append(a) + + return link_list if link_list else None + + + + def downloadFiles(self, path="."): + for file in self.files: + print(f"Downloading file {file.text}") + url = f"https://themis.housing.rug.nl{file['href']}" + with open(f"{path}/{file.text}", "wb") as f: + f.write(self.session.get(url).content) + return self.files + + # idea exercises and folders are identical, maybe merge them? + @property + def exercises(self) -> list: + if self.amExercise: + return self + + section = self.soup.find('div', class_="ass-children") try: submittables = section.find_all('a', class_="ass-submitable") except AttributeError: return None - return submittables + return [ + ExerciseGroup(f"https://themis.housing.rug.nl{x['href']}", + x, + self.session, + self) + for x in submittables] - # Returns a list of names of the folders - def getFolders(self) -> list: - r = self.session.get(self.url) - soup = BeautifulSoup(r.text, 'lxml') - section = soup.find('div', class_="ass-children") + @property + def folders(self) -> list: + section = self.soup.find('div', class_="ass-children") try: folders = section.find_all('a', class_="ass-group") except AttributeError: return None - return [x.text for x in folders] + return [ + ExerciseGroup(f"https://themis.housing.rug.nl{x['href']}", + x, + session, + self) + for x in folders] def recurse(self, folder:str): print(self.url) \ No newline at end of file diff --git a/src/exceptions/CourseUnavailable.py b/src/exceptions/CourseUnavailable.py index 465bf5a..87d83ca 100644 --- a/src/exceptions/CourseUnavailable.py +++ b/src/exceptions/CourseUnavailable.py @@ -1,4 +1,4 @@ class CourseUnavailable(Exception): - def __init__(self, message:str="Error in course"): - self.message = message + def __init__(self, message:str=""): + self.message = "Course Error: " + message super().__init__(self.message) \ No newline at end of file diff --git a/src/exceptions/IllegalAction.py b/src/exceptions/IllegalAction.py new file mode 100644 index 0000000..fddb072 --- /dev/null +++ b/src/exceptions/IllegalAction.py @@ -0,0 +1,4 @@ +class IllegalAction(Exception): + def __init__(self, message:str=""): + self.message = "Illegal action: " + message + super().__init__(self.message) \ No newline at end of file