From da026005118492b42baeaabb3fb906cfaa17033b Mon Sep 17 00:00:00 2001 From: egghead_yao Date: Tue, 2 Jun 2026 17:46:51 +0800 Subject: [PATCH 1/3] fix: ji to gc in config --- joint_teapot/config.py | 6 +++--- joint_teapot/utils/joj3.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/joint_teapot/config.py b/joint_teapot/config.py index c6bd025..8015a1b 100644 --- a/joint_teapot/config.py +++ b/joint_teapot/config.py @@ -16,19 +16,19 @@ class Settings(BaseSettings): canvas_course_id: int = 0 # gitea - gitea_domain_name: str = "focs.ji.sjtu.edu.cn" + gitea_domain_name: str = "focs.gc.sjtu.edu.cn" gitea_suffix: str = "/git" gitea_access_token: str = "" gitea_org_name: str = "" gitea_debug: bool = False # git - git_host: str = "ssh://git@focs.ji.sjtu.edu.cn:2222" + git_host: str = "ssh://git@focs.gc.sjtu.edu.cn:2222" repos_dir: str = "./repos" default_branch: str = "master" # mattermost - mattermost_domain_name: str = "focs.ji.sjtu.edu.cn" + mattermost_domain_name: str = "focs.gc.sjtu.edu.cn" mattermost_suffix: str = "/mm" mattermost_access_token: str = "" mattermost_team: str = "" diff --git a/joint_teapot/utils/joj3.py b/joint_teapot/utils/joj3.py index 9ab0e4b..9a99fd6 100644 --- a/joint_teapot/utils/joj3.py +++ b/joint_teapot/utils/joj3.py @@ -244,7 +244,7 @@ def generate_title_and_comment( f"Generated at {now} from [Gitea Actions #{run_number}]({action_link}), " f"commit {commit_hash}, " f"triggered by @{submitter}, " - f"run ID [`{run_id}`](https://focs.ji.sjtu.edu.cn/joj-mon/d/{settings.gitea_org_name}?var-Filters=RunID%7C%3D%7C{run_id}).\n" + f"run ID [`{run_id}`](https://focs.gc.sjtu.edu.cn/joj-mon/d/{settings.gitea_org_name}?var-Filters=RunID%7C%3D%7C{run_id}).\n" "Powered by [JOJ3](https://github.com/joint-online-judge/JOJ3) and " "[Joint-Teapot](https://github.com/BoYanZh/Joint-Teapot) with ❤️.\n" ) -- 2.30.2 From e80e319e42e84cb4b1a073f3dcf32738ea7b5bad Mon Sep 17 00:00:00 2001 From: egghead_yao Date: Tue, 9 Jun 2026 15:22:22 +0800 Subject: [PATCH 2/3] feat: new option --group for export-users --- joint_teapot/workers/canvas.py | 71 +++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/joint_teapot/workers/canvas.py b/joint_teapot/workers/canvas.py index 2ba1782..8c5e08a 100644 --- a/joint_teapot/workers/canvas.py +++ b/joint_teapot/workers/canvas.py @@ -3,10 +3,11 @@ import os import re from glob import glob from pathlib import Path -from typing import cast +from typing import Dict, List, Tuple, cast from canvasapi import Canvas as PyCanvas from canvasapi.assignment import Assignment +from canvasapi.group import GroupCategory from canvasapi.user import User from patoolib import extract_archive from patoolib.util import PatoolError @@ -86,7 +87,73 @@ Teaching Team""" print(f"Subject: [{settings.gitea_org_name}] Important: wrong Canvas email") print(f"Body:\n{SAMPLE_EMAIL_BODY}") - def export_users_to_csv(self, filename: Path) -> None: + def _get_group_set_by_prefix(self, group_prefix: str) -> GroupCategory: + group_sets = list(self.course.get_group_categories()) + exact_match = first(group_sets, lambda group_set: group_set.name == group_prefix) + if exact_match is not None: + return cast(GroupCategory, exact_match) + matched_group_sets = [ + group_set + for group_set in group_sets + if group_set.name.startswith(group_prefix) + ] + if len(matched_group_sets) == 0: + raise ValueError( + f'Canvas group set with prefix "{group_prefix}" not found' + ) + if len(matched_group_sets) > 1: + matched_names = ", ".join(group_set.name for group_set in matched_group_sets) + raise ValueError( + f'Multiple Canvas group sets match prefix "{group_prefix}": {matched_names}' + ) + return matched_group_sets[0] + + def _parse_group_name(self, group_set_name: str, group_name: str) -> Tuple[int, str]: + match = re.fullmatch(rf"{re.escape(group_set_name)}\s*(\d+)", group_name) + if match is None: + raise ValueError( + f'Canvas group "{group_name}" in group set "{group_set_name}" ' + + f'should be named as "{group_set_name} "' + ) + group_number = int(match.group(1)) + return group_number, f"{group_set_name}{group_number:02}" + + def _get_group_users_csv_rows(self, group_prefix: str) -> Tuple[str, List[List[str]]]: + group_set = self._get_group_set_by_prefix(group_prefix) + users_by_id: Dict[int, User] = {user.id: user for user in self.users} + rows = [] + groups = [] + for group in group_set.get_groups(): + group_number, normalized_group_name = self._parse_group_name( + group_set.name, group.name + ) + groups.append((group_number, normalized_group_name, group)) + for _, normalized_group_name, group in sorted(groups, key=lambda item: item[0]): + group_rows = [] + for membership in group.get_memberships(): + user = users_by_id.get(membership.user_id) + if user is None: + logger.warning( + f"user with id {membership.user_id} found in {group.name} " + + "but not found in course users" + ) + continue + group_rows.append( + [user.name, user.sis_id, user.login_id, normalized_group_name] + ) + rows.extend(sorted(group_rows)) + return group_set.name, rows + + def export_users_to_csv(self, filename: Path, group_prefix: str = "") -> None: + if group_prefix: + group_set_name, rows = self._get_group_users_csv_rows(group_prefix) + with open(filename, mode="w", newline="") as file: + writer = csv.writer(file) + for row in rows: + writer.writerow(row) + logger.info(f'Users in group set "{group_set_name}" exported to {filename}') + return + with open(filename, mode="w", newline="") as file: writer = csv.writer(file) for user in self.users: -- 2.30.2 From 666457ed150ce60ebcfef817454cf347f137bf44 Mon Sep 17 00:00:00 2001 From: egghead_yao Date: Tue, 9 Jun 2026 15:22:54 +0800 Subject: [PATCH 3/3] feat: regist --group in app --- joint_teapot/app.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/joint_teapot/app.py b/joint_teapot/app.py index c0bc0bb..569f6c3 100644 --- a/joint_teapot/app.py +++ b/joint_teapot/app.py @@ -35,8 +35,17 @@ tea = Tea() # lazy loader @app.command("export-users", help="export users from canvas to csv file") -def export_users_to_csv(output_file: Path = Argument("students.csv")) -> None: - tea.pot.canvas.export_users_to_csv(output_file) +def export_users_to_csv( + output_file: Path = Argument("students.csv"), + group_prefix: str = Option( + "", "--group", help="export members in matched canvas group set" + ), +) -> None: + try: + tea.pot.canvas.export_users_to_csv(output_file, group_prefix=group_prefix) + except ValueError as e: + logger.error(e) + raise Exit(code=1) @app.command( -- 2.30.2