forked from JOJ/Joint-Teapot
Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
14154fb59b | |||
cb5735ac40 | |||
083140079e | |||
aa9a69eaf1 | |||
2d7aba5ce0 | |||
0b45898b91 | |||
353797323d | |||
beeb45709f | |||
54a4f404fe | |||
9e31fc71be | |||
c3b053f0a5 |
|
@ -34,9 +34,9 @@ class Tea:
|
||||||
tea = Tea() # lazy loader
|
tea = Tea() # lazy loader
|
||||||
|
|
||||||
|
|
||||||
@app.command("export-students", help="export students from canvas to csv file")
|
@app.command("export-users", help="export users from canvas to csv file")
|
||||||
def export_students_to_csv(output_file: Path) -> None:
|
def export_users_to_csv(output_file: Path = Argument("students.csv")) -> None:
|
||||||
tea.pot.canvas.export_students_to_csv(output_file)
|
tea.pot.canvas.export_users_to_csv(output_file)
|
||||||
|
|
||||||
|
|
||||||
@app.command(
|
@app.command(
|
||||||
|
@ -295,6 +295,10 @@ def joj3_all_env(
|
||||||
False,
|
False,
|
||||||
help="skip creating failed table on gitea",
|
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(
|
submitter_in_issue_title: bool = Option(
|
||||||
True,
|
True,
|
||||||
help="whether to include submitter in issue title",
|
help="whether to include submitter in issue title",
|
||||||
|
@ -307,6 +311,10 @@ def joj3_all_env(
|
||||||
"#795548",
|
"#795548",
|
||||||
help="label color for the issue created by this command",
|
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),
|
end_time: Optional[datetime] = Option(None),
|
||||||
penalty_config: str = Option(
|
penalty_config: str = Option(
|
||||||
"",
|
"",
|
||||||
|
@ -360,6 +368,7 @@ def joj3_all_env(
|
||||||
submitter_repo_name,
|
submitter_repo_name,
|
||||||
issue_label_name,
|
issue_label_name,
|
||||||
issue_label_color,
|
issue_label_color,
|
||||||
|
issue_label_exclusive,
|
||||||
penalty_factor,
|
penalty_factor,
|
||||||
)
|
)
|
||||||
res["issue"] = issue_number
|
res["issue"] = issue_number
|
||||||
|
@ -398,11 +407,15 @@ def joj3_all_env(
|
||||||
raise Exit(code=1)
|
raise Exit(code=1)
|
||||||
repo.git.reset("--hard", "origin/grading")
|
repo.git.reset("--hard", "origin/grading")
|
||||||
if not skip_scoreboard:
|
if not skip_scoreboard:
|
||||||
|
exercise_name = env.joj3_conf_name
|
||||||
|
if scoreboard_column_by_ref:
|
||||||
|
exercise_name = env.github_ref
|
||||||
joj3.generate_scoreboard(
|
joj3.generate_scoreboard(
|
||||||
env.joj3_output_path,
|
env.joj3_output_path,
|
||||||
env.github_actor,
|
env.github_actor,
|
||||||
os.path.join(repo_path, scoreboard_filename),
|
os.path.join(repo_path, scoreboard_filename),
|
||||||
env.joj3_conf_name,
|
exercise_name,
|
||||||
|
submitter_repo_name,
|
||||||
)
|
)
|
||||||
tea.pot.git.add_commit(
|
tea.pot.git.add_commit(
|
||||||
grading_repo_name,
|
grading_repo_name,
|
||||||
|
|
|
@ -240,6 +240,7 @@ class Teapot:
|
||||||
submitter_repo_name: str,
|
submitter_repo_name: str,
|
||||||
issue_label_name: str,
|
issue_label_name: str,
|
||||||
issue_label_color: str,
|
issue_label_color: str,
|
||||||
|
issue_label_exclusive: bool,
|
||||||
penalty_factor: float,
|
penalty_factor: float,
|
||||||
) -> int:
|
) -> int:
|
||||||
title, comment = joj3.generate_title_and_comment(
|
title, comment = joj3.generate_title_and_comment(
|
||||||
|
@ -280,7 +281,11 @@ class Teapot:
|
||||||
label = self.gitea.issue_api.issue_create_label(
|
label = self.gitea.issue_api.issue_create_label(
|
||||||
self.gitea.org_name,
|
self.gitea.org_name,
|
||||||
submitter_repo_name,
|
submitter_repo_name,
|
||||||
body={"name": issue_label_name, "color": issue_label_color},
|
body={
|
||||||
|
"name": issue_label_name,
|
||||||
|
"color": issue_label_color,
|
||||||
|
"exclusive": issue_label_exclusive,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
label_id = label.id
|
label_id = label.id
|
||||||
joj3_issue = self.gitea.issue_api.issue_create_issue(
|
joj3_issue = self.gitea.issue_api.issue_create_issue(
|
||||||
|
@ -325,7 +330,7 @@ class Teapot:
|
||||||
f"[{end_time + timedelta(seconds=1)}, {penalty_end_time}].\n",
|
f"[{end_time + timedelta(seconds=1)}, {penalty_end_time}].\n",
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
else:
|
elif now > end_time:
|
||||||
return (
|
return (
|
||||||
"### Submission Time Check Passed\n"
|
"### Submission Time Check Passed\n"
|
||||||
f"Current time {now} is not in the valid range "
|
f"Current time {now} is not in the valid range "
|
||||||
|
|
|
@ -40,6 +40,7 @@ def generate_scoreboard(
|
||||||
submitter: str,
|
submitter: str,
|
||||||
scoreboard_file_path: str,
|
scoreboard_file_path: str,
|
||||||
exercise_name: str,
|
exercise_name: str,
|
||||||
|
submitter_repo_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not scoreboard_file_path.endswith(".csv"):
|
if not scoreboard_file_path.endswith(".csv"):
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@ -48,18 +49,25 @@ def generate_scoreboard(
|
||||||
return
|
return
|
||||||
os.makedirs(os.path.dirname(scoreboard_file_path), exist_ok=True)
|
os.makedirs(os.path.dirname(scoreboard_file_path), exist_ok=True)
|
||||||
# Load the csv file if it already exists
|
# 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):
|
if os.path.exists(scoreboard_file_path):
|
||||||
with open(scoreboard_file_path, newline="") as file:
|
with open(scoreboard_file_path, newline="") as file:
|
||||||
reader = csv.reader(file)
|
reader = csv.reader(file)
|
||||||
rows = list(reader)
|
rows = list(reader)
|
||||||
columns = rows[0]
|
columns = rows[0]
|
||||||
data = rows[1:]
|
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:
|
else:
|
||||||
columns = [
|
columns = fixed_headers
|
||||||
"",
|
|
||||||
"last_edit",
|
|
||||||
"total",
|
|
||||||
]
|
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
submitter_found = False
|
submitter_found = False
|
||||||
|
@ -69,8 +77,7 @@ def generate_scoreboard(
|
||||||
submitter_found = True
|
submitter_found = True
|
||||||
break
|
break
|
||||||
if not submitter_found:
|
if not submitter_found:
|
||||||
fixed_columns = [submitter, "", "0"]
|
submitter_row = fixed_defaults + [""] * (len(columns) - len(fixed_headers))
|
||||||
submitter_row = fixed_columns + [""] * (len(columns) - len(fixed_columns))
|
|
||||||
data.append(submitter_row)
|
data.append(submitter_row)
|
||||||
|
|
||||||
# Update data
|
# Update data
|
||||||
|
@ -85,9 +92,9 @@ def generate_scoreboard(
|
||||||
exercise_name = comment.split("-")[0]
|
exercise_name = comment.split("-")[0]
|
||||||
# Find if exercise in table:
|
# Find if exercise in table:
|
||||||
if exercise_name not in columns:
|
if exercise_name not in columns:
|
||||||
column_tail = columns[3:]
|
column_tail = columns[len(fixed_headers) :]
|
||||||
bisect.insort(column_tail, exercise_name)
|
bisect.insort(column_tail, exercise_name)
|
||||||
columns[3:] = column_tail
|
columns[len(fixed_headers) :] = column_tail
|
||||||
index = columns.index(exercise_name)
|
index = columns.index(exercise_name)
|
||||||
for row in data:
|
for row in data:
|
||||||
row.insert(index, "")
|
row.insert(index, "")
|
||||||
|
@ -101,17 +108,22 @@ def generate_scoreboard(
|
||||||
|
|
||||||
total = 0
|
total = 0
|
||||||
for col in columns:
|
for col in columns:
|
||||||
if col in ["", "total", "last_edit"]:
|
if col in fixed_headers:
|
||||||
continue
|
continue
|
||||||
idx = columns.index(col)
|
idx = columns.index(col)
|
||||||
if (submitter_row[idx] is not None) and (submitter_row[idx] != ""):
|
if (submitter_row[idx] is not None) and (submitter_row[idx] != ""):
|
||||||
total += int(submitter_row[idx])
|
try:
|
||||||
|
total += int(submitter_row[idx])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
submitter_row[columns.index("total")] = str(total)
|
submitter_row[columns.index("total")] = str(total)
|
||||||
|
|
||||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
submitter_row[columns.index("last_edit")] = now
|
submitter_row[columns.index("last_edit")] = now
|
||||||
|
|
||||||
|
submitter_row[columns.index("repo_name")] = submitter_repo_name
|
||||||
|
|
||||||
# Sort data by total, from low to high
|
# Sort data by total, from low to high
|
||||||
data.sort(key=lambda x: int(x[columns.index("total")]))
|
data.sort(key=lambda x: int(x[columns.index("total")]))
|
||||||
|
|
||||||
|
@ -236,9 +248,7 @@ def generate_title_and_comment(
|
||||||
"[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:
|
if penalty_factor != 1.0:
|
||||||
comment += (
|
comment += f"## ⚠️Total Score Penalty Warning⚠️\n**The total score is multiplied by {penalty_factor}.**\n"
|
||||||
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() == ""
|
||||||
|
@ -260,7 +270,7 @@ def generate_title_and_comment(
|
||||||
total_score += result["score"]
|
total_score += result["score"]
|
||||||
comment += "\n"
|
comment += "\n"
|
||||||
if penalty_factor != 1.0:
|
if penalty_factor != 1.0:
|
||||||
total_score = round(total_score * penalty_factor)
|
total_score = round(total_score - abs(total_score) * (1 - 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}"
|
||||||
|
@ -307,8 +317,10 @@ def get_penalty_factor(
|
||||||
) -> float:
|
) -> float:
|
||||||
if not end_time or not penalty_config:
|
if not end_time or not penalty_config:
|
||||||
return 1.0
|
return 1.0
|
||||||
penalties = parse_penalty_config(penalty_config)
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
if now < end_time:
|
||||||
|
return 1.0
|
||||||
|
penalties = parse_penalty_config(penalty_config)
|
||||||
res = 0.0
|
res = 0.0
|
||||||
for hour, factor in penalties[::-1]:
|
for hour, factor in penalties[::-1]:
|
||||||
if now < end_time + timedelta(hours=hour):
|
if now < end_time + timedelta(hours=hour):
|
||||||
|
|
|
@ -35,7 +35,7 @@ class Canvas:
|
||||||
# types = ["student", "observer"]
|
# types = ["student", "observer"]
|
||||||
types = ["student"]
|
types = ["student"]
|
||||||
|
|
||||||
def patch_student(student: User) -> User:
|
def patch_user(student: User) -> User:
|
||||||
student.name = (
|
student.name = (
|
||||||
re.sub(r"[^\x00-\x7F]+", "", student.name).strip().title()
|
re.sub(r"[^\x00-\x7F]+", "", student.name).strip().title()
|
||||||
) # We only care english name
|
) # We only care english name
|
||||||
|
@ -44,7 +44,7 @@ class Canvas:
|
||||||
return student
|
return student
|
||||||
|
|
||||||
self.students = [
|
self.students = [
|
||||||
patch_student(student)
|
patch_user(student)
|
||||||
for student in self.course.get_users(enrollment_type=types)
|
for student in self.course.get_users(enrollment_type=types)
|
||||||
]
|
]
|
||||||
for attr in ["login_id", "name"]:
|
for attr in ["login_id", "name"]:
|
||||||
|
@ -52,6 +52,7 @@ class Canvas:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"Unable to gather students' {attr}, please contact the Canvas site admin"
|
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")
|
logger.debug("Canvas students loaded")
|
||||||
self.assignments = self.course.get_assignments()
|
self.assignments = self.course.get_assignments()
|
||||||
logger.debug("Canvas assignments loaded")
|
logger.debug("Canvas assignments loaded")
|
||||||
|
@ -60,12 +61,12 @@ class Canvas:
|
||||||
self.grade_filename = grade_filename
|
self.grade_filename = grade_filename
|
||||||
logger.debug("Canvas initialized")
|
logger.debug("Canvas initialized")
|
||||||
|
|
||||||
def export_students_to_csv(self, filename: Path) -> None:
|
def export_users_to_csv(self, filename: Path) -> None:
|
||||||
with open(filename, mode="w", newline="") as file:
|
with open(filename, mode="w", newline="") as file:
|
||||||
writer = csv.writer(file)
|
writer = csv.writer(file)
|
||||||
for student in self.students:
|
for user in self.users:
|
||||||
writer.writerow([student.name, student.sis_id, student.login_id])
|
writer.writerow([user.name, user.sis_id, user.login_id])
|
||||||
logger.info(f"Students exported to {filename}")
|
logger.info(f"Users exported to {filename}")
|
||||||
|
|
||||||
def prepare_assignment_dir(
|
def prepare_assignment_dir(
|
||||||
self, dir_or_zip_file: str, create_grade_file: bool = True
|
self, dir_or_zip_file: str, create_grade_file: bool = True
|
||||||
|
|
|
@ -103,7 +103,10 @@ class Git:
|
||||||
"logs/HEAD.lock",
|
"logs/HEAD.lock",
|
||||||
"packed-refs.lock",
|
"packed-refs.lock",
|
||||||
"config.lock",
|
"config.lock",
|
||||||
|
f"refs/heads/{current_branch}.lock",
|
||||||
f"refs/remotes/origin/{current_branch}.lock",
|
f"refs/remotes/origin/{current_branch}.lock",
|
||||||
|
f"refs/heads/{checkout_dest}.lock",
|
||||||
|
f"refs/remotes/origin/{checkout_dest}.lock",
|
||||||
]
|
]
|
||||||
for lock_file in lock_files:
|
for lock_file in lock_files:
|
||||||
lock_path = os.path.join(repo_dir, ".git", lock_file)
|
lock_path = os.path.join(repo_dir, ".git", lock_file)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user