feat: better grade upload
This commit is contained in:
		
							parent
							
								
									a52ab405cf
								
							
						
					
					
						commit
						d6ee159a70
					
				|  | @ -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__": | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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__": | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user