diff --git a/examples/ve482.py b/examples/ve482.py index 2270d0e..5b77cf9 100644 --- a/examples/ve482.py +++ b/examples/ve482.py @@ -1,11 +1,17 @@ import glob +import json import ntpath +import os +from typing import cast + +from canvasapi.assignment import Assignment from joint_teapot import Teapot, logger +from joint_teapot.utils.main import default_repo_name_convertor, first, percentile -class MyTeapot(Teapot): - def ve482p1(self) -> None: +class VE482Teapot(Teapot): + def p1_check(self) -> None: fault_repos = [] for repo_name in self.gitea.get_all_repo_names(): if not repo_name.endswith("p1"): @@ -48,7 +54,68 @@ class MyTeapot(Teapot): fault_repos.append(repo_name) logger.info(f"{len(fault_repos)} fault repo(s): {fault_repos}") + def p1_submit(self) -> None: + res_dict = {} + assignment_name = "p1.3" + assignment = first(self.canvas.assignments, lambda x: x.name == assignment_name) + if assignment is None: + logger.info(f"Canvas assignment {assignment_name} not found") + return + assignment = cast(Assignment, assignment) + students = self.canvas.students + for submission in assignment.get_submissions(): + student = first(students, lambda x: x.id == submission.user_id) + if student is None: + continue + repo_name = default_repo_name_convertor(student) + "-p1" + repo_dir = os.path.join(self.git.repos_dir, repo_name) + base_score, base_url = self.joj.submit_dir( + "https://joj.sjtu.edu.cn/d/ve482_fall_2021/p/61c2d0b27fe7290006b27034", + repo_dir, + "make", + ) + bonus_score, bonus_url = self.joj.submit_dir( + "https://joj.sjtu.edu.cn/d/ve482_fall_2021/p/61c2d49e7fe7290006b2703e", + repo_dir, + "make", + ) + total_score = base_score / 520 * 100 + bonus_score / 220 * 30 + res_dict[student.sis_login_id] = total_score + data = { + "submission": {"posted_grade": round(total_score, 2)}, + "comment": { + "text_comment": ( + f"base score: {base_score} / 520, url: {base_url}\n" + f"bonus score: {bonus_score} / 220, url: {bonus_url}\n" + f"total score: {base_score} / 520 * 100 + " + f"{bonus_score} / 220 * 30" + ) + }, + } + submission.edit(**data) + float_grades = list(res_dict.values()) + summary = [ + min(float_grades), + percentile(float_grades, 0.25), + percentile(float_grades, 0.5), + percentile(float_grades, 0.75), + max(float_grades), + ] + average_grade = sum(float_grades) / len(float_grades) + logger.info( + f"Grades summary: " + f"Min: {summary[0]:.2f}, " + f"Q1: {summary[1]:.2f}, " + f"Q2: {summary[2]:.2f}, " + f"Q3: {summary[3]:.2f}, " + f"Max: {summary[4]:.2f}, " + f"Average: {average_grade:.2f}" + ) + json.dump( + res_dict, open("ve482_p1_grade.json", "w"), ensure_ascii=False, indent=4 + ) + if __name__ == "__main__": - teapot = MyTeapot() - teapot.ve482p1() + teapot = VE482Teapot() + teapot.p1_submit() diff --git a/joint_teapot/config.py b/joint_teapot/config.py index 6d3354e..bcb590c 100644 --- a/joint_teapot/config.py +++ b/joint_teapot/config.py @@ -19,6 +19,9 @@ class Settings(BaseSettings): # git repos_dir: str = "./repos" + # sid + joj_sid: str = "" + # log file log_file_path: str = "joint-teapot.log" diff --git a/joint_teapot/teapot.py b/joint_teapot/teapot.py index 55a497b..e4cf87d 100644 --- a/joint_teapot/teapot.py +++ b/joint_teapot/teapot.py @@ -6,6 +6,7 @@ from joint_teapot.config import settings from joint_teapot.utils.logger import logger from joint_teapot.utils.main import first from joint_teapot.workers import Canvas, Git, Gitea +from joint_teapot.workers.joj import JOJ _T = TypeVar("_T") @@ -39,6 +40,7 @@ class Teapot: _canvas = None _gitea = None _git = None + _joj = None @property def canvas(self) -> Canvas: @@ -58,6 +60,12 @@ class Teapot: self._git = Git() return self._git + @property + def joj(self) -> JOJ: + if not self._joj: + self._joj = JOJ() + return self._joj + def __init__(self) -> None: logger.info( "Settings loaded. " diff --git a/joint_teapot/utils/main.py b/joint_teapot/utils/main.py index 354c61f..c44c776 100644 --- a/joint_teapot/utils/main.py +++ b/joint_teapot/utils/main.py @@ -1,6 +1,9 @@ import math +import re from typing import Callable, Iterable, Optional, TypeVar +from canvasapi.user import User + _T = TypeVar("_T") @@ -24,3 +27,11 @@ def percentile( d0 = key(N[int(f)]) * (c - k) d1 = key(N[int(c)]) * (k - f) return d0 + d1 + + +def default_repo_name_convertor(user: User) -> str: + sis_login_id, name = user.sis_login_id, user.name + eng = re.sub("[\u4e00-\u9fa5]", "", name) + eng = eng.replace(",", "") + eng = "".join([word[0].capitalize() + word[1:] for word in eng.split()]) + return f"{eng}{sis_login_id}" diff --git a/joint_teapot/workers/git.py b/joint_teapot/workers/git.py index a8ee8a0..25f290d 100644 --- a/joint_teapot/workers/git.py +++ b/joint_teapot/workers/git.py @@ -3,13 +3,12 @@ import sys from time import sleep from typing import Optional -from git.exc import GitCommandError - from joint_teapot.utils.logger import logger current_path = sys.path[0] sys.path.remove(current_path) from git import Repo +from git.exc import GitCommandError sys.path.insert(0, current_path) diff --git a/joint_teapot/workers/gitea.py b/joint_teapot/workers/gitea.py index 7a5a086..c9ed772 100644 --- a/joint_teapot/workers/gitea.py +++ b/joint_teapot/workers/gitea.py @@ -1,4 +1,3 @@ -import re from enum import Enum from functools import lru_cache from typing import Any, Callable, Dict, List, Optional, Tuple @@ -11,7 +10,7 @@ from focs_gitea.rest import ApiException from joint_teapot.config import settings from joint_teapot.utils.logger import logger -from joint_teapot.utils.main import first +from joint_teapot.utils.main import default_repo_name_convertor, first class PermissionEnum(Enum): @@ -20,14 +19,6 @@ class PermissionEnum(Enum): admin = "admin" -def default_repo_name_convertor(user: User) -> Optional[str]: - sis_login_id, name = user.sis_login_id, user.name - eng = re.sub("[\u4e00-\u9fa5]", "", name) - eng = eng.replace(",", "") - eng = "".join([word[0].capitalize() + word[1:] for word in eng.split()]) - return f"{eng}{sis_login_id}" - - def list_all(method: Callable[..., Any], *args: Any, **kwargs: Any) -> Any: all_res = [] page = 1 diff --git a/joint_teapot/workers/joj.py b/joint_teapot/workers/joj.py index 7c8c9b8..eea71e8 100644 --- a/joint_teapot/workers/joj.py +++ b/joint_teapot/workers/joj.py @@ -1,2 +1,52 @@ +import io +import os +import zipfile +from typing import Tuple + +from colorama import Fore, Style, init +from joj_submitter import JOJSubmitter, Language + +from joint_teapot.config import settings +from joint_teapot.utils.logger import logger + + class JOJ: - ... + def __init__(self, sid: str = settings.joj_sid): + init() + self.submitter = JOJSubmitter(sid, logger) + + def submit_dir(self, problem_url: str, path: str, lang: str) -> Tuple[int, str]: + exclude_paths = [".git"] + zip_buffer = io.BytesIO() + zip_buffer.name = f"{os.path.basename(path)}.zip" + with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED) as zip_file: + for root, dirs, files in os.walk(path): + dirs[:] = [d for d in dirs if d not in exclude_paths] + for file in files: + zip_file.write( + os.path.join(root, file), + os.path.relpath(os.path.join(root, file), path), + ) + zip_buffer.seek(0) + response = self.submitter.upload_file(problem_url, zip_buffer, lang) + if response.status_code != 200: + logger.error( + f"{path} submit to JOJ error, status code {response.status_code}" + ) + return -1, "" + logger.info(f"{path} submit to JOJ succeed, record url {response.url}") + record = self.submitter.get_status(response.url) + fore_color = Fore.RED if record.status != "Accepted" else Fore.GREEN + logger.info( + f"status: {fore_color}{record.status}{Style.RESET_ALL}, " + + f"accept number: {Fore.BLUE}{record.accepted_count}{Style.RESET_ALL}, " + + f"score: {Fore.BLUE}{record.score}{Style.RESET_ALL}, " + + f"total time: {Fore.BLUE}{record.total_time}{Style.RESET_ALL}, " + + f"peak memory: {Fore.BLUE}{record.peak_memory}{Style.RESET_ALL}" + ) + score_int = 0 + try: + score_int = int(record.score) + except ValueError: + pass + return score_int, response.url