mirror of
https://github.com/Code-For-Groningen/temmies.git
synced 2025-03-15 15: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`
|
.. toctree::
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
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 requests import Session
|
||||||
from ExerciseGroup import ExerciseGroup
|
from ExerciseGroup import ExerciseGroup
|
||||||
import re
|
import re
|
||||||
from Base import Base
|
|
||||||
from exceptions.CourseUnavailable import CourseUnavailable
|
from exceptions.CourseUnavailable import CourseUnavailable
|
||||||
|
|
||||||
# PROBLEM: This implementation is bad due to inconsistencies in the website
|
# 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
|
# 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.
|
# 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
|
# Extend the Base class init
|
||||||
def __init__(self, url:str, name:str, session:Session, parent):
|
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.assignments = []
|
||||||
self.__courseAvailable(self.session.get(self.url))
|
self.__courseAvailable(self.session.get(self.url))
|
||||||
|
|
||||||
@ -26,7 +28,7 @@ class Course(Base):
|
|||||||
# Check if we got an error
|
# Check if we got an error
|
||||||
# print(self.url)
|
# print(self.url)
|
||||||
if "Something went wrong" in r.text:
|
if "Something went wrong" in r.text:
|
||||||
raise CourseUnavailable()
|
raise CourseUnavailable(message="'Something went wrong'. Course most likely not found. ")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def info(self):
|
def info(self):
|
||||||
@ -42,4 +44,11 @@ class Course(Base):
|
|||||||
soup = BeautifulSoup(r.text, 'lxml')
|
soup = BeautifulSoup(r.text, 'lxml')
|
||||||
section = soup.find('div', class_="ass-children")
|
section = soup.find('div', class_="ass-children")
|
||||||
entries = section.find_all('a', href=True)
|
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):
|
class ExerciseGroup():
|
||||||
# I can't tell if I'm already an exercise :C
|
def __init__(self, url:str, soup, session, parent):
|
||||||
|
self.url = url
|
||||||
def __init__(self, url:str, name:str, session, parent):
|
self.name = soup.text
|
||||||
super().__init__(url, name, session, parent)
|
self.__raw = soup
|
||||||
self.exercises = self.getExercises()
|
self.session = session
|
||||||
self.folders = self.getFolders()
|
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):
|
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:
|
@property
|
||||||
r = self.session.get(self.url)
|
def amExercise(self):
|
||||||
soup = BeautifulSoup(r.text, 'lxml')
|
return "ass-submitable" in self.__raw['class']
|
||||||
section = soup.find('div', class_="ass-children")
|
|
||||||
|
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:
|
try:
|
||||||
submittables = section.find_all('a', class_="ass-submitable")
|
submittables = section.find_all('a', class_="ass-submitable")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return None
|
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
|
@property
|
||||||
def getFolders(self) -> list:
|
def folders(self) -> list:
|
||||||
r = self.session.get(self.url)
|
section = self.soup.find('div', class_="ass-children")
|
||||||
soup = BeautifulSoup(r.text, 'lxml')
|
|
||||||
section = soup.find('div', class_="ass-children")
|
|
||||||
try:
|
try:
|
||||||
folders = section.find_all('a', class_="ass-group")
|
folders = section.find_all('a', class_="ass-group")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return None
|
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):
|
def recurse(self, folder:str):
|
||||||
print(self.url)
|
print(self.url)
|
@ -1,4 +1,4 @@
|
|||||||
class CourseUnavailable(Exception):
|
class CourseUnavailable(Exception):
|
||||||
def __init__(self, message:str="Error in course"):
|
def __init__(self, message:str=""):
|
||||||
self.message = message
|
self.message = "Course Error: " + message
|
||||||
super().__init__(self.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