mirror of
https://github.com/Code-For-Groningen/temmies.git
synced 2025-03-15 15:10:15 +01:00
Moved most logic to base Group class
This commit is contained in:
parent
514fcd2438
commit
9f99df54d8
@ -1,209 +1,51 @@
|
|||||||
from .group import Group
|
from .group import Group
|
||||||
from .exceptions.illegal_action import IllegalAction
|
|
||||||
from .submission import Submission
|
from .submission import Submission
|
||||||
from json import loads
|
|
||||||
from time import sleep
|
|
||||||
from typing import Optional
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
class ExerciseGroup(Group):
|
class ExerciseGroup(Group):
|
||||||
"""
|
"""
|
||||||
Represents a group of exercises or a single exercise.
|
Represents a submittable exercise.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, url: str, name: str, session, parent=None, full: bool = False, classes=None):
|
def __init__(self, session, path: str, title: str, parent, submitable: bool = True):
|
||||||
super().__init__(url, name, session, parent=parent, full=full, classes=classes)
|
super().__init__(session, path, title, parent, submitable=submitable)
|
||||||
self.am_exercise = "ass-submitable" in self.classes
|
self.submit_url = f"{self.base_url}/api/submit{self.path}"
|
||||||
|
self.__find_name()
|
||||||
|
|
||||||
def create_group(self, url: str, name: str, session, parent, full: bool, classes=None):
|
def __find_name(self):
|
||||||
"""
|
"""
|
||||||
Create an instance of ExerciseGroup for subgroups.
|
Find the name of the exercise group.
|
||||||
"""
|
"""
|
||||||
return ExerciseGroup(url, name, session, parent, full, classes)
|
if self.title == "":
|
||||||
|
# Find using beautiful soup (it is the last a with class 'fill accent large')
|
||||||
|
response = self.session.get(self.base_url + self.path)
|
||||||
|
soup = BeautifulSoup(response.text, "lxml")
|
||||||
|
title_elements = soup.find_all("a", class_="fill accent large")
|
||||||
|
if title_elements:
|
||||||
|
self.title = title_elements[-1].get_text(strip=True)
|
||||||
|
else:
|
||||||
|
self.title = self.path.split("/")[-1]
|
||||||
|
|
||||||
@classmethod
|
def submit(self, files: list[str]) -> Submission:
|
||||||
def create_group_from_url(cls, url: str, full: bool) -> 'ExerciseGroup':
|
|
||||||
"""
|
|
||||||
Create an instance of ExerciseGroup from a full URL of a Themis group.
|
|
||||||
This method will retrieve the name of the group from the URL.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
url (str): URL of the Themis group.
|
|
||||||
full (bool): Whether to traverse the whole group.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ExerciseGroup: An instance of ExerciseGroup.
|
|
||||||
"""
|
|
||||||
if "https://themis.housing.rug.nl/course/" not in url:
|
|
||||||
url = "https://themis.housing.rug.nl/course/" + url
|
|
||||||
|
|
||||||
# Find name of group (last of a with class fill accent large)
|
|
||||||
r = cls._session.get(url)
|
|
||||||
soup = BeautifulSoup(r.text, "lxml")
|
|
||||||
group_links = soup.find_all("a", class_="fill accent large")
|
|
||||||
name = group_links[-1].text
|
|
||||||
|
|
||||||
return cls(url, name, cls._session, parent=None, full=full)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def test_cases(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Get all test cases for this exercise.
|
|
||||||
"""
|
|
||||||
if not self.am_exercise:
|
|
||||||
return []
|
|
||||||
|
|
||||||
sections = self._raw.find_all("div", class_="subsec round shade")
|
|
||||||
tcs = []
|
|
||||||
for div in sections:
|
|
||||||
res = div.find("h4", class_="info")
|
|
||||||
if res and "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
|
|
||||||
|
|
||||||
def download_tcs(self, path=".") -> list[str]:
|
|
||||||
"""
|
|
||||||
Download all test cases for this exercise.
|
|
||||||
"""
|
|
||||||
if not self.am_exercise:
|
|
||||||
raise IllegalAction(
|
|
||||||
"You are downloading test cases from a folder.")
|
|
||||||
|
|
||||||
for tc in self.test_cases:
|
|
||||||
url = f"https://themis.housing.rug.nl{tc['href']}"
|
|
||||||
print(f"Downloading {tc.text}")
|
|
||||||
with open(f"{path}/{tc.text}", "wb") as f:
|
|
||||||
f.write(self._session.get(url).content)
|
|
||||||
return self.test_cases
|
|
||||||
|
|
||||||
@property
|
|
||||||
def files(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Get all downloadable files for this exercise or group.
|
|
||||||
"""
|
|
||||||
details = self._raw.find(
|
|
||||||
"div", id=lambda x: x and x.startswith("details"))
|
|
||||||
if not details:
|
|
||||||
return []
|
|
||||||
|
|
||||||
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():
|
|
||||||
links = line.find_all("span", class_="cfg-val")
|
|
||||||
for link in links:
|
|
||||||
a_tags = link.find_all("a")
|
|
||||||
for a in a_tags:
|
|
||||||
link_list.append(a)
|
|
||||||
return link_list
|
|
||||||
|
|
||||||
def download_files(self, path=".") -> list[str]:
|
|
||||||
"""
|
|
||||||
Download all files available for this exercise or group.
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
def submit(self, files: list[str], judge: bool = True, wait: bool = True, silent: bool = True) -> Optional[dict]:
|
|
||||||
"""
|
"""
|
||||||
Submit files to this exercise.
|
Submit files to this exercise.
|
||||||
Returns a dictionary of test case results or None if wait is False.
|
|
||||||
"""
|
"""
|
||||||
if not self.am_exercise:
|
if not self.submitable:
|
||||||
raise IllegalAction("You cannot submit to this assignment.")
|
raise ValueError(f"Cannot submit to non-submittable item '{self.title}'.")
|
||||||
|
|
||||||
form = self._raw.find("form")
|
# Prepare the files and data for submission
|
||||||
if not form:
|
files_payload = {}
|
||||||
raise IllegalAction("Submission form not found.")
|
for idx, file_path in enumerate(files):
|
||||||
|
file_key = f"file{idx}"
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
files_payload[file_key] = (file_path, f.read())
|
||||||
|
|
||||||
url = "https://themis.housing.rug.nl" + form["action"]
|
response = self.session.post(self.submit_url, files=files_payload)
|
||||||
file_types = loads(form["data-suffixes"])
|
if response.status_code != 200:
|
||||||
|
raise ConnectionError(f"Failed to submit to '{self.title}'.")
|
||||||
|
|
||||||
if isinstance(files, str):
|
submission_data = response.json()
|
||||||
files = [files]
|
return Submission(self.session, submission_data)
|
||||||
|
|
||||||
packaged_files = []
|
def __str__(self):
|
||||||
data = {}
|
return f"ExerciseGroup({self.title})"
|
||||||
found_type = ""
|
|
||||||
|
|
||||||
for file in files:
|
|
||||||
for suffix, lang in file_types.items():
|
|
||||||
if file.endswith(suffix):
|
|
||||||
found_type = lang
|
|
||||||
break
|
|
||||||
if not found_type:
|
|
||||||
print("WARNING: File type not recognized")
|
|
||||||
|
|
||||||
with open(file, "rb") as f:
|
|
||||||
packaged_files.append((found_type, (file, f.read())))
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"judgenow": "true" if judge else "false",
|
|
||||||
"judgeLanguage": found_type if found_type else "none"
|
|
||||||
}
|
|
||||||
|
|
||||||
if not silent:
|
|
||||||
print(f"Submitting to {self.name}")
|
|
||||||
for file in files:
|
|
||||||
print(f"• {file}")
|
|
||||||
|
|
||||||
resp = self._session.post(url, files=packaged_files, data=data)
|
|
||||||
|
|
||||||
if not wait or not judge:
|
|
||||||
return resp.url if "@submissions" in resp.url else None
|
|
||||||
|
|
||||||
return self.__wait_for_result(resp.url, not silent, [])
|
|
||||||
|
|
||||||
def __wait_for_result(self, url: str, verbose: bool, __printed: list) -> dict:
|
|
||||||
"""
|
|
||||||
Wait for the submission result and return the test case results.
|
|
||||||
"""
|
|
||||||
r = self._session.get(url)
|
|
||||||
soup = BeautifulSoup(r.text, "lxml")
|
|
||||||
return self.__parse_table(soup, url, verbose, __printed)
|
|
||||||
|
|
||||||
def __parse_table(self, soup: BeautifulSoup, url: str, verbose: bool, __printed: list) -> dict:
|
|
||||||
"""
|
|
||||||
Parse the results table from the submission result page.
|
|
||||||
"""
|
|
||||||
cases = soup.find_all("tr", class_="sub-casetop")
|
|
||||||
fail_pass = {}
|
|
||||||
for case in cases:
|
|
||||||
name = case.find("td", class_="sub-casename").text
|
|
||||||
status = case.find("td", class_="status-icon")
|
|
||||||
|
|
||||||
if "pending" in status.get("class"):
|
|
||||||
sleep(1)
|
|
||||||
return self.__wait_for_result(url, verbose, __printed)
|
|
||||||
|
|
||||||
statuses = {
|
|
||||||
"Passed": ("✅", True),
|
|
||||||
"Wrong output": ("❌", False),
|
|
||||||
"No status": ("🐛", None),
|
|
||||||
"error": ("🐛", None),
|
|
||||||
}
|
|
||||||
|
|
||||||
found = False
|
|
||||||
for k, v in statuses.items():
|
|
||||||
if k in status.text:
|
|
||||||
found = True
|
|
||||||
if verbose and int(name) not in __printed:
|
|
||||||
print(f"{name}: {v[0]}")
|
|
||||||
fail_pass[int(name)] = v[1]
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
fail_pass[int(name)] = None
|
|
||||||
if verbose and int(name) not in __printed:
|
|
||||||
print(f"{name}: Unrecognized status: {status.text}")
|
|
||||||
|
|
||||||
__printed.append(int(name))
|
|
||||||
return fail_pass
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user