From 85066532258e93b934638e17f9b58973bc425189 Mon Sep 17 00:00:00 2001 From: Justin Lee Date: Wed, 30 Oct 2024 15:52:51 -0700 Subject: [PATCH] fixed some bugs and updated readme (#69) --- README.md | 36 +++------ package.json | 1 + src/downloadFile.js | 61 +++++++++------ src/getQuizSubmissionEvents.js | 33 +------- src/getQuizSubmissions.js | 33 +------- src/index.js | 136 ++++++++++++++++----------------- 6 files changed, 121 insertions(+), 179 deletions(-) diff --git a/README.md b/README.md index 941e430..f4273bd 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest) # Canvas LMS API for Node.js -Canvas API functions bundled as a NPM package for Node.js. +Canvas API functions bundled as a NPM package for Node.js. Note: this package no longer supports CJS as of version 2.0.0. If you need CJS support, please use [version 1.8.0](https://www.npmjs.com/package/node-canvas-api/v/1.8.0). ## Getting Started These instructions will get you a copy of the project up and running on your local machine for use with your own API tokens and Canvas domains. @@ -14,33 +14,28 @@ These instructions will get you a copy of the project up and running on your loc ### Installation -To use with node: +To use with NodeJS: ```bash $ npm install node-canvas-api ``` -Then, in wherever you want to use this package: -```js -const canvasAPI = require('node-canvas-api') -``` Rename the `sample.env` file to `.env` and add your institution's domain and API access token. -Attached to the `canvasAPI` are a [bunch of functions](https://github.com/ubccapico/node-canvas-api/tree/master/src). +Attached to the `canvasAPI` are a [bunch of functions](https://github.com/ubc/node-canvas-api/tree/master/src). Run the attached functions! ### Example Usage #### Get information about self: ```js -const canvasAPI = require('node-canvas-api') +import { getSelf } from 'node-canvas-api' -canvasAPI.getSelf() - .then(self => console.log(self)) +getSelf().then(x => console.log(x)) ``` #### Get students in a course: ```js -const { getUsersInCourse, getOptions } = require('node-canvas-api') +import { getUsersInCourse, getOptions } from 'node-canvas-api' getUsersInCourse(12345, getOptions.users.enrollmentType.student) // first argument is Canvas course ID .then(students => console.log(students)) @@ -50,23 +45,14 @@ getUsersInCourse(12345, getOptions.users.enrollmentType.student) // first argume Contributions are welcome and greatly appreciated! ### How to contribute -1. Create an [issue](https://github.com/ubccapico/node-canvas-api/issues) describing what contribution you are planning to make. +1. Create an [issue](https://github.com/ubc/node-canvas-api/issues) describing what contribution you are planning to make. 1. Fork the repo. -1. Add your contributions to the [`source` directory](https://github.com/ubccapico/node-canvas-api/tree/master/source) (and not the `src` directory. This directory is generated by [Rollup](https://rollupjs.org/guide/en/) during the build phase, and is what gets published to [npm](https://www.npmjs.com/package/node-canvas-api).) -1. If you add a file to `source`, please add the file to the existing [`index.js` inside `source`](https://github.com/ubccapico/node-canvas-api/blob/master/source/index.js), so that during the build your contribution will be included in `src`. -1. Test your code by creating a file in the root of the project directory, importing your code addition from `src`, and running it. -1. Once you're happy with your contribution, open an [pull request](https://github.com/ubccapico/node-canvas-api/pulls) and I'll take a look. - -### Hypothetical scenario for adding new feature -1. Add new file to `source` folder (say that the new file you want to add is `getUsersWithGradeThreshold.js`) -2. In the `source` folder, there’s an `index.js` file that lists all of the files you want exported to `src`. There you will add: `export { default as getUsersWithGradeThreshold } from './getUsersWithGradeThreshold'` -3. Run `npm run build`, which will build the project and output your new function into `src`. -4. In the `index.js` at the root of the project that you create (this is not the `index.js` that’s in the `source` folder), import your new file from `src` like this: `const getUsersWithGradeThreshold = require('.src/getUsersWithGradeThreshold')` and test by running the code: `node index.js`. -5. Once you’re happy that it works, create a PR. +1. Add your contributions. +1. Once you're happy with your contribution, open an [pull request](https://github.com/ubc/node-canvas-api/pulls) and I'll take a look. ## Usage -* [Canvas Rubrics](https://github.com/ubccapico/canvas-rubric) -* [Canvas Discussions](https://github.com/ubccapico/canvas-discussion) +* [Canvas Rubrics](https://github.com/ubc/canvas-rubric) +* [Canvas Discussions](https://github.com/ubc/canvas-discussion) * [Canvas Syllabus](https://github.com/UBC-LFS/lfs-canvas-syllabus) ## License diff --git a/package.json b/package.json index 1e658e5..12eb7b5 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "files": [ "src" ], + "main": "src/index.js", "author": "Justin Lee", "license": "MIT", "type": "module", diff --git a/src/downloadFile.js b/src/downloadFile.js index 51caeeb..1dc57bd 100644 --- a/src/downloadFile.js +++ b/src/downloadFile.js @@ -1,36 +1,49 @@ -import request from 'request-promise' +import fetch from 'node-fetch' import getFile from './getFile.js' -import fs from 'fs.js' +import fs from 'fs' +import path from 'path' const token = process.env.CANVAS_API_TOKEN -const requestObj = url => ({ - 'method': 'GET', - 'uri': url, - 'json': true, - 'resolveWithFullResponse': true, - 'headers': { - 'Authorization': 'Bearer ' + token - } -}) - /** * Downloads specified fileID from Canvas to specified path * @param {Number} fileId the fileId of the file - * @param {String} path the path that the file should be downloaded to - * @return {Promise} A Promise that resolves to a log that inidicated what the filename of the downloaded file is. On error, logs the error + * @param {String} downloadPath the path that the file should be downloaded to + * @return {Promise} A Promise that resolves to a log that indicates what the filename of the downloaded file is. On error, logs the error */ -const downloadFile = (fileId, path) => - getFile(fileId, path) - .then(({ url, filename }) => { - const modifiedName = filename.replace(/%28/g, '(').replace(/%29/g, ')').replace(/%E2%80%93/g, '-') - const pdfStream = fs.createWriteStream(path + modifiedName) - request(requestObj(url)).pipe(pdfStream) - return new Promise((resolve, reject) => { - pdfStream.on('finish', () => resolve(modifiedName)) - pdfStream.on('error', err => reject(err)) - }) +const downloadFile = async (fileId, downloadPath) => { + try { + const { url, filename } = await getFile(fileId, downloadPath) + const modifiedName = filename + .replace(/%28/g, '(') + .replace(/%29/g, ')') + .replace(/%E2%80%93/g, '-') + + const fileStream = fs.createWriteStream(path.join(downloadPath, modifiedName)) + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}` + } }) + if (!response.ok) { + throw new Error(`Failed to download file: ${response.statusText}`) + } + + // Pipe the response body to the file stream + response.body.pipe(fileStream) + + return new Promise((resolve, reject) => { + fileStream.on('finish', () => resolve(modifiedName)) + fileStream.on('error', err => reject(err)) + }) + } catch (error) { + console.error(`Error downloading file: ${error.message}`) + throw error // Rethrow the error after logging + } +} + export default downloadFile diff --git a/src/getQuizSubmissionEvents.js b/src/getQuizSubmissionEvents.js index f3805d3..dc95e68 100644 --- a/src/getQuizSubmissionEvents.js +++ b/src/getQuizSubmissionEvents.js @@ -1,35 +1,6 @@ import buildOptions from './internal/util.js' -import request from 'request-promise' -import linkparser from 'parse-link-header' -import Bottleneck from 'bottleneck' - -const token = process.env.CANVAS_API_TOKEN - -const limiter = new Bottleneck({ - maxConcurrent: 20, - minTime: 100 -}) - -const requestObj = url => ({ - 'method': 'GET', - 'uri': url, - 'json': true, - 'resolveWithFullResponse': true, - 'headers': { - 'Authorization': 'Bearer ' + token - } -}) - -const fetchAll = (url, result = []) => - request(requestObj(url)) - .then(response => { - result = [...result, response.body] - const links = linkparser(response.headers.link) - return links.next ? fetchAll(links.next.url, result) : result - }).catch(err => console.log(err)) - -const fetchAllRateLimited = limiter.wrap(fetchAll) +import fetchAll from './internal/fetchAll.js' const canvasDomain = process.env.CANVAS_API_DOMAIN @@ -43,5 +14,5 @@ const canvasDomain = process.env.CANVAS_API_DOMAIN */ export default function getQuizSubmissionEvents (courseId, quizId, submissionId, ...options) { - return fetchAllRateLimited(canvasDomain + `/courses/${courseId}/quizzes/${quizId}/submissions/${submissionId}/events?` + buildOptions(options)) + return fetchAll(canvasDomain + `/courses/${courseId}/quizzes/${quizId}/submissions/${submissionId}/events?` + buildOptions(options)) } diff --git a/src/getQuizSubmissions.js b/src/getQuizSubmissions.js index 102b55c..cb37fff 100644 --- a/src/getQuizSubmissions.js +++ b/src/getQuizSubmissions.js @@ -1,35 +1,6 @@ import buildOptions from './internal/util.js' -import request from 'request-promise' -import linkparser from 'parse-link-header' -import Bottleneck from 'bottleneck' - -const token = process.env.CANVAS_API_TOKEN - -const limiter = new Bottleneck({ - maxConcurrent: 20, - minTime: 100 -}) - -const requestObj = url => ({ - 'method': 'GET', - 'uri': url, - 'json': true, - 'resolveWithFullResponse': true, - 'headers': { - 'Authorization': 'Bearer ' + token - } -}) - -const fetchAll = (url, result = []) => - request(requestObj(url)) - .then(response => { - result = [...result, response.body] - const links = linkparser(response.headers.link) - return links.next ? fetchAll(links.next.url, result) : result - }).catch(err => console.log(err)) - -const fetchAllRateLimited = limiter.wrap(fetchAll) +import fetchAll from './internal/fetchAll.js' const canvasDomain = process.env.CANVAS_API_DOMAIN @@ -42,5 +13,5 @@ const canvasDomain = process.env.CANVAS_API_DOMAIN */ export default function getQuizSubmissions (courseId, quizId, ...options) { - return fetchAllRateLimited(canvasDomain + `/courses/${courseId}/quizzes/${quizId}/submissions?` + buildOptions(options)) + return fetchAll(canvasDomain + `/courses/${courseId}/quizzes/${quizId}/submissions?` + buildOptions(options)) } diff --git a/src/index.js b/src/index.js index fafdced..51dd9ba 100644 --- a/src/index.js +++ b/src/index.js @@ -1,68 +1,68 @@ -export { default as batchCopyCourseContent } from './batchCopyCourseContent' -export { default as copyCourseContent } from './copyCourseContent' -export { default as createCourse } from './createCourse' -export { default as createCustomGradebookColumn } from './createCustomGradebookColumn' -export { default as createUser } from './createUser' -export { default as createUserCourseEnrollment } from './createUserCourseEnrollment' -export { default as createUserSectionEnrollment } from './createUserSectionEnrollment' -export { default as deleteAllCustomGradebookColumns } from './deleteAllCustomGradebookColumns' -export { default as downloadFile } from './downloadFile' -export { default as deleteCustomGradebookColumn } from './deleteCustomGradebookColumn' -export { default as getAccountIds } from './getAccountIds' -export { default as getAccounts } from './getAccounts' -export { default as getAllCoursesInAccount } from './getAllCoursesInAccount' -export { default as getAllCoursesInDept } from './getAllCoursesInDept' -export { default as getAllCourseSyllabiInAccount } from './getAllCourseSyllabiInAccount' -export { default as getAnalytics } from './getAnalytics' -export { default as getAssignments } from './getAssignments' -export { default as getCourses } from './getCourses' -export { default as getCoursesByUser } from './getCoursesByUser' -export { default as getCustomGradeBookColumns } from './getCustomGradeBookColumns' -export { default as getDeptIdsInAccount } from './getDeptIdsInAccount' -export { default as getProgress } from './getProgress' -export { default as getSubaccounts } from './getSubaccounts' -export { default as getSyllabusOfCourse } from './getSyllabusOfCourse' -export { default as getUserPageViews } from './getUserPageViews' -export { default as getUsersInAccount } from './getUsersInAccount' -export { default as getUsersInCourse } from './getUsersInCourse' -export { default as hideCustomGradebookColumn } from './hideCustomGradebookColumn' -export { default as hideCustomGradebookColumnsByName } from './hideCustomGradebookColumnsByName' -export { default as putStudentNumberInExistingCustomColumn } from './putStudentNumberInExistingCustomColumn' -export { default as putStudentNumberInGradeColumn } from './putStudentNumberInGradeColumn' -export { default as putStudentNumbersInGradebook } from './putStudentNumbersInGradebook' -export { default as showCustomGradebookColumn } from './showCustomGradebookColumn' -export { default as showCustomGradebookColumnsByName } from './showCustomGradebookColumnsByName' -export { default as checkProgressStatus } from './checkProgressStatus' -export { default as getOptions } from './internal/getOptions' -export { default as getUser } from './getUser' -export { default as getRubricsInCourse } from './getRubricsInCourse' -export { default as getRubric } from './getRubric' -export { default as getAssignmentSubmissions } from './getAssignmentSubmissions' -export { default as getEnrollmentsInCourse } from './getEnrollmentsInCourse' -export { default as getSections } from './getSections' -export { default as getModules } from './getModules' -export { default as getModuleItems } from './getModuleItems' -export { default as getDiscussionTopics } from './getDiscussionTopics' -export { default as getDiscussionTopic } from './getDiscussionTopic' -export { default as getFullDiscussion } from './getFullDiscussion' -export { default as getSelf } from './getSelf' -export { default as getOutcome } from './getOutcome' -export { default as getQuizSubmissions } from './getQuizSubmissions' -export { default as getQuizSubmissionEvents } from './getQuizSubmissionEvents' -export { default as getQuizQuestions } from './getQuizQuestions' -export { default as getCourseAnalytics } from './getCourseAnalytics' -export { default as getPlannerItemsByUser } from './getPlannerItemsByUser' -export { default as getGroupsInCourse } from './getGroupsInCourse' -export { default as getGroupDiscussionTopics } from './getGroupDiscussionTopics' -export { default as getFullGroupDiscussion } from './getFullGroupDiscussion' -export { default as getGroupDiscussionTopic } from './getGroupDiscussionTopic' -export { default as getQuizSubmission } from './getQuizSubmission' -export { default as postAssignmentSubmissionComment } from './postAssignmentSubmissionComment' -export { default as deleteSubmissionComment } from './deleteSubmissionComment' -export { default as getCourse } from './getCourse' -export { default as getCourseSections } from './getCourseSections' -export { default as getEnrollmentsInSection } from './getEnrollmentsInSection' -export { default as createGroup } from './createGroup' -export { default as createGroupCategories } from './createGroupCategories' -export { default as createGroupMembership } from './createGroupMembership' -export { default as getHistory } from './getHistory' +export { default as batchCopyCourseContent } from './batchCopyCourseContent.js' +export { default as copyCourseContent } from './copyCourseContent.js' +export { default as createCourse } from './createCourse.js' +export { default as createCustomGradebookColumn } from './createCustomGradebookColumn.js' +export { default as createUser } from './createUser.js' +export { default as createUserCourseEnrollment } from './createUserCourseEnrollment.js' +export { default as createUserSectionEnrollment } from './createUserSectionEnrollment.js' +export { default as deleteAllCustomGradebookColumns } from './deleteAllCustomGradebookColumns.js' +export { default as downloadFile } from './downloadFile.js' +export { default as deleteCustomGradebookColumn } from './deleteCustomGradebookColumn.js' +export { default as getAccountIds } from './getAccountIds.js' +export { default as getAccounts } from './getAccounts.js' +export { default as getAllCoursesInAccount } from './getAllCoursesInAccount.js' +export { default as getAllCoursesInDept } from './getAllCoursesInDept.js' +export { default as getAllCourseSyllabiInAccount } from './getAllCourseSyllabiInAccount.js' +export { default as getAnalytics } from './getAnalytics.js' +export { default as getAssignments } from './getAssignments.js' +export { default as getCourses } from './getCourses.js' +export { default as getCoursesByUser } from './getCoursesByUser.js' +export { default as getCustomGradeBookColumns } from './getCustomGradeBookColumns.js' +export { default as getDeptIdsInAccount } from './getDeptIdsInAccount.js' +export { default as getProgress } from './getProgress.js' +export { default as getSubaccounts } from './getSubaccounts.js' +export { default as getSyllabusOfCourse } from './getSyllabusOfCourse.js' +export { default as getUserPageViews } from './getUserPageViews.js' +export { default as getUsersInAccount } from './getUsersInAccount.js' +export { default as getUsersInCourse } from './getUsersInCourse.js' +export { default as hideCustomGradebookColumn } from './hideCustomGradebookColumn.js' +export { default as hideCustomGradebookColumnsByName } from './hideCustomGradebookColumnsByName.js' +export { default as putStudentNumberInExistingCustomColumn } from './putStudentNumberInExistingCustomColumn.js' +export { default as putStudentNumberInGradeColumn } from './putStudentNumberInGradeColumn.js' +export { default as putStudentNumbersInGradebook } from './putStudentNumbersInGradebook.js' +export { default as showCustomGradebookColumn } from './showCustomGradebookColumn.js' +export { default as showCustomGradebookColumnsByName } from './showCustomGradebookColumnsByName.js' +export { default as checkProgressStatus } from './checkProgressStatus.js' +export { default as getOptions } from './internal/getOptions.js' +export { default as getUser } from './getUser.js' +export { default as getRubricsInCourse } from './getRubricsInCourse.js' +export { default as getRubric } from './getRubric.js' +export { default as getAssignmentSubmissions } from './getAssignmentSubmissions.js' +export { default as getEnrollmentsInCourse } from './getEnrollmentsInCourse.js' +export { default as getSections } from './getSections.js' +export { default as getModules } from './getModules.js' +export { default as getModuleItems } from './getModuleItems.js' +export { default as getDiscussionTopics } from './getDiscussionTopics.js' +export { default as getDiscussionTopic } from './getDiscussionTopic.js' +export { default as getFullDiscussion } from './getFullDiscussion.js' +export { default as getSelf } from './getSelf.js' +export { default as getOutcome } from './getOutcome.js' +export { default as getQuizSubmissions } from './getQuizSubmissions.js' +export { default as getQuizSubmissionEvents } from './getQuizSubmissionEvents.js' +export { default as getQuizQuestions } from './getQuizQuestions.js' +export { default as getCourseAnalytics } from './getCourseAnalytics.js' +export { default as getPlannerItemsByUser } from './getPlannerItemsByUser.js' +export { default as getGroupsInCourse } from './getGroupsInCourse.js' +export { default as getGroupDiscussionTopics } from './getGroupDiscussionTopics.js' +export { default as getFullGroupDiscussion } from './getFullGroupDiscussion.js' +export { default as getGroupDiscussionTopic } from './getGroupDiscussionTopic.js' +export { default as getQuizSubmission } from './getQuizSubmission.js' +export { default as postAssignmentSubmissionComment } from './postAssignmentSubmissionComment.js' +export { default as deleteSubmissionComment } from './deleteSubmissionComment.js' +export { default as getCourse } from './getCourse.js' +export { default as getCourseSections } from './getCourseSections.js' +export { default as getEnrollmentsInSection } from './getEnrollmentsInSection.js' +export { default as createGroup } from './createGroup.js' +export { default as createGroupCategories } from './createGroupCategories.js' +export { default as createGroupMembership } from './createGroupMembership.js' +export { default as getHistory } from './getHistory.js'