mirror of
https://github.com/Code-For-Groningen/temmies.git
synced 2025-03-15 07: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 .exceptions.illegal_action import IllegalAction
|
||||
from .submission import Submission
|
||||
from json import loads
|
||||
from time import sleep
|
||||
from typing import Optional
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
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):
|
||||
super().__init__(url, name, session, parent=parent, full=full, classes=classes)
|
||||
self.am_exercise = "ass-submitable" in self.classes
|
||||
|
||||
def create_group(self, url: str, name: str, session, parent, full: bool, classes=None):
|
||||
def __init__(self, session, path: str, title: str, parent, submitable: bool = True):
|
||||
super().__init__(session, path, title, parent, submitable=submitable)
|
||||
self.submit_url = f"{self.base_url}/api/submit{self.path}"
|
||||
self.__find_name()
|
||||
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
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]:
|
||||
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]
|
||||
|
||||
def submit(self, files: list[str]) -> Submission:
|
||||
"""
|
||||
Submit files to this exercise.
|
||||
Returns a dictionary of test case results or None if wait is False.
|
||||
"""
|
||||
if not self.am_exercise:
|
||||
raise IllegalAction("You cannot submit to this assignment.")
|
||||
if not self.submitable:
|
||||
raise ValueError(f"Cannot submit to non-submittable item '{self.title}'.")
|
||||
|
||||
form = self._raw.find("form")
|
||||
if not form:
|
||||
raise IllegalAction("Submission form not found.")
|
||||
# Prepare the files and data for submission
|
||||
files_payload = {}
|
||||
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"]
|
||||
file_types = loads(form["data-suffixes"])
|
||||
response = self.session.post(self.submit_url, files=files_payload)
|
||||
if response.status_code != 200:
|
||||
raise ConnectionError(f"Failed to submit to '{self.title}'.")
|
||||
|
||||
if isinstance(files, str):
|
||||
files = [files]
|
||||
submission_data = response.json()
|
||||
return Submission(self.session, submission_data)
|
||||
|
||||
packaged_files = []
|
||||
data = {}
|
||||
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
|
||||
def __str__(self):
|
||||
return f"ExerciseGroup({self.title})"
|
||||
|
Loading…
x
Reference in New Issue
Block a user