feat: better grade upload

This commit is contained in:
张泊明518370910136 2021-10-30 19:52:56 +08:00
parent a52ab405cf
commit d6ee159a70
No known key found for this signature in database
GPG Key ID: FBEF5DE8B9F4C629
3 changed files with 82 additions and 21 deletions

View File

@ -118,12 +118,12 @@ def prepare_assignment_dir(dir_or_zip_file: Path) -> None:
@app.command( @app.command(
"upload-assignment-scores", "upload-assignment-grades",
help="upload assignment scores to canvas from score file (SCORE.txt by default), " help="upload assignment grades to canvas from grade file (GRADE.txt by default), "
+ "read the first line as score, the rest as comments", + "read the first line as grade, the rest as comments",
) )
def upload_assignment_scores(dir: Path, assignment_name: str) -> None: def upload_assignment_grades(dir: Path, assignment_name: str) -> None:
tea.pot.canvas.upload_assignment_scores(str(dir), assignment_name) tea.pot.canvas.upload_assignment_grades(str(dir), assignment_name)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,3 +1,4 @@
import math
from typing import Callable, Iterable, Optional, TypeVar from typing import Callable, Iterable, Optional, TypeVar
_T = TypeVar("_T") _T = TypeVar("_T")
@ -7,3 +8,19 @@ def first(
iterable: Iterable[_T], condition: Callable[[_T], bool] = lambda x: True iterable: Iterable[_T], condition: Callable[[_T], bool] = lambda x: True
) -> Optional[_T]: ) -> Optional[_T]:
return next((x for x in iterable if condition(x)), None) return next((x for x in iterable if condition(x)), None)
def percentile(
N: Iterable[float], percent: float, key: Callable[[float], float] = lambda x: x
) -> Optional[float]:
if not N:
return None
N = sorted(N)
k = (len(N) - 1) * percent
f = math.floor(k)
c = math.ceil(k)
if f == c:
return key(N[int(k)])
d0 = key(N[int(f)]) * (c - k)
d1 = key(N[int(c)]) * (k - f)
return d0 + d1

View File

@ -1,13 +1,15 @@
import os import os
from glob import glob from glob import glob
from typing import cast
from canvasapi import Canvas as PyCanvas from canvasapi import Canvas as PyCanvas
from canvasapi.assignment import Assignment
from patoolib import extract_archive from patoolib import extract_archive
from patoolib.util import PatoolError from patoolib.util import PatoolError
from joint_teapot.config import settings from joint_teapot.config import settings
from joint_teapot.utils.logger import logger from joint_teapot.utils.logger import logger
from joint_teapot.utils.main import first from joint_teapot.utils.main import first, percentile
class Canvas: class Canvas:
@ -15,7 +17,7 @@ class Canvas:
self, self,
access_token: str = settings.canvas_access_token, access_token: str = settings.canvas_access_token,
course_id: int = settings.canvas_course_id, course_id: int = settings.canvas_course_id,
score_filename: str = "SCORE.txt", grade_filename: str = "SCORE.txt",
): ):
self.canvas = PyCanvas("https://umjicanvas.com/", access_token) self.canvas = PyCanvas("https://umjicanvas.com/", access_token)
self.course = self.canvas.get_course(course_id) self.course = self.canvas.get_course(course_id)
@ -33,11 +35,11 @@ class Canvas:
logger.debug(f"Canvas assignments loaded") logger.debug(f"Canvas assignments loaded")
self.groups = self.course.get_groups() self.groups = self.course.get_groups()
logger.debug(f"Canvas groups loaded") logger.debug(f"Canvas groups loaded")
self.score_filename = score_filename self.grade_filename = grade_filename
logger.debug("Canvas initialized") logger.debug("Canvas initialized")
def prepare_assignment_dir( def prepare_assignment_dir(
self, dir_or_zip_file: str, create_score_file: bool = True self, dir_or_zip_file: str, create_grade_file: bool = True
) -> None: ) -> None:
if os.path.isdir(dir_or_zip_file): if os.path.isdir(dir_or_zip_file):
dir = dir_or_zip_file dir = dir_or_zip_file
@ -52,8 +54,10 @@ class Canvas:
new_path = os.path.join(dir, v) new_path = os.path.join(dir, v)
if not os.path.exists(new_path): if not os.path.exists(new_path):
os.mkdir(new_path) os.mkdir(new_path)
if create_score_file: if create_grade_file:
open(os.path.join(new_path, self.score_filename), mode="w") grade_file_path = os.path.join(new_path, self.grade_filename)
if not os.path.exists(grade_file_path):
open(grade_file_path, mode="w")
late_students = set() late_students = set()
submitted_ids = set() submitted_ids = set()
for path in glob(os.path.join(dir, "*")): for path in glob(os.path.join(dir, "*")):
@ -87,30 +91,70 @@ class Canvas:
if late_students: if late_students:
tmp = ", ".join([str(student) for student in late_students]) tmp = ", ".join([str(student) for student in late_students])
logger.info(f"Late student(s): {tmp}") logger.info(f"Late student(s): {tmp}")
if create_score_file:
open(os.path.join(target_dir, self.score_filename), mode="w")
def upload_assignment_scores(self, dir: str, assignment_name: str) -> None: def upload_assignment_grades(self, dir: str, assignment_name: str) -> None:
assignment = first(self.assignments, lambda x: x.name == assignment_name) assignment = first(self.assignments, lambda x: x.name == assignment_name)
if assignment is None: if assignment is None:
logger.info(f"Canvas assignment {assignment_name} not found") logger.info(f"Canvas assignment {assignment_name} not found")
return return
assignment = cast(Assignment, assignment)
submission_dict = {}
float_grades = []
is_float_grades = True
for submission in assignment.get_submissions(): for submission in assignment.get_submissions():
student = first(self.students, lambda x: x.id == submission.user_id) student = first(self.students, lambda x: x.id == submission.user_id)
if student is None: if student is None:
continue continue
score_file_path = os.path.join( grade_file_path = os.path.join(
dir, student.sis_login_id, self.score_filename dir, student.sis_login_id, self.grade_filename
) )
score, *comments = list(open(score_file_path)) try:
data = { grade, *comments = list(open(grade_file_path))
"submission": {"posted_grade": float(score)}, grade = grade.strip()
"comment": {"text_comment": "".join(comments)}, try:
} float_grades.append(float(grade))
except ValueError:
is_float_grades = False
data = {
"submission": {"posted_grade": grade},
"comment": {"text_comment": "".join(comments)},
}
submission_dict[(student, submission)] = data
comment_no_newline = (
data["comment"]["text_comment"].strip().replace("\n", " ")
)
logger.info(
f"Grade file parsed for {assignment} {student}: "
f"grade: {data['submission']['posted_grade']}, "
f'comment: "{comment_no_newline}"'
)
except Exception:
logger.error(f"Can not parse grade file {grade_file_path}")
return
for (student, submission), data in submission_dict.items():
logger.info( logger.info(
f"Uploading grade for {assignment} {student}: {data.__repr__()}" f"Uploading grade for {assignment} {student}: {data.__repr__()}"
) )
submission.edit(**data) submission.edit(**data)
if is_float_grades and float_grades:
summary = [
min(float_grades),
percentile(float_grades, 0.25),
percentile(float_grades, 0.5),
percentile(float_grades, 0.75),
max(float_grades),
]
average_grade = sum(float_grades) / len(float_grades)
logger.info(
f"Grades summary: "
f"Min: {summary[0]:.2f}, "
f"Q1: {summary[1]:.2f}, "
f"Q2: {summary[2]:.2f}, "
f"Q3: {summary[3]:.2f}, "
f"Max: {summary[4]:.2f}, "
f"Average: {average_grade:.2f}"
)
logger.info(f"Canvas assginemnt {assignment} grades upload succeed")
if __name__ == "__main__": if __name__ == "__main__":