mirror of
https://github.com/Code-For-Groningen/temmies.git
synced 2025-03-15 07:10:15 +01:00
Working. Testing out doc writing.
This commit is contained in:
parent
e3d863d7b2
commit
cff77bcc95
30
Folder.py
30
Folder.py
@ -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)
|
@ -13,9 +13,17 @@ Temmies!
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
Contents
|
||||
--------
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
.. toctree::
|
||||
|
||||
Home <self>
|
||||
quickstart
|
||||
install
|
||||
usage
|
||||
api
|
||||
Themis
|
||||
Year
|
||||
Course
|
||||
ExerciseGroup
|
@ -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)
|
72
src/Base.py
72
src/Base.py
@ -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
|
||||
|
@ -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]
|
||||
return [
|
||||
ExerciseGroup(
|
||||
f"https://themis.housing.rug.nl{x['href']}",
|
||||
x,
|
||||
self.session,
|
||||
self,
|
||||
)
|
||||
for x in entries]
|
@ -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]
|
||||
|
@ -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 <tr class="sub-casetop">
|
||||
# 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
|
@ -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)
|
@ -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)
|
4
src/exceptions/IllegalAction.py
Normal file
4
src/exceptions/IllegalAction.py
Normal file
@ -0,0 +1,4 @@
|
||||
class IllegalAction(Exception):
|
||||
def __init__(self, message:str=""):
|
||||
self.message = "Illegal action: " + message
|
||||
super().__init__(self.message)
|
Loading…
x
Reference in New Issue
Block a user