@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,91 @@
|
|
|
1
|
+
import {mockAssignmentData} from "../../content/__mocks__/mockContentData";
|
|
2
|
+
import {mockCourseData} from "../__mocks__/mockCourseData";
|
|
3
|
+
import {
|
|
4
|
+
Course
|
|
5
|
+
} from "../Course";
|
|
6
|
+
import mockTabData from "../../__mocks__/mockTabData";
|
|
7
|
+
import {assignmentDataGen} from "@/content/assignments";
|
|
8
|
+
import {baseCourseCode, parseCourseCode, stringIsCourseCode} from "@/course/code";
|
|
9
|
+
import {fetchJson} from "@/fetch/fetchJson";
|
|
10
|
+
import {returnMockAsyncGen} from "@/__mocks__/utils";
|
|
11
|
+
jest.mock('@/fetch/fetchJson')
|
|
12
|
+
|
|
13
|
+
jest.mock('@/content/assignments', () => ({
|
|
14
|
+
...jest.requireActual('@/content/assignments'),
|
|
15
|
+
assignmentDataGen: jest.fn(),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
describe('get content', () => {
|
|
20
|
+
const config = {};
|
|
21
|
+
test('Gets assignments', async () => {
|
|
22
|
+
const id = Math.floor(Math.random() * 1000);
|
|
23
|
+
const assignmentDatas = [{...mockAssignmentData, id}];
|
|
24
|
+
const course = new Course(mockCourseData);
|
|
25
|
+
(assignmentDataGen as jest.Mock).mockImplementation(returnMockAsyncGen(assignmentDatas))
|
|
26
|
+
|
|
27
|
+
const extractedAssignments = await course.getAssignments(config);
|
|
28
|
+
expect(extractedAssignments.length).toEqual(assignmentDatas.length);
|
|
29
|
+
expect(extractedAssignments[0].rawData).toEqual(assignmentDatas[0])
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
test('get tabs', async () => {
|
|
37
|
+
const fetchJsonMock = fetchJson as jest.Mock;
|
|
38
|
+
const mockCourse = new Course({...mockCourseData, id: 1});
|
|
39
|
+
const config = {};
|
|
40
|
+
const mockTabsData = [{...mockTabData, label: 'a'}];
|
|
41
|
+
fetchJsonMock.mockResolvedValue(mockTabsData);
|
|
42
|
+
const tabs = await mockCourse.getTabs(config);
|
|
43
|
+
expect(fetchJsonMock).toHaveBeenCalledWith(`/api/v1/courses/${mockCourse.id}/tabs`, config);
|
|
44
|
+
expect(tabs).toEqual(mockTabsData);
|
|
45
|
+
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
describe('parseCourseCode', () => {
|
|
51
|
+
test('parses course code with prefix', () => {
|
|
52
|
+
expect(parseCourseCode('BP_TEST123')).toBe('BP_TEST123');
|
|
53
|
+
expect(parseCourseCode('BPTEST123')).toBe('BP_TEST123');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('parses course code without prefix', () => {
|
|
57
|
+
expect(parseCourseCode('TEST123')).toBe('TEST123');
|
|
58
|
+
expect(parseCourseCode('TEST123')).not.toBe('TEST123_TEST123');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('returns null for invalid course codes', () => {
|
|
62
|
+
expect(parseCourseCode('BP')).toBeNull();
|
|
63
|
+
expect(parseCourseCode('_123')).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('baseCourseCode', () => {
|
|
68
|
+
test('returns base course code without prefix', () => {
|
|
69
|
+
expect(baseCourseCode('DEV_TEST123')).toBe('TEST123');
|
|
70
|
+
expect(baseCourseCode('TEST123')).toBe('TEST123');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('returns null for invalid course codes', () => {
|
|
74
|
+
expect(baseCourseCode('BP')).toBeNull();
|
|
75
|
+
expect(baseCourseCode('_123')).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('stringIsCourseCode', () => {
|
|
80
|
+
test('returns match for valid course codes', () => {
|
|
81
|
+
expect(stringIsCourseCode('BP_TEST123')).toBeTruthy();
|
|
82
|
+
expect(stringIsCourseCode('BPTEST123')).toBeTruthy();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('returns null for invalid course codes', () => {
|
|
86
|
+
expect(stringIsCourseCode('BP')).toBeNull();
|
|
87
|
+
expect(stringIsCourseCode('_123')).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
//We should really write integration tests for these eventually rather than just unit tests
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {deFormDataify, ICanvasCallConfig, range} from "../../canvasUtils";
|
|
5
|
+
import {
|
|
6
|
+
getBlueprintsFromCode, setAsBlueprint, unSetAsBlueprint, lockBlueprint, genBlueprintDataForCode
|
|
7
|
+
} from "../blueprint";
|
|
8
|
+
import {mockCourseData} from "../__mocks__/mockCourseData";
|
|
9
|
+
import fetchMock, {FetchMock} from "jest-fetch-mock";
|
|
10
|
+
import {IAccountData, IModuleData} from "../../canvasDataDefs";
|
|
11
|
+
import {mockTermData} from "../../__mocks__/mockTermData";
|
|
12
|
+
import {mockAccountData} from "../../__mocks__/mockAccountData";
|
|
13
|
+
import assert from "assert";
|
|
14
|
+
import mockModuleData, {mockModuleItemData} from "../__mocks__/mockModuleData";
|
|
15
|
+
import {Course} from "../Course";
|
|
16
|
+
import {GetCoursesFromAccountOptions} from "@/course/courseTypes";
|
|
17
|
+
import * as courseApi from '@/course/toolbox';
|
|
18
|
+
import * as fetchApi from '@/fetch/utils'
|
|
19
|
+
import {ICourseData} from "@/types";
|
|
20
|
+
import {getPagedDataGenerator} from "@/fetch/getPagedDataGenerator";
|
|
21
|
+
import {mockAsyncGen, returnMockAsyncGen} from "@/__mocks__/utils";
|
|
22
|
+
import {baseCourseCode, MalformedCourseCodeError} from "@/course/code";
|
|
23
|
+
import {fetchJson} from "@/fetch/fetchJson";
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
jest.mock('@/fetch/fetchJson');
|
|
27
|
+
jest.mock('@/fetch/getPagedDataGenerator');
|
|
28
|
+
fetchMock.enableMocks();
|
|
29
|
+
|
|
30
|
+
const getCourseGenerator = jest.spyOn(courseApi, 'getCourseGenerator')
|
|
31
|
+
const getCourseDataGenerator = jest.spyOn(courseApi, 'getCourseDataGenerator')
|
|
32
|
+
const fetchGetConfig = jest.spyOn(fetchApi, 'fetchGetConfig')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
function getDummyBlueprintCourse(blueprint: boolean, id: number = 0) {
|
|
36
|
+
let out: IBlueprintCourse;
|
|
37
|
+
out = new Course({
|
|
38
|
+
...mockCourseData,
|
|
39
|
+
id,
|
|
40
|
+
name: 'BP_TEST000',
|
|
41
|
+
courseCode: 'BP_TEST000',
|
|
42
|
+
blueprint,
|
|
43
|
+
getAssociatedCourses: () => getSections(out.id)
|
|
44
|
+
})
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test("Testing get associated courses logic", async () => {
|
|
49
|
+
const mockData = [...range(0, 9)].map(i => {
|
|
50
|
+
return {...mockCourseData, id: i}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
(getPagedDataGenerator as jest.Mock).mockImplementation(returnMockAsyncGen(mockData.map(a => new Course(a))));
|
|
54
|
+
const courses = await getSections(getDummyBlueprintCourse(true, 0).id)
|
|
55
|
+
const courseIds = courses.map(course => course.id).toSorted();
|
|
56
|
+
expect(courseIds).toStrictEqual([...range(0, 9)]);
|
|
57
|
+
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
describe("Testing blueprint retirement", () => {
|
|
62
|
+
it('throws an error when course is not named as the base blueprint', async () => {
|
|
63
|
+
const course = new Course({...mockCourseData, course_code:"BP-xxx-TEST104" , blueprint: false});
|
|
64
|
+
await expect(async () => await retireBlueprint(course, 'XXXXXXX')).rejects.toThrow(NotABlueprintError)
|
|
65
|
+
})
|
|
66
|
+
it('throws an error when course code is malformed', async () => {
|
|
67
|
+
const course = new Course({...mockCourseData, course_code: "X12375"});
|
|
68
|
+
await expect(async () => await retireBlueprint(course, 'XXXXXXX')).rejects.toThrow(MalformedCourseCodeError)
|
|
69
|
+
})
|
|
70
|
+
it('goes successfully through process', async() => {
|
|
71
|
+
const termName = 'DE8W03.11.24';
|
|
72
|
+
const mockBpData = {
|
|
73
|
+
...mockCourseData,
|
|
74
|
+
id: 0,
|
|
75
|
+
blueprint: true,
|
|
76
|
+
course_code: 'BP_TEST000',
|
|
77
|
+
name: 'BP_TEST000: Testing with Tests'
|
|
78
|
+
};
|
|
79
|
+
const notBpMockBpData = {...mockBpData, blueprint: false};
|
|
80
|
+
const badNameMockBpData = {...mockBpData, course_code: `BP-${termName}_TEST000`}
|
|
81
|
+
const mockBlueprint = new Course(mockBpData);
|
|
82
|
+
const notBpMockBlueprint: Course = new Course(notBpMockBpData);
|
|
83
|
+
const badNameMockBlueprint: Course = new Course(badNameMockBpData);
|
|
84
|
+
await expect(retireBlueprint(badNameMockBlueprint, termName)).rejects.toThrow("This blueprint is not named BP_")
|
|
85
|
+
|
|
86
|
+
const mockAssociatedCourseData: ICourseData[] = [{
|
|
87
|
+
...mockCourseData,
|
|
88
|
+
id: 1,
|
|
89
|
+
course_code: `${termName}_TEST000-01`,
|
|
90
|
+
enrollment_term_id: [10],
|
|
91
|
+
}];
|
|
92
|
+
|
|
93
|
+
(getPagedDataGenerator as jest.Mock).mockImplementationOnce(returnMockAsyncGen(mockAssociatedCourseData))
|
|
94
|
+
const sections = await mockBlueprint.getAssociatedCourses();
|
|
95
|
+
(getPagedDataGenerator as jest.Mock).mockImplementationOnce(returnMockAsyncGen([{
|
|
96
|
+
...mockAccountData,
|
|
97
|
+
id: 2,
|
|
98
|
+
root_account_id: null
|
|
99
|
+
}]));
|
|
100
|
+
sections.forEach(section => section.getTerm = jest.fn(async () => (new Term({
|
|
101
|
+
...mockTermData,
|
|
102
|
+
id: 10,
|
|
103
|
+
name: termName
|
|
104
|
+
}))))
|
|
105
|
+
const derivedTermName = await getTermNameFromSections(sections);
|
|
106
|
+
expect(derivedTermName).toBe(termName);
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
mockBlueprint.saveData = jest.fn();
|
|
110
|
+
const config: ICanvasCallConfig = {};
|
|
111
|
+
await retireBlueprint(mockBlueprint, derivedTermName, config);
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
expect(mockBlueprint.saveData).toHaveBeenCalledWith({
|
|
115
|
+
course: {
|
|
116
|
+
course_code: `BP-${termName}_TEST000`,
|
|
117
|
+
name: `BP-${termName}_TEST000: Testing with Tests`
|
|
118
|
+
}
|
|
119
|
+
}, config)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('getBlueprintFromCode', () => {
|
|
124
|
+
let dummyDev: Course;
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
dummyDev = new Course({...mockCourseData, name: 'DEV_TEST000', course_code: 'DEV_TEST000'})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("Returns null when there's no BP", async () => {
|
|
131
|
+
(getCourseGenerator as jest.Mock).mockReturnValue(mockAsyncGen([]))
|
|
132
|
+
const bp = dummyDev.parsedCourseCode && await getBlueprintsFromCode(dummyDev.parsedCourseCode, [0]);
|
|
133
|
+
expect(bp).toStrictEqual([]);
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test("Searches for BP from a dev course", async () => {
|
|
137
|
+
(getCourseGenerator as jest.Mock).mockImplementation((url: string) => {
|
|
138
|
+
const bpCode = `BP_${baseCourseCode(url)}`;
|
|
139
|
+
return mockAsyncGen([
|
|
140
|
+
{...mockCourseData, name: bpCode, course_code: bpCode, blueprint: true}
|
|
141
|
+
].map(a => new Course(a)))
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const [bp] = dummyDev.parsedCourseCode && await getBlueprintsFromCode(dummyDev.parsedCourseCode, [0]) || [];
|
|
145
|
+
expect(bp).toBeInstanceOf(Course);
|
|
146
|
+
assert(typeof bp === 'object');
|
|
147
|
+
expect(bp?.isBlueprint()).toBe(true);
|
|
148
|
+
expect(bp?.courseCode).toBe('BP_TEST000');
|
|
149
|
+
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test("Searches for BP from a section", async () => {
|
|
153
|
+
fetchMock.once(mockBpResponse)
|
|
154
|
+
const dummyCourse = new Course({
|
|
155
|
+
...mockCourseData,
|
|
156
|
+
name: 'DE8W12.4.26_TEST000',
|
|
157
|
+
course_code: 'DE8W12.4.26_DEV_TEST000'
|
|
158
|
+
})
|
|
159
|
+
const [bp] = dummyCourse.parsedCourseCode && await getBlueprintsFromCode(dummyCourse.parsedCourseCode, [0]) || [];
|
|
160
|
+
expect(bp).toBeInstanceOf(Course);
|
|
161
|
+
assert(typeof bp === 'object');
|
|
162
|
+
expect(bp?.isBlueprint()).toBe(true);
|
|
163
|
+
expect(bp?.courseCode).toBe('BP_TEST000');
|
|
164
|
+
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test("setAsBlueprint", async () => {
|
|
170
|
+
let responseData: ICourseData;
|
|
171
|
+
const payload = {
|
|
172
|
+
course: {
|
|
173
|
+
blueprint: true,
|
|
174
|
+
use_blueprint_restrictions_by_object_type: 0,
|
|
175
|
+
blueprint_restrictions: {
|
|
176
|
+
content: 1,
|
|
177
|
+
points: 1,
|
|
178
|
+
due_dates: 1,
|
|
179
|
+
availability_dates: 1,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
const config: ICanvasCallConfig = {};
|
|
186
|
+
responseData = await setAsBlueprint(0, config)
|
|
187
|
+
expect(fetchJson).toHaveBeenCalledWith(`/api/v1/courses/0`, apiWriteConfig('PUT', payload, config))
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test("unSetAsBlueprint", async () => {
|
|
191
|
+
const payload = {
|
|
192
|
+
course: {
|
|
193
|
+
blueprint: false
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const config = {
|
|
197
|
+
queryParams: {
|
|
198
|
+
include: ['a']
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
await unSetAsBlueprint(0, config);
|
|
202
|
+
expect(fetchJson).toHaveBeenCalledWith(`/api/v1/courses/0`, apiWriteConfig('PUT', payload, config))
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
test('lock blueprint', async () => {
|
|
207
|
+
(fetchJson as jest.Mock).mockClear()
|
|
208
|
+
const modules: IModuleData[] = [];
|
|
209
|
+
const moduleCount = 10;
|
|
210
|
+
const itemCount = 6;
|
|
211
|
+
for (let i = 0; i < moduleCount; i++) {
|
|
212
|
+
modules.push({
|
|
213
|
+
...mockModuleData,
|
|
214
|
+
name: `Week ${i}`,
|
|
215
|
+
items: [...range(0, itemCount - 1)].map(j => ({
|
|
216
|
+
...mockModuleItemData,
|
|
217
|
+
content_id: i * 100 + j,
|
|
218
|
+
name: `Week ${i} Assignment ${j}`
|
|
219
|
+
}))
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
(fetchJson as jest.Mock).mockResolvedValue({})
|
|
223
|
+
await lockBlueprint(0, modules);
|
|
224
|
+
const contentIds: number[] = [];
|
|
225
|
+
modules.forEach(module => module.items.forEach(item => contentIds.push(item.content_id)))
|
|
226
|
+
|
|
227
|
+
for (const call of (fetchJson as jest.Mock).mock.calls) {
|
|
228
|
+
expect(call[0]).toBe('/api/v1/courses/0/blueprint_templates/default/restrict_item')
|
|
229
|
+
}
|
|
230
|
+
expect(fetchJson).toHaveBeenCalledTimes(itemCount * moduleCount);
|
|
231
|
+
fetchMock.mockClear();
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
async function mockBpResponse(mockRequest: Request, numberToMock = 1) {
|
|
235
|
+
const dummyBpData: ICourseData = {...mockCourseData, blueprint: true};
|
|
236
|
+
const [_, requestCode] = mockRequest.url.match(/=[^=]*(\w{4}\d{3})/i) || [];
|
|
237
|
+
const outCourseData: ICourseData[] = [...range(1, numberToMock)].map((number) => ({
|
|
238
|
+
...dummyBpData, name: `BP_${requestCode}${number > 1 ? number : ''}`,
|
|
239
|
+
course_code: `BP_${requestCode}${number > 1 ? number : ''}`
|
|
240
|
+
}))
|
|
241
|
+
return JSON.stringify(outCourseData);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
import {ITermData, Term} from "@/term/Term";
|
|
246
|
+
|
|
247
|
+
import apiWriteConfig from "@/fetch/apiWriteConfig";
|
|
248
|
+
import {IBlueprintCourse} from "@canvas/course/IBlueprintCourse";
|
|
249
|
+
import {getSections} from "@canvas/course/getSections";
|
|
250
|
+
import {getTermNameFromSections} from "@canvas/course/getTermNameFromSections";
|
|
251
|
+
import {retireBlueprint} from "@canvas/course/retireBlueprint";
|
|
252
|
+
import {NotABlueprintError} from "@canvas/course/notABlueprintError";
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
describe("genBlueprintsForCode", () => {
|
|
256
|
+
const mockCourseDataGenerator = mockAsyncGen([]);
|
|
257
|
+
const courseCode = "BP_TEST123";
|
|
258
|
+
const accountIds = [1, 2, 3];
|
|
259
|
+
const config: ICanvasCallConfig<GetCoursesFromAccountOptions> = {queryParams: {search_term: "x"}}; // replace with actual config type
|
|
260
|
+
|
|
261
|
+
afterEach(() => {
|
|
262
|
+
jest.clearAllMocks();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn');
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
it("should return null for invalid course code", async () => {
|
|
270
|
+
(getCourseDataGenerator as jest.Mock).mockReturnValue(mockCourseDataGenerator);
|
|
271
|
+
const courseCode = 'BP_ST123'
|
|
272
|
+
const result = genBlueprintDataForCode("BP_ST123", accountIds);
|
|
273
|
+
expect(result).toBeNull();
|
|
274
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(`Code ${courseCode} invalid`);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should generate blueprints for a valid course code", async () => {
|
|
278
|
+
(fetchGetConfig as jest.Mock).mockReturnValue({blueprint: true});
|
|
279
|
+
(getCourseDataGenerator as jest.Mock).mockReturnValue(mockCourseDataGenerator);
|
|
280
|
+
const result = genBlueprintDataForCode(courseCode, accountIds, config.queryParams);
|
|
281
|
+
expect(fetchGetConfig).toHaveBeenCalled();
|
|
282
|
+
|
|
283
|
+
expect(result).toBe(mockCourseDataGenerator);
|
|
284
|
+
expect(getCourseDataGenerator).toHaveBeenCalledWith(
|
|
285
|
+
baseCourseCode(courseCode),
|
|
286
|
+
accountIds,
|
|
287
|
+
undefined,
|
|
288
|
+
{blueprint: true}
|
|
289
|
+
);
|
|
290
|
+
expect(fetchGetConfig).toHaveBeenCalledWith({blueprint: true, include: ['concluded']}, config);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getModuleUnlockStartDate, getNewTermName, getOldUgTermName, getStartDateAssignments, MalformedSyllabusError,
|
|
3
|
+
NoAssignmentsWithDueDatesError, NoOverviewModuleFoundError,
|
|
4
|
+
sortAssignmentsByDueDate, syllabusHeaderName,
|
|
5
|
+
updatedDateSyllabusHtml
|
|
6
|
+
} from '../changeStartDate'
|
|
7
|
+
import {Temporal} from "temporal-polyfill";
|
|
8
|
+
import {mockAssignmentData} from "@canvas/content/__mocks__/mockContentData";
|
|
9
|
+
import {range} from "@canvas/canvasUtils";
|
|
10
|
+
import mockModuleData from "@canvas/course/__mocks__/mockModuleData";
|
|
11
|
+
import {Assignment} from "@canvas/content/assignments/Assignment";
|
|
12
|
+
const baseSyllabus = jest.requireActual('@canvas/course/__mocks__/syllabus.gallant.html')
|
|
13
|
+
const gradSyllabus = jest.requireActual('@canvas/course/__mocks__/syllabus.grad.html')
|
|
14
|
+
|
|
15
|
+
describe('Syllabus date changes', () => {
|
|
16
|
+
|
|
17
|
+
test('Changing date works for grad courses', () => {
|
|
18
|
+
const now = Temporal.Now.plainDateISO();
|
|
19
|
+
const syllabus = gradSyllabus;
|
|
20
|
+
const newSyllabus = updatedDateSyllabusHtml(syllabus, now);
|
|
21
|
+
const month = now.toLocaleString('en-US', {month: "2-digit"})
|
|
22
|
+
const day = now.toLocaleString('en-US', {day: "2-digit"})
|
|
23
|
+
const year = now.toLocaleString('en-US', {year: "2-digit"})
|
|
24
|
+
expect(newSyllabus.html).toContain(`DE8W${month}.${day}.${year}`)
|
|
25
|
+
});
|
|
26
|
+
test('Changing date works for undergrad courses', () => {
|
|
27
|
+
const now = Temporal.Now.plainDateISO();
|
|
28
|
+
const syllabus = baseSyllabus
|
|
29
|
+
const newSyllabus = updatedDateSyllabusHtml(syllabus, now);
|
|
30
|
+
const month = now.toLocaleString('en-US', {month: "2-digit"})
|
|
31
|
+
const day = now.toLocaleString('en-US', {day: "2-digit"})
|
|
32
|
+
const year = now.toLocaleString('en-US', {year: "2-digit"})
|
|
33
|
+
expect(newSyllabus.html).toContain(`DE5W${month}.${day}.${year}`)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('changing date does not double paragraph tags', () => {
|
|
37
|
+
const now = Temporal.Now.plainDateISO();
|
|
38
|
+
const newSyllabus = updatedDateSyllabusHtml(baseSyllabus, now);
|
|
39
|
+
expect(newSyllabus).not.toContain('<p> </p>')
|
|
40
|
+
expect(newSyllabus).not.toContain('<p></p>')
|
|
41
|
+
expect(newSyllabus).not.toContain(/<p>\s*<\/p>/)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
function shuffle<T>(list: T[]) {
|
|
46
|
+
const source = [...list];
|
|
47
|
+
const dest: T[] = [];
|
|
48
|
+
while (source.length > 0) {
|
|
49
|
+
const index = Math.floor(Math.random() * source.length);
|
|
50
|
+
dest.push(...source.splice(index, 1))
|
|
51
|
+
}
|
|
52
|
+
return dest;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('sortAssignmentsByDate', () => {
|
|
56
|
+
const now = Temporal.Now;
|
|
57
|
+
const mockAssignments: Assignment[] = shuffle([...range(0, 10)].map(i => {
|
|
58
|
+
return new Assignment({
|
|
59
|
+
...mockAssignmentData,
|
|
60
|
+
id: i,
|
|
61
|
+
due_at: now.plainDateTimeISO().add({days: 100 - i}).toString(),
|
|
62
|
+
}, 0);
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
it('sorts assignment by date', () => {
|
|
67
|
+
|
|
68
|
+
const sorted = sortAssignmentsByDueDate(mockAssignments);
|
|
69
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
70
|
+
const prev = sorted[i - 1];
|
|
71
|
+
const current = sorted[i];
|
|
72
|
+
expect(prev.dueAt?.getTime()).toBeLessThan(current.dueAt!.getTime())
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('sorts non-due-date assignments to the end', () => {
|
|
77
|
+
const toSort = shuffle([...mockAssignments,
|
|
78
|
+
new Assignment({...mockAssignmentData, due_at: null}, -98),
|
|
79
|
+
new Assignment({...mockAssignmentData, due_at: null}, -99)
|
|
80
|
+
])
|
|
81
|
+
const sorted = sortAssignmentsByDueDate(toSort);
|
|
82
|
+
const dates = sorted.map(value => value.dueAt);
|
|
83
|
+
expect(dates[sorted.length - 1]).toBeNull();
|
|
84
|
+
expect(dates[sorted.length - 2]).toBeNull();
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('getCurrentStartDate', () => {
|
|
90
|
+
it('returns a temporal plainDate if there\'s a module lock date', () => {
|
|
91
|
+
const mockModules = [
|
|
92
|
+
{...mockModuleData, unlock_at: '2022-12-24T00:00:00Z'},
|
|
93
|
+
]
|
|
94
|
+
expect(getModuleUnlockStartDate(mockModules)).toEqual(new Temporal.PlainDate(
|
|
95
|
+
2022, 12, 24));
|
|
96
|
+
})
|
|
97
|
+
it("throws an error if it can't find overview module", () => {
|
|
98
|
+
expect(() => getModuleUnlockStartDate([])).toThrowError(NoOverviewModuleFoundError)
|
|
99
|
+
})
|
|
100
|
+
it("returns null if there's no lock date in the first module", () => {
|
|
101
|
+
const mockModules = [
|
|
102
|
+
{...mockModuleData, unlock_at: null}
|
|
103
|
+
]
|
|
104
|
+
expect(getModuleUnlockStartDate(mockModules)).toBeNull()
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('getStartDateAssignments', () => {
|
|
109
|
+
function datesToAssignment([year, month, day]: [string, string, string]) {
|
|
110
|
+
return new Assignment({
|
|
111
|
+
...mockAssignmentData,
|
|
112
|
+
due_at: `${year}-${month}-${day}T00:00:00Z`
|
|
113
|
+
}, 0)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
it('gets the first assignment due and returns the monday of that week', () => {
|
|
117
|
+
const assignments = ([
|
|
118
|
+
['2024', '07', '19'],
|
|
119
|
+
['2024', '08', '19'],
|
|
120
|
+
['2024', '07', '18'],
|
|
121
|
+
['2024', '09', '19'],
|
|
122
|
+
] as [string, string, string][]).map(datesToAssignment);
|
|
123
|
+
|
|
124
|
+
expect(getStartDateAssignments(assignments)).toEqual(new Temporal.PlainDate(2024, 7, 15))
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('throws an error if there are no assignments with due dates', () => {
|
|
128
|
+
const assignments = ([new Assignment({...mockAssignmentData, due_at: null}, 0)])
|
|
129
|
+
expect(() => getStartDateAssignments(assignments)).toThrow(NoAssignmentsWithDueDatesError)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('getNewTermName', () => {
|
|
133
|
+
const newTermStart = new Temporal.PlainDate(2024, 12, 1);
|
|
134
|
+
it('throws an error with a bad term name', () => {
|
|
135
|
+
expect(() => getNewTermName('ABCDEFGX', newTermStart)).toThrow(MalformedSyllabusError)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('returns a new style grad term name from old style grad term name', () => {
|
|
139
|
+
expect(getNewTermName('DE8W05.07.22', newTermStart)).toEqual('DE8W12.01.24')
|
|
140
|
+
})
|
|
141
|
+
it('returns a new style undergrad term name from old style undergrad term name', () => {
|
|
142
|
+
expect(getNewTermName('DE/HL-22-Dec', newTermStart)).toEqual('DE5W12.01.24')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('getOldUgTermName', () => {
|
|
148
|
+
it('returns a legacy old styl ug term name', () => {
|
|
149
|
+
expect(getOldUgTermName(new Temporal.PlainDate(2024, 12, 24))).toEqual('DE-24-Dec')
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('syllabusHeaderName', () => {
|
|
155
|
+
function fakeHeader(text: string) {
|
|
156
|
+
const el = document.createElement('p');
|
|
157
|
+
el.innerHTML = text;
|
|
158
|
+
return el
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
it('works', () => {
|
|
162
|
+
expect(syllabusHeaderName(fakeHeader('<strong>Credits:</strong> 3'))).toEqual('Credits')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('handles multiple strong tags', () => {
|
|
166
|
+
expect(syllabusHeaderName(fakeHeader('<strong>Course</strong> <strong>Inclusive Dates:</strong>'))).toEqual('Course Inclusive Dates')
|
|
167
|
+
})
|
|
168
|
+
it('handles outside the tags colon', () => {
|
|
169
|
+
expect(syllabusHeaderName(fakeHeader('<strong>Course</strong> <strong>Inclusive Dates</strong>:'))).toEqual('Course Inclusive Dates')
|
|
170
|
+
expect(syllabusHeaderName(fakeHeader('<strong>Credits</strong>:'))).toEqual('Credits')
|
|
171
|
+
|
|
172
|
+
})
|
|
173
|
+
it('return undefined when : not found', () => {
|
|
174
|
+
expect(syllabusHeaderName(fakeHeader('<strong>Course</strong> <strong>Inclusive Dates</strong>'))).toBeUndefined();
|
|
175
|
+
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {formDataify, ICanvasCallConfig, range} from "../../canvasUtils";
|
|
2
|
+
import {createNewCourse, getCourseGenerator, getCourseName, setGradingStandardForCourse} from "../toolbox";
|
|
3
|
+
import {mockCourseData} from "../__mocks__/mockCourseData";
|
|
4
|
+
import assert from "assert";
|
|
5
|
+
import fetchMock from "jest-fetch-mock";
|
|
6
|
+
import {Course} from "../Course";
|
|
7
|
+
|
|
8
|
+
import {ICourseData} from "@/types";
|
|
9
|
+
import {fetchJson} from "@/fetch/fetchJson";
|
|
10
|
+
|
|
11
|
+
jest.mock('@/fetch/fetchJson', () => ({
|
|
12
|
+
fetchJson: jest.fn(),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
test('Create new course', async () => {
|
|
17
|
+
const courseCode = 'DEV_ABC1234';
|
|
18
|
+
const name = 'DEV_ABC134: Test Course';
|
|
19
|
+
const accountId = 0;
|
|
20
|
+
|
|
21
|
+
const courseData: ICourseData = {...mockCourseData, name, course_code: courseCode};
|
|
22
|
+
(fetchJson as jest.Mock).mockResolvedValue(courseData)
|
|
23
|
+
const createdCourse = await createNewCourse(courseCode, accountId, name)
|
|
24
|
+
expect(createdCourse).toStrictEqual(courseData);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('Course Generators', () => {
|
|
28
|
+
|
|
29
|
+
fetchMock.enableMocks();
|
|
30
|
+
|
|
31
|
+
test('Course generator generates courses', async () => {
|
|
32
|
+
|
|
33
|
+
const accountIds = [...range(0, 10)]
|
|
34
|
+
const courseGenerator = getCourseGenerator(
|
|
35
|
+
'x',
|
|
36
|
+
accountIds,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
for (const enrollment_term_id of accountIds) {
|
|
40
|
+
const codes = ['TEST000', 'ANIM123', 'CHEM897']
|
|
41
|
+
const courseDatas: ICourseData[] = codes.map(course_code => {
|
|
42
|
+
return {
|
|
43
|
+
...mockCourseData,
|
|
44
|
+
enrollment_term_id,
|
|
45
|
+
course_code,
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
fetchMock.mockResponseOnce(JSON.stringify(courseDatas));
|
|
50
|
+
for (const code of codes) {
|
|
51
|
+
const {done, value: course} = await courseGenerator.next();
|
|
52
|
+
assert(course instanceof Course);
|
|
53
|
+
expect(done).toBe(false);
|
|
54
|
+
expect(course.termId).toBe(enrollment_term_id);
|
|
55
|
+
expect(course.parsedCourseCode).toBe(code);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
const {done, value} = await courseGenerator.next();
|
|
60
|
+
expect(done).toBe(true);
|
|
61
|
+
expect(value).toBe(undefined);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
function testSave<SaveValue>(
|
|
70
|
+
func:(courseId:number, saveValue:SaveValue, config?:ICanvasCallConfig)=>Promise<ICourseData>,
|
|
71
|
+
rawDataKey: string,
|
|
72
|
+
testSubmitValue: SaveValue)
|
|
73
|
+
{
|
|
74
|
+
const fetchJsonMock = fetchJson as jest.Mock;
|
|
75
|
+
fetchJsonMock.mockResolvedValue({...mockCourseData, [rawDataKey]: testSubmitValue})
|
|
76
|
+
const newCourseData = func(0, testSubmitValue);
|
|
77
|
+
const [url, config] = fetchJsonMock.mock.lastCall as [string, ICanvasCallConfig];
|
|
78
|
+
const {body} = config.fetchInit as RequestInit & { body: FormData };
|
|
79
|
+
expect(url).toEqual(`/api/v1/courses/0`)
|
|
80
|
+
expect([...body.entries()]).toStrictEqual([...formDataify({course: {[rawDataKey]: testSubmitValue}}).entries()])
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('saving data tests', () => {
|
|
85
|
+
const fetchJsonMock = fetchJson as jest.Mock;
|
|
86
|
+
it('saves grading standard id', () => testSave(
|
|
87
|
+
setGradingStandardForCourse,
|
|
88
|
+
'grading_standard_id', 5)
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
describe('getCourseName', () => {
|
|
95
|
+
it('works for BP and various term codes', () => {
|
|
96
|
+
expect(getCourseName({...mockCourseData, name: 'BP_BIOL203: The Biology of Today'})).toEqual('The Biology of Today')
|
|
97
|
+
expect(getCourseName({...mockCourseData, name: 'DEV_BIOL203: The Biology of Today'})).toEqual('The Biology of Today')
|
|
98
|
+
expect(getCourseName({...mockCourseData, name: 'DE08W01.01.24_BIOL203: The Biology of Today-01'})).toEqual('The Biology of Today-01')
|
|
99
|
+
})
|
|
100
|
+
it('returns full name if there are no colons', () => {
|
|
101
|
+
expect(getCourseName({...mockCourseData, name: 'The Biology of Today'})).toEqual('The Biology of Today')
|
|
102
|
+
expect(getCourseName({...mockCourseData, name: 'DEV_BIOL203'})).toEqual('DEV_BIOL203')
|
|
103
|
+
})
|
|
104
|
+
})
|