lms.backend.canvas.model
1import logging 2import typing 3 4import lms.backend.canvas.common 5import lms.model.assignments 6import lms.model.backend 7import lms.model.courses 8import lms.model.groups 9import lms.model.groupsets 10import lms.model.scores 11import lms.model.users 12import lms.util.parse 13 14_logger = logging.getLogger(__name__) 15 16ENROLLMENT_TYPE_TO_ROLE: typing.Dict[str, lms.model.users.CourseRole] = { 17 'ObserverEnrollment': lms.model.users.CourseRole.OTHER, 18 'StudentEnrollment': lms.model.users.CourseRole.STUDENT, 19 'TaEnrollment': lms.model.users.CourseRole.GRADER, 20 'DesignerEnrollment': lms.model.users.CourseRole.ADMIN, 21 'TeacherEnrollment': lms.model.users.CourseRole.OWNER, 22} 23""" 24Canvas enrollment types mapped to roles. 25This map is ordered by priority/power. 26The later in the dict, the more power. 27""" 28 29_testing_override: bool = False # pylint: disable=invalid-name 30""" A special override to signal testing. """ 31 32class _ParsedText: 33 """ The result of parsing Canvas HTML into markdown. """ 34 35 def __init__(self, 36 html: str, 37 text: str, 38 resources: typing.List[str], 39 ) -> None: 40 self.html: str = html 41 """ The original HTML. """ 42 43 self.text: str = text 44 """ The parsed text. """ 45 46 self.resources: typing.List[str] = resources 47 """ The path to any resources parsed from this text. """ 48 49def assignment(data: typing.Dict[str, typing.Any]) -> lms.model.assignments.Assignment: 50 """ 51 Create a Canvas assignment associated with a course. 52 53 See: https://developerdocs.instructure.com/services/canvas/resources/assignments 54 """ 55 56 _parse_assignment_data(data, 'assignment') 57 return lms.model.assignments.Assignment(**data) 58 59def assignment_score(data: typing.Dict[str, typing.Any]) -> lms.model.scores.AssignmentScore: 60 """ 61 Create a Canvas assignment score. 62 63 See: https://developerdocs.instructure.com/services/canvas/resources/scores 64 """ 65 66 # Check for important fields. 67 for field in ['id', 'assignment_id', 'user_id']: 68 if (field not in data): 69 raise ValueError(f"Canvas assignment score is missing '{field}' field.") 70 71 # Modify specific arguments before creation. 72 data['id'] = lms.util.parse.required_string(data.get('id', None), 'id') 73 data['score'] = lms.util.parse.optional_float(data.get('score', None), 'score') 74 data['points_possible'] = lms.util.parse.optional_float(data.get('points_possible', None), 'points_possible') 75 data['submission_date'] = lms.backend.canvas.common.parse_timestamp(data.get('submitted_at', None)) 76 data['graded_date'] = lms.backend.canvas.common.parse_timestamp(data.get('graded_at', None)) 77 78 assignment_id = lms.util.parse.required_string(data.get('assignment_id', None), 'assignment_id') 79 data['assignment'] = lms.model.assignments.AssignmentQuery(id = assignment_id) 80 81 user_id = lms.util.parse.required_string(data.get('user_id', None), 'user_id') 82 data['user'] = lms.model.users.UserQuery(id = user_id) 83 84 return lms.model.scores.AssignmentScore(**data) 85 86def course(data: typing.Dict[str, typing.Any]) -> lms.model.courses.Course: 87 """ 88 Create a Canvas course. 89 90 See: https://developerdocs.instructure.com/services/canvas/resources/courses 91 """ 92 93 # Check for important fields. 94 for field in ['id']: 95 if (field not in data): 96 raise ValueError(f"Canvas course is missing '{field}' field.") 97 98 # Modify specific arguments before creation. 99 data['id'] = lms.util.parse.required_string(data.get('id', None), 'id') 100 101 return lms.model.courses.Course(**data) 102 103def course_user(backend: lms.model.backend.APIBackend, data: typing.Dict[str, typing.Any]) -> lms.model.users.CourseUser: 104 """ 105 Create a Canvas user associated with a course. 106 107 See: https://developerdocs.instructure.com/services/canvas/resources/users 108 """ 109 110 # Check for important fields. 111 for field in ['id']: 112 if (field not in data): 113 raise ValueError(f"Canvas user is missing '{field}' field.") 114 115 # Modify specific arguments before sending them to super. 116 data['id'] = lms.util.parse.required_string(data.get('id', None), 'id') 117 118 # Canvas sometimes has email under different fields. 119 if ((data.get('email', None) is None) or (len(data.get('email', '')) == 0)): 120 data['email'] = data.get('login_id', None) 121 122 enrollments = data.get('enrollments', None) 123 if (enrollments is not None): 124 data['raw_role'] = _parse_role_from_enrollments(enrollments) 125 data['role'] = ENROLLMENT_TYPE_TO_ROLE.get(data['raw_role'], None) 126 127 # Canvas has a discontinuity with its default course roles. 128 # We need to patch this during testing. 129 if ((backend.is_testing() or _testing_override) and data['email'] == 'course-admin@test.edulinq.org'): 130 data['role'] = lms.model.users.CourseRole.ADMIN 131 132 return lms.model.users.CourseUser(**data) 133 134def group(data: typing.Dict[str, typing.Any]) -> lms.model.groups.Group: 135 """ 136 Create a Canvas group associated with a course. 137 138 See: https://developerdocs.instructure.com/services/canvas/resources/groups 139 """ 140 141 # Check for important fields. 142 for field in ['id']: 143 if (field not in data): 144 raise ValueError(f"Canvas group is missing '{field}' field.") 145 146 # Modify specific arguments before creation. 147 data['id'] = lms.util.parse.required_string(data.get('id', None), 'id') 148 149 return lms.model.groups.Group(**data) 150 151def group_set(data: typing.Dict[str, typing.Any]) -> lms.model.groupsets.GroupSet: 152 """ 153 Create a Canvas group set associated with a course. 154 155 See: https://developerdocs.instructure.com/services/canvas/resources/group_categories 156 """ 157 158 # Check for important fields. 159 for field in ['id']: 160 if (field not in data): 161 raise ValueError(f"Canvas group set is missing '{field}' field.") 162 163 # Modify specific arguments before creation. 164 data['id'] = lms.util.parse.required_string(data.get('id', None), 'id') 165 166 return lms.model.groupsets.GroupSet(**data) 167 168def _parse_assignment_data(data: typing.Dict[str, typing.Any], label: str) -> None: 169 """ 170 Parse core assignment data. 171 """ 172 173 for field in ['id']: 174 if (field not in data): 175 raise ValueError(f"Canvas {label} is missing '{field}' field.") 176 177 # Modify specific arguments before creation. 178 data['id'] = lms.util.parse.required_string(data.get('id', None), 'id') 179 data['due_date'] = lms.backend.canvas.common.parse_timestamp(data.get('due_at', None)) 180 data['open_date'] = lms.backend.canvas.common.parse_timestamp(data.get('unlock_at', None)) 181 data['close_date'] = lms.backend.canvas.common.parse_timestamp(data.get('lock_at', None)) 182 183 data['description'] = data.get('description', None) 184 if (data['description'] is not None): 185 data['description'] = lms.backend.canvas.common.html_to_markdown(data['description']) 186 187 # If there is no name, look for a title. 188 if (data.get('name', None) is None): 189 data['name'] = lms.util.parse.optional_string(data.get('title', None)) 190 191def _parse_role_from_enrollments(enrollments: typing.Any) -> typing.Union[str, None]: 192 """ 193 Try to parse the user's role from their enrollments. 194 If multiple roles are discovered, take the "highest" one. 195 196 See: https://developerdocs.instructure.com/services/canvas/resources/enrollments 197 """ 198 199 if (not isinstance(enrollments, list)): 200 return None 201 202 best_role = None 203 best_index = -1 204 205 enrollment_types = list(ENROLLMENT_TYPE_TO_ROLE.keys()) 206 207 for enrollment in enrollments: 208 if (not isinstance(enrollment, dict)): 209 continue 210 211 if (enrollment.get('enrollment_state', None) != 'active'): 212 continue 213 214 role = enrollment.get('role', None) 215 216 role_index = -1 217 if (role in enrollment_types): 218 role_index = enrollment_types.index(role) 219 220 if ((best_role is None) or (role_index > best_index)): 221 best_role = role 222 best_index = role_index 223 224 return best_role
Canvas enrollment types mapped to roles. This map is ordered by priority/power. The later in the dict, the more power.
50def assignment(data: typing.Dict[str, typing.Any]) -> lms.model.assignments.Assignment: 51 """ 52 Create a Canvas assignment associated with a course. 53 54 See: https://developerdocs.instructure.com/services/canvas/resources/assignments 55 """ 56 57 _parse_assignment_data(data, 'assignment') 58 return lms.model.assignments.Assignment(**data)
Create a Canvas assignment associated with a course.
See: https://developerdocs.instructure.com/services/canvas/resources/assignments
60def assignment_score(data: typing.Dict[str, typing.Any]) -> lms.model.scores.AssignmentScore: 61 """ 62 Create a Canvas assignment score. 63 64 See: https://developerdocs.instructure.com/services/canvas/resources/scores 65 """ 66 67 # Check for important fields. 68 for field in ['id', 'assignment_id', 'user_id']: 69 if (field not in data): 70 raise ValueError(f"Canvas assignment score is missing '{field}' field.") 71 72 # Modify specific arguments before creation. 73 data['id'] = lms.util.parse.required_string(data.get('id', None), 'id') 74 data['score'] = lms.util.parse.optional_float(data.get('score', None), 'score') 75 data['points_possible'] = lms.util.parse.optional_float(data.get('points_possible', None), 'points_possible') 76 data['submission_date'] = lms.backend.canvas.common.parse_timestamp(data.get('submitted_at', None)) 77 data['graded_date'] = lms.backend.canvas.common.parse_timestamp(data.get('graded_at', None)) 78 79 assignment_id = lms.util.parse.required_string(data.get('assignment_id', None), 'assignment_id') 80 data['assignment'] = lms.model.assignments.AssignmentQuery(id = assignment_id) 81 82 user_id = lms.util.parse.required_string(data.get('user_id', None), 'user_id') 83 data['user'] = lms.model.users.UserQuery(id = user_id) 84 85 return lms.model.scores.AssignmentScore(**data)
Create a Canvas assignment score.
See: https://developerdocs.instructure.com/services/canvas/resources/scores
87def course(data: typing.Dict[str, typing.Any]) -> lms.model.courses.Course: 88 """ 89 Create a Canvas course. 90 91 See: https://developerdocs.instructure.com/services/canvas/resources/courses 92 """ 93 94 # Check for important fields. 95 for field in ['id']: 96 if (field not in data): 97 raise ValueError(f"Canvas course is missing '{field}' field.") 98 99 # Modify specific arguments before creation. 100 data['id'] = lms.util.parse.required_string(data.get('id', None), 'id') 101 102 return lms.model.courses.Course(**data)
Create a Canvas course.
See: https://developerdocs.instructure.com/services/canvas/resources/courses
104def course_user(backend: lms.model.backend.APIBackend, data: typing.Dict[str, typing.Any]) -> lms.model.users.CourseUser: 105 """ 106 Create a Canvas user associated with a course. 107 108 See: https://developerdocs.instructure.com/services/canvas/resources/users 109 """ 110 111 # Check for important fields. 112 for field in ['id']: 113 if (field not in data): 114 raise ValueError(f"Canvas user is missing '{field}' field.") 115 116 # Modify specific arguments before sending them to super. 117 data['id'] = lms.util.parse.required_string(data.get('id', None), 'id') 118 119 # Canvas sometimes has email under different fields. 120 if ((data.get('email', None) is None) or (len(data.get('email', '')) == 0)): 121 data['email'] = data.get('login_id', None) 122 123 enrollments = data.get('enrollments', None) 124 if (enrollments is not None): 125 data['raw_role'] = _parse_role_from_enrollments(enrollments) 126 data['role'] = ENROLLMENT_TYPE_TO_ROLE.get(data['raw_role'], None) 127 128 # Canvas has a discontinuity with its default course roles. 129 # We need to patch this during testing. 130 if ((backend.is_testing() or _testing_override) and data['email'] == 'course-admin@test.edulinq.org'): 131 data['role'] = lms.model.users.CourseRole.ADMIN 132 133 return lms.model.users.CourseUser(**data)
Create a Canvas user associated with a course.
See: https://developerdocs.instructure.com/services/canvas/resources/users
135def group(data: typing.Dict[str, typing.Any]) -> lms.model.groups.Group: 136 """ 137 Create a Canvas group associated with a course. 138 139 See: https://developerdocs.instructure.com/services/canvas/resources/groups 140 """ 141 142 # Check for important fields. 143 for field in ['id']: 144 if (field not in data): 145 raise ValueError(f"Canvas group is missing '{field}' field.") 146 147 # Modify specific arguments before creation. 148 data['id'] = lms.util.parse.required_string(data.get('id', None), 'id') 149 150 return lms.model.groups.Group(**data)
Create a Canvas group associated with a course.
See: https://developerdocs.instructure.com/services/canvas/resources/groups
152def group_set(data: typing.Dict[str, typing.Any]) -> lms.model.groupsets.GroupSet: 153 """ 154 Create a Canvas group set associated with a course. 155 156 See: https://developerdocs.instructure.com/services/canvas/resources/group_categories 157 """ 158 159 # Check for important fields. 160 for field in ['id']: 161 if (field not in data): 162 raise ValueError(f"Canvas group set is missing '{field}' field.") 163 164 # Modify specific arguments before creation. 165 data['id'] = lms.util.parse.required_string(data.get('id', None), 'id') 166 167 return lms.model.groupsets.GroupSet(**data)
Create a Canvas group set associated with a course.
See: https://developerdocs.instructure.com/services/canvas/resources/group_categories