feat: support exporting Canvas group members with group info (#7)
Some checks failed
build / trigger-build-image (push) Has been cancelled
Some checks failed
build / trigger-build-image (push) Has been cancelled
## Summary Add an optional `--group` argument to `export-users`. When `--group` is not provided, the command keeps the original behavior and exports all Canvas users as: `name,sis_id,login_id` When `--group <group_set_prefix>` is provided, the command looks up the corresponding Canvas group set, exports only users in that group set, and appends the normalized group name as the fourth column: `name,sis_id,login_id,group_name` For example, if the group set is `p2team`, the exported group names will be formatted as `p2team01`, `p2team02`, `p2team10`, etc. ## Changes - Add optional `--group` support to `export-users` - Look up Canvas group sets by exact name first, then by prefix - Export only members in the matched group set when `--group` is used - Append the user's group name as the fourth CSV column - Normalize Canvas group names like `p1team 1` and `p1team10` into `p1team01` and `p1team10` - Sort exported rows by group number in ascending order - Handle invalid input more gracefully: - if no matching group set is found, exit cleanly with an error log - if a Canvas group name does not follow the expected `group_set_name + number` pattern, exit cleanly with an error log ## Behavior ### Default `joint-teapot export-users students.csv` Output format: `name,sis_id,login_id` ### With group export `joint-teapot export-users p2.csv --group p2team` Output format: `name,sis_id,login_id,p2team01` ## Notes This change keeps the original export behavior unchanged unless `--group` is explicitly provided. Reviewed-on: #7 Reviewed-by: 张泊明518370910136 <bomingzh@sjtu.edu.cn> Co-authored-by: egghead_yao <egghead_yao@sjtu.edu.cn> Co-committed-by: egghead_yao <egghead_yao@sjtu.edu.cn>
This commit is contained in:
parent
0543701712
commit
2ac29a6d89
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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} <number>"'
|
||||
)
|
||||
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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user