Skip to content
Snippets Groups Projects
Commit d0fab743 authored by Christopher Bohn's avatar Christopher Bohn :thinking:
Browse files

Added static typing to classes that interface directly with Canvas API

parent 80ec2650
Branches
No related tags found
No related merge requests found
......@@ -18,9 +18,12 @@ of repositories for student assignments.
- config.py
- provides URLs and API keys
- ignored (not present in remote repository)
- config-example.py
- sample config.py file, without API keys
- *do not* commit API keys to repository
- you will need to replace `None` with your API keys (as strings)
- use `git update-index --skip-worktree config.py` to get git to ignore all updates to config.py
- you will need to copy to config.py and replace `None` with your API keys (as strings)
- course.py
- provides namespace and group ID
......
from canvasapi import Canvas
from canvasapi.assignment import Assignment, AssignmentGroup
from canvasapi.group import Group, GroupCategory
from canvasapi.quiz import QuizSubmission, QuizSubmissionQuestion
from canvasapi.submission import Submission
from canvasapi.user import User
from config import Config
from typing import Iterable, List, Union, Dict
class CanvasSession:
__instance = None
@staticmethod
def get_session():
def get_session() -> Canvas:
if CanvasSession.__instance is None:
CanvasSession.__instance = Canvas(Config.canvas_url, Config.canvas_api_key)
return CanvasSession.__instance
......@@ -16,42 +23,52 @@ class CanvasSession:
class CanvasUser:
def __init__(self, user):
# @overload
# def __init__(self, user: int): # by NUID
# super().__init__()
# self.canvas_user = CanvasSession.get_session().get_user(user, 'sis_user_id')
#
# @overload
# def __init__(self, user: User): # by existing gitlab user
# super().__init__()
# self.canvas_user = user
#
def __init__(self, user: Union[int, User]):
super().__init__()
if isinstance(user, int): # by NUID
self.canvas_user = CanvasSession.get_session().get_user(user, 'sis_user_id')
else:
self.canvas_user = user
def get_name(self):
def get_name(self) -> str:
return self.canvas_user.name
def get_sortable_name(self):
def get_sortable_name(self) -> str:
return self.canvas_user.sortable_name
def get_username(self):
def get_username(self) -> str:
return self.canvas_user.login_id
def get_canvas_id(self):
def get_canvas_id(self) -> int:
return self.canvas_user.id
def get_nuid(self):
return self.canvas_user.sis_user_id
def get_nuid(self) -> int:
return int(self.canvas_user.sis_user_id)
def get_email(self):
def get_email(self) -> str:
return self.canvas_user.email
def __repr__(self):
def __repr__(self) -> str:
username = self.get_username()
return f'{username}'
def __eq__(self, other):
if isinstance(other, CanvasUser):
def __eq__(self, other: "CanvasUser") -> bool:
# if isinstance(other, CanvasUser):
return self.get_username() == other.get_username()
else:
return False
# else:
# return False
def __ne__(self, other):
def __ne__(self, other: "CanvasUser") -> bool:
return not self.__eq__(other)
......@@ -105,104 +122,28 @@ class CanvasUser:
"""
class CanvasUserGroupSet: # aka, group_category
def __init__(self, group_category):
super().__init__()
self.canvas_group_category = group_category
def get_name(self):
return self.canvas_group_category.name
def get_groups(self):
canvas_groups = self.canvas_group_category.get_groups()
groups = []
for group in canvas_groups:
groups.append(CanvasUserGroup(group))
return groups
def create_group(self, group_name):
canvas_group = self.canvas_group_category.create_group(name=group_name)
return CanvasUserGroup(canvas_group)
def create_groups(self, number_of_groups):
base_name = self.get_name()
groups = []
for group_number in range(1, number_of_groups + 1):
canvas_group = self.create_group(f'{base_name} {group_number}')
groups.append(CanvasUserGroup(canvas_group))
return groups
def __repr__(self):
return self.get_name()
"""
{
// The ID of the group category.
"id": 17,
// The display name of the group category.
"name": "Math Groups",
// Certain types of group categories have special role designations. Currently,
// these include: 'communities', 'student_organized', and 'imported'. Regular
// course/account group categories have a role of null.
"role": "communities",
// If the group category allows users to join a group themselves, thought they
// may only be a member of one group per group category at a time. Values
// include 'restricted', 'enabled', and null 'enabled' allows students to assign
// themselves to a group 'restricted' restricts them to only joining a group in
// their section null disallows students from joining groups
"self_signup": null,
// Gives instructors the ability to automatically have group leaders assigned.
// Values include 'random', 'first', and null; 'random' picks a student from the
// group at random as the leader, 'first' sets the first student to be assigned
// to the group as the leader
"auto_leader": null,
// The course or account that the category group belongs to. The pattern here is
// that whatever the context_type is, there will be an _id field named after
// that type. So if instead context_type was 'Course', the course_id field would
// be replaced by an course_id field.
"context_type": "Account",
"account_id": 3,
// If self-signup is enabled, group_limit can be set to cap the number of users
// in each group. If null, there is no limit.
"group_limit": null,
// The SIS identifier for the group category. This field is only included if the
// user has permission to manage or view SIS information.
"sis_group_category_id": null,
// The unique identifier for the SIS import. This field is only included if the
// user has permission to manage SIS information.
"sis_import_id": null,
// If the group category has not yet finished a randomly student assignment
// request, a progress object will be attached, which will contain information
// related to the progress of the assignment request. Refer to the Progress API
// for more information
"progress": null
}
"""
class CanvasUserGroup:
def __init__(self, group):
def __init__(self, group: Group):
super().__init__()
self.canvas_user_group = group
def get_name(self):
def get_name(self) -> str:
return self.canvas_user_group.name
def get_number_of_students(self):
def get_number_of_students(self) -> int:
return self.canvas_user_group.members_count
def get_students(self):
canvas_users = self.canvas_user_group.get_users()
students = []
def get_students(self) -> List[CanvasUser]:
canvas_users: Iterable[User] = self.canvas_user_group.get_users()
students: List[CanvasUser] = []
for student in canvas_users:
students.append(CanvasUser(student))
return students
def add_student(self, user):
def add_student(self, user: CanvasUser) -> None:
self.canvas_user_group.create_membership(user.get_canvas_id())
def __repr__(self):
def __repr__(self) -> str:
return self.get_name()
......@@ -264,22 +205,99 @@ class CanvasUserGroup:
"""
class CanvasUserGroupSet: # aka, group_category
def __init__(self, group_category: GroupCategory):
super().__init__()
self.canvas_group_category = group_category
def get_name(self) -> str:
return self.canvas_group_category.name
def get_groups(self) -> List[CanvasUserGroup]:
canvas_groups: Iterable[Group] = self.canvas_group_category.get_groups()
groups: List[CanvasUserGroup] = []
for group in canvas_groups:
groups.append(CanvasUserGroup(group))
return groups
def create_group(self, group_name: str) -> CanvasUserGroup:
canvas_group: Group = self.canvas_group_category.create_group(name=group_name)
return CanvasUserGroup(canvas_group)
def create_groups(self, number_of_groups: int) -> List[CanvasUserGroup]:
base_name: str = self.get_name()
groups: List[CanvasUserGroup] = []
for group_number in range(1, number_of_groups + 1):
canvas_group = self.create_group(f'{base_name} {group_number}')
# groups.append(CanvasUserGroup(canvas_group))
groups.append(canvas_group)
return groups
def __repr__(self) -> str:
return self.get_name()
"""
{
// The ID of the group category.
"id": 17,
// The display name of the group category.
"name": "Math Groups",
// Certain types of group categories have special role designations. Currently,
// these include: 'communities', 'student_organized', and 'imported'. Regular
// course/account group categories have a role of null.
"role": "communities",
// If the group category allows users to join a group themselves, thought they
// may only be a member of one group per group category at a time. Values
// include 'restricted', 'enabled', and null 'enabled' allows students to assign
// themselves to a group 'restricted' restricts them to only joining a group in
// their section null disallows students from joining groups
"self_signup": null,
// Gives instructors the ability to automatically have group leaders assigned.
// Values include 'random', 'first', and null; 'random' picks a student from the
// group at random as the leader, 'first' sets the first student to be assigned
// to the group as the leader
"auto_leader": null,
// The course or account that the category group belongs to. The pattern here is
// that whatever the context_type is, there will be an _id field named after
// that type. So if instead context_type was 'Course', the course_id field would
// be replaced by an course_id field.
"context_type": "Account",
"account_id": 3,
// If self-signup is enabled, group_limit can be set to cap the number of users
// in each group. If null, there is no limit.
"group_limit": null,
// The SIS identifier for the group category. This field is only included if the
// user has permission to manage or view SIS information.
"sis_group_category_id": null,
// The unique identifier for the SIS import. This field is only included if the
// user has permission to manage SIS information.
"sis_import_id": null,
// If the group category has not yet finished a randomly student assignment
// request, a progress object will be attached, which will contain information
// related to the progress of the assignment request. Refer to the Progress API
// for more information
"progress": null
}
"""
# ASSIGNMENT-RELATED CLASSES
class CanvasAssignment:
def __init__(self, assignment):
def __init__(self, assignment: Assignment):
super().__init__()
self.canvas_assignment = assignment
def get_name(self):
def get_name(self) -> str:
return self.canvas_assignment.name
def is_quiz(self):
def is_quiz(self) -> bool:
return 'online_quiz' in self.canvas_assignment.submission_types
def get_submission_text(self, canvas_user):
submission = self.canvas_assignment.get_submission(canvas_user.get_canvas_id())
def get_submission_text(self, canvas_user: CanvasUser) -> Union[str, None]:
submission: Submission = self.canvas_assignment.get_submission(canvas_user.get_canvas_id())
if submission.submission_type == 'online_text_entry':
return submission.body # TODO: what happens if there is no user submission?
elif submission.submission_type == 'online_url':
......@@ -289,24 +307,26 @@ class CanvasAssignment:
else:
return None
def get_quiz_response(self, canvas_user):
questions_and_answers = []
questions = []
answers = []
def get_quiz_response(self, canvas_user: CanvasUser) -> List[Dict[str, str]]:
questions_and_answers: List[Dict[str, str]] = []
questions: List[str] = []
answers: List[str] = []
if self.is_quiz():
quiz_id = self.canvas_assignment.quiz_id
course_id = self.canvas_assignment.course_id
quiz_submissions = CanvasCourse(course_id).canvas_course.get_quiz(
quiz_id: int = self.canvas_assignment.quiz_id
course_id: int = self.canvas_assignment.course_id
quiz_submissions: Iterable[QuizSubmission] = CanvasCourse(course_id).canvas_course.get_quiz(
quiz_id).get_submissions() # breaking encapsulation
candidate_submission = list(filter(lambda q: q.user_id == canvas_user.get_canvas_id(), quiz_submissions))
candidate_submission: List[QuizSubmission] = list(
filter(lambda q: q.user_id == canvas_user.get_canvas_id(), quiz_submissions))
if len(candidate_submission) > 0:
full_questions = candidate_submission[0].get_submission_questions()
full_questions: List[QuizSubmissionQuestion] = candidate_submission[0].get_submission_questions()
for question in full_questions:
questions.append(question.question_text)
submission = self.canvas_assignment.get_submission(canvas_user.get_canvas_id(),
submission: Submission = self.canvas_assignment.get_submission(canvas_user.get_canvas_id(),
include=['submission_history'])
history = sorted(list(submission.submission_history), key=lambda s: s['submitted_at'], reverse=True)
if len(history) > 0 and 'submission_data' in history[0]:
history: List[Submission] = sorted(list(submission.submission_history), key=lambda s: s['submitted_at'],
reverse=True)
if len(history) > 0 and 'submission_data' in history[0]: # TODO: add static typing to this section
submission_data = history[0]['submission_data']
for datum in submission_data:
answers.append(datum['text'])
......@@ -319,7 +339,7 @@ class CanvasAssignment:
questions_and_answers = None
return questions_and_answers
def __repr__(self):
def __repr__(self) -> str:
return self.get_name()
......@@ -555,21 +575,22 @@ class CanvasAssignment:
class CanvasAssignmentGroup:
# from canvasapi.assignment import Assignment
def __init__(self, group):
def __init__(self, group: AssignmentGroup):
super().__init__()
self.canvas_assignment_group = group
def get_name(self):
def get_name(self) -> str:
return self.canvas_assignment_group.name
def get_assignments(self):
course_id = self.canvas_assignment_group.course_id
all_assignments = CanvasCourse(course_id).get_assignments()
assignments = list(filter(lambda a: a.canvas_assignment.assignment_group_id == self.canvas_assignment_group.id,
def get_assignments(self) -> List[CanvasAssignment]:
course_id: int = self.canvas_assignment_group.course_id
all_assignments: List[CanvasAssignment] = CanvasCourse(course_id).get_assignments()
assignments: List[CanvasAssignment] = list(
filter(lambda a: a.canvas_assignment.assignment_group_id == self.canvas_assignment_group.id,
all_assignments)) # breaking encapsulation
return assignments
def __repr__(self):
def __repr__(self) -> str:
return self.get_name()
......@@ -600,63 +621,64 @@ class CanvasAssignmentGroup:
class CanvasCourse:
def __init__(self, course_id):
def __init__(self, course_id: int):
self.canvas_course = CanvasSession.get_session().get_course(course_id)
def get_all_users(self):
canvas_users = self.canvas_course.get_users()
users = []
def get_all_users(self) -> List[CanvasUser]:
canvas_users: Iterable[User] = self.canvas_course.get_users()
users: List[CanvasUser] = []
for user in canvas_users:
users.append(CanvasUser(user))
return users
def get_students(self):
canvas_users = self.canvas_course.get_users(enrollment_type=['student'])
students = []
def get_students(self) -> List[CanvasUser]:
canvas_users: Iterable[User] = self.canvas_course.get_users(enrollment_type=['student'])
students: List[CanvasUser] = []
for user in canvas_users:
students.append(CanvasUser(user))
return students
def get_instructional_team(self):
canvas_users = self.canvas_course.get_users(enrollment_type=['teacher', 'ta'])
instructors = []
def get_instructional_team(self) -> List[CanvasUser]:
canvas_users: Iterable[User] = self.canvas_course.get_users(enrollment_type=['teacher', 'ta'])
instructors: List[CanvasUser] = []
for user in canvas_users:
instructors.append(CanvasUser(user))
return instructors
def get_all_user_groups(self):
canvas_groups = self.canvas_course.get_groups()
groups = []
def get_all_user_groups(self) -> List[CanvasUserGroup]:
canvas_groups: Iterable[Group] = self.canvas_course.get_groups()
groups: List[CanvasUserGroup] = []
for group in canvas_groups:
groups.append(CanvasUserGroup(group))
return groups
def get_user_groupsets(self):
canvas_group_categories = self.canvas_course.get_group_categories()
group_sets = []
def get_user_groupsets(self) -> List[CanvasUserGroupSet]:
canvas_group_categories: Iterable[GroupCategory] = self.canvas_course.get_group_categories()
group_sets: List[CanvasUserGroupSet] = []
for group_category in canvas_group_categories:
group_sets.append(CanvasUserGroupSet(group_category))
return group_sets
def create_user_groupset(self, groupset_name):
group_category = self.canvas_course.create_group_category(groupset_name)
def create_user_groupset(self, groupset_name: str) -> CanvasUserGroupSet:
group_category: GroupCategory = self.canvas_course.create_group_category(groupset_name)
return CanvasUserGroupSet(group_category)
def get_assignment_groups(self):
canvas_assignment_groups = self.canvas_course.get_assignment_groups(include=['assignments'])
assignment_groups = []
def get_assignment_groups(self) -> List[CanvasAssignmentGroup]:
canvas_assignment_groups: Iterable[AssignmentGroup] = self.canvas_course.get_assignment_groups(
include=['assignments'])
assignment_groups: List[CanvasAssignmentGroup] = []
for assignment_group in canvas_assignment_groups:
assignment_groups.append(CanvasAssignmentGroup(assignment_group))
return assignment_groups
def get_assignments(self):
canvas_assignments = self.canvas_course.get_assignments()
assignments = []
def get_assignments(self) -> List[CanvasAssignment]:
canvas_assignments: Iterable[Assignment] = self.canvas_course.get_assignments()
assignments: List[CanvasAssignment] = []
for assignment in canvas_assignments:
assignments.append(CanvasAssignment(assignment))
return assignments
def __repr__(self):
def __repr__(self) -> str:
return f'{self.canvas_course.course_code}: {self.canvas_course.name}'
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment