Compare commits
No commits in common. "master" and "master" have entirely different histories.
|
@ -1,41 +1,41 @@
|
|||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: requirements-txt-fixer
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.18.2"
|
||||
rev: "v1.4.1"
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
- pydantic
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.20.0
|
||||
rev: v3.9.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
- repo: https://github.com/hadialqattan/pycln
|
||||
rev: v2.5.0
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: pycln
|
||||
args: [-a]
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: '1.8.6'
|
||||
rev: '1.7.5'
|
||||
hooks:
|
||||
- id: bandit
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 6.0.1
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black", "--filter-files"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 25.9.0
|
||||
rev: 23.7.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.5
|
||||
rev: v1.5.1
|
||||
hooks:
|
||||
- id: remove-crlf
|
||||
- id: remove-tabs
|
||||
|
|
|
@ -34,17 +34,9 @@ class Tea:
|
|||
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)
|
||||
|
||||
|
||||
@app.command(
|
||||
"export-wrong-email-users",
|
||||
help="export users with wrong email from canvas in stdout",
|
||||
)
|
||||
def export_wrong_email_users() -> None:
|
||||
tea.pot.canvas.export_wrong_email_users()
|
||||
@app.command("export-students", help="export students from canvas to csv file")
|
||||
def export_students_to_csv(output_file: Path) -> None:
|
||||
tea.pot.canvas.export_students_to_csv(output_file)
|
||||
|
||||
|
||||
@app.command(
|
||||
|
@ -303,10 +295,6 @@ def joj3_all_env(
|
|||
False,
|
||||
help="skip creating failed table on gitea",
|
||||
),
|
||||
scoreboard_column_by_ref: bool = Option(
|
||||
False,
|
||||
help="use git ref as scoreboard column name",
|
||||
),
|
||||
submitter_in_issue_title: bool = Option(
|
||||
True,
|
||||
help="whether to include submitter in issue title",
|
||||
|
@ -319,10 +307,6 @@ def joj3_all_env(
|
|||
"#795548",
|
||||
help="label color for the issue created by this command",
|
||||
),
|
||||
issue_label_exclusive: bool = Option(
|
||||
False,
|
||||
help="label set as exclusive for the issue created by this command",
|
||||
),
|
||||
end_time: Optional[datetime] = Option(None),
|
||||
penalty_config: str = Option(
|
||||
"",
|
||||
|
@ -376,7 +360,6 @@ def joj3_all_env(
|
|||
submitter_repo_name,
|
||||
issue_label_name,
|
||||
issue_label_color,
|
||||
issue_label_exclusive,
|
||||
penalty_factor,
|
||||
)
|
||||
res["issue"] = issue_number
|
||||
|
@ -415,15 +398,11 @@ def joj3_all_env(
|
|||
raise Exit(code=1)
|
||||
repo.git.reset("--hard", "origin/grading")
|
||||
if not skip_scoreboard:
|
||||
exercise_name = env.joj3_conf_name
|
||||
if scoreboard_column_by_ref:
|
||||
exercise_name = env.github_ref
|
||||
joj3.generate_scoreboard(
|
||||
env.joj3_output_path,
|
||||
env.github_actor,
|
||||
os.path.join(repo_path, scoreboard_filename),
|
||||
exercise_name,
|
||||
submitter_repo_name,
|
||||
env.joj3_conf_name,
|
||||
)
|
||||
tea.pot.git.add_commit(
|
||||
grading_repo_name,
|
||||
|
@ -533,16 +512,6 @@ def joj3_check_env(
|
|||
logger.info("joj3-check-env done")
|
||||
|
||||
|
||||
@app.command("joj3-check-gitea-token")
|
||||
def joj3_check_gitea_token(
|
||||
env_path: str = Argument("", help="path to .env file")
|
||||
) -> None:
|
||||
app.pretty_exceptions_enable = False
|
||||
set_settings(Settings(_env_file=env_path))
|
||||
set_logger(settings.stderr_log_level)
|
||||
tea.pot.gitea.organization_api.org_list_repos(settings.gitea_org_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
app()
|
||||
|
|
|
@ -240,7 +240,6 @@ class Teapot:
|
|||
submitter_repo_name: str,
|
||||
issue_label_name: str,
|
||||
issue_label_color: str,
|
||||
issue_label_exclusive: bool,
|
||||
penalty_factor: float,
|
||||
) -> int:
|
||||
title, comment = joj3.generate_title_and_comment(
|
||||
|
@ -281,11 +280,7 @@ class Teapot:
|
|||
label = self.gitea.issue_api.issue_create_label(
|
||||
self.gitea.org_name,
|
||||
submitter_repo_name,
|
||||
body={
|
||||
"name": issue_label_name,
|
||||
"color": issue_label_color,
|
||||
"exclusive": issue_label_exclusive,
|
||||
},
|
||||
body={"name": issue_label_name, "color": issue_label_color},
|
||||
)
|
||||
label_id = label.id
|
||||
joj3_issue = self.gitea.issue_api.issue_create_issue(
|
||||
|
@ -330,7 +325,7 @@ class Teapot:
|
|||
f"[{end_time + timedelta(seconds=1)}, {penalty_end_time}].\n",
|
||||
True,
|
||||
)
|
||||
elif now > end_time:
|
||||
else:
|
||||
return (
|
||||
"### Submission Time Check Passed\n"
|
||||
f"Current time {now} is not in the valid range "
|
||||
|
@ -386,13 +381,11 @@ class Teapot:
|
|||
time_windows.append(since)
|
||||
valid_items.append((name, max_count, time_period, since))
|
||||
logger.info(f"valid items: {valid_items}, time windows: {time_windows}")
|
||||
matched_commits = []
|
||||
all_commits_length = 0
|
||||
all_commits = []
|
||||
if time_windows:
|
||||
earliest_since = min(time_windows).strftime("%Y-%m-%dT%H:%M:%S")
|
||||
commits = repo.iter_commits(paths=scoreboard_filename, since=earliest_since)
|
||||
for commit in commits:
|
||||
all_commits_length += 1
|
||||
lines = commit.message.strip().splitlines()
|
||||
if not lines:
|
||||
continue
|
||||
|
@ -410,19 +403,17 @@ class Teapot:
|
|||
commit_groups = (
|
||||
groups_line[len("groups: ") :].split(",") if groups_line else []
|
||||
)
|
||||
matched_commits.append(
|
||||
all_commits.append(
|
||||
{
|
||||
"time": commit.committed_datetime,
|
||||
"groups": [g.strip() for g in commit_groups],
|
||||
}
|
||||
)
|
||||
logger.info(
|
||||
f"matched commits length: {len(matched_commits)}, all commits length: {all_commits_length}"
|
||||
)
|
||||
logger.info(f"all commits length: {len(all_commits)}")
|
||||
for name, max_count, time_period, since in valid_items:
|
||||
submit_count = 0
|
||||
time_limit = now - timedelta(hours=time_period)
|
||||
for commit in matched_commits:
|
||||
for commit in all_commits:
|
||||
if commit["time"] < time_limit:
|
||||
continue
|
||||
if name:
|
||||
|
|
|
@ -40,7 +40,6 @@ def generate_scoreboard(
|
|||
submitter: str,
|
||||
scoreboard_file_path: str,
|
||||
exercise_name: str,
|
||||
submitter_repo_name: str,
|
||||
) -> None:
|
||||
if not scoreboard_file_path.endswith(".csv"):
|
||||
logger.error(
|
||||
|
@ -49,25 +48,18 @@ def generate_scoreboard(
|
|||
return
|
||||
os.makedirs(os.path.dirname(scoreboard_file_path), exist_ok=True)
|
||||
# Load the csv file if it already exists
|
||||
fixed_headers = ["", "repo_name", "last_edit", "total"]
|
||||
fixed_defaults = [submitter, submitter_repo_name, "", "0"]
|
||||
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:]
|
||||
|
||||
def migrate_scoreboard() -> None:
|
||||
if "repo_name" in columns:
|
||||
return
|
||||
columns.insert(1, "repo_name")
|
||||
for row in data:
|
||||
row.insert(1, "")
|
||||
|
||||
migrate_scoreboard()
|
||||
else:
|
||||
columns = fixed_headers
|
||||
columns = [
|
||||
"",
|
||||
"last_edit",
|
||||
"total",
|
||||
]
|
||||
data = []
|
||||
|
||||
submitter_found = False
|
||||
|
@ -77,7 +69,8 @@ def generate_scoreboard(
|
|||
submitter_found = True
|
||||
break
|
||||
if not submitter_found:
|
||||
submitter_row = fixed_defaults + [""] * (len(columns) - len(fixed_headers))
|
||||
fixed_columns = [submitter, "", "0"]
|
||||
submitter_row = fixed_columns + [""] * (len(columns) - len(fixed_columns))
|
||||
data.append(submitter_row)
|
||||
|
||||
# Update data
|
||||
|
@ -92,9 +85,9 @@ def generate_scoreboard(
|
|||
exercise_name = comment.split("-")[0]
|
||||
# Find if exercise in table:
|
||||
if exercise_name not in columns:
|
||||
column_tail = columns[len(fixed_headers) :]
|
||||
column_tail = columns[3:]
|
||||
bisect.insort(column_tail, exercise_name)
|
||||
columns[len(fixed_headers) :] = column_tail
|
||||
columns[3:] = column_tail
|
||||
index = columns.index(exercise_name)
|
||||
for row in data:
|
||||
row.insert(index, "")
|
||||
|
@ -108,22 +101,17 @@ def generate_scoreboard(
|
|||
|
||||
total = 0
|
||||
for col in columns:
|
||||
if col in fixed_headers:
|
||||
if col in ["", "total", "last_edit"]:
|
||||
continue
|
||||
idx = columns.index(col)
|
||||
if (submitter_row[idx] is not None) and (submitter_row[idx] != ""):
|
||||
try:
|
||||
total += int(submitter_row[idx])
|
||||
except ValueError:
|
||||
pass
|
||||
total += int(submitter_row[idx])
|
||||
|
||||
submitter_row[columns.index("total")] = str(total)
|
||||
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
submitter_row[columns.index("last_edit")] = now
|
||||
|
||||
submitter_row[columns.index("repo_name")] = submitter_repo_name
|
||||
|
||||
# Sort data by total, from low to high
|
||||
data.sort(key=lambda x: int(x[columns.index("total")]))
|
||||
|
||||
|
@ -248,7 +236,9 @@ def generate_title_and_comment(
|
|||
"[Joint-Teapot](https://github.com/BoYanZh/Joint-Teapot) with ❤️.\n"
|
||||
)
|
||||
if penalty_factor != 1.0:
|
||||
comment += f"## ⚠️Total Score Penalty Warning⚠️\n**The total score is multiplied by {penalty_factor}.**\n"
|
||||
comment += (
|
||||
f"## Total Score Penalty Warning\nThe total score is multiplied by 0.75.\n"
|
||||
)
|
||||
for stage in stages:
|
||||
if all(
|
||||
result["score"] == 0 and result["comment"].strip() == ""
|
||||
|
@ -270,7 +260,7 @@ def generate_title_and_comment(
|
|||
total_score += result["score"]
|
||||
comment += "\n"
|
||||
if penalty_factor != 1.0:
|
||||
total_score = round(total_score - abs(total_score) * (1 - penalty_factor))
|
||||
total_score = round(total_score * penalty_factor)
|
||||
title = get_title_prefix(exercise_name, submitter, submitter_in_title)
|
||||
if max_total_score >= 0:
|
||||
title += f"{total_score} / {max_total_score}"
|
||||
|
@ -317,10 +307,8 @@ def get_penalty_factor(
|
|||
) -> float:
|
||||
if not end_time or not penalty_config:
|
||||
return 1.0
|
||||
now = datetime.now()
|
||||
if now < end_time:
|
||||
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):
|
||||
|
|
|
@ -38,7 +38,10 @@ def set_logger(
|
|||
) -> None:
|
||||
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
|
||||
logger.remove()
|
||||
logger.add(stderr, level=stderr_log_level, colorize=stderr.isatty())
|
||||
logger.add(
|
||||
stderr,
|
||||
level=stderr_log_level,
|
||||
)
|
||||
logger.add(settings.log_file_path, level="DEBUG")
|
||||
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ class Canvas:
|
|||
# types = ["student", "observer"]
|
||||
types = ["student"]
|
||||
|
||||
def patch_user(student: User) -> User:
|
||||
def patch_student(student: User) -> User:
|
||||
student.name = (
|
||||
re.sub(r"[^\x00-\x7F]+", "", student.name).strip().title()
|
||||
) # We only care english name
|
||||
|
@ -44,7 +44,7 @@ class Canvas:
|
|||
return student
|
||||
|
||||
self.students = [
|
||||
patch_user(student)
|
||||
patch_student(student)
|
||||
for student in self.course.get_users(enrollment_type=types)
|
||||
]
|
||||
for attr in ["login_id", "name"]:
|
||||
|
@ -52,7 +52,6 @@ class Canvas:
|
|||
raise Exception(
|
||||
f"Unable to gather students' {attr}, please contact the Canvas site admin"
|
||||
)
|
||||
self.users = [patch_user(student) for student in self.course.get_users()]
|
||||
logger.debug("Canvas students loaded")
|
||||
self.assignments = self.course.get_assignments()
|
||||
logger.debug("Canvas assignments loaded")
|
||||
|
@ -61,26 +60,12 @@ class Canvas:
|
|||
self.grade_filename = grade_filename
|
||||
logger.debug("Canvas initialized")
|
||||
|
||||
def export_wrong_email_users(self) -> None:
|
||||
SAMPLE_EMAIL_BODY = """Dear Student,
|
||||
|
||||
We have noticed that you have changed your email address on Canvas. While this can clearly cause privacy issues, this also prevents you from joining Gitea which will be intensively used in this course. Please revert back to your SJTU email address (`jaccount@sjtu.edu.cn`) as soon as possible. Note that if your email address is still incorrect in 24 hours, we will have to apply penalties as this is slowing down the whole course progress.
|
||||
|
||||
Best regards,
|
||||
Teaching Team"""
|
||||
emails = [
|
||||
user.email for user in self.users if not user.email.endswith("@sjtu.edu.cn")
|
||||
]
|
||||
print(f"To: {','.join(emails)}")
|
||||
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 export_students_to_csv(self, filename: Path) -> None:
|
||||
with open(filename, mode="w", newline="") as file:
|
||||
writer = csv.writer(file)
|
||||
for user in self.users:
|
||||
writer.writerow([user.name, user.sis_id, user.login_id])
|
||||
logger.info(f"Users exported to {filename}")
|
||||
for student in self.students:
|
||||
writer.writerow([student.name, student.sis_id, student.login_id])
|
||||
logger.info(f"Students exported to {filename}")
|
||||
|
||||
def prepare_assignment_dir(
|
||||
self, dir_or_zip_file: str, create_grade_file: bool = True
|
||||
|
|
|
@ -90,23 +90,25 @@ class Git:
|
|||
retry_interval = 2
|
||||
while retry_interval and auto_retry:
|
||||
try:
|
||||
current_branch = ""
|
||||
if repo.head.is_detached:
|
||||
current_branch = repo.head.commit.hexsha
|
||||
else:
|
||||
current_branch = repo.active_branch.name
|
||||
if clean_git_lock:
|
||||
locks_removed_count = 0
|
||||
for root, _, files in os.walk(os.path.join(repo_dir, ".git")):
|
||||
for filename in files:
|
||||
if filename.endswith(".lock"):
|
||||
lock_file_path = os.path.join(root, filename)
|
||||
if (
|
||||
os.path.join(".git", filename)
|
||||
== settings.joj3_lock_file_path
|
||||
):
|
||||
continue
|
||||
try:
|
||||
os.remove(lock_file_path)
|
||||
locks_removed_count += 1
|
||||
except OSError as e:
|
||||
logger.warning(f"Error removing lock file: {e}")
|
||||
logger.info(f"Removed {locks_removed_count} lock files")
|
||||
lock_files = [
|
||||
"index.lock",
|
||||
"HEAD.lock",
|
||||
"fetch-pack.lock",
|
||||
"logs/HEAD.lock",
|
||||
"packed-refs.lock",
|
||||
"config.lock",
|
||||
f"refs/remotes/origin/{current_branch}.lock",
|
||||
]
|
||||
for lock_file in lock_files:
|
||||
lock_path = os.path.join(repo_dir, ".git", lock_file)
|
||||
if os.path.exists(lock_path):
|
||||
os.remove(lock_path)
|
||||
repo.git.fetch("--tags", "--all", "-f")
|
||||
repo.git.reset("--hard", reset_target)
|
||||
repo.git.clean("-d", "-f", "-x")
|
||||
|
|
|
@ -494,7 +494,6 @@ class Gitea:
|
|||
self.repository_api.user_current_delete_subscription(
|
||||
self.org_name, repo.name
|
||||
)
|
||||
logger.info(f"Unwatched {repo.name}")
|
||||
|
||||
def get_all_teams(self) -> Dict[str, List[str]]:
|
||||
res: Dict[str, List[str]] = {}
|
||||
|
|
Loading…
Reference in New Issue
Block a user