caccl-api
Version:
A class that defines a set of smart Canvas endpoints that actually behave how you'd expect them to.
1,236 lines (1,167 loc) • 44.6 kB
text/typescript
/**
* Functions for interacting with courses
* @namespace api.course
*/
// Import shared classes
import CACCLError from 'caccl-error';
import ErrorCode from '../../shared/types/ErrorCode';
import EndpointCategory from '../../shared/EndpointCategory';
// Import shared types
import APIConfig from '../../shared/types/APIConfig';
import CanvasCourse from '../../types/CanvasCourse';
import InitPack from '../../shared/types/InitPack';
import CanvasEnrollment from '../../types/CanvasEnrollment';
import { DateHandlingType, dayOfWeekToNumber, DateShiftOptions } from './types/DateHandling';
// Import shared helpers
import utils from '../../shared/helpers/utils';
// Import shared constants
import API_PREFIX from '../../shared/constants/API_PREFIX';
// Import subcategories
import ECatAnalytics from './ECatAnalytics';
import ECatAnnouncement from './ECatAnnouncement';
import ECatApp from './ECatApp';
import ECatAssignment from './ECatAssignment';
import ECatAssignmentGroup from './ECatAssignmentGroup';
import ECatDiscussionTopic from './ECatDiscussionTopic';
import ECatFile from './ECatFile';
import ECatFolder from './ECatFolder';
import ECatGradebookColumn from './ECatGradebookColumn';
import ECatGroup from './ECatGroup';
import ECatGroupSet from './ECatGroupSet';
import ECatModule from './ECatModule';
import ECatNavMenuItem from './ECatNavMenuItem';
import ECatPage from './ECatPage';
import ECatQuiz from './ECatQuiz';
import ECatRubric from './ECatRubric';
import ECatSection from './ECatSection';
/*------------------------------------------------------------------------*/
/* Constants */
/*------------------------------------------------------------------------*/
const assignmentTagPrefix = '#CurrentlyBeingMigrated#';
/*------------------------------------------------------------------------*/
/* Endpoint Category */
/*------------------------------------------------------------------------*/
// Endpoint category
class ECatCourse extends EndpointCategory {
// Sub-categories
public analytics: ECatAnalytics;
public announcement: ECatAnnouncement;
public app: ECatApp;
public assignment: ECatAssignment;
public assignmentGroup: ECatAssignmentGroup;
public discussionTopic: ECatDiscussionTopic;
public file: ECatFile;
public folder: ECatFolder;
public gradebookColumn: ECatGradebookColumn;
public group: ECatGroup;
public groupSet: ECatGroupSet;
public module: ECatModule;
public navMenuItem: ECatNavMenuItem;
public page: ECatPage;
public quiz: ECatQuiz;
public rubric: ECatRubric;
public section: ECatSection;
/**
* Initialize endpoint category
* @param initPack package of info for initializing the endpoint category
*/
constructor(initPack: InitPack) {
super(initPack);
// Initialize subcategories
this.analytics = new ECatAnalytics(initPack);
this.announcement = new ECatAnnouncement(initPack);
this.app = new ECatApp(initPack);
this.assignment = new ECatAssignment(initPack);
this.assignmentGroup = new ECatAssignmentGroup(initPack);
this.discussionTopic = new ECatDiscussionTopic(initPack);
this.file = new ECatFile(initPack);
this.folder = new ECatFolder(initPack);
this.gradebookColumn = new ECatGradebookColumn(initPack);
this.group = new ECatGroup(initPack);
this.groupSet = new ECatGroupSet(initPack);
this.module = new ECatModule(initPack);
this.navMenuItem = new ECatNavMenuItem(initPack);
this.page = new ECatPage(initPack);
this.quiz = new ECatQuiz(initPack);
this.rubric = new ECatRubric(initPack);
this.section = new ECatSection(initPack);
}
/*------------------------------------------------------------------------*/
/* Course */
/*------------------------------------------------------------------------*/
/**
* Gets info on a specific course
* @author Gabe Abrams
* @method get
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to get info on
* @param {boolean} [opts.includeSyllabus] If truthy, includes
* syllabus body
* @param {boolean} [opts.includeTerm] If truthy, includes term
* @param {boolean} [opts.includeAccount] If truthy, includes account
* Id
* @param {boolean} [opts.includeDescription] If truthy, includes
* public description
* @param {boolean} [opts.includeSections] If truthy, includes
* sections
* @param {boolean} [opts.includeTeachers] If truthy, includes
* teachers
* @param {boolean} [opts.includeCourseImage] If truthy, includes the
* course image
* @param {boolean} [opts.includeNeedsGradingCount] If truthy,
* includes the number of students who still need to be graded
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasCourse>} Canvas course {@link https://canvas.instructure.com/doc/api/courses.html#Course}
*/
public async get(
opts: {
courseId?: number,
includeSyllabus?: boolean,
includeTerm?: boolean,
includeAccount?: boolean,
includeDescription?: boolean,
includeSections?: boolean,
includeTeachers?: boolean,
includeCourseImage?: boolean,
includeNeedsGradingCount?: boolean,
} = {},
config?: APIConfig,
): Promise<CanvasCourse> {
return this.visitEndpoint({
config,
action: 'get info on a specific course',
path: `${API_PREFIX}/courses/${opts.courseId ?? this.defaultCourseId}`,
method: 'GET',
params: {
include: utils.genIncludesList({
syllabus_body: opts.includeSyllabus,
term: opts.includeTerm,
account: opts.includeAccount,
public_description: opts.includeDescription,
sections: opts.includeSections,
teachers: opts.includeTeachers,
course_image: opts.includeCourseImage,
needs_grading_count: opts.includeNeedsGradingCount,
}),
},
});
}
/**
* Update whether the course is published or not
* @author Gabe Abrams
* @method updatePublishState
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to
* modify
* @param {boolean} [opts.isPublished] if true, publish the course. Otherwise,
* unpublish the course
*/
public async updatePublishState(
opts: {
courseId?: number,
isPublished?: boolean,
} = {},
config?: APIConfig,
): Promise<CanvasCourse> {
const course = await this.visitEndpoint({
config,
action: 'update the published state of a specific course',
path: `${API_PREFIX}/courses/${opts.courseId ?? this.defaultCourseId}`,
method: 'PUT',
params: {
'course[event]': (
opts.isPublished
? 'offer'
: 'claim'
),
},
});
// Throw an error if the state could not be changed
const nowPublished = (course.workflow_state !== 'unpublished');
if (nowPublished !== opts.isPublished) {
throw new CACCLError({
message: 'The course published state could not be updated, probably because the course already has graded content.',
code: ErrorCode.CoursePublishedStateNotUpdated,
});
}
return course;
}
/*------------------------------------------------------------------------*/
/* Enrollments */
/*------------------------------------------------------------------------*/
/**
* Gets the list of enrollments in a course
* @author Gabe Abrams
* @method listEnrollments
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {string} [opts.types=all] list of enrollment types to include:
* ['student', 'ta', 'teacher', 'designer', 'observer']
* Defaults to all types.
* @param {boolean} [opts.activeOnly] If truthy, only active
* enrollments included
* @param {boolean} [opts.includeAvatar] If truthy, avatar_url is
* included
* @param {boolean} [opts.includeGroups] If truthy, group_ids is
* included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasEnrollment[]>} list of Canvas Enrollments {@link https://canvas.instructure.com/doc/api/enrollments.html#Enrollment}
*/
public async listEnrollments(
opts: {
courseId?: number,
types?: (
'student'
| 'ta'
| 'teacher'
| 'designer'
| 'observer'
)[],
activeOnly?: boolean,
includeAvatar?: boolean,
includeGroups?: boolean,
} = {},
config?: APIConfig,
): Promise<CanvasEnrollment[]> {
// Create empty flexible params object
const params: { [k: string]: any } = {};
// Pre-process enrollment types
if (opts.types) {
params.type = opts.types.map((type) => {
if (type.includes('Enrollment')) {
return type;
}
return `${type.charAt(0).toUpperCase()}${type.substring(1)}Enrollment`;
});
}
// Filter to only active
if (opts.activeOnly) {
params.state = ['active'];
}
// Include avatar
if (opts.includeAvatar) {
params.include = ['avatar_url'];
}
// Include groups
if (opts.includeGroups) {
if (!params.include) {
params.include = [];
}
params.include.push('group_ids');
}
return this.visitEndpoint({
config,
action: 'get enrollments from a course',
params,
path: `${API_PREFIX}/courses/${opts.courseId ?? this.defaultCourseId}/enrollments`,
method: 'GET',
});
}
/**
* Get the list of student enrollments in a course
* @author Gabe Abrams
* @method listStudentEnrollments
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {string} [opts.activeOnly] If truthy, only active
* enrollments included
* @param {string} [opts.includeAvatar] If truthy, avatar_url is
* included
* @param {string} [opts.includeGroups] If truthy, group_ids is
* included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasEnrollment[]>} list of Canvas Enrollments {@link https://canvas.instructure.com/doc/api/enrollments.html#Enrollment}
*/
public async listStudentEnrollments(
opts: {
courseId?: number,
activeOnly?: boolean,
includeAvatar?: boolean,
includeGroups?: boolean,
} = {},
config?: APIConfig,
): Promise<CanvasEnrollment[]> {
return this.api.course.listEnrollments(
{
...opts,
types: ['student'],
},
config,
);
}
/**
* Gets the list of TAs and Teacher enrollments in a course
* @author Gabe Abrams
* @method listTeachingTeamMemberEnrollments
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {string} [opts.activeOnly] If truthy, only active
* enrollments included
* @param {string} [opts.includeAvatar] If truthy, avatar_url is
* included
* @param {string} [opts.includeGroups] If truthy, group_ids is
* included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasEnrollment[]>} list of Canvas Enrollments {@link https://canvas.instructure.com/doc/api/enrollments.html#Enrollment}
*/
public async listTeachingTeamMemberEnrollments(
opts: {
courseId?: number,
activeOnly?: boolean,
includeAvatar?: boolean,
includeGroups?: boolean,
} = {},
config?: APIConfig,
): Promise<CanvasEnrollment[]> {
return this.api.course.listEnrollments(
{
...opts,
types: ['ta', 'teacher'],
},
config,
);
}
/**
* Gets the list of designer enrollments in a course
* @author Gabe Abrams
* @method listDesignerEnrollments
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {string} [opts.activeOnly] If truthy, only active
* enrollments included
* @param {string} [opts.includeAvatar] If truthy, avatar_url is
* included
* @param {string} [opts.includeGroups] If truthy, group_ids is
* included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasEnrollment[]>} list of Canvas Enrollments {@link https://canvas.instructure.com/doc/api/enrollments.html#Enrollment}
*/
public async listDesignerEnrollments(
opts: {
courseId?: number,
activeOnly?: boolean,
includeAvatar?: boolean,
includeGroups?: boolean,
} = {},
config?: APIConfig,
): Promise<CanvasEnrollment[]> {
return this.api.course.listEnrollments(
{
...opts,
types: ['designer'],
},
config,
);
}
/**
* Gets the list of observer enrollments in a course
* @author Gabe Abrams
* @method listObserverEnrollments
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {string} [opts.activeOnly] If truthy, only active
* enrollments included
* @param {string} [opts.includeAvatar] If truthy, avatar_url is
* included
* @param {string} [opts.includeGroups] If truthy, group_ids is
* included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasEnrollment[]>} list of Canvas Enrollments {@link https://canvas.instructure.com/doc/api/enrollments.html#Enrollment}
*/
public async listObserverEnrollments(
opts: {
courseId?: number,
activeOnly?: boolean,
includeAvatar?: boolean,
includeGroups?: boolean,
} = {},
config?: APIConfig,
): Promise<CanvasEnrollment[]> {
return this.api.course.listEnrollments(
{
...opts,
types: ['observer'],
},
config,
);
}
/*------------------------------------------------------------------------*/
/* Users */
/*------------------------------------------------------------------------*/
/**
* Gets info on a specific user in a course
* @author Gabe Abrams
* @method getUser
* @memberof api.course
* @instance
* @async
* @param {object} opts object containing all arguments
* @param {number} opts.userId Canvas user Id to get
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {boolean} [opts.includeEmail] If true, user email is included
* @param {boolean} [opts.includeEnrollments] If true, user's enrollments
* in this course are included
* @param {boolean} [opts.includeLocked] If true, includes whether this
* enrollment is locked
* @param {boolean} [opts.includeAvatar] If true, user avatar url is
* included
* @param {boolean} [opts.includeBio] If true, user bio is included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasUser>} Canvas user {@link https://canvas.instructure.com/doc/api/users.html#User}
*/
public async getUser(
opts: {
userId: number,
courseId?: number,
includeEmail?: boolean,
includeEnrollments?: boolean,
includeLocked?: boolean,
includeAvatar?: boolean,
includeBio?: boolean,
},
config?: APIConfig,
) {
return this.visitEndpoint({
config,
action: 'get info on a user in a course',
path: `${API_PREFIX}/courses/${opts.courseId ?? this.defaultCourseId}/users/${opts.userId}`,
method: 'GET',
params: {
include: utils.genIncludesList({
email: opts.includeEmail,
enrollments: opts.includeEnrollments,
locked: opts.includeLocked,
avatar_url: opts.includeAvatar,
bio: opts.includeBio,
}),
},
});
}
/**
* Gets info on all users in a course
* @author Gabe Abrams
* @method listUsers
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {string} [opts.types=all] list of enrollment types to include:
* ['student', 'ta', 'teacher', 'designer', 'observer']
* Defaults to all types.
* @param {boolean} [opts.includeEmail] If true, user email is included
* @param {boolean} [opts.includeEnrollments] If true, user's enrollments
* in this course are included
* @param {boolean} [opts.includeLocked] If true, includes whether this
* enrollment is locked
* @param {boolean} [opts.includeAvatar] If true, user avatar url is
* included
* @param {boolean} [opts.includeBio] If true, user bio is included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasUser[]>} Canvas users {@link https://canvas.instructure.com/doc/api/users.html#User}
*/
public async listUsers(
opts: {
courseId?: number,
types?: (
'student'
| 'ta'
| 'teacher'
| 'designer'
| 'observer'
)[],
includeEmail?: boolean,
includeEnrollments?: boolean,
includeLocked?: boolean,
includeAvatar?: boolean,
includeBio?: boolean,
} = {},
config?: APIConfig,
) {
return this.visitEndpoint({
config,
action: 'get info on all users in a course',
path: `${API_PREFIX}/courses/${opts.courseId ?? this.defaultCourseId}/users`,
method: 'GET',
params: {
enrollment_type: opts.types,
include: utils.genIncludesList({
email: opts.includeEmail,
enrollments: opts.includeEnrollments,
locked: opts.includeLocked,
avatar_url: opts.includeAvatar,
bio: opts.includeBio,
}),
},
});
}
/**
* Gets the list of students in a course
* @author Gabe Abrams
* @method listStudents
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {string} [opts.activeOnly] If truthy, only active
* enrollments included
* @param {boolean} [opts.includeEmail] If true, user email is included
* @param {boolean} [opts.includeEnrollments] If true, user's enrollments
* in this course are included
* @param {boolean} [opts.includeLocked] If true, includes whether this
* enrollment is locked
* @param {boolean} [opts.includeAvatar] If true, user avatar url is
* included
* @param {boolean} [opts.includeBio] If true, user bio is included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasUser[]>} list of Canvas Users {@link https://canvas.instructure.com/doc/api/users.html#User}
*/
public async listStudents(
opts: {
courseId?: number,
includeEmail?: boolean,
includeEnrollments?: boolean,
includeLocked?: boolean,
includeAvatar?: boolean,
includeBio?: boolean,
} = {},
config?: APIConfig,
) {
return this.api.course.listUsers(
{
...opts,
types: ['student'],
},
config,
);
}
/**
* Gets the list of TAs and Teachers in a course
* @author Gabe Abrams
* @method listTeachingTeamMembers
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {string} [opts.activeOnly] If truthy, only active
* enrollments included
* @param {boolean} [opts.includeEmail] If true, user email is included
* @param {boolean} [opts.includeEnrollments] If true, user's enrollments
* in this course are included
* @param {boolean} [opts.includeLocked] If true, includes whether this
* enrollment is locked
* @param {boolean} [opts.includeAvatar] If true, user avatar url is
* included
* @param {boolean} [opts.includeBio] If true, user bio is included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasUser[]>} list of Canvas Users {@link https://canvas.instructure.com/doc/api/users.html#User}
*/
public async listTeachingTeamMembers(
opts: {
courseId?: number,
includeEmail?: boolean,
includeEnrollments?: boolean,
includeLocked?: boolean,
includeAvatar?: boolean,
includeBio?: boolean,
} = {},
config?: APIConfig,
) {
return this.api.course.listUsers(
{
...opts,
types: ['ta', 'teacher'],
},
config,
);
}
/**
* Gets the list of TAs in a course
* @author Gabe Abrams
* @method listTAs
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {string} [opts.activeOnly] If truthy, only active
* enrollments included
* @param {boolean} [opts.includeEmail] If true, user email is included
* @param {boolean} [opts.includeEnrollments] If true, user's enrollments
* in this course are included
* @param {boolean} [opts.includeLocked] If true, includes whether this
* enrollment is locked
* @param {boolean} [opts.includeAvatar] If true, user avatar url is
* included
* @param {boolean} [opts.includeBio] If true, user bio is included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasUser[]>} list of Canvas Users {@link https://canvas.instructure.com/doc/api/users.html#User}
*/
public async listTAs(
opts: {
courseId?: number,
includeEmail?: boolean,
includeEnrollments?: boolean,
includeLocked?: boolean,
includeAvatar?: boolean,
includeBio?: boolean,
} = {},
config?: APIConfig,
) {
return this.api.course.listUsers(
{
...opts,
types: ['ta'],
},
config,
);
}
/**
* Gets the list of teachers in a course
* @author Gabe Abrams
* @method listTeachers
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {string} [opts.activeOnly] If truthy, only active
* enrollments included
* @param {boolean} [opts.includeEmail] If true, user email is included
* @param {boolean} [opts.includeEnrollments] If true, user's enrollments
* in this course are included
* @param {boolean} [opts.includeLocked] If true, includes whether this
* enrollment is locked
* @param {boolean} [opts.includeAvatar] If true, user avatar url is
* included
* @param {boolean} [opts.includeBio] If true, user bio is included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasUser[]>} list of Canvas Users {@link https://canvas.instructure.com/doc/api/users.html#User}
*/
public async listTeachers(
opts: {
courseId?: number,
includeEmail?: boolean,
includeEnrollments?: boolean,
includeLocked?: boolean,
includeAvatar?: boolean,
includeBio?: boolean,
} = {},
config?: APIConfig,
) {
return this.api.course.listUsers(
{
...opts,
types: ['teacher'],
},
config,
);
}
/**
* Gets the list of designers in a course
* @author Gabe Abrams
* @method listDesigners
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {string} [opts.activeOnly] If truthy, only active
* enrollments included
* @param {boolean} [opts.includeEmail] If true, user email is included
* @param {boolean} [opts.includeEnrollments] If true, user's enrollments
* in this course are included
* @param {boolean} [opts.includeLocked] If true, includes whether this
* enrollment is locked
* @param {boolean} [opts.includeAvatar] If true, user avatar url is
* included
* @param {boolean} [opts.includeBio] If true, user bio is included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasUser[]>} list of Canvas Users {@link https://canvas.instructure.com/doc/api/users.html#User}
*/
public async listDesigners(
opts: {
courseId?: number,
includeEmail?: boolean,
includeEnrollments?: boolean,
includeLocked?: boolean,
includeAvatar?: boolean,
includeBio?: boolean,
} = {},
config?: APIConfig,
) {
return this.api.course.listUsers(
{
...opts,
types: ['designer'],
},
config,
);
}
/**
* Gets the list of observers in a course
* @author Gabe Abrams
* @method listObservers
* @memberof api.course
* @instance
* @async
* @param {object} [opts] object containing all arguments
* @param {number} [opts.courseId=default course id] Canvas course Id to query
* @param {string} [opts.activeOnly] If truthy, only active
* enrollments included
* @param {boolean} [opts.includeEmail] If true, user email is included
* @param {boolean} [opts.includeEnrollments] If true, user's enrollments
* in this course are included
* @param {boolean} [opts.includeLocked] If true, includes whether this
* enrollment is locked
* @param {boolean} [opts.includeAvatar] If true, user avatar url is
* included
* @param {boolean} [opts.includeBio] If true, user bio is included
* @param {APIConfig} [config] custom configuration for this specific endpoint
* call (overwrites defaults that were included when api was initialized)
* @returns {Promise<CanvasUser[]>} list of Canvas Users {@link https://canvas.instructure.com/doc/api/users.html#User}
*/
public async listObservers(
opts: {
courseId?: number,
includeEmail?: boolean,
includeEnrollments?: boolean,
includeLocked?: boolean,
includeAvatar?: boolean,
includeBio?: boolean,
} = {},
config?: APIConfig,
) {
return this.api.course.listUsers(
{
...opts,
types: ['observer'],
},
config,
);
}
/*------------------------------------------------------------------------*/
/* Migrations */
/*------------------------------------------------------------------------*/
/**
* Perform a course content migration
* @author Yuen Ler Chow
* @method migrateContent
* @memberof api.course
* @instance
* @async
* @param {object} opts object containing all arguments
* @param {number} [opts.sourceCourseId=default course id] Canvas course Id of
* the source course
* @param {number} opts.destinationCourseId Canvas course Id of the
* destination course
* @param {object} opts.include object containing all items and their ids to
* include
* @param {number[]} [opts.include.fileIds = []] list of file ids to include
* @param {number[]} [opts.include.quizIds = []] list of quiz ids to include
* @param {number[]} [opts.include.assignmentIds = []] list of assignment ids
* to include
* @param {number[]} [opts.include.announcementIds = []] list of announcement
* ids to include
* @param {number[]} [opts.include.discussionIds = []] list of discussion ids
* to include
* @param {number[]} [opts.include.moduleIds = []] list of module ids to
* include
* @param {number[]} [opts.include.pageIds = []] list of page ids to include
* @param {number[]} [opts.include.rubricIds = []] list of rubric ids to
* include
* @param {DateShiftOptions} opts.dateShiftOptions options for shifting dates
* @param {number} [opts.timeoutMs = 5 minutes] maximum time in milliseconds
* to wait for course migration to finish
* @param {APIConfig} [config] custom configuration for this specific endpoint
*/
public async migrateContent(
opts: {
sourceCourseId: number,
destinationCourseId: number,
include: {
fileIds?: number[],
quizIds?: number[],
assignmentIds?: number[],
announcementIds?: number[],
discussionTopicsIds?: number[],
moduleIds?: number[],
pageIds?: number[],
rubricIds?: number[],
},
dateShiftOptions: DateShiftOptions,
timeoutMs?: number,
},
) {
const {
sourceCourseId,
destinationCourseId,
include,
dateShiftOptions,
timeoutMs = 300000, // 5 minutes
} = opts;
// If the user didn't specify the ids for an item,
// just make it an empty array
const {
fileIds = [],
quizIds = [],
assignmentIds = [],
announcementIds = [],
discussionTopicsIds = [],
moduleIds = [],
pageIds = [],
rubricIds = [],
} = include;
// Create a params object that we'll dynamically fill
// with params depending on the request
const params: { [k: string]: any } = {
migration_type: 'course_copy_importer',
settings: {
source_course_id: sourceCourseId,
overwrite_quizzes: true,
},
};
// Add selected ids to the request
params.select = {
files: fileIds,
quizzes: quizIds,
assignments: assignmentIds,
announcements: announcementIds,
discussion_topics: discussionTopicsIds,
modules: moduleIds,
pages: pageIds,
rubrics: rubricIds,
};
// if we remove dates we don't need to provide start and end dates,
// but if we shift dates, we do
if (dateShiftOptions.dateHandling === DateHandlingType.RemoveDates) {
params.date_shift_options = {
remove_dates: true,
};
} else if (dateShiftOptions.dateHandling === DateHandlingType.ShiftDates) {
const {
oldStart,
oldEnd,
newStart,
newEnd,
daySubstitutionMap = {},
} = dateShiftOptions;
// Translate input (day week map) to number-based params that Canvas uses
const dayNumberSubstitutionMap: { [k: number]: number } = {};
Object.keys(daySubstitutionMap).forEach((k) => {
const key = k as keyof typeof daySubstitutionMap;
dayNumberSubstitutionMap[dayOfWeekToNumber[key]] = (
dayOfWeekToNumber[daySubstitutionMap[key]]
);
});
// Add date shift info to the request
params.date_shift_options = {
shift_dates: true,
old_start_date: oldStart,
old_end_date: oldEnd,
new_start_date: newStart,
new_end_date: newEnd,
day_substitutions: dayNumberSubstitutionMap,
};
}
// Iterate through each assignment and change the name to be
// current name + [id]
for (let i = 0; i < assignmentIds.length; i++) {
const id = assignmentIds[i];
const { name } = await this.api.course.assignment.get({
assignmentId: id,
courseId: sourceCourseId,
});
this.api.course.assignment.update({
assignmentId: id,
courseId: sourceCourseId,
name: `${name}${assignmentTagPrefix}${id}`,
});
}
// Create the migration
try {
const contentMigration = await this.visitEndpoint({
path: `${API_PREFIX}/courses/${destinationCourseId}/content_migrations`,
action: 'perform a course content migration',
method: 'POST',
params,
});
// Initialize status variables that are updated on each check
let workflowState = 'running';
let migrationIssuesCount = 0;
const CHECK_INTERVAL_MS = 500;
// Calculate num iterations
const numIterations = Math.ceil(timeoutMs / CHECK_INTERVAL_MS);
// Continuously check every CHECK_INTERVAL_MS if the migration is
// finished, failed, or timed out
for (let i = 0; i < numIterations; i++) {
// Wait for CHECK_INTERVAL_MS
await new Promise((resolve) => {
setTimeout(resolve, CHECK_INTERVAL_MS);
});
// Go to the api endpoint to get the status of the content migration
const status = await this.visitEndpoint({
path: `${API_PREFIX}/courses/${destinationCourseId}/content_migrations/${contentMigration.id}`,
action: 'check the status of a content migration',
method: 'GET',
});
workflowState = status.workflow_state;
migrationIssuesCount = status.migration_issues_count;
// If the workflow is no longer running, end the loop
if (workflowState === 'completed' || workflowState === 'failed') {
break;
}
}
// Detect a timeout (if the workflow never left the pending state)
if (workflowState !== 'completed' && workflowState !== 'failed') {
throw new CACCLError({
message: 'Migration timed out',
code: ErrorCode.MigrationTimeout,
});
}
if (migrationIssuesCount > 0) {
// Go to the api endpoint to get a list of migration issues
const migrationIssues = await this.visitEndpoint({
path: `${API_PREFIX}/courses/${destinationCourseId}/content_migrations/${contentMigration.id}/migration_issues`,
action: 'get migration issues',
method: 'GET',
});
let errorsAsText: string;
// If there is only 1 issue, we simply print the issue.
// If there is more than 1, we need to concatenate these
// issues with commas + ands
if (migrationIssuesCount === 1) {
errorsAsText = migrationIssues[0].description;
} else if (migrationIssuesCount === 2) {
errorsAsText = `${migrationIssues[0].description} and ${migrationIssues[1].description}`;
} else {
errorsAsText = (
migrationIssues
// Extract only the descriptions and add "and" to last item
.map((migrationIssue: any, i: number) => {
if (i === migrationIssues.length - 1) {
return `and ${migrationIssue.description}`;
}
return migrationIssue.description;
})
// Put together
.join(', ')
);
}
const errorMessage = `We ran into an error while migrating your course content: ${errorsAsText}.`;
throw new CACCLError({
message: errorMessage,
code: ErrorCode.MigrationIssue,
});
}
} catch (err) {
if (err instanceof CACCLError) {
// Rethrow the error (it's already in the right format)
throw err;
}
// An unknown error occurred. Throw a new error
throw new CACCLError({
message: err,
code: ErrorCode.MigrationIssue,
});
}
let sourceAssignments = await this.api.course.assignment.list({
courseId: sourceCourseId,
});
// filter sourceAssignments to only those that were migrated
sourceAssignments = sourceAssignments.filter((assignment) => {
return assignmentIds.includes(assignment.id);
});
const destinationAssignments = await this.api.course.assignment.list({
courseId: destinationCourseId,
});
// mapping source group id to destination group id
const assignmentGroupMap: { [k: number]: number } = {};
// mapping source assignment id to destination assignment id
const assignmentMap: { [k: number]: number } = {};
// iterate through each source assignment to determine the mapping
sourceAssignments.forEach((sourceAssignment) => {
const destinationAssignment = destinationAssignments.find((assignment) => {
return assignment.name === sourceAssignment.name;
});
if (destinationAssignment) {
assignmentMap[sourceAssignment.id] = destinationAssignment.id;
} else {
throw new CACCLError({
message: 'Could not find a migrated assignment in the destination course.',
code: ErrorCode.CouldNotFindDestinationAssignment,
});
}
});
// iterate through each assignment group in the source course and
// create the same assignment group in the destination course
const sourceAssignmentGroups = await this.api.course.assignmentGroup.list({
courseId: sourceCourseId,
});
// Get the list of existing assignment groups in the destination course
const destinationAssignmentGroups = await this.api.course.assignmentGroup.list({
courseId: destinationCourseId,
});
// check if apply_assignment_group_weights is true in the source course
const sourceCourse = await this.api.course.get({
courseId: sourceCourseId,
});
const applyAssignmentGroupWeights = sourceCourse.apply_assignment_group_weights;
for (let i = 0; i < sourceAssignmentGroups.length; i++) {
const sourceId = sourceAssignmentGroups[i].id;
const sourceAssignmentGroup = await this.api.course.assignmentGroup.get({
assignmentGroupId: sourceId,
courseId: sourceCourseId,
});
// Check if the assignment group name already exists in the destination course,
// in which we case we do not create a new assignment group
// instead, get the id of this matching assignment group and update weights if needed
// and also add this assignment group to the map
let destinationAssignmentGroup = destinationAssignmentGroups.find((assignmentGroup) => {
return assignmentGroup.name === sourceAssignmentGroup.name;
});
if (destinationAssignmentGroup) {
// assignment group exists: update weights
destinationAssignmentGroup = await this.api.course.assignmentGroup.update({
courseId: destinationCourseId,
assignmentGroupId: destinationAssignmentGroup.id,
weight: (
applyAssignmentGroupWeights
? sourceAssignmentGroup.group_weight
: undefined
),
});
} else {
// assignment group doesn't exist: create it, start it off with the weights
destinationAssignmentGroup = await this.api.course.assignmentGroup.create({
courseId: destinationCourseId,
name: sourceAssignmentGroup.name,
weight: (
applyAssignmentGroupWeights
? sourceAssignmentGroup.group_weight
: undefined
),
});
}
// set apply_assignment_group_weights to true/false in the destination course
await this.visitEndpoint({
path: `${API_PREFIX}/courses/${destinationCourseId}`,
action: 'set apply_assignment_group_weights to true',
method: 'PUT',
params: {
course: {
apply_assignment_group_weights: applyAssignmentGroupWeights,
},
},
});
// add assignment group mapping
assignmentGroupMap[sourceId] = destinationAssignmentGroup.id;
}
// iterate through each source assignment
for (let i = 0; i < sourceAssignments.length; i++) {
const sourceAssignment = sourceAssignments[i];
// Get the assignment group id of the assignment
const assignmentGroupId = sourceAssignment.assignment_group_id;
const destinationAssignmentGroupId = assignmentGroupMap[assignmentGroupId];
// throw an error if the assignment group id is not in the map
if (!destinationAssignmentGroupId) {
throw new CACCLError({
message: 'Could not find assignment group id in map',
code: ErrorCode.CouldNotFindDestinationAssignmentGroup,
});
}
// determine the id of the assignment in the new course by using the map
const destinationAssignmentId = assignmentMap[sourceAssignment.id];
// throw an error if the assignment id is not in the map
if (!destinationAssignmentId) {
throw new CACCLError({
message: 'Could not find assignment id in map',
code: ErrorCode.CouldNotFindDestinationAssignment,
});
}
// Remove tag from assignment names
const parts = sourceAssignment.name.split('#');
const tag = parts[parts.length - 1];
const originalAssignmentName = sourceAssignment.name.substring(
// Start at beginning of name
0,
// Cut off the tag from the end
sourceAssignment.name.length - (`${assignmentTagPrefix}${tag}`).length,
);
// Update the assignment group id of the assignment and remove the tag from the name in the destination course
await this.api.course.assignment.update({
courseId: destinationCourseId,
assignmentId: destinationAssignmentId,
assignmentGroupId: destinationAssignmentGroupId,
name: originalAssignmentName,
});
// remove tag from name in original course
await this.api.course.assignment.update({
courseId: sourceCourseId,
assignmentId: sourceAssignment.id,
name: originalAssignmentName,
});
}
// iterate through the source assignment groups and update the drop rules in the destination groups
for (let i = 0; i < sourceAssignmentGroups.length; i++) {
const sourceAssignmentGroup = sourceAssignmentGroups[i];
// use map to find destination assignment group id
const destinationAssignmentGroupId = assignmentGroupMap[sourceAssignmentGroup.id];
// use the assignment map to map ids in sourceAssignmentGroup.rules.never_drop
let destinationNeverDrop: number[] = [];
if (sourceAssignmentGroup.rules.never_drop) {
destinationNeverDrop = sourceAssignmentGroup.rules.never_drop.map(
(id: number) => { return assignmentMap[id]; },
);
}
// update destination assignment group
await this.api.course.assignmentGroup.update({
courseId: destinationCourseId,
assignmentGroupId: destinationAssignmentGroupId,
dropLowest: sourceAssignmentGroup.rules.drop_lowest,
dropHighest: sourceAssignmentGroup.rules.drop_highest,
neverDrop: destinationNeverDrop,
});
}
}
}
/*------------------------------------------------------------------------*/
/* Export */
/*------------------------------------------------------------------------*/
export default ECatCourse;