@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,173 @@
|
|
|
1
|
+
import fetchMock, {FetchMock} from "jest-fetch-mock";
|
|
2
|
+
import {mockCourseData} from "../../__mocks__/mockCourseData";
|
|
3
|
+
import {
|
|
4
|
+
copyToNewCourseGenerator,
|
|
5
|
+
courseMigrationGenerator, getMigration, migrationsForCourseGen,
|
|
6
|
+
IMigrationData,
|
|
7
|
+
IProgressData
|
|
8
|
+
} from "../index";
|
|
9
|
+
import {ICanvasCallConfig, range} from "@/canvasUtils";
|
|
10
|
+
import {mockMigrationData} from "@/course/migration/__mocks__/mockMigrationData";
|
|
11
|
+
|
|
12
|
+
import {Course} from "@/course/Course";
|
|
13
|
+
import {config} from "dotenv";
|
|
14
|
+
import {ICourseData} from "@/types";
|
|
15
|
+
import {mockProgressData} from "@/course/__mocks__/mockProgressData";
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
describe('Getting migrations', ()=> {
|
|
25
|
+
|
|
26
|
+
it('gets migrations from course', async ()=> {
|
|
27
|
+
const id = 123;
|
|
28
|
+
const config = { fetchInit: {}};
|
|
29
|
+
fetchMock.once(JSON.stringify([mockMigrationData, {...mockMigrationData, id: 321}]));
|
|
30
|
+
const generator = migrationsForCourseGen(id, config);
|
|
31
|
+
let result = await generator.next();
|
|
32
|
+
let migration = result.done? null : result.value;
|
|
33
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
34
|
+
expect(fetchMock).toHaveBeenCalledWith(`/api/v1/courses/${id}/content_migrations`, config.fetchInit);
|
|
35
|
+
expect(migration).toEqual(mockMigrationData);
|
|
36
|
+
result = await generator.next();
|
|
37
|
+
migration = result.done? null : result.value;
|
|
38
|
+
expect(migration?.id).toEqual(321);
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('gets a single mock by id', async ()=> {
|
|
42
|
+
const courseId = 123;
|
|
43
|
+
const migrationId = 321;
|
|
44
|
+
const config = { fetchInit: {} };
|
|
45
|
+
fetchMock.once(JSON.stringify(mockMigrationData));
|
|
46
|
+
const migration = await getMigration(courseId, migrationId, config);
|
|
47
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
48
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
49
|
+
`/api/v1/courses/${courseId}/content_migrations/${migrationId}`,
|
|
50
|
+
config.fetchInit
|
|
51
|
+
);
|
|
52
|
+
expect(migration).toEqual(expect.objectContaining(mockMigrationData));
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('returns null if no migrations are found', async () => {
|
|
56
|
+
fetchMock.once('null');
|
|
57
|
+
const migration = await getMigration(1, 2, {});
|
|
58
|
+
expect(migration).toBeNull()
|
|
59
|
+
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
describe('Course migration', () => {
|
|
65
|
+
fetchMock.enableMocks();
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
jest.clearAllMocks();
|
|
68
|
+
})
|
|
69
|
+
it('Yields values while still polling', async () => {
|
|
70
|
+
|
|
71
|
+
const workflowStates = [
|
|
72
|
+
...[...range(0, 2)].map(_ => 'queued'),
|
|
73
|
+
...[...range(0, 2)].map(_ => 'running'),
|
|
74
|
+
'completed'
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
fetchMock.mockResponseOnce(JSON.stringify(<IMigrationData>{
|
|
78
|
+
...mockMigrationData, workflow_state: 'queued'
|
|
79
|
+
}))
|
|
80
|
+
const migrationGenerator = courseMigrationGenerator(0, 1, 20);
|
|
81
|
+
|
|
82
|
+
for (const state of workflowStates) {
|
|
83
|
+
fetchMock.mockResponseOnce(JSON.stringify(<IProgressData>{
|
|
84
|
+
...mockProgressData, workflow_state: state
|
|
85
|
+
}))
|
|
86
|
+
}
|
|
87
|
+
const workflowIterator = workflowStates.values();
|
|
88
|
+
for await (const progress of migrationGenerator) {
|
|
89
|
+
expect(progress?.workflow_state).toBe(workflowIterator.next().value)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('Throws an error on a failed migration', async () => {
|
|
94
|
+
const workflowStates: IMigrationData["workflow_state"][] = [
|
|
95
|
+
'queued',
|
|
96
|
+
'running',
|
|
97
|
+
'failed'
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
fetchMock.mockResponseOnce(JSON.stringify(<IMigrationData>{
|
|
101
|
+
...mockMigrationData, workflow_state: 'queued'
|
|
102
|
+
}))
|
|
103
|
+
for (const state of workflowStates) {
|
|
104
|
+
fetchMock.mockResponseOnce(JSON.stringify(<IProgressData>{
|
|
105
|
+
...mockProgressData, workflow_state: state
|
|
106
|
+
}))
|
|
107
|
+
}
|
|
108
|
+
const migrationGenerator = courseMigrationGenerator(0, 1, 0);
|
|
109
|
+
|
|
110
|
+
await expect(async () => {
|
|
111
|
+
for await (const progress of migrationGenerator) {
|
|
112
|
+
expect(progress?.id).toBe(mockProgressData.id)
|
|
113
|
+
}
|
|
114
|
+
}).rejects.toThrow(/Migration Error/)
|
|
115
|
+
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('Course Copying', () => {
|
|
121
|
+
test('Copy course wholesale', async () => {
|
|
122
|
+
fetchMock.enableMocks();
|
|
123
|
+
|
|
124
|
+
const fetch = (global.fetch as FetchMock);
|
|
125
|
+
const courseName = 'Test course the testing course';
|
|
126
|
+
const courseCode = "TEST000";
|
|
127
|
+
const sourceCourseData = {
|
|
128
|
+
...mockCourseData,
|
|
129
|
+
name: `DEV_${courseCode}: ${courseName}`,
|
|
130
|
+
course_code: `DEV_${courseCode}`
|
|
131
|
+
};
|
|
132
|
+
const sourceCourse = new Course(sourceCourseData);
|
|
133
|
+
const newCode = 'BP_TEST000';
|
|
134
|
+
const newName = `BP_TEST000: ${courseName}`;
|
|
135
|
+
|
|
136
|
+
const workflowStates = [
|
|
137
|
+
...[...range(0, 2)].map(_ => 'queued'),
|
|
138
|
+
...[...range(0, 2)].map(_ => 'running'),
|
|
139
|
+
'completed',
|
|
140
|
+
|
|
141
|
+
]
|
|
142
|
+
const workflowIterator = workflowStates.values();
|
|
143
|
+
fetchMock.mockClear();
|
|
144
|
+
fetchMock.mockResponses(...[
|
|
145
|
+
sourceCourseData,
|
|
146
|
+
{...mockMigrationData, workflow_state: 'queued'},
|
|
147
|
+
|
|
148
|
+
...workflowStates.map((state, i) => ({
|
|
149
|
+
...mockProgressData,
|
|
150
|
+
workflow_state: state,
|
|
151
|
+
message: i.toString(),
|
|
152
|
+
} as IProgressData)),
|
|
153
|
+
|
|
154
|
+
{...sourceCourse.rawData, name: newName, course_code: newCode} as ICourseData,
|
|
155
|
+
].map(value => JSON.stringify(value)))
|
|
156
|
+
const migrationGenerator = copyToNewCourseGenerator(sourceCourse, newCode, 20);
|
|
157
|
+
|
|
158
|
+
async function testCourseMigration() {
|
|
159
|
+
let result;
|
|
160
|
+
while (true) {
|
|
161
|
+
result = await migrationGenerator.next();
|
|
162
|
+
const {value} = workflowIterator.next();
|
|
163
|
+
if (result.done) return result.value;
|
|
164
|
+
expect(result.value.workflow_state).toBe(value)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const course: ICourseData = await testCourseMigration();
|
|
169
|
+
expect(course.name).toBe(newName);
|
|
170
|
+
expect(course.course_code).toBe(newCode);
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {loadCachedCourseMigrations, cacheCourseMigrations, loadCachedMigrations} from "../migrationCache";
|
|
2
|
+
import {mockMigrationData} from "@/course/migration/__mocks__/mockMigrationData";
|
|
3
|
+
import {range} from "../../../canvasUtils";
|
|
4
|
+
|
|
5
|
+
describe('Saving and loading migration states', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
localStorage.clear();
|
|
8
|
+
})
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
localStorage.clear();
|
|
11
|
+
})
|
|
12
|
+
it('has base functions that save and load migration data', () => {
|
|
13
|
+
const migrations = loadCachedCourseMigrations(0);
|
|
14
|
+
expect(migrations).toHaveLength(0);
|
|
15
|
+
|
|
16
|
+
const cachedKeys:{[key:number] : number} = {};
|
|
17
|
+
for(const i of [...range(0, 5)]) {
|
|
18
|
+
const key = i * 2;
|
|
19
|
+
const migrationsToCache = [...range(1,i)].map( _ => mockMigrationData)
|
|
20
|
+
cacheCourseMigrations(i * 2, migrationsToCache);
|
|
21
|
+
expect(Object.entries(loadCachedMigrations())).toHaveLength(i + 1)
|
|
22
|
+
cachedKeys[key] = i;
|
|
23
|
+
//Make sure all previous cached migrations have the same value going on.
|
|
24
|
+
for(const [key, value] of Object.entries(cachedKeys)) {
|
|
25
|
+
expect(loadCachedCourseMigrations(key)).toHaveLength(value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('adds new migrations to cached migration list', () => {
|
|
31
|
+
cacheCourseMigrations(1, [{...mockMigrationData, id: 1}])
|
|
32
|
+
expect(loadCachedCourseMigrations(1)).toHaveLength(1);
|
|
33
|
+
cacheCourseMigrations(1, [{...mockMigrationData, id: 2}])
|
|
34
|
+
expect(loadCachedCourseMigrations(1)).toHaveLength(2);
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('adds new migrations to cached migration list', () => {
|
|
38
|
+
cacheCourseMigrations(1, [{...mockMigrationData, id: 1, workflow_state: 'queued'}])
|
|
39
|
+
expect(loadCachedCourseMigrations(1)).toHaveLength(1);
|
|
40
|
+
cacheCourseMigrations(1, [{...mockMigrationData, id: 1, workflow_state: 'completed', cleanedUp: false}]);
|
|
41
|
+
expect(loadCachedCourseMigrations(1)).toHaveLength(1);
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deepObjectMerge,
|
|
3
|
+
formDataify,
|
|
4
|
+
ICanvasCallConfig
|
|
5
|
+
} from "@/canvasUtils";
|
|
6
|
+
import {createNewCourse, getCourseData} from "../toolbox";
|
|
7
|
+
import {Course} from "../Course";
|
|
8
|
+
|
|
9
|
+
import {getPagedDataGenerator} from "@/fetch/getPagedDataGenerator";
|
|
10
|
+
|
|
11
|
+
import {fetchJson} from "@/fetch/fetchJson";
|
|
12
|
+
import {GetCourseOptions} from "@/course/courseTypes";
|
|
13
|
+
import {sleep} from "@/toolbox";
|
|
14
|
+
export type WorkflowState = 'queued' | 'running' | 'completed' | 'failed';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export interface IMigrationData {
|
|
18
|
+
migration_type: string;
|
|
19
|
+
migration_type_title: string;
|
|
20
|
+
pre_attachment: { upload_params: {}; upload_url: string; message: string };
|
|
21
|
+
attachment: { url: string };
|
|
22
|
+
finished_at: string;
|
|
23
|
+
user_id: number;
|
|
24
|
+
progress_url: string;
|
|
25
|
+
started_at: string;
|
|
26
|
+
workflow_state: WorkflowState;
|
|
27
|
+
id: number;
|
|
28
|
+
migration_issues_url: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IProgressData {
|
|
32
|
+
completion: number;
|
|
33
|
+
updated_at: string;
|
|
34
|
+
user_id: number;
|
|
35
|
+
context_type: string;
|
|
36
|
+
created_at: string;
|
|
37
|
+
context_id: number;
|
|
38
|
+
workflow_state: WorkflowState;
|
|
39
|
+
id: number;
|
|
40
|
+
tag: string;
|
|
41
|
+
message: string;
|
|
42
|
+
results: { id: string };
|
|
43
|
+
url: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export function migrationsForCourseGen(courseId:number, config?:ICanvasCallConfig) {
|
|
48
|
+
const url = `/api/v1/courses/${courseId}/content_migrations`
|
|
49
|
+
return getPagedDataGenerator<IMigrationData>(url, config);
|
|
50
|
+
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function getMigration(courseId:number,migrationId:number, config?:ICanvasCallConfig) {
|
|
54
|
+
try {
|
|
55
|
+
const data = await fetchJson(
|
|
56
|
+
`/api/v1/courses/${courseId}/content_migrations/${migrationId}`,
|
|
57
|
+
config,
|
|
58
|
+
);
|
|
59
|
+
return data as IMigrationData;
|
|
60
|
+
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
export async function* courseMigrationGenerator(
|
|
68
|
+
sourceCourseId: number,
|
|
69
|
+
destCourseId: number,
|
|
70
|
+
pollDelay: number = 500, //in ms
|
|
71
|
+
migrationConfig?: ICanvasCallConfig,
|
|
72
|
+
pollConfig?: ICanvasCallConfig
|
|
73
|
+
) {
|
|
74
|
+
|
|
75
|
+
const migrationData = await startMigration(sourceCourseId, destCourseId, migrationConfig);
|
|
76
|
+
for await (const result of genCourseMigrationProgress(migrationData, pollDelay, pollConfig)) {
|
|
77
|
+
yield result;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function startMigration(sourceCourseId:number, destCourseId: number, config?:ICanvasCallConfig) {
|
|
82
|
+
const copyUrl = `/api/v1/courses/${destCourseId}/content_migrations`;
|
|
83
|
+
const migrationInit: RequestInit = {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: formDataify({
|
|
86
|
+
migration_type: 'course_copy_importer',
|
|
87
|
+
settings: {
|
|
88
|
+
source_course_id: sourceCourseId,
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
config = deepObjectMerge(config, {fetchInit: migrationInit}, true);
|
|
94
|
+
return await fetchJson<IMigrationData>(copyUrl, config);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function* genCourseMigrationProgress(migration:IMigrationData, pollDelay: number = 500, pollConfig?: ICanvasCallConfig) {
|
|
98
|
+
const {progress_url: progressUrl} = migration;
|
|
99
|
+
while (true) {
|
|
100
|
+
await sleep(pollDelay);
|
|
101
|
+
const progress = await fetchJson<IProgressData>(progressUrl, pollConfig);
|
|
102
|
+
yield progress;
|
|
103
|
+
if (progress.workflow_state === 'completed') return
|
|
104
|
+
if (progress.workflow_state === 'failed') {
|
|
105
|
+
throw new Error(`Migration Error: ${progress.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function* copyToNewCourseGenerator(
|
|
111
|
+
sourceCourse: Course,
|
|
112
|
+
newCode: string,
|
|
113
|
+
pollDelay: number = 500,
|
|
114
|
+
newName?: string,
|
|
115
|
+
courseConfig?: ICanvasCallConfig<GetCourseOptions>,
|
|
116
|
+
migrationConfig?: ICanvasCallConfig,
|
|
117
|
+
pollConfig?: ICanvasCallConfig,
|
|
118
|
+
returnCourseConfig?: ICanvasCallConfig<GetCourseOptions>,
|
|
119
|
+
) {
|
|
120
|
+
if (!newName && sourceCourse.parsedCourseCode) {
|
|
121
|
+
newName = sourceCourse.name.replace(sourceCourse.parsedCourseCode, newCode);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
returnCourseConfig ??= courseConfig;
|
|
125
|
+
newName ??= sourceCourse.name + "_COPY";
|
|
126
|
+
const {destId} = await createNewCourse(newCode, sourceCourse.accountId, newName, courseConfig);
|
|
127
|
+
const migration = courseMigrationGenerator(sourceCourse.id, destId, pollDelay, migrationConfig, pollConfig);
|
|
128
|
+
|
|
129
|
+
for await (const progress of migration) {
|
|
130
|
+
yield progress;
|
|
131
|
+
}
|
|
132
|
+
return await getCourseData(destId, returnCourseConfig);
|
|
133
|
+
|
|
134
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {IMigrationData} from "./index";
|
|
2
|
+
|
|
3
|
+
export type SavedMigration = IMigrationData & {
|
|
4
|
+
tracked?: boolean,
|
|
5
|
+
cleanedUp?: boolean,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function loadCachedMigrations() {
|
|
9
|
+
if (!localStorage.getItem('migrations')) localStorage.setItem('migrations', '{}')
|
|
10
|
+
const migrationDataString = localStorage.getItem('migrations') as string;
|
|
11
|
+
const migrationDataList: Record<string, SavedMigration[]> = JSON.parse(migrationDataString) ?? {};
|
|
12
|
+
return migrationDataList;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadCachedCourseMigrations(courseId: number | string) {
|
|
16
|
+
const key = courseId.toString();
|
|
17
|
+
return loadCachedMigrations()[key] ?? [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export type CacheCourseMigrationOptions = {
|
|
22
|
+
cachedMigrations?: ReturnType<typeof loadCachedMigrations>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function cacheCourseMigrations(courseId: number, toCacheMigrations: SavedMigration[], options?:CacheCourseMigrationOptions) {
|
|
26
|
+
let {cachedMigrations} = options ?? {};
|
|
27
|
+
cachedMigrations ??= loadCachedMigrations();
|
|
28
|
+
const cachedCourseMigrations = loadCachedCourseMigrations(courseId);
|
|
29
|
+
|
|
30
|
+
function getToCacheVersion(migrationId: number) {
|
|
31
|
+
const migrations = toCacheMigrations.filter(toCache => toCache.id === migrationId);
|
|
32
|
+
if(migrations.length === 0) return undefined;
|
|
33
|
+
return migrations[0] as SavedMigration;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasCachedVersion(migrationId: number) {
|
|
37
|
+
const migrations = cachedCourseMigrations.filter(cached => cached.id === migrationId);
|
|
38
|
+
return migrations.length > 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
let updatingMigrations = cachedCourseMigrations.map(cachedCourseMigration => ({
|
|
43
|
+
...cachedCourseMigration,
|
|
44
|
+
...getToCacheVersion(cachedCourseMigration.id),
|
|
45
|
+
}))
|
|
46
|
+
const remainingMigrations = toCacheMigrations.filter(migration => !hasCachedVersion(migration.id))
|
|
47
|
+
|
|
48
|
+
updatingMigrations = [
|
|
49
|
+
...updatingMigrations,
|
|
50
|
+
...remainingMigrations,
|
|
51
|
+
].toSorted((a, b) => a.id - b.id);
|
|
52
|
+
|
|
53
|
+
cachedMigrations[courseId.toString()] = updatingMigrations;
|
|
54
|
+
localStorage.setItem('migrations', JSON.stringify(cachedMigrations));
|
|
55
|
+
return cachedMigrations;
|
|
56
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {Temporal} from "temporal-polyfill";
|
|
2
|
+
import {IModuleData, IModuleItemData} from "../canvasDataDefs";
|
|
3
|
+
import {formDataify, ICanvasCallConfig} from "../canvasUtils";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import {fetchJson} from "@canvas/fetch/fetchJson";
|
|
7
|
+
import {Page} from "@canvas/content/pages/Page";
|
|
8
|
+
import {getPagedDataGenerator} from "@canvas/fetch/getPagedDataGenerator";
|
|
9
|
+
import {IPageData} from "@canvas/content/pages/types";
|
|
10
|
+
import {Discussion} from "@canvas/content/discussions/Discussion";
|
|
11
|
+
|
|
12
|
+
export interface IModuleHaver {
|
|
13
|
+
getModules(config: ICanvasCallConfig): IModuleData[],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GenerateModulesOptions {
|
|
17
|
+
include?: ('items' | 'content_details')[],
|
|
18
|
+
search_term?: string,
|
|
19
|
+
student_id?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export async function saveModuleItem(
|
|
24
|
+
courseId:number,
|
|
25
|
+
moduleId:number,
|
|
26
|
+
moduleItemId: number,
|
|
27
|
+
moduleItem: Partial<IModuleItemData>
|
|
28
|
+
) {
|
|
29
|
+
return await fetchJson(`/api/v1/courses/${courseId}/modules/${moduleId}/modules/items/${moduleItemId}`, {
|
|
30
|
+
fetchInit: {
|
|
31
|
+
method: "PUT",
|
|
32
|
+
body: formDataify({ moduleItem: moduleItem }),
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function moduleGenerator(courseId:number, config?: ICanvasCallConfig<GenerateModulesOptions>) {
|
|
38
|
+
return getPagedDataGenerator<IModuleData>(`/api/v1/courses/${courseId}/modules`, config);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function changeModuleLockDate(courseId: number, module: IModuleData, targetDate: Temporal.PlainDate) {
|
|
42
|
+
const payload = {
|
|
43
|
+
module: {
|
|
44
|
+
unlock_at: targetDate.toString()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const url = `/api/v1/courses/${courseId}/modules/${module.id}`;
|
|
48
|
+
const result = fetchJson(url, {
|
|
49
|
+
fetchInit: {
|
|
50
|
+
method: 'PUT',
|
|
51
|
+
body: formDataify(payload)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
export async function getModuleOverview(module: IModuleData, courseId: number) {
|
|
58
|
+
const overview = module.items.find(item =>
|
|
59
|
+
item.type === "Page" &&
|
|
60
|
+
item.title.toLowerCase().includes('overview')
|
|
61
|
+
);
|
|
62
|
+
if (!overview?.url) return; //skip this if it's not an overview
|
|
63
|
+
|
|
64
|
+
const url = overview.url.replace(/.*\/api\/v1/, '/api/v1')
|
|
65
|
+
const pageData = await fetchJson(url) as IPageData;
|
|
66
|
+
return new Page(pageData, courseId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getModuleWeekNumber(module: Record<string, any>) {
|
|
70
|
+
const regex = /(week|module) (\d+)/i;
|
|
71
|
+
const match = module.name.match(regex);
|
|
72
|
+
let weekNumber = !match ? null : Number(match[1]);
|
|
73
|
+
if (!weekNumber) {
|
|
74
|
+
for (const moduleItem of module.items) {
|
|
75
|
+
if (!moduleItem.hasOwnProperty('title')) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const match = moduleItem.title.match(regex);
|
|
79
|
+
if (match) {
|
|
80
|
+
weekNumber = match[2];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return weekNumber;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function getModulesByWeekNumber(modules: IModuleData[]) {
|
|
88
|
+
const modulesByWeekNumber: Record<string | number, IModuleData> = {};
|
|
89
|
+
for (const module of modules) {
|
|
90
|
+
const weekNumber = getModuleWeekNumber(module);
|
|
91
|
+
if (weekNumber) {
|
|
92
|
+
modulesByWeekNumber[weekNumber] = module;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return modulesByWeekNumber;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const isModuleItemTypeFunc = <T extends IModuleItemData>(typeString: string) => (item: IModuleItemData): item is T => {
|
|
99
|
+
return item.type === typeString;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type PageItemData = IModuleItemData & { type: "Page" };
|
|
103
|
+
export const isPageItemData = isModuleItemTypeFunc<PageItemData>("Page");
|
|
104
|
+
|
|
105
|
+
export type AssignmentItemData = IModuleItemData & { type: "Assignment"; };
|
|
106
|
+
export const isAssignmentItemData = isModuleItemTypeFunc<AssignmentItemData>("Assignment");
|
|
107
|
+
|
|
108
|
+
export type DiscussionItemData = IModuleItemData & { type: "Discussion"; };
|
|
109
|
+
export const isDiscussionItemData = isModuleItemTypeFunc<DiscussionItemData>("Discussion");
|
|
110
|
+
|
|
111
|
+
export type QuizItemData = IModuleItemData & { type: "Quiz"; };
|
|
112
|
+
export const isQuizItemData = isModuleItemTypeFunc<QuizItemData>("Quiz");
|
|
113
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import getReferencesForText from "@/course/references/getReferencesForText";
|
|
2
|
+
import {fetchJson} from "@/fetch/fetchJson";
|
|
3
|
+
import {jest} from "@jest/globals";
|
|
4
|
+
import mockCiteAsResponse from "@/course/references/mockCiteAsResponse";
|
|
5
|
+
|
|
6
|
+
jest.mock('@/fetch/fetchJson', () => ({
|
|
7
|
+
fetchJson: jest.fn(),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
describe('getReferencesForText', () => {
|
|
11
|
+
const userEmail = 'ttestersson@unity.edu';
|
|
12
|
+
const testText = "The Hungry, Hungry Caterpillar";
|
|
13
|
+
(fetchJson as jest.Mock).mockImplementation(async () => mockCiteAsResponse)
|
|
14
|
+
it('passes email and text into url', async () => {
|
|
15
|
+
const refs = await getReferencesForText(testText, userEmail);
|
|
16
|
+
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {fetchJson} from "@/fetch/fetchJson";
|
|
2
|
+
import {IQueryParams} from "@/canvasUtils";
|
|
3
|
+
import {apiGetConfig} from "@canvas/fetch/apiGetConfig";
|
|
4
|
+
|
|
5
|
+
const baseUrl = 'https://api.citeas.org/product'
|
|
6
|
+
|
|
7
|
+
export default async function getReferencesForText(text:string, userEmail:string, queryParams?:IQueryParams) {
|
|
8
|
+
queryParams ??= {};
|
|
9
|
+
queryParams.email = userEmail;
|
|
10
|
+
const result = await fetchJson(baseUrl, apiGetConfig(queryParams))
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {DEV_TEMPLATE_COURSE_ID, REFERENCES_PAGE_URL_NAME} from "@/consts";
|
|
2
|
+
import {IPageData} from "@/content/pages/types";
|
|
3
|
+
import {Page} from "@/content/pages/Page";
|
|
4
|
+
import PageKind from "@/content/pages/PageKind";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const url = `/api/v1/courses/${DEV_TEMPLATE_COURSE_ID}/pages/${REFERENCES_PAGE_URL_NAME}`;
|
|
8
|
+
|
|
9
|
+
export enum ReferenceExportType {
|
|
10
|
+
string,
|
|
11
|
+
pageData,
|
|
12
|
+
page,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getReferenceTemplate() : Promise<string|undefined>;
|
|
16
|
+
function getReferenceTemplate(type: ReferenceExportType.string) : Promise<string|undefined>;
|
|
17
|
+
function getReferenceTemplate(type: ReferenceExportType.pageData) : Promise<IPageData|undefined>;
|
|
18
|
+
async function getReferenceTemplate(type: ReferenceExportType.page) : Promise<Page|undefined>;
|
|
19
|
+
async function getReferenceTemplate(type?: NonNullable<unknown>) {
|
|
20
|
+
const pageData = await PageKind.getByString(DEV_TEMPLATE_COURSE_ID, REFERENCES_PAGE_URL_NAME)
|
|
21
|
+
|
|
22
|
+
if(typeof type === 'undefined' || type === ReferenceExportType.string) return 'body' in pageData ? pageData.body : pageData.message;
|
|
23
|
+
if('message' in pageData) return undefined;
|
|
24
|
+
if(type === ReferenceExportType.pageData) return pageData;
|
|
25
|
+
if(type === ReferenceExportType.page) return pageData ? new Page(pageData, DEV_TEMPLATE_COURSE_ID) : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default getReferenceTemplate;
|