@ueu/ueu-canvas 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/html-loader.js +7 -0
- package/jest.config.js +20 -0
- package/package.json +45 -0
- package/src/Account.ts +69 -0
- package/src/NotImplementedException.ts +3 -0
- package/src/__mocks__/Account.ts +55 -0
- package/src/__mocks__/index.ts +8 -0
- package/src/__mocks__/mockAccountData.ts +43 -0
- package/src/__mocks__/mockEnrollment.ts +22 -0
- package/src/__mocks__/mockRubricData.ts +71 -0
- package/src/__mocks__/mockTabData.ts +13 -0
- package/src/__mocks__/mockTermData.ts +7 -0
- package/src/__mocks__/mockUserData.ts +14 -0
- package/src/__mocks__/utils.ts +9 -0
- package/src/__tests__/canvasUtils.test.ts +527 -0
- package/src/__tests__/fetch.test.ts +310 -0
- package/src/__tests__/profile.test.ts +19 -0
- package/src/__tests__/rubrics.test.ts +131 -0
- package/src/baseCanvasObject.ts +139 -0
- package/src/canvasDataDefs.ts +124 -0
- package/src/canvasUtils.ts +420 -0
- package/src/consts.ts +2 -0
- package/src/content/BaseContentItem.ts +219 -0
- package/src/content/ContentKind.ts +85 -0
- package/src/content/__mocks__/determineContent.ts +27 -0
- package/src/content/__mocks__/mockAssignmentSubmission.ts +37 -0
- package/src/content/__mocks__/mockContentData.ts +98 -0
- package/src/content/__mocks__/openThisContentInTarget.ts +2 -0
- package/src/content/__testingUtils__/utils.ts +47 -0
- package/src/content/__tests__/determineContent.test.ts +101 -0
- package/src/content/__tests__/getContentFuncs.test.ts +78 -0
- package/src/content/assignments/Assignment.ts +69 -0
- package/src/content/assignments/AssignmentKind.ts +29 -0
- package/src/content/assignments/__tests__/AssignmentKind.test.ts +31 -0
- package/src/content/assignments/__tests__/assignments.test.ts +72 -0
- package/src/content/assignments/deleteAssignmentGroup.ts +15 -0
- package/src/content/assignments/genAssignmentGroups.ts +15 -0
- package/src/content/assignments/index.ts +31 -0
- package/src/content/assignments/legacy.ts +11 -0
- package/src/content/assignments/updateAssignmentDueDates.ts +25 -0
- package/src/content/determineContent.ts +72 -0
- package/src/content/discussions/Discussion.ts +32 -0
- package/src/content/discussions/DiscussionKind.ts +30 -0
- package/src/content/discussions/__tests__/DiscussionKind.test.ts +31 -0
- package/src/content/discussions/genEquivalentDiscussionData.ts +97 -0
- package/src/content/discussions/index.ts +2 -0
- package/src/content/getContentFuncs.ts +30 -0
- package/src/content/index.ts +11 -0
- package/src/content/openThisContentInTarget.ts +38 -0
- package/src/content/pages/Page.ts +35 -0
- package/src/content/pages/PageKind.ts +39 -0
- package/src/content/pages/__tests__/PageKind.test.ts +86 -0
- package/src/content/pages/__tests__/learningMaterialsForModule.ts +57 -0
- package/src/content/pages/index.ts +12 -0
- package/src/content/pages/learningMaterialsForModule.ts +16 -0
- package/src/content/pages/types.ts +11 -0
- package/src/content/quizzes/Quiz.ts +25 -0
- package/src/content/quizzes/QuizKind.ts +23 -0
- package/src/content/quizzes/__tests__/QuizKind.test.ts +30 -0
- package/src/content/quizzes/index.ts +3 -0
- package/src/content/quizzes/types.ts +101 -0
- package/src/content/types.ts +302 -0
- package/src/course/Course.ts +538 -0
- package/src/course/IBlueprintCourse.ts +8 -0
- package/src/course/__mocks__/blueprint.ts +31 -0
- package/src/course/__mocks__/mockCourseData.ts +49 -0
- package/src/course/__mocks__/mockLatePolicy.ts +14 -0
- package/src/course/__mocks__/mockModuleData.ts +60 -0
- package/src/course/__mocks__/mockProgressData.ts +28 -0
- package/src/course/__mocks__/syllabus.gallant.html +302 -0
- package/src/course/__mocks__/syllabus.goofus.html +288 -0
- package/src/course/__mocks__/syllabus.grad.html +282 -0
- package/src/course/__tests__/Course.test.ts +91 -0
- package/src/course/__tests__/blueprint.test.ts +292 -0
- package/src/course/__tests__/changeStartDate.test.ts +180 -0
- package/src/course/__tests__/index.test.ts +104 -0
- package/src/course/blueprint.ts +116 -0
- package/src/course/cachedGetAssociatedCoursesFunc.ts +13 -0
- package/src/course/changeStartDate.ts +139 -0
- package/src/course/code.ts +32 -0
- package/src/course/courseTypes.ts +161 -0
- package/src/course/getCourseIdFromUrl.ts +7 -0
- package/src/course/getSections.ts +9 -0
- package/src/course/getTermNameFromSections.ts +9 -0
- package/src/course/index.ts +12 -0
- package/src/course/migration/__mocks__/mockMigrationData.ts +39 -0
- package/src/course/migration/__tests__/migration.test.ts +173 -0
- package/src/course/migration/__tests__/migrationCache.test.ts +45 -0
- package/src/course/migration/index.ts +134 -0
- package/src/course/migration/migrationCache.ts +56 -0
- package/src/course/modules.ts +113 -0
- package/src/course/notABlueprintError.ts +3 -0
- package/src/course/references/getReferencesForText.test.ts +18 -0
- package/src/course/references/getReferencesForText.ts +12 -0
- package/src/course/references/getReferencesTemplate.ts +28 -0
- package/src/course/references/mockCiteAsResponse.ts +320 -0
- package/src/course/retireBlueprint.ts +21 -0
- package/src/course/toolbox.ts +107 -0
- package/src/date.ts +119 -0
- package/src/enrollments/EnrollmentTypes.ts +76 -0
- package/src/enrollments/enrollmentRole.ts +6 -0
- package/src/enrollments/getEnrollmentGenerator.ts +79 -0
- package/src/enrollments/index.ts +3 -0
- package/src/fetch/__mocks__/fetchJson.ts +1 -0
- package/src/fetch/__mocks__/getPagedDataGenerator.ts +8 -0
- package/src/fetch/__mocks__/index.ts +6 -0
- package/src/fetch/apiGetConfig.ts +10 -0
- package/src/fetch/apiWriteConfig.ts +13 -0
- package/src/fetch/canvasDataFetchGenFunc.ts +20 -0
- package/src/fetch/fetchJson.ts +22 -0
- package/src/fetch/getPagedDataGenerator.ts +157 -0
- package/src/fetch/index.ts +5 -0
- package/src/fetch/utils.ts +16 -0
- package/src/files.ts +60 -0
- package/src/fixes/annotations.ts +105 -0
- package/src/fixes/index.ts +81 -0
- package/src/index.ts +29 -0
- package/src/profile.ts +253 -0
- package/src/rubricTypes.ts +82 -0
- package/src/rubrics.ts +48 -0
- package/src/term/Term.ts +77 -0
- package/src/term/__tests__/dateFromTermName.test.ts +22 -0
- package/src/term/__tests__/getTermsGenerator.test.ts +24 -0
- package/src/term/dateFromTermName.ts +17 -0
- package/src/term/getTermsGenerator.ts +21 -0
- package/src/term/index.ts +3 -0
- package/src/toolbox.ts +19 -0
- package/src/types.ts +134 -0
- package/src/users/getUserGenerator.ts +39 -0
- package/src/users/index.ts +1 -0
- package/tsconfig.json +46 -0
- package/webpack.config.js +122 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {fetchJson} from "@canvas/fetch/fetchJson";
|
|
2
|
+
import {IFile, uploadFile} from "@canvas/files";
|
|
3
|
+
import {BaseCanvasObject} from "@canvas/baseCanvasObject";
|
|
4
|
+
import {CanvasData} from "@canvas/canvasDataDefs";
|
|
5
|
+
import assert from "assert";
|
|
6
|
+
import {deepObjectMerge, formDataify, ICanvasCallConfig} from "@canvas/canvasUtils";
|
|
7
|
+
import {getPagedData} from "@canvas/fetch/getPagedDataGenerator";
|
|
8
|
+
//import {getResizedBlob} from "@/image";
|
|
9
|
+
import getCourseIdFromUrl from "@canvas/course/getCourseIdFromUrl";
|
|
10
|
+
import {ContentKind} from "@canvas/content/ContentKind";
|
|
11
|
+
import {NotImplementedException} from "@canvas/NotImplementedException";
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export class BaseContentItem extends BaseCanvasObject<CanvasData> {
|
|
16
|
+
static bodyProperty: string;
|
|
17
|
+
static nameProperty: string = 'name';
|
|
18
|
+
kind: ContentKind<any, any, any>|undefined = undefined;
|
|
19
|
+
|
|
20
|
+
_courseId: number;
|
|
21
|
+
|
|
22
|
+
constructor(canvasData: CanvasData, courseId: number) {
|
|
23
|
+
super(canvasData);
|
|
24
|
+
this._courseId = courseId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get htmlContentUrl() {
|
|
28
|
+
return `${this.contentUrlPath}`.replace('/api/v1/', '/');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
static get contentUrlPart() {
|
|
33
|
+
assert(this.allContentUrlTemplate, "Not a content url template");
|
|
34
|
+
const urlTermMatch = /\/([\w_]+)$/.exec(this.allContentUrlTemplate);
|
|
35
|
+
if (!urlTermMatch) return null;
|
|
36
|
+
return urlTermMatch[1];
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static async getAllInCourse<T extends BaseContentItem>(courseId: number, config: ICanvasCallConfig | null = null) {
|
|
41
|
+
const url = this.getAllUrl(courseId);
|
|
42
|
+
const data = await getPagedData(url, config);
|
|
43
|
+
return data.map(item => new this(item, courseId)) as T[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static clearAddedContentTags(text: string) {
|
|
47
|
+
if(!text) return null;
|
|
48
|
+
let out = text.replace(/<\/?link[^>]*>/g, '');
|
|
49
|
+
out = out.replace(/<\/?script[^>]*>/g, '');
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static async getFromUrl(url: string | null = null, courseId: number | null = null) {
|
|
54
|
+
if (url === null) {
|
|
55
|
+
url = document.documentURI;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
url = url.replace(/\.com/, '.com/api/v1')
|
|
59
|
+
const data = await fetchJson(url);
|
|
60
|
+
if (!courseId) {
|
|
61
|
+
courseId = getCourseIdFromUrl(url)
|
|
62
|
+
if (!courseId) return null;
|
|
63
|
+
}
|
|
64
|
+
//If this is a collection of data, we can't process it as a Canvas Object
|
|
65
|
+
if (Array.isArray(data)) return null;
|
|
66
|
+
assert(!Array.isArray(data));
|
|
67
|
+
if (data) {
|
|
68
|
+
return new this(data, courseId);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static async getById<T extends BaseContentItem>(contentId: number, courseId: number) {
|
|
74
|
+
return new this(await this.getDataById<T>(contentId, courseId), courseId)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
get bodyKey() {
|
|
79
|
+
return this.myClass.bodyProperty;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get body() {
|
|
83
|
+
if (!this.bodyKey) return null;
|
|
84
|
+
return this.myClass.clearAddedContentTags(this.canvasData[this.bodyKey]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get dueAt() {
|
|
88
|
+
if (!this.canvasData.hasOwnProperty('due_at')) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (!this.canvasData.due_at) return null;
|
|
92
|
+
return new Date(this.canvasData.due_at);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async setDueAt(date: Date): Promise<Record<string, any>> {
|
|
96
|
+
throw new NotImplementedException();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async dueAtTimeDelta(timeDelta: number) {
|
|
100
|
+
if (!this.dueAt) return null;
|
|
101
|
+
const result = new Date(this.dueAt);
|
|
102
|
+
result.setDate(result.getDate() + timeDelta)
|
|
103
|
+
|
|
104
|
+
return await this.setDueAt(result);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get contentUrlPath() {
|
|
108
|
+
let url = (<typeof BaseContentItem>this.constructor).contentUrlTemplate;
|
|
109
|
+
assert(url);
|
|
110
|
+
url = url.replace('{course_id}', this.courseId.toString());
|
|
111
|
+
url = url.replace('{content_id}', this.id.toString());
|
|
112
|
+
|
|
113
|
+
return url;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get courseId() {
|
|
117
|
+
return this._courseId;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async updateContent(text?: string | null, name?: string | null, config?: ICanvasCallConfig) {
|
|
121
|
+
const data: Record<string, any> = {};
|
|
122
|
+
const constructor = <typeof BaseContentItem>this.constructor;
|
|
123
|
+
assert(constructor.bodyProperty);
|
|
124
|
+
assert(constructor.nameProperty);
|
|
125
|
+
const nameProp = constructor.nameProperty;
|
|
126
|
+
const bodyProp = constructor.bodyProperty;
|
|
127
|
+
if (text && bodyProp) {
|
|
128
|
+
this.canvasData[bodyProp] = text;
|
|
129
|
+
data[bodyProp] = text;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (name && nameProp) {
|
|
133
|
+
this.canvasData[nameProp] = name;
|
|
134
|
+
data[nameProp] = name;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return this.saveData(data, config);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async getMeInAnotherCourse(targetCourseId: number) {
|
|
141
|
+
const ContentClass = this.constructor as typeof BaseContentItem
|
|
142
|
+
const targets = await ContentClass.getAllInCourse(
|
|
143
|
+
targetCourseId,
|
|
144
|
+
{queryParams: {search_term: this.name}}
|
|
145
|
+
)
|
|
146
|
+
return targets.find((target: BaseContentItem) => target.name == this.name);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getAllLinks(): string[] {
|
|
150
|
+
const el = this.bodyAsElement;
|
|
151
|
+
const anchors = el.querySelectorAll('a');
|
|
152
|
+
const urls: string[] = [];
|
|
153
|
+
for (const link of anchors) urls.push(link.href);
|
|
154
|
+
return urls;
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
get bodyAsElement() {
|
|
160
|
+
assert(this.body, "This content item has no body property")
|
|
161
|
+
const el = document.createElement('div');
|
|
162
|
+
el.innerHTML = this.body;
|
|
163
|
+
return el;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// async resizeBanner(maxWidth = 2000) {
|
|
167
|
+
// const bannerImg = getBannerImage(this);
|
|
168
|
+
// if (!bannerImg) throw new Error("No banner");
|
|
169
|
+
// const fileData = await getFileDataFromUrl(bannerImg.src, this.courseId)
|
|
170
|
+
// if (!fileData) throw new Error("File not found");
|
|
171
|
+
// if (bannerImg.naturalWidth < maxWidth) return; //Dont resize image unless we're shrinking it
|
|
172
|
+
// const resizedImageBlob = await getResizedBlob(bannerImg.src, maxWidth);
|
|
173
|
+
// const fileName = fileData.filename;
|
|
174
|
+
// const fileUploadUrl = `/api/v1/courses/${this.courseId}/files`
|
|
175
|
+
// assert(resizedImageBlob);
|
|
176
|
+
// const file = new File([resizedImageBlob], fileName)
|
|
177
|
+
// return await uploadFile(file, fileData.folder_id, fileUploadUrl);
|
|
178
|
+
// }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function getFileDataFromUrl(url: string, courseId: number) {
|
|
182
|
+
const match = /.*\/files\/(\d+)/.exec(url);
|
|
183
|
+
if (!match) return null;
|
|
184
|
+
if (match) {
|
|
185
|
+
const fileId = parseInt(match[1]);
|
|
186
|
+
return await getFileData(fileId, courseId);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getBannerImage(overviewPage: BaseContentItem) {
|
|
191
|
+
const pageBody = document.createElement('html');
|
|
192
|
+
if (!overviewPage.body) throw new Error(`Content item ${overviewPage.name} has no html body`)
|
|
193
|
+
pageBody.innerHTML = overviewPage.body;
|
|
194
|
+
return pageBody.querySelector<HTMLImageElement>('.cbt-banner-image img');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function getFileData(fileId: number, courseId: number) {
|
|
198
|
+
const url = `/api/v1/courses/${courseId}/files/${fileId}`
|
|
199
|
+
return await fetchJson(url) as IFile;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function putContentConfig<T extends Record<string, any>>(data: T, config?: ICanvasCallConfig) {
|
|
203
|
+
return deepObjectMerge(config, {
|
|
204
|
+
fetchInit: {
|
|
205
|
+
method: 'PUT',
|
|
206
|
+
body: formDataify(data)
|
|
207
|
+
}
|
|
208
|
+
}, true);
|
|
209
|
+
}
|
|
210
|
+
export function postContentConfig<T extends Record<string, any>>(data: T, config?: ICanvasCallConfig) {
|
|
211
|
+
return deepObjectMerge(config, {
|
|
212
|
+
fetchInit: {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
body: formDataify(data)
|
|
215
|
+
}
|
|
216
|
+
}, true);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {ICanvasCallConfig, IQueryParams} from "@/canvasUtils";
|
|
2
|
+
import {CanvasData} from "@/canvasDataDefs";
|
|
3
|
+
import {ContentData} from "@/content/types";
|
|
4
|
+
import {fetchJson} from "@/fetch/fetchJson";
|
|
5
|
+
import {postContentConfig, putContentConfig} from "@/content/BaseContentItem";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export type ContentKind<
|
|
9
|
+
DataType extends ContentData,
|
|
10
|
+
GetQueryOptions extends IQueryParams = Record<string, any>,
|
|
11
|
+
PutDataType extends CanvasData = DataType,
|
|
12
|
+
PostDataType extends CanvasData = PutDataType,
|
|
13
|
+
IdType = number,
|
|
14
|
+
> = {
|
|
15
|
+
getId: (data: DataType) => IdType,
|
|
16
|
+
dataIsThisKind: (data: Record<string, any>) => data is DataType,
|
|
17
|
+
getName: (data: DataType) => string,
|
|
18
|
+
getBody: (data: DataType) => string | undefined,
|
|
19
|
+
get: (courseId: number, contentId: number, config?: ICanvasCallConfig<GetQueryOptions>) => Promise<DataType>
|
|
20
|
+
getByString?: (courseId: number, contentId: string, config?: ICanvasCallConfig<GetQueryOptions>) => Promise<DataType|{ message: string}>
|
|
21
|
+
dataGenerator: (courseId: number, config?: ICanvasCallConfig<GetQueryOptions>) => AsyncGenerator<DataType>
|
|
22
|
+
put: (courseId: number, contentId: number, data: PutDataType) => Promise<DataType>,
|
|
23
|
+
post?: (courseId: number, data: PostDataType) => Promise<DataType>,
|
|
24
|
+
} & ReturnType<typeof contentUrlFuncs>
|
|
25
|
+
|
|
26
|
+
export function contentUrlFuncs(contentUrlPart: string) {
|
|
27
|
+
|
|
28
|
+
const urlRegex = new RegExp(`courses\/(\\d+)\/${contentUrlPart}/(\\d+)`, 'i');
|
|
29
|
+
|
|
30
|
+
const getApiUrl = courseContentUrlFunc(`/api/v1/courses/{courseId}/${contentUrlPart}/{contentId}`);
|
|
31
|
+
const getAllApiUrl = (courseId: number) => `/api/v1/courses/${courseId}/${contentUrlPart}`;
|
|
32
|
+
const getHtmlUrl = courseContentUrlFunc(`/courses/{courseId}/${contentUrlPart}/{contentId}`);
|
|
33
|
+
|
|
34
|
+
function getCourseAndContentIdFromUrl(url: string) {
|
|
35
|
+
const [full, courseId, contentId] = url.match(urlRegex) ?? [undefined, undefined, undefined];
|
|
36
|
+
return [courseId, contentId].map(a => a ? parseInt(a) : undefined);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isValidUrl = (url?: string) => typeof url === 'string' && typeof getCourseAndContentIdFromUrl(url)[0] !== 'undefined';
|
|
40
|
+
return {
|
|
41
|
+
contentUrlPart,
|
|
42
|
+
getApiUrl,
|
|
43
|
+
getAllApiUrl,
|
|
44
|
+
getHtmlUrl,
|
|
45
|
+
getCourseAndContentIdFromUrl,
|
|
46
|
+
isValidUrl,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function courseContentUrlFunc<T extends number | string = number>(url: string) {
|
|
51
|
+
return (courseId: number, contentId: T) => url
|
|
52
|
+
.replaceAll('{courseId}', courseId.toString())
|
|
53
|
+
.replaceAll('{contentId}', contentId.toString())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function putContentFunc<
|
|
57
|
+
PutOptionsType extends Record<string, any>,
|
|
58
|
+
ResponseDataType extends Record<string, any>
|
|
59
|
+
>(getApiUrl: (courseId: number, contentId: number) => string) {
|
|
60
|
+
return async function (
|
|
61
|
+
courseId: number,
|
|
62
|
+
contentId: number,
|
|
63
|
+
content: PutOptionsType,
|
|
64
|
+
config?: ICanvasCallConfig
|
|
65
|
+
) {
|
|
66
|
+
const url = getApiUrl(courseId, contentId);
|
|
67
|
+
return await fetchJson<ResponseDataType>(url, putContentConfig(content, config))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
export function postContentFunc<
|
|
73
|
+
PostOptionsType extends Record<string, any>,
|
|
74
|
+
ResponseDataType extends Record<string, any>
|
|
75
|
+
>(getApiUrl: (courseId: number) => string) {
|
|
76
|
+
return async function (
|
|
77
|
+
courseId: number,
|
|
78
|
+
content: PostOptionsType,
|
|
79
|
+
config?: ICanvasCallConfig
|
|
80
|
+
) {
|
|
81
|
+
const url = getApiUrl(courseId);
|
|
82
|
+
return await fetchJson<ResponseDataType>(url, postContentConfig(content, config))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {mockAssignmentData} from "@/content/__mocks__/mockContentData";
|
|
2
|
+
import {mockCourseData} from "@/course/__mocks__/mockCourseData";
|
|
3
|
+
import {Assignment} from "@/content/assignments/Assignment";
|
|
4
|
+
import {ContentData} from "@/content/types";
|
|
5
|
+
import {ICanvasCallConfig} from "@/canvasUtils";
|
|
6
|
+
|
|
7
|
+
import AssignmentKind from "@/content/assignments/AssignmentKind";
|
|
8
|
+
|
|
9
|
+
export const getContentClassFromUrl = jest.fn((url: string | null = null) => {
|
|
10
|
+
return Assignment;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const getContentItemFromUrl = jest.fn(
|
|
14
|
+
async (url: string | null = null) =>
|
|
15
|
+
new Assignment(mockAssignmentData, mockCourseData.id))
|
|
16
|
+
|
|
17
|
+
export function getContentKindFromUrl(url:string) {
|
|
18
|
+
return AssignmentKind;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getContentKindFromContent(contentData:ContentData) {
|
|
22
|
+
return AssignmentKind;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getContentDataFromUrl(url:string, config:ICanvasCallConfig) {
|
|
26
|
+
return {...mockAssignmentData}
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {IAssignmentSubmission} from "@canvas/content/types";
|
|
2
|
+
|
|
3
|
+
const mockSubmission: IAssignmentSubmission = {
|
|
4
|
+
assignment_id: 23,
|
|
5
|
+
assignment: null,
|
|
6
|
+
course: null,
|
|
7
|
+
attempt: 1,
|
|
8
|
+
body: "There are three factors too...",
|
|
9
|
+
grade: "A-",
|
|
10
|
+
grade_matches_current_submission: true,
|
|
11
|
+
html_url: "https://example.com/courses/255/assignments/543/submissions/134",
|
|
12
|
+
preview_url: "https://example.com/courses/255/assignments/543/submissions/134?preview=1",
|
|
13
|
+
score: 13.5,
|
|
14
|
+
submission_comments: null,
|
|
15
|
+
submission_type: 'online_text_entry',
|
|
16
|
+
submitted_at: "2012-01-01T01:00:00Z",
|
|
17
|
+
url: null,
|
|
18
|
+
user_id: 134,
|
|
19
|
+
grader_id: 86,
|
|
20
|
+
graded_at: "2012-01-02T03:05:34Z",
|
|
21
|
+
user: null,
|
|
22
|
+
late: false,
|
|
23
|
+
assignment_visible: true,
|
|
24
|
+
excused: true,
|
|
25
|
+
missing: true,
|
|
26
|
+
late_policy_status: 'missing',
|
|
27
|
+
points_deducted: 12.3,
|
|
28
|
+
seconds_late: 300,
|
|
29
|
+
workflow_state: 'submitted',
|
|
30
|
+
extra_attempts: 10,
|
|
31
|
+
anonymous_id: "acJ4Q",
|
|
32
|
+
posted_at: "2020-01-02T11:10:30Z",
|
|
33
|
+
read_status: 'read',
|
|
34
|
+
redo_request: true
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default mockSubmission;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {IQuizData} from "@/content/quizzes/types";
|
|
2
|
+
import {IPageData} from "@/content/pages/types";
|
|
3
|
+
import {IAssignmentData, IDiscussionData} from "@canvas/content/types";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export const mockPageData: IPageData = {
|
|
7
|
+
created_at: "", front_page: false, updated_at: "",
|
|
8
|
+
page_id: 0,
|
|
9
|
+
url: 'http://localhost',
|
|
10
|
+
title: 'X',
|
|
11
|
+
body: '<div></div>'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
//front docs
|
|
16
|
+
export const mockQuizData: IQuizData = {
|
|
17
|
+
id: 0,
|
|
18
|
+
title: "",
|
|
19
|
+
html_url: "",
|
|
20
|
+
mobile_url: "",
|
|
21
|
+
preview_url: "",
|
|
22
|
+
description: "",
|
|
23
|
+
quiz_type: "assignment",
|
|
24
|
+
assignment_group_id: 0,
|
|
25
|
+
shuffle_answers: false,
|
|
26
|
+
hide_results: null,
|
|
27
|
+
one_time_results: false,
|
|
28
|
+
allowed_attempts: 0,
|
|
29
|
+
one_question_at_a_time: false,
|
|
30
|
+
question_count: 0,
|
|
31
|
+
points_possible: 0,
|
|
32
|
+
published: false,
|
|
33
|
+
unpublishable: false,
|
|
34
|
+
locked_for_user: false,
|
|
35
|
+
speedgrader_url: "",
|
|
36
|
+
quiz_extensions_url: "",
|
|
37
|
+
all_dates: null,
|
|
38
|
+
version_number: 0,
|
|
39
|
+
question_types: []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const mockAssignmentData: IAssignmentData = {
|
|
43
|
+
anonymous_grading: false,
|
|
44
|
+
final_grader_id: 0,
|
|
45
|
+
grader_comments_visible_to_graders: false,
|
|
46
|
+
grader_count: 0,
|
|
47
|
+
grader_names_visible_to_final_grader: false,
|
|
48
|
+
graders_anonymous_to_graders: false,
|
|
49
|
+
hide_in_gradebook: false,
|
|
50
|
+
moderated_grading: false,
|
|
51
|
+
omit_from_final_grade: false,
|
|
52
|
+
allowed_extensions: [],
|
|
53
|
+
assignment_group_id: 0,
|
|
54
|
+
automatic_peer_reviews: false,
|
|
55
|
+
course_id: 0,
|
|
56
|
+
created_at: "",
|
|
57
|
+
description: "",
|
|
58
|
+
due_at: null,
|
|
59
|
+
due_date_required: false,
|
|
60
|
+
grade_group_students_individually: false,
|
|
61
|
+
has_overrides: false,
|
|
62
|
+
html_url: "",
|
|
63
|
+
integration_data: undefined,
|
|
64
|
+
intra_group_peer_reviews: false,
|
|
65
|
+
lock_at: null,
|
|
66
|
+
max_name_length: 0,
|
|
67
|
+
peer_reviews: false,
|
|
68
|
+
points_possible: 0,
|
|
69
|
+
position: 0,
|
|
70
|
+
submission_types: [],
|
|
71
|
+
submissions_download_url: "",
|
|
72
|
+
unlock_at: null,
|
|
73
|
+
updated_at: null,
|
|
74
|
+
id: 0, name: "", rubric: []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const mockDiscussionData: IDiscussionData = {
|
|
78
|
+
allow_rating: false,
|
|
79
|
+
assignment_id: 1,
|
|
80
|
+
discussion_subentry_count: 0,
|
|
81
|
+
discussion_type: 'threaded',
|
|
82
|
+
locked: false,
|
|
83
|
+
locked_for_user: false,
|
|
84
|
+
only_graders_can_rate: false,
|
|
85
|
+
permissions: {},
|
|
86
|
+
pinned: false,
|
|
87
|
+
published: false,
|
|
88
|
+
read_state: 'unread',
|
|
89
|
+
sort_by_grading: false,
|
|
90
|
+
subscribed: false,
|
|
91
|
+
topic_children: [],
|
|
92
|
+
unread_count: 0,
|
|
93
|
+
user_can_see_posts: false,
|
|
94
|
+
user_name: "",
|
|
95
|
+
html_url: "", id: 0, last_reply_at: "", message: "", posted_at: "", require_initial_post: false, title: "",
|
|
96
|
+
delayed_post_at: ""
|
|
97
|
+
|
|
98
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {fetchJson} from "@/fetch/fetchJson";
|
|
2
|
+
import {ICanvasCallConfig, renderAsyncGen} from "@/canvasUtils";
|
|
3
|
+
import {getPagedDataGenerator} from "@/fetch/getPagedDataGenerator";
|
|
4
|
+
import {mockAsyncGen} from "@/__mocks__/utils";
|
|
5
|
+
import assert from "assert";
|
|
6
|
+
import {putContentConfig} from "@/content/BaseContentItem";
|
|
7
|
+
import {ContentKind, contentUrlFuncs} from "@/content/ContentKind";
|
|
8
|
+
|
|
9
|
+
export function kindUrlTests(urlFuncs: ReturnType<typeof contentUrlFuncs>, courseId: number, contentId: number, apiUrl: string, htmlUrl: string, allUrl: string) {
|
|
10
|
+
return () => {
|
|
11
|
+
it("getApiUrl", () => expect(urlFuncs.getApiUrl(1, 3)).toEqual(apiUrl))
|
|
12
|
+
it("getHtmlUrl", () => expect(urlFuncs.getHtmlUrl(1, 3)).toEqual(htmlUrl))
|
|
13
|
+
it("getAllUrl", () => expect(urlFuncs.getAllApiUrl(1)).toEqual(allUrl))
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type KindDataType<T extends ContentKind<any, any, any>> = Awaited<ReturnType<T['get']>>;
|
|
18
|
+
export type KindSaveType<T> = T extends ContentKind<infer A, infer B, infer C> ? B : never;
|
|
19
|
+
|
|
20
|
+
export function getDataTests<Kind extends ContentKind<any, any, any>>(contentKind: Kind, mockDatas: KindDataType<Kind>[], mockSaveParams: KindSaveType<Kind>) {
|
|
21
|
+
type DataType = KindDataType<Kind>
|
|
22
|
+
type SaveType = KindSaveType<Kind>
|
|
23
|
+
return () => {
|
|
24
|
+
it('get works', async () => {
|
|
25
|
+
const content = {...mockDatas[0], id: 101};
|
|
26
|
+
(fetchJson as jest.Mock).mockResolvedValue(content);
|
|
27
|
+
const config: ICanvasCallConfig = {};
|
|
28
|
+
const received = await contentKind.get(0, 0, config);
|
|
29
|
+
expect(received).toEqual(content);
|
|
30
|
+
expect(fetchJson).toHaveBeenCalledWith(contentKind.getApiUrl(0, 0), config)
|
|
31
|
+
});
|
|
32
|
+
it('dataGenerator works', async () => {
|
|
33
|
+
(getPagedDataGenerator as jest.Mock).mockReturnValue(mockAsyncGen(mockDatas));
|
|
34
|
+
expect(await renderAsyncGen(contentKind.dataGenerator(1))).toEqual(mockDatas);
|
|
35
|
+
});
|
|
36
|
+
it('puts data', async() => {
|
|
37
|
+
(fetchJson as jest.Mock).mockResolvedValue(mockDatas[0])
|
|
38
|
+
if (!contentKind.put) {
|
|
39
|
+
expect(mockSaveParams).toBeUndefined();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const result = await contentKind.put(1, 1, mockSaveParams);
|
|
43
|
+
expect(contentKind.dataIsThisKind(result))
|
|
44
|
+
expect(fetchJson as jest.Mock).toHaveBeenCalledWith(contentKind.getApiUrl(1, 1), putContentConfig(mockSaveParams, {}))
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getContentClassFromUrl,
|
|
3
|
+
getContentItemFromUrl,
|
|
4
|
+
getContentKindFromContent,
|
|
5
|
+
getContentKindFromUrl
|
|
6
|
+
} from "@/content/determineContent";
|
|
7
|
+
import assert from "assert";
|
|
8
|
+
import {
|
|
9
|
+
mockAssignmentData,
|
|
10
|
+
mockDiscussionData,
|
|
11
|
+
mockPageData,
|
|
12
|
+
mockQuizData
|
|
13
|
+
} from "@/content/__mocks__/mockContentData";
|
|
14
|
+
import {fetchJson} from "@/fetch/fetchJson";
|
|
15
|
+
import {Quiz} from "@/content/quizzes/Quiz";
|
|
16
|
+
import {Page} from "@/content/pages/Page";
|
|
17
|
+
import {Discussion} from "@/content/discussions/Discussion";
|
|
18
|
+
import {Assignment} from "@/content/assignments/Assignment";
|
|
19
|
+
import AssignmentKind from "@/content/assignments/AssignmentKind";
|
|
20
|
+
import QuizKind from "@/content/quizzes/QuizKind";
|
|
21
|
+
import PageKind from "@/content/pages/PageKind";
|
|
22
|
+
import DiscussionKind from "@/content/discussions/DiscussionKind";
|
|
23
|
+
|
|
24
|
+
jest.mock('@/fetch/fetchJson', () => ({
|
|
25
|
+
fetchJson: jest.fn(),
|
|
26
|
+
}))
|
|
27
|
+
describe('getContentClassFromUrl', () => {
|
|
28
|
+
it('should return Assignment class when URL includes assignment', () => {
|
|
29
|
+
const url = 'https://example.com/assignments';
|
|
30
|
+
expect(getContentClassFromUrl(url)).toBe(Assignment);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return Quiz class when URL includes quiz', () => {
|
|
34
|
+
const url = 'https://example.com/quizzes';
|
|
35
|
+
expect(getContentClassFromUrl(url)).toBe(Quiz);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return Page class when URL includes page', () => {
|
|
39
|
+
const url = 'https://example.com/pages';
|
|
40
|
+
expect(getContentClassFromUrl(url)).toBe(Page);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return Discussion class when URL includes discussion', () => {
|
|
44
|
+
const url = 'https://example.com/discussion_topics';
|
|
45
|
+
expect(getContentClassFromUrl(url)).toBe(Discussion);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return null when URL does not match any class', () => {
|
|
49
|
+
const url = 'https://example.com/unknown';
|
|
50
|
+
expect(getContentClassFromUrl(url)).toBe(null);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('getContentItemFromUrl', () => {
|
|
55
|
+
let getContentApi: typeof import('../determineContent');
|
|
56
|
+
beforeEach(async () => {
|
|
57
|
+
getContentApi = await import('../determineContent');
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should return null if getContentClassFromUrl returns null', async () => {
|
|
61
|
+
const result = await getContentItemFromUrl('https://example.com');
|
|
62
|
+
expect(result).toBe(null);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should call getAssignment from an assignment url', async () => {
|
|
66
|
+
|
|
67
|
+
const url = 'https://example.com/assignments';
|
|
68
|
+
const contentClass = getContentClassFromUrl(url);
|
|
69
|
+
assert(contentClass !== null)
|
|
70
|
+
const getAssignmentSpy = jest.spyOn(contentClass, 'getFromUrl');
|
|
71
|
+
(fetchJson as jest.Mock).mockResolvedValue(mockAssignmentData);
|
|
72
|
+
|
|
73
|
+
const assignment = await getContentItemFromUrl(url);
|
|
74
|
+
expect(getAssignmentSpy).toHaveBeenCalledWith(url);
|
|
75
|
+
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
describe('getContentKindFromUrl', () => {
|
|
81
|
+
it('Finds Assignments', () => {
|
|
82
|
+
expect(getContentKindFromUrl('/api/v1/courses/1/assignments/5')).toEqual(AssignmentKind)
|
|
83
|
+
})
|
|
84
|
+
it('Finds Discussions', () => {
|
|
85
|
+
expect(getContentKindFromUrl('/api/v1/courses/1/discussion_topics/5')).toEqual(DiscussionKind)
|
|
86
|
+
})
|
|
87
|
+
it('Finds Quizzes', () => {
|
|
88
|
+
expect(getContentKindFromUrl('/api/v1/courses/1/quizzes/5')).toEqual(QuizKind)
|
|
89
|
+
})
|
|
90
|
+
it('Finds Pages', () => {
|
|
91
|
+
expect(getContentKindFromUrl('/api/v1/courses/1/pages/5')).toEqual(PageKind)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('getContentKindFromContent', () => {
|
|
97
|
+
it('works for pages', () => expect(getContentKindFromContent(mockPageData)).toBe(PageKind))
|
|
98
|
+
it('works for discussions', () => expect(getContentKindFromContent(mockDiscussionData)).toBe(DiscussionKind))
|
|
99
|
+
it('works for assignments', () => expect(getContentKindFromContent(mockAssignmentData)).toBe(AssignmentKind))
|
|
100
|
+
it('works for quizzes', () => expect(getContentKindFromContent(mockQuizData)).toBe(QuizKind))
|
|
101
|
+
})
|