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
ENROLLMENT_TYPE_TO_ROLE: Dict[str, lms.model.users.CourseRole] = {'ObserverEnrollment': <CourseRole.OTHER: 'other'>, 'StudentEnrollment': <CourseRole.STUDENT: 'student'>, 'TaEnrollment': <CourseRole.GRADER: 'grader'>, 'DesignerEnrollment': <CourseRole.ADMIN: 'admin'>, 'TeacherEnrollment': <CourseRole.OWNER: 'owner'>}

Canvas enrollment types mapped to roles. This map is ordered by priority/power. The later in the dict, the more power.

def assignment(data: Dict[str, Any]) -> lms.model.assignments.Assignment:
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

def assignment_score(data: Dict[str, Any]) -> lms.model.scores.AssignmentScore:
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)
def course(data: Dict[str, Any]) -> lms.model.courses.Course:
 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)
def course_user( backend: lms.model.backend.APIBackend, data: Dict[str, Any]) -> lms.model.users.CourseUser:
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

def group(data: Dict[str, Any]) -> lms.model.groups.Group:
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

def group_set(data: Dict[str, Any]) -> lms.model.groupsets.GroupSet:
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