mirror of
https://github.com/Code-For-Groningen/temmies.git
synced 2025-03-15 23:10:15 +01:00
Compare commits
No commits in common. "2daee84d4f8489fc511edb46e0d90d26b9e8429d" and "de6262a4bb0ebb31d2a1fe8b8a669f6a4d3e3070" have entirely different histories.
2daee84d4f
...
de6262a4bb
96
docs/api.md
96
docs/api.md
@ -186,101 +186,5 @@ Submits the files to the exercise group. Default arguments are `judge=True`, `wa
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `get_status(section=None, text=False)`
|
|
||||||
Parses the status of the exercise group(from a given section). If `section` is not `None`, it will return the status of the section. Don't set `section` if you don't know what you're doing.
|
|
||||||
|
|
||||||
When `text` is set to `True`, it will return the status as a dictionary of strings. Otherwise, it will return a tuple in the form `(dict(str:str), dict(str:Submission))`. Refer to the [Submission](#submission) class for more information.
|
|
||||||
|
|
||||||
```python
|
|
||||||
pf = year.get_course("Programming Fundamentals (for CS)")
|
|
||||||
pf_as = pf.get_group("Lab Session 2")
|
|
||||||
|
|
||||||
# Get exercise
|
|
||||||
exercise = pf_as.get_group("Recurrence")
|
|
||||||
|
|
||||||
# Get status
|
|
||||||
status = exercise.get_status()
|
|
||||||
print(status)
|
|
||||||
|
|
||||||
>>> (
|
|
||||||
>>> { # Information [0]
|
|
||||||
>>> 'assignment': 'Recurrence'
|
|
||||||
>>> 'group': 'Y.N. Here'
|
|
||||||
>>> 'status': 'passed: Passed all test cases'
|
|
||||||
>>> 'grade': '2.00'
|
|
||||||
>>> 'total': '2'
|
|
||||||
>>> 'output limit': '1'
|
|
||||||
>>> 'passed': '1'
|
|
||||||
>>> 'leading': '/submission/2023-2024/progfun/lab2/recurrence/@submissions/s1234567/s1234567-1'
|
|
||||||
>>> 'best': '/submission/2023-2024/progfun/lab2/recurrence/@submissions/s1234567/s1234567-1'
|
|
||||||
>>> 'latest': '/submission/2023-2024/progfun/lab2/recurrence/@submissions/s1234567/s1234567-1'
|
|
||||||
>>> 'first pass': '/submission/2023-2024/progfun/lab2/recurrence/@submissions/s1234567/s1234567-1'
|
|
||||||
>>> 'last pass': '/submission/2023-2024/progfun/lab2/recurrence/@submissions/s1234567/s1234567-1'
|
|
||||||
>>> 'visible': 'Yes'
|
|
||||||
>>> }
|
|
||||||
>>> { # Submission instances [1]
|
|
||||||
>>> 'leading': <submission.Submission object at 0x774ea7a48cd0>
|
|
||||||
>>> 'best': <submission.Submission object at 0x774ea79af910>
|
|
||||||
>>> 'latest': <submission.Submission object at 0x774eaa7d3c10>
|
|
||||||
>>> 'first_pass': <submission.Submission object at 0x774ea77ee810>
|
|
||||||
>>> 'last_pass': <submission.Submission object at 0x774ea755de10>
|
|
||||||
>>> }
|
|
||||||
>>>)
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `get_all_statuses(text=False)
|
|
||||||
Does the same as `get_status`, but for all visible status sections.
|
|
||||||
|
|
||||||
|
|
||||||
## `Submission`
|
|
||||||
### Usage
|
|
||||||
```python
|
|
||||||
submission = pf.get_group("Week 1").get_group("Exercise 1").get_group("Part 1").get_status()[1]["leading"] # Week 1 -> Exercise 1 -> Part 1 -> Leading submission
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Methods
|
|
||||||
#### `test_cases()`
|
|
||||||
Returns a list of `TestCase` instances corresponding to all test cases in the submission.
|
|
||||||
|
|
||||||
```python
|
|
||||||
submission = pf.get_group("Week 1").get_group("Exercise 1").get_group("Part 1").get_status()[1]["leading"]
|
|
||||||
submission.test_cases()
|
|
||||||
>>> {'1': 'passed', '2': 'passed', '3': 'passed', '4': 'passed', '5': 'passed', '6': 'passed', '7': 'passed', '8': 'passed', '9': 'passed', '10': 'passed'}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `info()`
|
|
||||||
Returns a dictionary of information about the submission.
|
|
||||||
|
|
||||||
```python
|
|
||||||
submission = pf.get_group("Week 1").get_group("Exercise 1").get_group("Part 1").get_status()[1]["leading"]
|
|
||||||
submission.info()
|
|
||||||
|
|
||||||
>>> {
|
|
||||||
>>>'assignment': 'Part 1',
|
|
||||||
>>>'group': 'Y.N. Here',
|
|
||||||
>>>'uploaded_by': 'Y.N. Here s1234567',
|
|
||||||
>>>'created_on': 'Wed Sep 13 2023 12:51:37 GMT+02002023-09-13T10:51:37.338Z',
|
|
||||||
>>>'submitted_on': 'Wed Sep 13 2023 12:51:37 GMT+02002023-09-13T10:51:37.344Z',
|
|
||||||
>>>'status': 'passed: Passed all test cases',
|
|
||||||
>>>'files': [('recurrence.c',
|
|
||||||
>>>'/file/2023-2024/progfun/lab2/recurrence/%40submissions/s1234567/s1234567-1/source/recurrence.c'),
|
|
||||||
>>>('compile.log',
|
|
||||||
>>>'/file/2023-2024/progfun/lab2/recurrence/%40submissions/s1234567/s1234567-1/output/compile.log')],
|
|
||||||
>>>'language': 'c'
|
|
||||||
>>> }
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `files()`
|
|
||||||
Returns a list of files in the form `(name, link)`.
|
|
||||||
|
|
||||||
```python
|
|
||||||
submission = pf.get_group("Week 1").get_group("Exercise 1").get_group("Part 1").get_status()[1]["leading"]
|
|
||||||
submission.files()
|
|
||||||
|
|
||||||
>>> [('recurrence.c', '/file/2023-2024/progfun/lab2/recurrence/%40submissions/s1234567/s1234567-1/source/recurrence.c'), ('compile.log', '/file/2023-2024/progfun/lab2/recurrence/%40submissions/s1234567/s1234567-1/output/compile.log')]
|
|
||||||
```
|
|
||||||
|
|
||||||
|
@ -8,29 +8,23 @@ from json import loads
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from exceptions.illegal_action import IllegalAction
|
from exceptions.illegal_action import IllegalAction
|
||||||
from submission import Submission
|
|
||||||
|
|
||||||
class ExerciseGroup:
|
class ExerciseGroup:
|
||||||
"""
|
"""
|
||||||
Methods:
|
am_exercise: returns bool which tells you if the instance is an exercise
|
||||||
`submit`: submit to an exercise
|
submit: submit to an exercise
|
||||||
`get_group`: get a group by name
|
get_group: get a group by name
|
||||||
`download_tcs`: download test cases
|
get_groups: get all groups
|
||||||
`download_files`: download files
|
folders: folders in the folder
|
||||||
|
exercises: exercises in the folder
|
||||||
`find_status`: get status for an exercise by name
|
test_cases: test cases in the exercise(if it is an exercise)
|
||||||
`get_all_statuses`: get all available statuses(useful for multiple exercises)
|
download_tcs: download test cases
|
||||||
`get_status(idx=0)`: get the available statuses for the exercise. Set the idx if you want to get a specific submission.
|
files: files in the exercise/folder
|
||||||
Attributes:
|
download_files: download files
|
||||||
|
|
||||||
`am_exercise`: returns bool which tells you if the instance is an exercise
|
|
||||||
`folders`: folders in the folder
|
|
||||||
`exercises`: exercises in the folder
|
|
||||||
`test_cases`: test cases in the exercise(if it is an exercise)
|
|
||||||
`files`: files in the exercise/folder
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, url: str, soup:BeautifulSoup, session, full: bool):
|
def __init__(self, url: str, soup, session, full: bool):
|
||||||
self.url = url
|
self.url = url
|
||||||
self.name = soup.text
|
self.name = soup.text
|
||||||
self.__prev_raw = soup
|
self.__prev_raw = soup
|
||||||
@ -150,7 +144,7 @@ class ExerciseGroup:
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Get by name
|
# Get by name
|
||||||
def get_group( # <- 🗿
|
def get_group(
|
||||||
self, name: str, full: bool = False, link: str = None
|
self, name: str, full: bool = False, link: str = None
|
||||||
) -> "ExerciseGroup":
|
) -> "ExerciseGroup":
|
||||||
"""
|
"""
|
||||||
@ -242,6 +236,7 @@ class ExerciseGroup:
|
|||||||
|
|
||||||
url = "https://themis.housing.rug.nl" + form["action"]
|
url = "https://themis.housing.rug.nl" + form["action"]
|
||||||
file_types = loads(form["data-suffixes"])
|
file_types = loads(form["data-suffixes"])
|
||||||
|
|
||||||
if isinstance(files, str):
|
if isinstance(files, str):
|
||||||
temp = []
|
temp = []
|
||||||
temp.append(files)
|
temp.append(files)
|
||||||
@ -256,12 +251,12 @@ class ExerciseGroup:
|
|||||||
found_type = file_types[t]
|
found_type = file_types[t]
|
||||||
break
|
break
|
||||||
if not found_type:
|
if not found_type:
|
||||||
print("WARNING: File type not recognized")
|
raise IllegalAction(message="Illegal filetype for this assignment.")
|
||||||
|
|
||||||
with open(file, "rb") as f:
|
with open(file, "rb") as f:
|
||||||
packaged_files.append((found_type, (file, f.read())))
|
packaged_files.append((found_type, (file, f.read())))
|
||||||
|
|
||||||
data = {"judgenow": "true" if judge else "false", "judgeLanguage": found_type if found_type else "none"}
|
data = {"judgenow": "true" if judge else "false", "judgeLanguage": found_type}
|
||||||
|
|
||||||
if not silent:
|
if not silent:
|
||||||
print(f"Submitting to {self.name}")
|
print(f"Submitting to {self.name}")
|
||||||
@ -273,75 +268,3 @@ class ExerciseGroup:
|
|||||||
return resp.url if "@submissions" in resp.url else None
|
return resp.url if "@submissions" in resp.url else None
|
||||||
|
|
||||||
return self.__wait_for_result(resp.url, not silent, [])
|
return self.__wait_for_result(resp.url, not silent, [])
|
||||||
|
|
||||||
def __status_sections(self) -> list[BeautifulSoup]:
|
|
||||||
r = self.__session.get("https://themis.housing.rug.nl" + self.__raw.find("a", text="Status")["href"])
|
|
||||||
|
|
||||||
soup = BeautifulSoup(r.text, "html.parser")
|
|
||||||
sections = soup.find_all('section', class_=lambda class_: class_ and 'status' in class_.split())
|
|
||||||
|
|
||||||
return sections
|
|
||||||
|
|
||||||
def __parse_section(self, section:BeautifulSoup, text) -> dict[str, Submission] | dict[str, str]:
|
|
||||||
# The section has a heading and a body. We only care about the body
|
|
||||||
body = section.find("div", class_="sec-body") # Find the body of the section
|
|
||||||
body = body.find("div", class_="subsec-container") # Find the subsec-container
|
|
||||||
body = body.find("div", class_="cfg-container")
|
|
||||||
|
|
||||||
# Parse the cfg-container
|
|
||||||
parsed = {}
|
|
||||||
|
|
||||||
# Submission instances go here
|
|
||||||
submissions = {}
|
|
||||||
|
|
||||||
cfg_lines = body.find_all("div", class_="cfg-line")
|
|
||||||
for line in cfg_lines:
|
|
||||||
key = line.find("span", class_="cfg-key").text.strip().split("\n")[0].replace(":", "").lower()
|
|
||||||
value = line.find("span", class_="cfg-val").text.strip()
|
|
||||||
|
|
||||||
# If there is a span with class tip in the key, it means that the value is a link to a submission
|
|
||||||
if tip := line.find("span", class_="tip"):
|
|
||||||
value = line.find("a")["href"]
|
|
||||||
if not text:
|
|
||||||
submissions[key.split("\n")[0].lower().replace(" ", "_")] = Submission(value, self.__session)
|
|
||||||
parsed[key] = value
|
|
||||||
|
|
||||||
if text:
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
return (parsed, submissions)
|
|
||||||
|
|
||||||
# I assume that the user would usually request submissions for an assignment,
|
|
||||||
# so I will add a default parameter to the method.
|
|
||||||
|
|
||||||
def get_status(self, section:list[BeautifulSoup]=None, text:bool=False) -> dict[str, Submission] | dict[str, str]:
|
|
||||||
"""Get the available submissions for the exercise.
|
|
||||||
Set text to True to get the text representation of the submission."""
|
|
||||||
if not section:
|
|
||||||
section = self.__status_sections()
|
|
||||||
|
|
||||||
try:
|
|
||||||
section = section[0] # When looking at a single exercise, there is only one status section
|
|
||||||
except IndexError as exc:
|
|
||||||
raise IllegalAction("Invalid status") from exc
|
|
||||||
|
|
||||||
return self.__parse_section(section, text)
|
|
||||||
|
|
||||||
def get_all_statuses(self, text:bool=False) -> list[dict[str, str]] | list[dict[str, Submission]]:
|
|
||||||
""" Parses every visible status section. """
|
|
||||||
|
|
||||||
# This is useless for singular exercises, but if you want the submissions for multiple exercises, you can use this.
|
|
||||||
statuses = []
|
|
||||||
for section in self.__status_sections():
|
|
||||||
if parse := self.__parse_section(section, text):
|
|
||||||
# Find name of the exercise
|
|
||||||
name = section.find("h3").text.replace("Status: ", "").replace("\n", "").replace("\t", "")
|
|
||||||
statuses.append((name,parse))
|
|
||||||
return statuses
|
|
||||||
|
|
||||||
def find_status(self, name:str, text:bool=False) -> dict[str, Submission] | dict[str, str] | None:
|
|
||||||
""" Find a status block for an exercise by name. """
|
|
||||||
# Find a section which has h3 with the name
|
|
||||||
for section in self.__status_sections():
|
|
||||||
if section.find("h3").text.replace("Status: ", "").replace("\n", "").replace("\t", "") == name:
|
|
||||||
return self.__parse_section(section, text)
|
|
@ -1,85 +0,0 @@
|
|||||||
"""
|
|
||||||
File to define the submission class
|
|
||||||
"""
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
|
|
||||||
class Submission:
|
|
||||||
"""
|
|
||||||
Submission class
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
test_cases: Get a dict of test cases status
|
|
||||||
info: Submission information (in details)
|
|
||||||
files: Get a list of uploaded files(as names)
|
|
||||||
"""
|
|
||||||
def __init__(self, url: str, session):
|
|
||||||
self.url = "https://themis.housing.rug.nl" + url
|
|
||||||
self.__session = session
|
|
||||||
self.__request = self.__session.get(self.url)
|
|
||||||
self.__raw = BeautifulSoup(self.__request.text, "lxml")
|
|
||||||
self.__info = None
|
|
||||||
|
|
||||||
def __clean(self, text: str, value: bool = False) -> str:
|
|
||||||
"""Clean text"""
|
|
||||||
clean = text.replace("\t", "").replace("\n", "")
|
|
||||||
if value:
|
|
||||||
return clean
|
|
||||||
return clean.replace(" ", "_").replace(":", "").lower()
|
|
||||||
|
|
||||||
def test_cases(self) -> dict[str, str]:
|
|
||||||
"""Get a dict of test cases status"""
|
|
||||||
# In the submission page, the test cases are in a div with class "sub-cases subsec round shade"
|
|
||||||
# print(self.__raw.prettify())
|
|
||||||
cases = self.__raw.find("div", class_=lambda x: x and "sub-cases" in x.split())
|
|
||||||
if not cases:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# The test cases are in a table in a div with class "cfg-container"
|
|
||||||
cases = cases.find("div", class_="cfg-container")
|
|
||||||
cases = cases.find("table")
|
|
||||||
# For each test case, there is a tr with class sub-casetop, which contains 2 tds:
|
|
||||||
# * a td with class "sub-case name" which is a name
|
|
||||||
# * a td with a variable class, which is the status text
|
|
||||||
|
|
||||||
results = {}
|
|
||||||
for entry in cases.find_all("tr", class_="sub-casetop"):
|
|
||||||
name = entry.find("td", class_="sub-casename").text
|
|
||||||
status = entry.find(
|
|
||||||
"td", class_=lambda x: x and "status-icon" in x.split()
|
|
||||||
).text
|
|
||||||
results[name] = self.__clean(status)
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
def info(self) -> dict[str, str] | None:
|
|
||||||
"""Submission information (in details)"""
|
|
||||||
# in div with class subsec round shade where there is an h4 with class info
|
|
||||||
# The info is in a div with class "cfg-container"
|
|
||||||
if self.__info:
|
|
||||||
return self.__info
|
|
||||||
|
|
||||||
for div in self.__raw.find_all("div", class_="subsec round shade"):
|
|
||||||
if h4 := div.find("h4", class_=lambda x: x and "info" in x.split()):
|
|
||||||
if "Details" in h4.text:
|
|
||||||
# The information is in divs with class "cfg-line"
|
|
||||||
# With key in span with class "cfg-key" and value in span with class "cfg-value"
|
|
||||||
info = div.find("div", class_="cfg-container")
|
|
||||||
info = info.find_all("div", class_="cfg-line")
|
|
||||||
return {
|
|
||||||
self.__clean(
|
|
||||||
key := line.find("span", class_="cfg-key").text
|
|
||||||
):
|
|
||||||
self.__clean(line.find("span", class_="cfg-val").text, value=True) if "Files" not in key else
|
|
||||||
([(self.__clean(x.text), x["href"]) for x in line.find("span", class_="cfg-val").find_all("a")])
|
|
||||||
for line in info
|
|
||||||
}
|
|
||||||
return None
|
|
||||||
|
|
||||||
def files(self) -> list[str] | None:
|
|
||||||
"""Get a list of uploaded files in the format [(name, url)]"""
|
|
||||||
if not self.__info:
|
|
||||||
self.__info = self.info()
|
|
||||||
|
|
||||||
return self.__info.get("files", None)
|
|
Loading…
x
Reference in New Issue
Block a user