-
Notifications
You must be signed in to change notification settings - Fork 526
Expand file tree
/
Copy pathprogress.js
More file actions
396 lines (364 loc) · 12.4 KB
/
Copy pathprogress.js
File metadata and controls
396 lines (364 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
import $ from 'jquery';
import _ from 'lodash';
import queryString from 'query-string';
import React from 'react';
import {setVerified} from '@cdo/apps/code-studio/verifiedInstructorRedux';
import {TestResults} from '@cdo/apps/constants';
import {
setUserRoleInCourse,
CourseRoles,
} from '@cdo/apps/templates/currentUserRedux';
import {createReactRoot} from '@cdo/apps/util/createReactRoot';
import clientState from './clientState';
import DisabledBubblesAlert from './components/DisabledBubblesAlert';
import DisabledBubblesModal from './components/DisabledBubblesModal';
import {getHiddenLessons} from './hiddenLessonRedux';
import {
initProgress,
overwriteResults,
setScriptProgress,
disablePostMilestone,
setIsAge13Required,
setLessonExtrasEnabled,
queryUserProgress as reduxQueryUserProgress,
useDbProgress,
} from './progressRedux';
import {getStore} from './redux';
import {setViewType, ViewType} from './viewAsRedux';
var progress = module.exports;
export default progress;
function showDisabledBubblesModal() {
const div = $('<div>');
$(document.body).append(div);
createReactRoot(<DisabledBubblesModal />, div[0], {
legacyReactDomRender: true,
});
}
/**
* If milestone posts are disabled, show an alert about progress not being tracked.
*/
progress.showDisabledBubblesAlert = function () {
const store = getStore();
const {postMilestoneDisabled} = store.getState().progress;
if (!postMilestoneDisabled) {
return;
}
const div = $('<div>').css({
position: 'absolute',
left: 0,
right: 0,
top: 45,
zIndex: 1000,
});
$(document.body).append(div);
createReactRoot(<DisabledBubblesAlert />, div[0], {
legacyReactDomRender: true,
});
};
/**
* @param {object} scriptData (Note - This is only a subset of the information
* we have in renderCourseProgress)
* @param {object} lessonData
* @param {object} progressData
* @param {string} currentLevelid The id of the level the user is currently on.
* This gets used in the url and as a key in many objects. Therefore, it is a
* string despite always being a numerical value
* @param {boolean} saveAnswersBeforeNavigation
* @param {boolean} signedIn True/false if we know the sign in state of the
* user, null otherwise
* @param {boolean} lessonExtrasEnabled Whether this user is in a section with
* lessonExtras enabled for this script
* @param {boolean} isLessonExtras Boolean indicating we are not on a script
* level and therefore are on lesson extras
* @param {number} currentPageNumber The page we are on if this is a multi-
* page level.
* @returns {Promise<void>}
*/
progress.generateLessonProgress = function (
scriptData,
lessonGroupData,
lessonData,
progressData,
currentLevelId,
saveAnswersBeforeNavigation,
signedIn,
lessonExtrasEnabled,
isLessonExtras,
currentPageNumber
) {
const store = getStore();
const {
name,
displayName,
disablePostMilestone,
age_13_required,
hasUnnumberedLessons,
course_name,
course_id,
} = scriptData;
initializeStoreWithProgress(
store,
{
name,
displayName,
lessonGroups: lessonGroupData,
lessons: [lessonData],
disablePostMilestone,
age_13_required,
id: lessonData.script_id,
hasUnnumberedLessons,
courseName: course_name,
course_id: course_id,
},
currentLevelId,
false,
saveAnswersBeforeNavigation,
isLessonExtras,
currentPageNumber
);
if (lessonExtrasEnabled) {
store.dispatch(setLessonExtrasEnabled(true));
}
return populateProgress(store, signedIn, progressData, name);
};
/**
* Populates progress data in the given redux strore.
* @param {Store} store redux store
* @param {boolean|null} signedIn true if the user is signed in, false if the
* user is not signed in, null if we don't know
* @param {Object} progressData progress data if it is already known, only used
* if signedIn === true
* @param {string} scriptName
* @returns {Promise<void>}
*/
function populateProgress(store, signedIn, progressData, scriptName) {
return getLevelProgress(signedIn, progressData, scriptName).then(data => {
if (data.usingDbProgress) {
store.dispatch(useDbProgress());
clientState.clearProgress();
store.dispatch(setScriptProgress(data.unitProgress));
}
if (data.levelResults) {
store.dispatch(overwriteResults(data.levelResults));
}
if (data.isVerifiedInstructor) {
store.dispatch(setVerified());
}
if (signedIn) {
progress.showDisabledBubblesAlert();
}
});
}
/**
* Returns a promise that, when resolved, returns an object with two properties:
* - usingDbProgress: indicates if level progress came from the database
* - levelResults: map from level id to result
*
* This function contains logic that determines whether progress data should
* come from the data embedded in the html page (passed in as progressData),
* from session storage, or from an ajax request to the server.
*
* @param {boolean|null} signedIn true if the user is signed in, false if the
* user is not signed in, null if we don't know
* @param {Object} progressData progress data if it is already known, only used
* if signedIn === true
* @param {string} scriptName
* @returns {Promise}
*/
function getLevelProgress(signedIn, progressData, scriptName) {
switch (signedIn) {
case true:
// User is signed in, return a resolved promise with the given progress data
return Promise.resolve({
usingDbProgress: true,
levelResults: extractLevelResults(progressData),
unitProgress: progressData.progress,
});
case false:
// User is not signed in, return a resolved promise with progress data
// retrieved from session storage
return Promise.resolve({
usingDbProgress: false,
levelResults: clientState.levelProgress(scriptName),
});
case null:
// We do not know if user is signed in or not, send a request to the server
// to find out if the user is signed in and retrieve progress information
return $.ajax({
url: `/api/user_progress/${scriptName}`,
data: {user_id: clientState.queryParams('user_id')},
})
.then(data => {
if (data.signedIn) {
return {
usingDbProgress: true,
levelResults: extractLevelResults(data),
unitProgress: data.progress,
};
} else {
return {
usingDbProgress: false,
levelResults: clientState.levelProgress(scriptName),
};
}
})
.fail(() =>
// TODO: Show an error to the user here? (LP-1815)
console.error(
'Could not load user progress. User progress may not be saved.'
)
);
}
}
/**
* Extracts the level results from the response to /api/user_progress.
* @param {object} userProgressResponse parsed response object to a
* /api/user_progress request
* @returns {Object<string, TestResults>} map from level id to level result
*/
function extractLevelResults(userProgressResponse) {
return _.mapValues(userProgressResponse.progress, level =>
level.submitted ? TestResults.SUBMITTED_RESULT : level.result
);
}
/**
* @param {object} scriptData
* @param {string} scriptData.id
* @param {boolean} scriptData.plc
* @param {object[]} scriptData.lessons
* @param {string} scriptData.name
* @param {boolean} scriptData.hideable_lessons
* @param {boolean} scriptData.age_13_required
* Fetch and store progress for the course overview page.
*/
progress.initCourseProgress = function (scriptData) {
const store = getStore();
initializeStoreWithProgress(store, scriptData, null, true);
queryUserProgress(store, scriptData, null, false);
};
/* Set our initial view type (Participant or Instructor) from current user's user_type
* or our query string. */
progress.initViewAs = function (store, isSignedInUser, isInstructor) {
progress.initViewAsWithoutStore(store.dispatch, isSignedInUser, isInstructor);
};
progress.initViewAsWithoutStore = function (
dispatch,
isSignedInUser,
isInstructor
) {
// Default to Participant, unless current user is a teacher
let initialViewAs = ViewType.Participant;
if (isInstructor) {
initialViewAs = ViewType.Instructor;
dispatch(setUserRoleInCourse(CourseRoles.Instructor));
}
// If current user is signed out or an instructor, allow the
// 'viewAs' query parameter to override;
if (!isSignedInUser || isInstructor) {
const query = queryString.parse(location.search);
initialViewAs = query.viewAs || initialViewAs;
}
dispatch(setViewType(initialViewAs));
};
progress.retrieveProgress = function (scriptName, scriptData, currentLevelId) {
const store = getStore();
const courseName = scriptData?.course_name;
const unitPosition = scriptData?.unit_position;
let fetchURL = `/api/script_structure/${scriptName}`;
if (courseName && unitPosition) {
fetchURL = `/api/script_structure/courses/${courseName}/units/${unitPosition}`;
}
return $.getJSON(fetchURL, scriptData => {
initializeStoreWithProgress(store, scriptData, currentLevelId, true);
queryUserProgress(store, scriptData, currentLevelId);
});
};
/**
* Query the server for user_progress data for this script, and update the store
* as appropriate. If the user is not signed in, level progress data is populated
* from session storage.
*/
function queryUserProgress(store, scriptData, currentLevelId) {
const userId = clientState.queryParams('user_id');
store.dispatch(reduxQueryUserProgress(userId)).then(data => {
const onOverviewPage = !currentLevelId;
if (!onOverviewPage) {
return;
}
// If the user is not signed in, retrieve level progress from session storage.
// (If the user is signed in, level progress would have been set by the call
// to reduxQueryUserProgress above.)
if (!data.signedIn) {
store.dispatch(
overwriteResults(clientState.levelProgress(scriptData.name))
);
}
const postMilestoneDisabled =
store.getState().progress.postMilestoneDisabled;
if (data.signedIn && postMilestoneDisabled) {
showDisabledBubblesModal();
}
});
}
/**
* Initializes our redux store with initial progress
* @param {object} store - Our redux store
* @param {object} scriptData
* @param {string} scriptData.name
* @param {boolean} scriptData.disablePostMilestone
* @param {boolean} [scriptData.plc]
* @param {object[]} [scriptData.lessons]
* @param {boolean} scriptData.age_13_required
* @param {string} currentLevelId The id of the level the user is currently on.
* This gets used in the url and as a key in many objects. Therefore, it is a
* string despite always being a numerical value
* @param {boolean} isFullProgress - True if this contains progress for the entire
* script vs. a single lesson.
* @param {boolean} [saveAnswersBeforeNavigation]
* @param {boolean} [isLessonExtras] Optional boolean indicating we are not on
* a script level and therefore are on lesson extras
* @param {number} [currentPageNumber] Optional. The page we are on if this is
* a multi-page level.
*/
function initializeStoreWithProgress(
store,
scriptData,
currentLevelId,
isFullProgress,
saveAnswersBeforeNavigation = false,
isLessonExtras = false,
currentPageNumber,
displayName
) {
store.dispatch(
initProgress({
currentLevelId: currentLevelId,
deeperLearningCourse: scriptData.plc,
saveAnswersBeforeNavigation: saveAnswersBeforeNavigation,
lessons: scriptData.lessons,
lessonGroups: scriptData.lessonGroups,
peerReviewLessonInfo: scriptData.peerReviewLessonInfo,
scriptId: scriptData.id,
scriptName: scriptData.name,
scriptDisplayName: scriptData.displayName,
unitTitle: scriptData.title,
unitDescription: scriptData.description,
unitStudentDescription: scriptData.studentDescription,
unitHasUnnumberedLessons: scriptData.hasUnnumberedLessons || false,
courseVersionId: scriptData.courseVersionId,
courseId: scriptData.course_id,
isFullProgress: isFullProgress,
isLessonExtras: isLessonExtras,
currentPageNumber: currentPageNumber,
courseName: scriptData.courseName,
})
);
if (scriptData.disablePostMilestone) {
store.dispatch(disablePostMilestone());
}
if (scriptData.hideable_lessons) {
// Note: This call is async
store.dispatch(getHiddenLessons(scriptData.name, true));
}
store.dispatch(setIsAge13Required(scriptData.age_13_required));
}