From 1605335a1c183a50fc101e7196f475696cc05702 Mon Sep 17 00:00:00 2001 From: ZJChe Date: Sun, 9 Jun 2024 05:12:29 +0800 Subject: [PATCH] feat: JOJ3 scoreboard (#28) Co-authored-by: BoYanZh --- joint_teapot/app.py | 44 +++++++++++++++ joint_teapot/utils/joj3.py | 104 ++++++++++++++++++++++++++++++++++++ joint_teapot/workers/git.py | 18 ++++++- requirements.txt | 2 +- 4 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 joint_teapot/utils/joj3.py diff --git a/joint_teapot/app.py b/joint_teapot/app.py index dcd6da0..de098d7 100644 --- a/joint_teapot/app.py +++ b/joint_teapot/app.py @@ -1,10 +1,13 @@ +import os from datetime import datetime from pathlib import Path from typing import List +from git import Repo from typer import Argument, Option, Typer, echo from joint_teapot.teapot import Teapot +from joint_teapot.utils import joj3 from joint_teapot.utils.logger import logger app = Typer(add_completion=False) @@ -192,6 +195,47 @@ def unsubscribe_from_repos(pattern: str = Argument("")) -> None: tea.pot.gitea.unsubscribe_from_repos(pattern) +@app.command( + "JOJ3-scoreboard", + help="parse JOJ3 scoreboard json file and upload to gitea", +) +def JOJ3_scoreboard( + scorefile_path: str = Argument( + "", help="path to score json file generated by JOJ3" + ), + student_name: str = Argument("", help="name of student"), + student_id: str = Argument("", help="id of student"), + repo_name: str = Argument( + "", + help="name of local gitea repo folder, or link to remote gitea repo, to push scoreboard file", + ), + scoreboard_file_name: str = Argument( + "", help="name of scoreboard file in the gitea repo" + ), +) -> None: + repo_path = tea.pot.git.repo_clean_and_checkout(repo_name, "grading") + repo: Repo = tea.pot.git.get_repo(repo_name) + if "grading" not in repo.remote().refs: + logger.error( + '"grading" branch not found in remote, create and push it to origin first.' + ) + return + if "grading" not in repo.branches: + logger.error('"grading" branch not found in local, create it first.') + return + repo.git.reset("--hard", "origin/grading") + joj3.generate_scoreboard( + scorefile_path, + student_name, + student_id, + os.path.join(repo_path, scoreboard_file_name), + ) + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + tea.pot.git.add_commit_and_push( + repo_name, [scoreboard_file_name], f"test: JOJ3-dev testing at {now}" + ) + + if __name__ == "__main__": try: app() diff --git a/joint_teapot/utils/joj3.py b/joint_teapot/utils/joj3.py new file mode 100644 index 0000000..6272581 --- /dev/null +++ b/joint_teapot/utils/joj3.py @@ -0,0 +1,104 @@ +import csv +import json +import os +from datetime import datetime +from typing import Any, Dict + +from joint_teapot.utils.logger import logger + + +def generate_scoreboard( + score_file_path: str, student_name: str, student_id: str, scoreboard_file_path: str +) -> None: + if not scoreboard_file_path.endswith(".csv"): + logger.error( + f"Scoreboard file should be a .csv file, but now it is {scoreboard_file_path}" + ) + return + + # Load the csv file if it already exists + if os.path.exists(scoreboard_file_path): + with open(scoreboard_file_path, newline="") as file: + reader = csv.reader(file) + rows = list(reader) + columns = rows[0] + data = rows[1:] + else: + columns = [ + "", + "last_edit", # FIXME: + # This is just to make changes in the file so that it can be pushed. + # Only used in development stage. Will be removed in the future. + "total", + ] + data = [] + + column_updated = [False] * len(columns) # Record wether a score has been updated + # Update data + with open(score_file_path) as json_file: + scorefile: Dict[str, Any] = json.load(json_file) + + student = f"{student_name} {student_id}" + student_found = False + for row in data: + if row[0] == student: + student_row = row # This is a reference of the original data + student_found = True + break + if not student_found: + student_row = [student, "", "0"] + [""] * ( + len(columns) - 3 + ) # FIXME: In formal version should be -2 + data.append(student_row) + + for stagerecord in scorefile["stagerecords"]: + stagename = stagerecord["stagename"] + for stageresult in stagerecord["stageresults"]: + name = stageresult["name"] + for i, result in enumerate(stageresult["results"]): + score = result["score"] + colname = f"{stagename}/{name}" + if len(stageresult["results"]) != 1: + colname = f"{colname}/{i}" + if colname not in columns: + columns.append(colname) + column_updated.append(True) + for row in data: + row.append("") + student_row[columns.index(colname)] = score + column_updated[columns.index(colname)] = True + # Score of any unupdated columns should be cleared + for i, column in enumerate(columns): + if column in ["", "last_edit", "total"]: + continue + if column_updated[i] == False: + student_row[i] = "" + + total = 0 + for col in columns: + if col in ["", "total", "last_edit"]: + continue + idx = columns.index(col) + if (student_row[idx] is not None) and (student_row[idx] != ""): + total += int(student_row[idx]) + + student_row[columns.index("total")] = str(total) + + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + student_row[ + columns.index("last_edit") + ] = now # FIXME: Delete this in formal version + + # Sort data by total + data.sort(key=lambda x: int(x[columns.index("total")]), reverse=True) + + # Write back to the csv file: + with open(scoreboard_file_path, mode="w", newline="") as file: + writer = csv.writer(file) + writer.writerow(columns) + writer.writerows(data) + + +def generate_comment(score_file_path: str) -> str: + # TODO + return "" diff --git a/joint_teapot/workers/git.py b/joint_teapot/workers/git.py index 905808a..ced1b72 100644 --- a/joint_teapot/workers/git.py +++ b/joint_teapot/workers/git.py @@ -1,7 +1,7 @@ import os import sys from time import sleep -from typing import Optional +from typing import List, Optional from joint_teapot.utils.logger import logger @@ -97,3 +97,19 @@ def repo_clean_and_checkout( else: raise return repo_dir + + def add_commit_and_push( + self, repo_name: str, files_to_add: List[str], commit_message: str + ) -> None: + repo: Repo = self.get_repo(repo_name) + for file in files_to_add: + try: + repo.index.add(file) + except OSError: + logger.warning( + f'File path "{file}" does not exist. Skipping this file.' + ) + continue + repo.index.commit(commit_message) + origin = repo.remote(name="origin") + origin.push() diff --git a/requirements.txt b/requirements.txt index 2673928..651349f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,4 @@ mattermostdriver>=7.3.2 patool>=1.12 pydantic>=2.0.2 pydantic-settings>=2.0.1 -typer[all]>=0.3.2 +typer>=0.12.3