musora-content-services 2.90.0 → 2.92.6
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/CHANGELOG.md +40 -0
- package/package.json +11 -3
- package/src/index.d.ts +9 -31
- package/src/index.js +12 -34
- package/src/services/content-org/learning-paths.ts +33 -3
- package/src/services/contentAggregator.js +2 -2
- package/src/services/contentLikes.js +6 -39
- package/src/services/contentProgress.js +181 -479
- package/src/services/dataContext.js +0 -2
- package/src/services/progress-row/method-card.js +2 -1
- package/src/services/railcontent.js +12 -135
- package/src/services/sentry/.indexignore +0 -0
- package/src/services/sentry/index.ts +23 -0
- package/src/services/sync/.indexignore +0 -0
- package/src/services/sync/adapters/factory.ts +26 -0
- package/src/services/sync/adapters/lokijs.ts +1 -0
- package/src/services/sync/adapters/sqlite.ts +1 -0
- package/src/services/sync/concurrency-safety.ts +4 -0
- package/src/services/sync/context/index.ts +43 -0
- package/src/services/sync/context/providers/base.ts +4 -0
- package/src/services/sync/context/providers/connectivity.ts +14 -0
- package/src/services/sync/context/providers/durability.ts +5 -0
- package/src/services/sync/context/providers/index.ts +5 -0
- package/src/services/sync/context/providers/session.ts +8 -0
- package/src/services/sync/context/providers/tabs.ts +18 -0
- package/src/services/sync/context/providers/visibility.ts +14 -0
- package/src/services/sync/database/factory.ts +10 -0
- package/src/services/sync/errors/boundary.ts +45 -0
- package/src/services/sync/errors/index.ts +49 -0
- package/src/services/sync/fetch.ts +313 -0
- package/src/services/sync/index.ts +80 -0
- package/src/services/sync/manager.ts +139 -0
- package/src/services/sync/models/Base.ts +47 -0
- package/src/services/sync/models/ContentLike.ts +16 -0
- package/src/services/sync/models/ContentProgress.ts +69 -0
- package/src/services/sync/models/Practice.ts +72 -0
- package/src/services/sync/models/PracticeDayNote.ts +23 -0
- package/src/services/sync/models/index.ts +4 -0
- package/src/services/sync/repositories/base.ts +247 -0
- package/src/services/sync/repositories/content-likes.ts +26 -0
- package/src/services/sync/repositories/content-progress.ts +160 -0
- package/src/services/sync/repositories/index.ts +4 -0
- package/src/services/sync/repositories/practice-day-notes.ts +4 -0
- package/src/services/sync/repositories/practices.ts +52 -0
- package/src/services/sync/repository-proxy.ts +48 -0
- package/src/services/sync/resolver.ts +84 -0
- package/src/services/sync/retry.ts +88 -0
- package/src/services/sync/run-scope.ts +30 -0
- package/src/services/sync/schema/index.ts +66 -0
- package/src/services/sync/serializers/index.ts +2 -0
- package/src/services/sync/serializers/model.ts +32 -0
- package/src/services/sync/serializers/raw.ts +21 -0
- package/src/services/sync/store/index.ts +779 -0
- package/src/services/sync/store/push-coalescer.ts +57 -0
- package/src/services/sync/store-configs.ts +41 -0
- package/src/services/sync/strategies/base.ts +21 -0
- package/src/services/sync/strategies/index.ts +12 -0
- package/src/services/sync/strategies/initial.ts +11 -0
- package/src/services/sync/strategies/polling.ts +54 -0
- package/src/services/sync/telemetry/index.ts +140 -0
- package/src/services/sync/telemetry/sampling.ts +91 -0
- package/src/services/sync/utils/event-emitter.ts +24 -0
- package/src/services/sync/utils/index.ts +1 -0
- package/src/services/sync/utils/throttle.ts +93 -0
- package/src/services/sync/utils/timers.ts +9 -0
- package/src/services/userActivity.js +83 -148
- package/test/contentProgress.test.js +6 -39
- package/test/live/contentProgressLive.test.js +2 -31
- package/tools/generate-index.cjs +10 -4
- package/babel.config.cjs +0 -3
- package/docs/Content.html +0 -269
- package/docs/ContentOrganization.html +0 -245
- package/docs/Forums.html +0 -269
- package/docs/Gamification.html +0 -245
- package/docs/TestUser.html +0 -260
- package/docs/UserManagementSystem.html +0 -317
- package/docs/api_types.js.html +0 -97
- package/docs/config.js.html +0 -140
- package/docs/content-org_content-org.js.html +0 -76
- package/docs/content-org_guided-courses.ts.html +0 -110
- package/docs/content-org_learning-paths.ts.html +0 -379
- package/docs/content-org_playlists-types.js.html +0 -128
- package/docs/content-org_playlists.js.html +0 -440
- package/docs/content.js.html +0 -603
- package/docs/content_artist.ts.html +0 -206
- package/docs/content_content.ts.html +0 -77
- package/docs/content_genre.ts.html +0 -209
- package/docs/content_instructor.ts.html +0 -206
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -978
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -1049
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/forums_categories.ts.html +0 -156
- package/docs/forums_discussions.js.html +0 -95
- package/docs/forums_forum.js.html +0 -95
- package/docs/forums_forums.ts.html +0 -160
- package/docs/forums_posts.ts.html +0 -284
- package/docs/forums_threads.ts.html +0 -284
- package/docs/gamification_awards.js.html +0 -165
- package/docs/gamification_awards.ts.html +0 -195
- package/docs/gamification_gamification.js.html +0 -76
- package/docs/gamification_types.js.html +0 -80
- package/docs/global.html +0 -6019
- package/docs/index.html +0 -167
- package/docs/liveTesting.ts.html +0 -103
- package/docs/module-Accounts.html +0 -2283
- package/docs/module-Artist.html +0 -993
- package/docs/module-Awards.html +0 -836
- package/docs/module-Categories.html +0 -711
- package/docs/module-Config.html +0 -431
- package/docs/module-Content-Services-V2.html +0 -2998
- package/docs/module-ForumCategories.html +0 -687
- package/docs/module-ForumDiscussions.html +0 -370
- package/docs/module-Forums.html +0 -16599
- package/docs/module-Genre.html +0 -981
- package/docs/module-GuidedCourses.html +0 -108
- package/docs/module-Instructor.html +0 -929
- package/docs/module-Interests.html +0 -1066
- package/docs/module-LearningPaths.html +0 -2298
- package/docs/module-Onboarding.html +0 -882
- package/docs/module-Payments.html +0 -392
- package/docs/module-Permissions.html +0 -406
- package/docs/module-Playlists.html +0 -3030
- package/docs/module-ProgressRow.html +0 -108
- package/docs/module-Railcontent-Services.html +0 -6735
- package/docs/module-Sanity-Services.html +0 -8244
- package/docs/module-Sessions.html +0 -575
- package/docs/module-Threads.html +0 -1119
- package/docs/module-UserActivity.html +0 -4580
- package/docs/module-UserChat.html +0 -410
- package/docs/module-UserManagement.html +0 -1932
- package/docs/module-UserMemberships.html +0 -829
- package/docs/module-UserNotifications.html +0 -2595
- package/docs/module-UserProfile.html +0 -370
- package/docs/progress-row_method-card.js.html +0 -183
- package/docs/railcontent.js.html +0 -847
- package/docs/sanity.js.html +0 -2322
- package/docs/scripts/collapse.js +0 -39
- package/docs/scripts/commonNav.js +0 -28
- package/docs/scripts/linenumber.js +0 -25
- package/docs/scripts/nav.js +0 -12
- package/docs/scripts/polyfill.js +0 -4
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
- package/docs/scripts/prettify/lang-css.js +0 -2
- package/docs/scripts/prettify/prettify.js +0 -28
- package/docs/scripts/search.js +0 -99
- package/docs/styles/jsdoc.css +0 -776
- package/docs/styles/prettify.css +0 -80
- package/docs/userActivity.js.html +0 -1577
- package/docs/user_account.ts.html +0 -265
- package/docs/user_chat.js.html +0 -98
- package/docs/user_interests.js.html +0 -150
- package/docs/user_management.js.html +0 -258
- package/docs/user_memberships.js.html +0 -144
- package/docs/user_memberships.ts.html +0 -292
- package/docs/user_notifications.js.html +0 -374
- package/docs/user_onboarding.ts.html +0 -325
- package/docs/user_payments.ts.html +0 -146
- package/docs/user_permissions.js.html +0 -110
- package/docs/user_profile.js.html +0 -115
- package/docs/user_sessions.js.html +0 -170
- package/docs/user_types.js.html +0 -224
- package/docs/user_user-management-system.js.html +0 -79
|
@@ -1,54 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
postContentStart,
|
|
6
|
-
postRecordWatchSession,
|
|
7
|
-
} from './railcontent.js'
|
|
8
|
-
import { DataContext, ContentProgressVersionKey } from './dataContext.js'
|
|
9
|
-
import {
|
|
10
|
-
fetchHierarchy,
|
|
11
|
-
fetchMethodV2StructureFromId
|
|
12
|
-
} from './sanity.js'
|
|
13
|
-
import { recordUserPractice, findIncompleteLesson } from './userActivity'
|
|
1
|
+
import { fetchHierarchy } from './sanity.js'
|
|
2
|
+
import { db } from './sync'
|
|
3
|
+
import { STATE } from './sync/models/ContentProgress'
|
|
4
|
+
import { trackUserPractice, findIncompleteLesson } from './userActivity'
|
|
14
5
|
import { getNextLessonLessonParentTypes } from '../contentTypeConfig.js'
|
|
15
6
|
|
|
16
|
-
const STATE_STARTED =
|
|
17
|
-
const STATE_COMPLETED =
|
|
18
|
-
const
|
|
19
|
-
const DATA_KEY_PROGRESS = 'p'
|
|
20
|
-
const DATA_KEY_RESUME_TIME = 't'
|
|
21
|
-
const DATA_KEY_LAST_UPDATED_TIME = 'u'
|
|
22
|
-
const DATA_KEY_BRAND = 'b'
|
|
23
|
-
const DATA_KEY_COLLECTION = 'c'
|
|
24
|
-
const DATA_CONTENT_ID = 'i'
|
|
7
|
+
const STATE_STARTED = STATE.STARTED
|
|
8
|
+
const STATE_COMPLETED = STATE.COMPLETED
|
|
9
|
+
const MAX_DEPTH = 3
|
|
25
10
|
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
let sessionData = []
|
|
29
|
-
|
|
30
|
-
export async function getProgressPercentage(contentId, collection = null) {
|
|
31
|
-
return getById(contentId, collection, DATA_KEY_PROGRESS, 0)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function getProgressPercentageByIds(contentIds, collection = null) {
|
|
35
|
-
return getByIds(contentIds, collection, DATA_KEY_PROGRESS, 0)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function getProgressState(contentId, collection = null) {
|
|
39
|
-
return getById(contentId, collection, DATA_KEY_STATUS, '')
|
|
11
|
+
export async function getProgressState(contentId) {
|
|
12
|
+
return getById(contentId, 'state', '')
|
|
40
13
|
}
|
|
41
14
|
|
|
42
15
|
export async function getProgressStateByIds(contentIds, collection = null) {
|
|
43
|
-
return getByIds(contentIds, collection,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function getResumeTimeSeconds(contentId, collection = null) {
|
|
47
|
-
return getById(contentId, collection, DATA_KEY_RESUME_TIME, 0)
|
|
16
|
+
return getByIds(contentIds, collection, 'state', '')
|
|
48
17
|
}
|
|
49
18
|
|
|
50
19
|
export async function getResumeTimeSecondsByIds(contentIds, collection = null) {
|
|
51
|
-
return getByIds(contentIds, collection,
|
|
20
|
+
return getByIds(contentIds, collection, 'resume_time_seconds', 0)
|
|
52
21
|
}
|
|
53
22
|
|
|
54
23
|
export async function getNavigateTo(data, collection = null) {
|
|
@@ -82,7 +51,7 @@ export async function getNavigateTo(data, collection = null) {
|
|
|
82
51
|
const contentState = await getProgressState(content.id, collection)
|
|
83
52
|
if (contentState !== STATE_STARTED) {
|
|
84
53
|
const firstChild = validChildren[0]
|
|
85
|
-
let lastInteractedChildNavToData = await getNavigateTo([firstChild])
|
|
54
|
+
let lastInteractedChildNavToData = await getNavigateTo([firstChild], collection)
|
|
86
55
|
lastInteractedChildNavToData = lastInteractedChildNavToData[firstChild.id] ?? null
|
|
87
56
|
navigateToData[content.id] = buildNavigateTo(firstChild, lastInteractedChildNavToData, collection) //no G-child for LP
|
|
88
57
|
} else {
|
|
@@ -109,7 +78,7 @@ export async function getNavigateTo(data, collection = null) {
|
|
|
109
78
|
if (childrenStates[lastInteractedChildId] === STATE_COMPLETED) {
|
|
110
79
|
// TODO: packs have an extra situation where we need to jump to the next course if all lessons in the last engaged course are completed
|
|
111
80
|
}
|
|
112
|
-
let lastInteractedChildNavToData = await getNavigateTo(firstChildren)
|
|
81
|
+
let lastInteractedChildNavToData = await getNavigateTo(firstChildren, collection)
|
|
113
82
|
lastInteractedChildNavToData = lastInteractedChildNavToData[lastInteractedChildId]
|
|
114
83
|
navigateToData[content.id] = buildNavigateTo(
|
|
115
84
|
children.get(lastInteractedChildId),
|
|
@@ -147,154 +116,89 @@ function buildNavigateTo(content, child = null, collection = null) {
|
|
|
147
116
|
* @returns {Promise<number>}
|
|
148
117
|
*/
|
|
149
118
|
export async function getLastInteractedOf(contentIds, collection = null) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
119
|
+
return db.contentProgress.mostRecentlyUpdatedId(contentIds, collection).then(r => r.data ? parseInt(r.data) : undefined)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function getProgressDataByIds(contentIds, collection) {
|
|
123
|
+
const progress = Object.fromEntries(contentIds.map(id => [id, {
|
|
124
|
+
last_update: 0,
|
|
125
|
+
progress: 0,
|
|
126
|
+
status: '',
|
|
127
|
+
}]))
|
|
128
|
+
|
|
129
|
+
await db.contentProgress.getSomeProgressByContentIds(contentIds, collection).then(r => {
|
|
130
|
+
r.data.forEach(p => {
|
|
131
|
+
progress[p.content_id] = {
|
|
132
|
+
last_update: p.updated_at,
|
|
133
|
+
progress: p.progress_percent,
|
|
134
|
+
status: p.state,
|
|
135
|
+
}
|
|
161
136
|
})
|
|
162
|
-
|
|
163
|
-
return sorted[0]
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export async function getProgressDateByIds(contentIds, collection = null) {
|
|
167
|
-
let data = await dataContext.getData()
|
|
168
|
-
let progress = {}
|
|
169
|
-
contentIds?.forEach((id) => {
|
|
170
|
-
const key = generateRecordKey(id, collection)
|
|
171
|
-
progress[id] = {
|
|
172
|
-
last_update: data[key]?.[DATA_KEY_LAST_UPDATED_TIME] ?? 0,
|
|
173
|
-
progress: data[key]?.[DATA_KEY_PROGRESS] ?? 0,
|
|
174
|
-
status: data[key]?.[DATA_KEY_STATUS] ?? '',
|
|
175
|
-
}
|
|
176
137
|
})
|
|
138
|
+
|
|
177
139
|
return progress
|
|
178
140
|
}
|
|
179
141
|
|
|
180
|
-
async function getById(contentId,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return data[contentKey]?.[dataKey] ?? defaultValue
|
|
142
|
+
async function getById(contentId, dataKey, defaultValue) {
|
|
143
|
+
if (!contentId) return defaultValue
|
|
144
|
+
return db.contentProgress.getOneProgressByContentId(contentId).then(r => r.data?.[dataKey] ?? defaultValue)
|
|
184
145
|
}
|
|
185
146
|
|
|
186
147
|
async function getByIds(contentIds, collection, dataKey, defaultValue) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
148
|
+
const progress = Object.fromEntries(contentIds.map(id => [id, defaultValue]))
|
|
149
|
+
await db.contentProgress.getSomeProgressByContentIds(contentIds, collection).then(r => {
|
|
150
|
+
r.data.forEach(p => {
|
|
151
|
+
progress[p.content_id] = p[dataKey] ?? defaultValue
|
|
152
|
+
})
|
|
153
|
+
})
|
|
190
154
|
return progress
|
|
191
155
|
}
|
|
192
156
|
|
|
193
|
-
export async function getAllStarted(limit = null
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
let ids = Object.keys(data)
|
|
197
|
-
.filter(function (id) {
|
|
198
|
-
const key = generateRecordKey(id, collection)
|
|
199
|
-
return data[key][DATA_KEY_STATUS] === STATE_STARTED
|
|
200
|
-
})
|
|
201
|
-
.map(function (id) {
|
|
202
|
-
return parseInt(id)
|
|
203
|
-
})
|
|
204
|
-
.sort(function (a, b) {
|
|
205
|
-
let v1 = data[a][DATA_KEY_LAST_UPDATED_TIME]
|
|
206
|
-
let v2 = data[b][DATA_KEY_LAST_UPDATED_TIME]
|
|
207
|
-
if (v1 > v2) return -1
|
|
208
|
-
else if (v1 < v2) return 1
|
|
209
|
-
return 0
|
|
210
|
-
})
|
|
211
|
-
if (limit) {
|
|
212
|
-
ids = ids.slice(0, limit)
|
|
213
|
-
}
|
|
214
|
-
return ids
|
|
157
|
+
export async function getAllStarted(limit = null) {
|
|
158
|
+
return db.contentProgress.startedIds(limit).then(r => r.data.map(id => parseInt(id)))
|
|
215
159
|
}
|
|
216
160
|
|
|
217
|
-
export async function getAllCompleted(limit = null
|
|
218
|
-
|
|
161
|
+
export async function getAllCompleted(limit = null) {
|
|
162
|
+
return db.contentProgress.completedIds(limit).then(r => r.data.map(id => parseInt(id)))
|
|
163
|
+
}
|
|
219
164
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
else if (v1 < v2) return 1
|
|
233
|
-
return 0
|
|
234
|
-
})
|
|
235
|
-
if (limit) {
|
|
236
|
-
ids = ids.slice(0, limit)
|
|
237
|
-
}
|
|
238
|
-
return ids
|
|
165
|
+
/**
|
|
166
|
+
*
|
|
167
|
+
* @param {array} contentIds List of content children within learning path
|
|
168
|
+
* @param {object} collection Learning path object
|
|
169
|
+
* @returns {Promise<array>} Filtered list of contentIds that are completed
|
|
170
|
+
*/
|
|
171
|
+
export async function getAllCompletedByIds(contentIds, collection) {
|
|
172
|
+
// TODO - implement collection filtering
|
|
173
|
+
return db.contentProgress.queryAllIds(
|
|
174
|
+
Q.whereIn('content_id', contentIds),
|
|
175
|
+
Q.where('state', STATE_COMPLETED)
|
|
176
|
+
)
|
|
239
177
|
}
|
|
240
178
|
|
|
241
179
|
export async function getAllStartedOrCompleted({
|
|
242
|
-
limit = null,
|
|
243
180
|
onlyIds = true,
|
|
244
181
|
brand = null,
|
|
245
|
-
|
|
246
|
-
collection = null,
|
|
182
|
+
limit = null
|
|
247
183
|
} = {}) {
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
.filter(([key, item]) => {
|
|
254
|
-
const isRelevantStatus =
|
|
255
|
-
item[DATA_KEY_STATUS] === STATE_STARTED || item[DATA_KEY_STATUS] === STATE_COMPLETED
|
|
256
|
-
const isRecent = item[DATA_KEY_LAST_UPDATED_TIME] >= oneMonthAgoInSeconds
|
|
257
|
-
const isCorrectBrand = !brand || !item.b || item.b === brand
|
|
258
|
-
const isNotExcluded = !excludedSet.has(extractContentIdFromRecordKey(key))
|
|
259
|
-
const matchesCollection =
|
|
260
|
-
(!collection && !item[DATA_KEY_COLLECTION]) ||
|
|
261
|
-
(item[DATA_KEY_COLLECTION]?.type === collection?.type &&
|
|
262
|
-
item[DATA_KEY_COLLECTION]?.id === collection?.id)
|
|
263
|
-
return matchesCollection && isRelevantStatus && isCorrectBrand && isNotExcluded
|
|
264
|
-
})
|
|
265
|
-
.sort(([, a], [, b]) => {
|
|
266
|
-
const v1 = a[DATA_KEY_LAST_UPDATED_TIME]
|
|
267
|
-
const v2 = b[DATA_KEY_LAST_UPDATED_TIME]
|
|
268
|
-
if (v1 > v2) return -1
|
|
269
|
-
else if (v1 < v2) return 1
|
|
270
|
-
return 0
|
|
271
|
-
})
|
|
272
|
-
//maps to content_id
|
|
273
|
-
.reduce((acc, [key, item]) => {
|
|
274
|
-
const newKey = extractContentIdFromRecordKey(key)
|
|
275
|
-
acc[newKey] = item
|
|
276
|
-
return acc
|
|
277
|
-
}, {})
|
|
278
|
-
|
|
279
|
-
if (limit) {
|
|
280
|
-
filtered = Object.fromEntries(Object.entries(filtered).slice(0, limit))
|
|
184
|
+
const agoInSeconds = Math.floor(Date.now() / 1000) - 60 * 24 * 60 * 60 // 60 days in seconds
|
|
185
|
+
const filters = {
|
|
186
|
+
brand: brand ?? undefined,
|
|
187
|
+
updatedAfter: agoInSeconds,
|
|
188
|
+
limit: limit ?? undefined,
|
|
281
189
|
}
|
|
282
190
|
|
|
283
191
|
if (onlyIds) {
|
|
284
|
-
return
|
|
192
|
+
return db.contentProgress.startedOrCompletedIds(filters).then(r => r.data.map(id => parseInt(id)))
|
|
285
193
|
} else {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
collection: item?.[DATA_KEY_COLLECTION],
|
|
294
|
-
brand: item?.b ?? '',
|
|
295
|
-
}
|
|
194
|
+
return db.contentProgress.startedOrCompleted(filters).then(r => {
|
|
195
|
+
return Object.fromEntries(r.data.map(p => [p.content_id, {
|
|
196
|
+
last_update: p.updated_at,
|
|
197
|
+
progress: p.progress_percent,
|
|
198
|
+
status: p.state,
|
|
199
|
+
brand: p.content_brand,
|
|
200
|
+
}]))
|
|
296
201
|
})
|
|
297
|
-
return progress
|
|
298
202
|
}
|
|
299
203
|
}
|
|
300
204
|
|
|
@@ -315,371 +219,169 @@ export async function getAllStartedOrCompleted({
|
|
|
315
219
|
* const progressMap = await getStartedOrCompletedProgressOnly({ brand: 'drumeo' });
|
|
316
220
|
* console.log(progressMap[123]); // => 52
|
|
317
221
|
*/
|
|
318
|
-
export async function getStartedOrCompletedProgressOnly({
|
|
319
|
-
brand
|
|
320
|
-
|
|
321
|
-
} = {}) {
|
|
322
|
-
const data = await dataContext.getData()
|
|
323
|
-
const result = {}
|
|
324
|
-
|
|
325
|
-
Object.entries(data).forEach(([key, item]) => {
|
|
326
|
-
const id = extractContentIdFromRecordKey(key)
|
|
327
|
-
const isRelevantStatus =
|
|
328
|
-
item[DATA_KEY_STATUS] === STATE_STARTED || item[DATA_KEY_STATUS] === STATE_COMPLETED
|
|
329
|
-
const isCorrectBrand = !brand || item.b === brand
|
|
330
|
-
const matchesCollection =
|
|
331
|
-
(!collection && !item[DATA_KEY_COLLECTION]) ||
|
|
332
|
-
(item[DATA_KEY_COLLECTION]?.type === collection?.type &&
|
|
333
|
-
item[DATA_KEY_COLLECTION]?.id === collection?.id)
|
|
334
|
-
|
|
335
|
-
if (matchesCollection && isRelevantStatus && isCorrectBrand) {
|
|
336
|
-
result[id] = item?.[DATA_KEY_PROGRESS] ?? 0
|
|
337
|
-
}
|
|
222
|
+
export async function getStartedOrCompletedProgressOnly({ brand = undefined } = {}) {
|
|
223
|
+
return db.contentProgress.startedOrCompleted({ brand: brand }).then(r => {
|
|
224
|
+
return Object.fromEntries(r.data.map(p => [p.content_id, p.progress_percent]))
|
|
338
225
|
})
|
|
339
|
-
|
|
340
|
-
return result
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
export async function contentStatusCompleted(contentId, collection = null) {
|
|
344
|
-
return await dataContext.update(
|
|
345
|
-
async function (localContext) {
|
|
346
|
-
let hierarchy = await getContentHierarchy(contentId, collection)
|
|
347
|
-
completeStatusInLocalContext(localContext, contentId, hierarchy, collection)
|
|
348
|
-
},
|
|
349
|
-
async function () {
|
|
350
|
-
return postContentComplete(contentId, collection)
|
|
351
|
-
}
|
|
352
|
-
)
|
|
353
|
-
}
|
|
354
|
-
export async function contentStatusStarted(contentId, collection = null) {
|
|
355
|
-
return await dataContext.update(
|
|
356
|
-
async function (localContext) {
|
|
357
|
-
let hierarchy = await getContentHierarchy(contentId, collection)
|
|
358
|
-
startStatusInLocalContext(localContext, contentId, hierarchy, collection)
|
|
359
|
-
},
|
|
360
|
-
async function () {
|
|
361
|
-
return postContentStart(contentId, collection)
|
|
362
|
-
}
|
|
363
|
-
)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function saveContentProgress(localContext, contentId, progress, currentSeconds, hierarchy, collection = null) {
|
|
367
|
-
if (progress === 100) {
|
|
368
|
-
completeStatusInLocalContext(localContext, contentId, hierarchy, collection)
|
|
369
|
-
return
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const key = generateRecordKey(contentId, collection)
|
|
373
|
-
let data = localContext.data[key] ?? {}
|
|
374
|
-
const currentProgress = data[DATA_KEY_STATUS]
|
|
375
|
-
if (!currentProgress || currentProgress !== STATE_COMPLETED) {
|
|
376
|
-
data[DATA_KEY_PROGRESS] = progress
|
|
377
|
-
data[DATA_KEY_STATUS] = STATE_STARTED
|
|
378
|
-
}
|
|
379
|
-
data[DATA_KEY_RESUME_TIME] = currentSeconds
|
|
380
|
-
data[DATA_KEY_LAST_UPDATED_TIME] = Math.round(new Date().getTime() / 1000)
|
|
381
|
-
localContext.data[key] = data
|
|
382
|
-
|
|
383
|
-
bubbleProgress(hierarchy, contentId, localContext)
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function completeStatusInLocalContext(localContext, contentId, hierarchy, collection = null) {
|
|
387
|
-
setStartedOrCompletedStatusInLocalContext(localContext, contentId, true, hierarchy, collection)
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function startStatusInLocalContext(localContext, contentId, hierarchy, collection = null) {
|
|
391
|
-
setStartedOrCompletedStatusInLocalContext(localContext, contentId, false, hierarchy, collection)
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function setStartedOrCompletedStatusInLocalContext(
|
|
395
|
-
localContext,
|
|
396
|
-
contentId,
|
|
397
|
-
isCompleted,
|
|
398
|
-
hierarchy,
|
|
399
|
-
collection = null
|
|
400
|
-
) {
|
|
401
|
-
const key = generateRecordKey(contentId, collection)
|
|
402
|
-
let data = localContext.data[key] ?? {}
|
|
403
|
-
data[DATA_KEY_PROGRESS] = isCompleted ? 100 : 0
|
|
404
|
-
data[DATA_KEY_STATUS] = isCompleted ? STATE_COMPLETED : STATE_STARTED
|
|
405
|
-
data[DATA_KEY_LAST_UPDATED_TIME] = Math.round(new Date().getTime() / 1000)
|
|
406
|
-
data[DATA_KEY_COLLECTION] = collection
|
|
407
|
-
data[DATA_CONTENT_ID] = contentId
|
|
408
|
-
|
|
409
|
-
localContext.data[key] = data
|
|
410
|
-
|
|
411
|
-
if (!hierarchy) return
|
|
412
|
-
|
|
413
|
-
if (collection && collection.type === 'learning-path-v2') {
|
|
414
|
-
bubbleOrTrickleLearningPathProgress(hierarchy, contentId, localContext, isCompleted, collection)
|
|
415
|
-
return
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
let children = hierarchy.children[contentId] ?? []
|
|
419
|
-
for (let i = 0; i < children.length; i++) {
|
|
420
|
-
let childId = children[i]
|
|
421
|
-
setStartedOrCompletedStatusInLocalContext(localContext, childId, isCompleted, hierarchy)
|
|
422
|
-
}
|
|
423
|
-
bubbleProgress(hierarchy, contentId, localContext)
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function getChildrenToDepth(parentId, hierarchy, depth = 1) {
|
|
427
|
-
let childIds = hierarchy.children[parentId] ?? []
|
|
428
|
-
let allChildrenIds = childIds
|
|
429
|
-
childIds.forEach((id) => {
|
|
430
|
-
allChildrenIds = allChildrenIds.concat(getChildrenToDepth(id, hierarchy, depth - 1))
|
|
431
|
-
})
|
|
432
|
-
return allChildrenIds
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
export async function contentStatusReset(contentId, collection = null) {
|
|
436
|
-
await dataContext.update(
|
|
437
|
-
async function (localContext) {
|
|
438
|
-
let hierarchy = await getContentHierarchy(contentId, collection)
|
|
439
|
-
resetStatusInLocalContext(localContext, contentId, hierarchy, collection)
|
|
440
|
-
},
|
|
441
|
-
async function () {
|
|
442
|
-
return postContentReset(contentId, collection)
|
|
443
|
-
}
|
|
444
|
-
)
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function resetStatusInLocalContext(localContext, contentId, hierarchy, collection = null) {
|
|
448
|
-
let keys = []
|
|
449
|
-
|
|
450
|
-
console.log('all', [localContext, contentId, hierarchy, collection])
|
|
451
|
-
keys.push(generateRecordKey(contentId, collection))
|
|
452
|
-
|
|
453
|
-
let allChildIds
|
|
454
|
-
let learningPathId = null
|
|
455
|
-
let childrenIds = []
|
|
456
|
-
if (collection && collection.type === 'learning-path-v2') {
|
|
457
|
-
[learningPathId, childrenIds] = findLearningPathAndChildren(hierarchy, contentId)
|
|
458
|
-
allChildIds = (learningPathId === contentId) ? childrenIds : []
|
|
459
|
-
} else {
|
|
460
|
-
allChildIds = getChildrenToDepth(contentId, hierarchy, 5)
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
allChildIds.forEach((id) => {
|
|
464
|
-
keys.push(generateRecordKey(id, collection))
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
keys.forEach((key) => {
|
|
468
|
-
const index = Object.keys(localContext.data).indexOf(key.toString())
|
|
469
|
-
if (index > -1) {
|
|
470
|
-
// only splice array when item is found
|
|
471
|
-
delete localContext.data[key]
|
|
472
|
-
}
|
|
473
|
-
})
|
|
474
|
-
if (collection && collection.type === 'learning-path-v2') { // manual bubbling for LP
|
|
475
|
-
if (learningPathId !== contentId) {
|
|
476
|
-
bubbleLearningPathProgress(hierarchy, contentId, localContext, collection)
|
|
477
|
-
}
|
|
478
|
-
} else {
|
|
479
|
-
bubbleProgress(hierarchy, contentId, localContext)
|
|
480
|
-
}
|
|
481
226
|
}
|
|
482
227
|
|
|
483
228
|
/**
|
|
484
229
|
* Record watch session
|
|
485
230
|
* @return {string} sessionId - provide in future calls to update progress
|
|
486
231
|
* @param {int} contentId
|
|
487
|
-
* @param {string} mediaType - options are video, assignment, practice
|
|
488
|
-
* @param {string} mediaCategory - options are youtube, vimeo, soundslice, play-alongs
|
|
489
232
|
* @param {int} mediaLengthSeconds
|
|
490
233
|
* @param {int} currentSeconds
|
|
491
234
|
* @param {int} secondsPlayed
|
|
492
235
|
* @param {string} sessionId - This function records a sessionId to pass into future updates to progress on the same video
|
|
493
236
|
* @param {int} instrumentId - enum value of instrument id
|
|
494
237
|
* @param {int} categoryId - enum value of category id
|
|
495
|
-
* @param {object|null} collection - optional collection info { type: 'learning-path-v2', id: 123 }
|
|
496
238
|
*/
|
|
497
|
-
// NOTE: have not set up collection because its not super important for testing and this will change soon with watermelon
|
|
498
239
|
export async function recordWatchSession(
|
|
499
240
|
contentId,
|
|
500
|
-
|
|
501
|
-
mediaCategory,
|
|
241
|
+
collection = null,
|
|
502
242
|
mediaLengthSeconds,
|
|
503
243
|
currentSeconds,
|
|
504
244
|
secondsPlayed,
|
|
505
|
-
|
|
245
|
+
prevSession = null,
|
|
506
246
|
instrumentId = null,
|
|
507
|
-
categoryId = null
|
|
508
|
-
collection = null
|
|
247
|
+
categoryId = null
|
|
509
248
|
) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
249
|
+
const [session] = await Promise.all([
|
|
250
|
+
trackPractice(contentId, secondsPlayed, prevSession, { instrumentId, categoryId }),
|
|
251
|
+
trackProgress(contentId, collection, currentSeconds, mediaLengthSeconds),
|
|
252
|
+
])
|
|
514
253
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (!sessionId) {
|
|
518
|
-
sessionId = uuidv4()
|
|
519
|
-
}
|
|
254
|
+
return session
|
|
255
|
+
}
|
|
520
256
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
sessionData[sessionId] = sessionData[sessionId] || {}
|
|
524
|
-
const secondsSinceLastUpdate = Math.ceil(
|
|
525
|
-
secondsPlayed - (sessionData[sessionId][contentId] ?? 0)
|
|
526
|
-
)
|
|
527
|
-
await recordUserPractice({
|
|
528
|
-
content_id: contentId,
|
|
529
|
-
duration_seconds: secondsSinceLastUpdate,
|
|
530
|
-
instrument_id: instrumentId,
|
|
531
|
-
})
|
|
532
|
-
} catch (error) {
|
|
533
|
-
console.error('Failed to record user practice:', error)
|
|
534
|
-
}
|
|
535
|
-
sessionData[sessionId][contentId] = secondsPlayed
|
|
257
|
+
async function trackPractice(contentId, secondsPlayed, prevSession, details = {}) {
|
|
258
|
+
const session = prevSession || new Map()
|
|
536
259
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (contentId && updateLocalProgress) {
|
|
540
|
-
if (mediaLengthSeconds <= 0) {
|
|
541
|
-
return
|
|
542
|
-
}
|
|
543
|
-
let progress = Math.min(
|
|
544
|
-
99,
|
|
545
|
-
Math.round(((currentSeconds ?? 0) / Math.max(1, mediaLengthSeconds ?? 0)) * 100)
|
|
546
|
-
)
|
|
547
|
-
let hierarchy = await fetchHierarchy(contentId)
|
|
548
|
-
saveContentProgress(localContext, contentId, progress, currentSeconds, hierarchy)
|
|
549
|
-
}
|
|
550
|
-
},
|
|
551
|
-
async function () {
|
|
552
|
-
return postRecordWatchSession(
|
|
553
|
-
contentId,
|
|
554
|
-
mediaTypeId,
|
|
555
|
-
mediaLengthSeconds,
|
|
556
|
-
currentSeconds,
|
|
557
|
-
secondsPlayed,
|
|
558
|
-
sessionId
|
|
559
|
-
)
|
|
560
|
-
}
|
|
260
|
+
const secondsSinceLastUpdate = Math.ceil(
|
|
261
|
+
secondsPlayed - (session.get(contentId) ?? 0)
|
|
561
262
|
)
|
|
562
|
-
|
|
563
|
-
}
|
|
263
|
+
session.set(contentId, secondsPlayed)
|
|
564
264
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
return 1
|
|
569
|
-
case 'video_vimeo':
|
|
570
|
-
return 2
|
|
571
|
-
case 'assignment_soundslice':
|
|
572
|
-
return 3
|
|
573
|
-
case 'practice_play-alongs':
|
|
574
|
-
return 4
|
|
575
|
-
case 'video_soundslice':
|
|
576
|
-
return 3
|
|
577
|
-
default:
|
|
578
|
-
return 5
|
|
579
|
-
}
|
|
265
|
+
await trackUserPractice(contentId, secondsSinceLastUpdate, details)
|
|
266
|
+
|
|
267
|
+
return session
|
|
580
268
|
}
|
|
581
269
|
|
|
582
|
-
function
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
270
|
+
async function trackProgress(contentId, collection, currentSeconds, mediaLengthSeconds) {
|
|
271
|
+
const progress = Math.min(
|
|
272
|
+
99,
|
|
273
|
+
Math.round(((currentSeconds ?? 0) / Math.max(1, mediaLengthSeconds)) * 100)
|
|
274
|
+
)
|
|
275
|
+
return saveContentProgress(contentId, collection, progress, currentSeconds)
|
|
588
276
|
}
|
|
589
277
|
|
|
590
|
-
function
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const brand = localContext.data[contentId]?.[DATA_KEY_BRAND] ?? null
|
|
599
|
-
data[DATA_KEY_PROGRESS] = progress
|
|
600
|
-
data[DATA_KEY_STATUS] = progress === 100 ? STATE_COMPLETED : STATE_STARTED
|
|
601
|
-
data[DATA_KEY_LAST_UPDATED_TIME] = Math.round(new Date().getTime() / 1000)
|
|
602
|
-
data[DATA_KEY_BRAND] = brand
|
|
603
|
-
localContext.data[parentId] = data
|
|
604
|
-
bubbleProgress(hierarchy, parentId, localContext)
|
|
278
|
+
export async function contentStatusCompleted(contentId, collection = null) {
|
|
279
|
+
return setStartedOrCompletedStatus(contentId, collection, true)
|
|
280
|
+
}
|
|
281
|
+
export async function contentStatusStarted(contentId, collection = null) {
|
|
282
|
+
return setStartedOrCompletedStatus(contentId, collection, false)
|
|
283
|
+
}
|
|
284
|
+
export async function contentStatusReset(contentId, collection = null) {
|
|
285
|
+
return resetStatus(contentId, collection)
|
|
605
286
|
}
|
|
606
287
|
|
|
607
|
-
function
|
|
608
|
-
const
|
|
288
|
+
async function saveContentProgress(contentId, collection, progress, currentSeconds) {
|
|
289
|
+
const response = await db.contentProgress.recordProgressRemotely(contentId, collection, progress, currentSeconds)
|
|
609
290
|
|
|
610
|
-
|
|
291
|
+
// note - previous implementation explicitly did not trickle progress to children here
|
|
292
|
+
// (only to siblings/parents via le bubbles)
|
|
611
293
|
|
|
612
|
-
const
|
|
613
|
-
|
|
294
|
+
const bubbledProgresses = bubbleProgress(await fetchHierarchy(contentId), contentId, collection)
|
|
295
|
+
await db.contentProgress.recordProgressesTentative(bubbledProgresses, collection)
|
|
614
296
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
return localContext.data[childKey]?.[DATA_KEY_PROGRESS] ?? 0
|
|
618
|
-
})
|
|
619
|
-
let progress = Math.round(childProgress.reduce((a, b) => a + b, 0) / childProgress.length)
|
|
297
|
+
return response
|
|
298
|
+
}
|
|
620
299
|
|
|
621
|
-
|
|
622
|
-
const
|
|
300
|
+
async function setStartedOrCompletedStatus(contentId, collection, isCompleted) {
|
|
301
|
+
const progress = isCompleted ? 100 : 0
|
|
302
|
+
// we explicitly pessimistically await a remote push here
|
|
303
|
+
// because awards may be generated (on server) on completion
|
|
304
|
+
// which we would want to toast the user about *in band*
|
|
305
|
+
const response = await db.contentProgress.recordProgressRemotely(contentId, collection, progress)
|
|
623
306
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
data[DATA_KEY_LAST_UPDATED_TIME] = Math.round(new Date().getTime() / 1000)
|
|
627
|
-
data[DATA_KEY_BRAND] = brand
|
|
628
|
-
data[DATA_KEY_COLLECTION] = collection
|
|
629
|
-
data[DATA_CONTENT_ID] = parentId
|
|
307
|
+
if (response.pushStatus === 'success') {
|
|
308
|
+
const hierarchy = await fetchHierarchy(contentId)
|
|
630
309
|
|
|
631
|
-
|
|
310
|
+
await Promise.all([
|
|
311
|
+
db.contentProgress.recordProgressesTentative(trickleProgress(hierarchy, contentId, collection, progress), collection),
|
|
312
|
+
bubbleProgress(hierarchy, contentId, collection).then(bubbledProgresses => db.contentProgress.recordProgressesTentative(bubbledProgresses, collection))
|
|
313
|
+
])
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return response
|
|
632
317
|
}
|
|
633
318
|
|
|
634
|
-
function
|
|
635
|
-
|
|
319
|
+
async function resetStatus(contentId, collection = null) {
|
|
320
|
+
const response = await db.contentProgress.eraseProgress(contentId, collection)
|
|
321
|
+
const hierarchy = await fetchHierarchy(contentId)
|
|
322
|
+
|
|
323
|
+
await Promise.all([
|
|
324
|
+
db.contentProgress.recordProgressesTentative(trickleProgress(hierarchy, contentId, collection, 0), collection),
|
|
325
|
+
bubbleProgress(hierarchy, contentId, collection).then(bubbledProgresses => db.contentProgress.recordProgressesTentative(bubbledProgresses, collection))
|
|
326
|
+
])
|
|
327
|
+
|
|
328
|
+
return response
|
|
636
329
|
}
|
|
637
330
|
|
|
638
|
-
|
|
639
|
-
|
|
331
|
+
// agnostic to collection - makes returned data structure simpler,
|
|
332
|
+
// as long as callers remember to pass collection where needed
|
|
333
|
+
function trickleProgress(hierarchy, contentId, _collection, progress) {
|
|
334
|
+
const descendantIds = getChildrenToDepth(contentId, hierarchy, MAX_DEPTH)
|
|
335
|
+
return Object.fromEntries(descendantIds.map(id => [id, progress]))
|
|
640
336
|
}
|
|
641
337
|
|
|
642
|
-
async function
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
return await fetchHierarchy(contentId)
|
|
338
|
+
async function bubbleProgress(hierarchy, contentId, collection = null) {
|
|
339
|
+
const ids = getAncestorAndSiblingIds(hierarchy, contentId)
|
|
340
|
+
const progresses = await getByIds(ids, collection, 'progress_percent', 0)
|
|
341
|
+
return averageProgressesFor(hierarchy, contentId, progresses)
|
|
647
342
|
}
|
|
648
343
|
|
|
649
|
-
function
|
|
650
|
-
|
|
651
|
-
let children = []
|
|
344
|
+
function getAncestorAndSiblingIds(hierarchy, contentId, depth = 1) {
|
|
345
|
+
if (depth > MAX_DEPTH) return []
|
|
652
346
|
|
|
653
|
-
|
|
347
|
+
const parentId = hierarchy?.parents?.[contentId]
|
|
348
|
+
if (!parentId) return []
|
|
654
349
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
children = lp.children ?? []
|
|
659
|
-
break
|
|
660
|
-
}
|
|
661
|
-
if (Array.isArray(lp.children) && lp.children.includes(contentId)) {
|
|
662
|
-
learningPathId = lp.id
|
|
663
|
-
children = lp.children ?? []
|
|
664
|
-
break
|
|
665
|
-
}
|
|
350
|
+
if (parentId === contentId) {
|
|
351
|
+
console.error('Circular dependency detected for contentId', contentId)
|
|
352
|
+
return []
|
|
666
353
|
}
|
|
667
354
|
|
|
668
|
-
return [
|
|
355
|
+
return [
|
|
356
|
+
...(hierarchy?.children?.[parentId] ?? []),
|
|
357
|
+
...getAncestorAndSiblingIds(hierarchy, parentId, depth + 1)
|
|
358
|
+
]
|
|
669
359
|
}
|
|
670
360
|
|
|
671
|
-
|
|
672
|
-
|
|
361
|
+
// doesn't accept collection - assumes progresses are already filtered appropriately
|
|
362
|
+
// caller would do well to remember this, i doth say
|
|
363
|
+
function averageProgressesFor(hierarchy, contentId, progressData, depth = 1) {
|
|
364
|
+
if (depth > MAX_DEPTH) return {}
|
|
673
365
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
return
|
|
677
|
-
}
|
|
366
|
+
const parentId = hierarchy?.parents?.[contentId]
|
|
367
|
+
if (!parentId) return {}
|
|
678
368
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
369
|
+
const parentChildProgress = hierarchy?.children?.[parentId]?.map(childId => {
|
|
370
|
+
return progressData[childId] ?? 0
|
|
371
|
+
})
|
|
372
|
+
const avgParentProgress = parentChildProgress.length > 0 ? Math.round(parentChildProgress.reduce((a, b) => a + b, 0) / parentChildProgress.length) : 0
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
...averageProgressesFor(hierarchy, parentId, progressData, depth + 1),
|
|
376
|
+
[parentId]: avgParentProgress,
|
|
684
377
|
}
|
|
685
378
|
}
|
|
379
|
+
|
|
380
|
+
function getChildrenToDepth(parentId, hierarchy, depth = 1) {
|
|
381
|
+
let childIds = hierarchy.children[parentId] ?? []
|
|
382
|
+
let allChildrenIds = childIds
|
|
383
|
+
childIds.forEach((id) => {
|
|
384
|
+
allChildrenIds = allChildrenIds.concat(getChildrenToDepth(id, hierarchy, depth - 1))
|
|
385
|
+
})
|
|
386
|
+
return allChildrenIds
|
|
387
|
+
}
|