Compare commits

..

1 Commits

Author SHA1 Message Date
fabcce0893
fix(regex): full match 2025-06-20 22:58:29 +08:00
4 changed files with 49 additions and 202 deletions

View File

@ -50,17 +50,13 @@ def add_all_canvas_students_to_teams(team_names: List[str]) -> None:
"create-personal-repos", "create-personal-repos",
help="create personal repos on gitea for all canvas students", help="create personal repos on gitea for all canvas students",
) )
def create_personal_repos_for_all_canvas_students( def create_personal_repos_for_all_canvas_students(suffix: str = Option("")) -> None:
suffix: str = Option(""), template: str = Option("", help="generate from template") tea.pot.create_personal_repos_for_all_canvas_students(suffix)
) -> None:
tea.pot.create_personal_repos_for_all_canvas_students(suffix, template)
@app.command("create-teams", help="create teams on gitea by canvas groups") @app.command("create-teams", help="create teams on gitea by canvas groups")
def create_teams_and_repos_by_canvas_groups( def create_teams_and_repos_by_canvas_groups(group_prefix: str) -> None:
group_prefix: str, template: str = Option("", help="generate from template") tea.pot.create_teams_and_repos_by_canvas_groups(group_prefix)
) -> None:
tea.pot.create_teams_and_repos_by_canvas_groups(group_prefix, template)
@app.command("get-public-keys", help="list all public keys on gitea") @app.command("get-public-keys", help="list all public keys on gitea")
@ -299,23 +295,6 @@ def joj3_all_env(
True, True,
help="whether to include submitter in issue title", help="whether to include submitter in issue title",
), ),
issue_label_name: str = Option(
"Kind/Testing",
help="label name for the issue created by this command",
),
issue_label_color: str = Option(
"#795548",
help="label color for the issue created by this command",
),
end_time: Optional[datetime] = Option(None),
penalty_config: str = Option(
"",
help=(
"Configuration for penalties in the format "
"'hours=factor'. "
"Example: --penalty-config 24=0.75,48=0.5"
),
),
) -> None: ) -> None:
app.pretty_exceptions_enable = False app.pretty_exceptions_enable = False
set_settings(Settings(_env_file=env_path)) set_settings(Settings(_env_file=env_path))
@ -331,7 +310,6 @@ def joj3_all_env(
logger.error("missing required env var") logger.error("missing required env var")
raise Exit(code=1) raise Exit(code=1)
submitter_repo_name = env.github_repository.split("/")[-1] submitter_repo_name = env.github_repository.split("/")[-1]
penalty_factor = joj3.get_penalty_factor(end_time, penalty_config)
total_score = joj3.get_total_score(env.joj3_output_path) total_score = joj3.get_total_score(env.joj3_output_path)
res = { res = {
"totalScore": total_score, "totalScore": total_score,
@ -358,9 +336,6 @@ def joj3_all_env(
gitea_actions_url, gitea_actions_url,
submitter_in_issue_title, submitter_in_issue_title,
submitter_repo_name, submitter_repo_name,
issue_label_name,
issue_label_color,
penalty_factor,
) )
res["issue"] = issue_number res["issue"] = issue_number
gitea_issue_url = f"{submitter_repo_url}/issues/{issue_number}" gitea_issue_url = f"{submitter_repo_url}/issues/{issue_number}"
@ -471,16 +446,8 @@ def joj3_check_env(
"Example: --group-config joj=10:24,run=20:48" "Example: --group-config joj=10:24,run=20:48"
), ),
), ),
begin_time: Optional[datetime] = Option(None), valid_after: Optional[datetime] = Option(None),
end_time: Optional[datetime] = Option(None), valid_before: Optional[datetime] = Option(None),
penalty_config: str = Option(
"",
help=(
"Configuration for penalties in the format "
"'hours=factor'. "
"Example: --penalty-config 24=0.75,48=0.5"
),
),
) -> None: ) -> None:
app.pretty_exceptions_enable = False app.pretty_exceptions_enable = False
set_settings(Settings(_env_file=env_path)) set_settings(Settings(_env_file=env_path))
@ -494,9 +461,8 @@ def joj3_check_env(
logger.error("missing required env var") logger.error("missing required env var")
raise Exit(code=1) raise Exit(code=1)
time_msg, time_failed = tea.pot.joj3_check_submission_time( time_msg, time_failed = tea.pot.joj3_check_submission_time(
begin_time, valid_after,
end_time, valid_before,
penalty_config,
) )
count_msg, count_failed = tea.pot.joj3_check_submission_count( count_msg, count_failed = tea.pot.joj3_check_submission_count(
env, grading_repo_name, group_config, scoreboard_filename env, grading_repo_name, group_config, scoreboard_filename

View File

@ -96,16 +96,15 @@ class Teapot:
return self.gitea.add_canvas_students_to_teams(self.canvas.students, team_names) return self.gitea.add_canvas_students_to_teams(self.canvas.students, team_names)
def create_personal_repos_for_all_canvas_students( def create_personal_repos_for_all_canvas_students(
self, suffix: str = "", template: str = "" self, suffix: str = ""
) -> List[str]: ) -> List[str]:
return self.gitea.create_personal_repos_for_canvas_students( return self.gitea.create_personal_repos_for_canvas_students(
self.canvas.students, self.canvas.students,
lambda user: default_repo_name_convertor(user) + suffix, lambda user: default_repo_name_convertor(user) + suffix,
template,
) )
def create_teams_and_repos_by_canvas_groups( def create_teams_and_repos_by_canvas_groups(
self, group_prefix: str = "", template: str = "" self, group_prefix: str = ""
) -> List[str]: ) -> List[str]:
def convertor(name: str) -> Optional[str]: def convertor(name: str) -> Optional[str]:
if group_prefix and not name.startswith(group_prefix): if group_prefix and not name.startswith(group_prefix):
@ -115,7 +114,7 @@ class Teapot:
return f"{team_name}{number:02}" return f"{team_name}{number:02}"
return self.gitea.create_teams_and_repos_by_canvas_groups( return self.gitea.create_teams_and_repos_by_canvas_groups(
self.canvas.students, self.canvas.groups, convertor, convertor, template self.canvas.students, self.canvas.groups, convertor, convertor
) )
def get_public_key_of_all_canvas_students(self) -> Dict[str, List[str]]: def get_public_key_of_all_canvas_students(self) -> Dict[str, List[str]]:
@ -238,9 +237,6 @@ class Teapot:
gitea_actions_url: str, gitea_actions_url: str,
submitter_in_issue_title: bool, submitter_in_issue_title: bool,
submitter_repo_name: str, submitter_repo_name: str,
issue_label_name: str,
issue_label_color: str,
penalty_factor: float,
) -> int: ) -> int:
title, comment = joj3.generate_title_and_comment( title, comment = joj3.generate_title_and_comment(
env.joj3_output_path, env.joj3_output_path,
@ -252,7 +248,6 @@ class Teapot:
submitter_in_issue_title, submitter_in_issue_title,
env.joj3_run_id, env.joj3_run_id,
max_total_score, max_total_score,
penalty_factor,
) )
title_prefix = joj3.get_title_prefix( title_prefix = joj3.get_title_prefix(
env.joj3_conf_name, env.github_actor, submitter_in_issue_title env.joj3_conf_name, env.github_actor, submitter_in_issue_title
@ -269,24 +264,10 @@ class Teapot:
break break
else: else:
new_issue = True new_issue = True
labels = self.gitea.issue_api.issue_list_labels(
self.gitea.org_name, submitter_repo_name
)
label_id = 0
label = first(labels, lambda label: label.name == issue_label_name)
if label:
label_id = label.id
else:
label = self.gitea.issue_api.issue_create_label(
self.gitea.org_name,
submitter_repo_name,
body={"name": issue_label_name, "color": issue_label_color},
)
label_id = label.id
joj3_issue = self.gitea.issue_api.issue_create_issue( joj3_issue = self.gitea.issue_api.issue_create_issue(
self.gitea.org_name, self.gitea.org_name,
submitter_repo_name, submitter_repo_name,
body={"title": title, "body": comment, "labels": [label_id]}, body={"title": title, "body": comment},
) )
logger.info(f"created joj3 issue: #{joj3_issue.number}") logger.info(f"created joj3 issue: #{joj3_issue.number}")
gitea_issue_url = joj3_issue.html_url gitea_issue_url = joj3_issue.html_url
@ -302,49 +283,21 @@ class Teapot:
def joj3_check_submission_time( def joj3_check_submission_time(
self, self,
begin_time: Optional[datetime] = None, valid_after: Optional[datetime] = None,
end_time: Optional[datetime] = None, valid_before: Optional[datetime] = None,
penalty_config: str = "",
) -> Tuple[str, bool]: ) -> Tuple[str, bool]:
now = datetime.now() now = datetime.now()
penalties = joj3.parse_penalty_config(penalty_config) if (valid_after and now < valid_after) or (valid_before and now > valid_before):
if penalties and end_time: return (
penalty_end_time = end_time + timedelta(hours=penalties[-1][0]) "### Submission Time Check Failed:\n"
if begin_time and now < begin_time: f"Current time {now} is not in the valid range "
return ( f"[{valid_after}, {valid_before}].\n",
"### Submission Time Check Failed\n" True,
f"Current time {now} is not in the valid range " )
f"[{begin_time}, {end_time}].\n",
True,
)
elif now > penalty_end_time:
return (
"### Submission Time Check Failed\n"
f"Current time {now} is not in the valid range "
f"[{begin_time}, {end_time}], and the penalty range "
f"[{end_time + timedelta(seconds=1)}, {penalty_end_time}].\n",
True,
)
else:
return (
"### Submission Time Check Passed\n"
f"Current time {now} is not in the valid range "
f"[{begin_time}, {end_time}], but in the penalty range "
f"[{end_time + timedelta(seconds=1)}, {penalty_end_time}].\n",
False,
)
else:
if (begin_time and now < begin_time) or (end_time and now > end_time):
return (
"### Submission Time Check Failed\n"
f"Current time {now} is not in the valid range "
f"[{begin_time}, {end_time}].\n",
True,
)
return ( return (
"### Submission Time Check Passed\n" "### Submission Time Check Passed:\n"
f"Current time {now} is in the valid range " f"Current time {now} is in the valid range "
f"[{begin_time}, {end_time}].\n", f"[{valid_after}, {valid_before}].\n",
False, False,
) )
@ -369,11 +322,7 @@ class Teapot:
time_windows = [] time_windows = []
valid_items = [] valid_items = []
for item in items: for item in items:
if "=" not in item:
continue
name, values = item.split("=") name, values = item.split("=")
if ":" not in values:
continue
max_count, time_period = map(int, values.split(":")) max_count, time_period = map(int, values.split(":"))
if max_count < 0 or time_period < 0: if max_count < 0 or time_period < 0:
continue continue
@ -445,9 +394,9 @@ class Teapot:
comment += "." comment += "."
comment += "\n" comment += "\n"
if failed: if failed:
title = "### Submission Count Check Failed" title = "### Submission Count Check Failed:"
else: else:
title = "### Submission Count Check Passed" title = "### Submission Count Check Passed:"
msg = f"{title}\n{comment}\n" msg = f"{title}\n{comment}\n"
return msg, failed return msg, failed

View File

@ -2,8 +2,8 @@ import bisect
import csv import csv
import json import json
import os import os
from datetime import datetime, timedelta from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Tuple
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
@ -215,7 +215,6 @@ def generate_title_and_comment(
submitter_in_title: bool = True, submitter_in_title: bool = True,
run_id: str = "unknown", run_id: str = "unknown",
max_total_score: int = -1, max_total_score: int = -1,
penalty_factor: float = 1.0,
) -> Tuple[str, str]: ) -> Tuple[str, str]:
with open(score_file_path) as json_file: with open(score_file_path) as json_file:
stages: List[Dict[str, Any]] = json.load(json_file) stages: List[Dict[str, Any]] = json.load(json_file)
@ -235,10 +234,6 @@ def generate_title_and_comment(
"Powered by [JOJ3](https://github.com/joint-online-judge/JOJ3) and " "Powered by [JOJ3](https://github.com/joint-online-judge/JOJ3) and "
"[Joint-Teapot](https://github.com/BoYanZh/Joint-Teapot) with ❤️.\n" "[Joint-Teapot](https://github.com/BoYanZh/Joint-Teapot) with ❤️.\n"
) )
if penalty_factor != 1.0:
comment += (
f"## Total Score Penalty Warning\nThe total score is multiplied by 0.75.\n"
)
for stage in stages: for stage in stages:
if all( if all(
result["score"] == 0 and result["comment"].strip() == "" result["score"] == 0 and result["comment"].strip() == ""
@ -259,8 +254,6 @@ def generate_title_and_comment(
comment += "</details>\n\n" comment += "</details>\n\n"
total_score += result["score"] total_score += result["score"]
comment += "\n" comment += "\n"
if penalty_factor != 1.0:
total_score = round(total_score * penalty_factor)
title = get_title_prefix(exercise_name, submitter, submitter_in_title) title = get_title_prefix(exercise_name, submitter, submitter_in_title)
if max_total_score >= 0: if max_total_score >= 0:
title += f"{total_score} / {max_total_score}" title += f"{total_score} / {max_total_score}"
@ -288,31 +281,3 @@ def get_title_prefix(
if not submitter_in_title: if not submitter_in_title:
title = f"JOJ3 Result for {exercise_name} - Score: " title = f"JOJ3 Result for {exercise_name} - Score: "
return title return title
def parse_penalty_config(penalty_config: str) -> List[Tuple[float, float]]:
res = []
for penalty in penalty_config.split(","):
if "=" not in penalty:
continue
hour, factor = map(float, penalty.split("="))
res.append((hour, factor))
res.sort(key=lambda x: x[0])
return res
def get_penalty_factor(
end_time: Optional[datetime],
penalty_config: str,
) -> float:
if not end_time or not penalty_config:
return 1.0
penalties = parse_penalty_config(penalty_config)
now = datetime.now()
res = 0.0
for hour, factor in penalties[::-1]:
if now < end_time + timedelta(hours=hour):
res = factor
else:
break
return res

View File

@ -116,7 +116,6 @@ class Gitea:
repo_name_convertor: Callable[ repo_name_convertor: Callable[
[User], Optional[str] [User], Optional[str]
] = default_repo_name_convertor, ] = default_repo_name_convertor,
template: str = "",
) -> List[str]: ) -> List[str]:
repo_names = [] repo_names = []
for student in students: for student in students:
@ -124,32 +123,17 @@ class Gitea:
if repo_name is None: if repo_name is None:
continue continue
repo_names.append(repo_name) repo_names.append(repo_name)
body = {
"auto_init": False,
"default_branch": settings.default_branch,
"name": repo_name,
"private": True,
"template": False,
"trust_model": "default",
}
try: try:
try: try:
if template == "": self.organization_api.create_org_repo(self.org_name, body=body)
body = {
"auto_init": False,
"default_branch": settings.default_branch,
"name": repo_name,
"private": True,
"template": False,
"trust_model": "default",
}
self.organization_api.create_org_repo(self.org_name, body=body)
else:
body = {
"default_branch": settings.default_branch,
"git_content": True,
"git_hooks": True,
"labels": True,
"name": repo_name,
"owner": self.org_name,
"private": True,
"protected_branch": True,
}
self.repository_api.generate_repo(
self.org_name, template, body=body
)
logger.info( logger.info(
f"Personal repo {self.org_name}/{repo_name} for {student} created" f"Personal repo {self.org_name}/{repo_name} for {student} created"
) )
@ -174,7 +158,6 @@ class Gitea:
groups: PaginatedList, groups: PaginatedList,
team_name_convertor: Callable[[str], Optional[str]] = lambda name: name, team_name_convertor: Callable[[str], Optional[str]] = lambda name: name,
repo_name_convertor: Callable[[str], Optional[str]] = lambda name: name, repo_name_convertor: Callable[[str], Optional[str]] = lambda name: name,
template: str = "",
permission: PermissionEnum = PermissionEnum.write, permission: PermissionEnum = PermissionEnum.write,
) -> List[str]: ) -> List[str]:
repo_names = [] repo_names = []
@ -207,37 +190,21 @@ class Gitea:
], ],
}, },
) )
logger.info(f"Team {team_name} created") logger.info(f"{self.org_name}/{team_name} created")
if first(repos, lambda repo: repo.name == repo_name) is None: if first(repos, lambda repo: repo.name == repo_name) is None:
repo_names.append(repo_name) repo_names.append(repo_name)
if template == "": self.organization_api.create_org_repo(
self.organization_api.create_org_repo( self.org_name,
self.org_name, body={
body={ "auto_init": False,
"auto_init": False, "default_branch": settings.default_branch,
"default_branch": settings.default_branch, "name": repo_name,
"name": repo_name, "private": True,
"private": True, "template": False,
"template": False, "trust_model": "default",
"trust_model": "default", },
}, )
) logger.info(f"Team {team_name} created")
else:
self.repository_api.generate_repo(
self.org_name,
template,
body={
"default_branch": settings.default_branch,
"git_content": True,
"git_hooks": True,
"labels": True,
"name": repo_name,
"owner": self.org_name,
"private": True,
"protected_branch": True,
},
)
logger.info(f"{self.org_name}/{team_name} created")
try: try:
self.organization_api.org_add_team_repository( self.organization_api.org_add_team_repository(
team.id, self.org_name, repo_name team.id, self.org_name, repo_name