Skip to content

Commit 53d0f07

Browse files
authored
Merge pull request #520 from endlessm/T34559-migrate-to-16-alpha
T34559 migrate to 16 alpha
2 parents 4595bc1 + d76212d commit 53d0f07

10 files changed

Lines changed: 114 additions & 47 deletions

File tree

kolibri_explore_plugin/assets/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"deep-object-diff": "^1.1.0",
1111
"eos-components": "^0.1.0",
1212
"lodash": "^4.17.21",
13-
"vue-material-design-icons": "^4.12.1"
13+
"vue-material-design-icons": "^4.12.1",
14+
"kolibri-constants": "0.1.42"
1415
},
1516
"devDependencies": {
1617
"vuex-router-sync": "^5.0.0",

kolibri_explore_plugin/assets/src/composables/useContentNodeProgress.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ import { ContentNodeProgressResource } from 'kolibri.resources';
1313
const contentNodeProgressMap = reactive({});
1414

1515
export function setContentNodeProgress(progress) {
16-
set(contentNodeProgressMap, progress.content_id, progress.progress);
16+
// Avoid setting stale progress data - assume that progress increases monotonically
17+
if (
18+
!contentNodeProgressMap[progress.content_id] ||
19+
progress.progress > contentNodeProgressMap[progress.content_id]
20+
) {
21+
set(contentNodeProgressMap, progress.content_id, progress.progress);
22+
}
1723
}
1824

1925
export default function useContentNodeProgress() {
@@ -32,7 +38,7 @@ export default function useContentNodeProgress() {
3238
force: true,
3339
}).then(progressData => {
3440
const progresses = progressData ? progressData : [];
35-
for (let progress of progresses) {
41+
for (const progress of progresses) {
3642
setContentNodeProgress(progress);
3743
}
3844
});
@@ -53,7 +59,7 @@ export default function useContentNodeProgress() {
5359
id,
5460
}).then(progressData => {
5561
const progresses = progressData ? progressData : [];
56-
for (let progress of progresses) {
62+
for (const progress of progresses) {
5763
setContentNodeProgress(progress);
5864
}
5965
});

kolibri_explore_plugin/assets/src/composables/useProgressTracking.js

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import isNumber from 'lodash/isNumber';
1111
import isPlainObject from 'lodash/isPlainObject';
1212
import isUndefined from 'lodash/isUndefined';
1313
import { diff } from 'deep-object-diff';
14+
import Modalities from 'kolibri-constants/Modalities';
1415
import client from 'kolibri.client';
1516
import logger from 'kolibri.lib.logging';
1617
import urls from 'kolibri.urls';
@@ -61,6 +62,12 @@ const replaceBlocklist = {
6162
replace: true,
6263
};
6364

65+
function clearObject(obj) {
66+
for (const key in obj) {
67+
delete obj[key];
68+
}
69+
}
70+
6471
export default function useProgressTracking(store) {
6572
store = store || getCurrentInstance().proxy.$store;
6673
const complete = ref(null);
@@ -145,33 +152,36 @@ export default function useProgressTracking(store) {
145152
const data = response.data;
146153
set(context, valOrNull(data.context));
147154
set(complete, valOrNull(data.complete));
148-
set(progress_state, threeDecimalPlaceRoundup(valOrNull(data.progress)));
155+
set(progress_state, valOrNull(data.progress));
149156
set(progress_delta, 0);
150157
set(time_spent, valOrNull(data.time_spent));
151158
set(time_spent_delta, 0);
152159
set(session_id, valOrNull(data.session_id));
160+
clearObject(extra_fields);
153161
Object.assign(extra_fields, data.extra_fields || {});
154162
set(mastery_criterion, valOrNull(data.mastery_criterion));
163+
pastattempts.splice(0);
155164
pastattempts.push(...(data.pastattempts || []));
165+
clearObject(pastattemptMap);
156166
Object.assign(
157167
pastattemptMap,
158168
data.pastattempts ? fromPairs(data.pastattempts.map(a => [a.id, a])) : {}
159169
);
160170
set(totalattempts, valOrNull(data.totalattempts));
161-
set(unsaved_interactions, []);
171+
unsaved_interactions.splice(0);
162172
});
163173
}
164174

165175
/**
166176
* Initialize a content session for progress tracking
167177
* To be called on page load for content renderers
168178
*/
169-
function initContentSession({ nodeId, lessonId, quizId } = {}) {
179+
function initContentSession({ node, lessonId, quizId, repeat = false } = {}) {
170180
const data = {};
171-
if (!nodeId && !quizId) {
172-
throw TypeError('Must define either nodeId or quizId');
181+
if (!node && !quizId) {
182+
throw TypeError('Must define either node or quizId');
173183
}
174-
if ((nodeId || lessonId) && quizId) {
184+
if ((node || lessonId) && quizId) {
175185
throw TypeError('quizId must be the only defined parameter if defined');
176186
}
177187
let sessionStarted = false;
@@ -181,16 +191,61 @@ export default function useProgressTracking(store) {
181191
data.quiz_id = quizId;
182192
}
183193

184-
if (nodeId) {
185-
sessionStarted = get(context) && get(context).node_id === nodeId;
186-
data.node_id = nodeId;
194+
if (node) {
195+
if (!node.id) {
196+
throw TypeError('node must have id property');
197+
}
198+
if (!node.content_id) {
199+
throw TypeError('node must have content_id property');
200+
}
201+
if (!node.channel_id) {
202+
throw TypeError('node must have channel_id property');
203+
}
204+
if (!node.kind) {
205+
throw TypeError('node must have kind property');
206+
}
207+
sessionStarted = get(context) && get(context).node_id === node.id;
208+
data.node_id = node.id;
209+
data.content_id = node.content_id;
210+
data.channel_id = node.channel_id;
211+
data.kind = node.kind;
187212
if (lessonId) {
188213
sessionStarted = sessionStarted && get(context) && get(context).lesson_id === lessonId;
189214
data.lesson_id = lessonId;
190215
}
216+
if (node.kind === 'exercise') {
217+
if (!node.assessmentmetadata) {
218+
throw new TypeError('node must have assessmentmetadata property');
219+
}
220+
if (!node.assessmentmetadata.mastery_model) {
221+
throw new TypeError(
222+
'node must have assessmentmetadata property with mastery_model property'
223+
);
224+
}
225+
if (!isPlainObject(node.assessmentmetadata.mastery_model)) {
226+
throw new TypeError(
227+
'node must have assessmentmetadata property with plain object mastery_model property'
228+
);
229+
}
230+
if (!node.assessmentmetadata.mastery_model.type) {
231+
throw new TypeError(
232+
'node must have assessmentmetadata property with mastery_model property with type property'
233+
);
234+
}
235+
data.mastery_model = node.assessmentmetadata.mastery_model;
236+
if (node.options && node.options.modality === Modalities.QUIZ) {
237+
// The mastery model and the modalities have different
238+
// casing, so we don't reuse it here.
239+
data.mastery_model = { type: 'quiz' };
240+
}
241+
}
191242
}
192243

193-
if (sessionStarted) {
244+
if (repeat) {
245+
data.repeat = repeat;
246+
}
247+
248+
if (sessionStarted && !repeat) {
194249
return;
195250
}
196251

@@ -207,9 +262,8 @@ export default function useProgressTracking(store) {
207262
);
208263
Object.assign(nowSavedInteraction, interaction);
209264
pastattemptMap[nowSavedInteraction.id] = nowSavedInteraction;
210-
set(totalattempts, get(totalattempts) + 1);
211265
} else {
212-
for (let key in interaction) {
266+
for (const key in interaction) {
213267
if (!blocklist[key]) {
214268
pastattemptMap[interaction.id][key] = interaction[key];
215269
}
@@ -226,12 +280,13 @@ export default function useProgressTracking(store) {
226280
data,
227281
}).then(response => {
228282
if (response.data.attempts) {
229-
for (let attempt of response.data.attempts) {
283+
for (const attempt of response.data.attempts) {
230284
updateAttempt(attempt);
231285
}
232286
}
233287
if (response.data.complete) {
234288
set(complete, true);
289+
set(progress_state, 1);
235290
if (store.getters.isUserLoggedIn && !wasComplete) {
236291
store.commit('INCREMENT_TOTAL_PROGRESS', 1);
237292
}
@@ -307,7 +362,7 @@ export default function useProgressTracking(store) {
307362
// If it is successful call all of the resolve functions that we have stored
308363
// from all the Promises that have been returned while this specific debounce
309364
// has been active.
310-
for (let [resolve] of updateContentSessionResolveRejectStack) {
365+
for (const [resolve] of updateContentSessionResolveRejectStack) {
311366
resolve(result);
312367
}
313368
// Reset the stack for resolve/reject functions, so that future invocations
@@ -316,7 +371,7 @@ export default function useProgressTracking(store) {
316371
})
317372
.catch(err => {
318373
// If there is an error call reject for all previously returned promises.
319-
for (let [, reject] of updateContentSessionResolveRejectStack) {
374+
for (const [, reject] of updateContentSessionResolveRejectStack) {
320375
reject(err);
321376
}
322377
// Likewise reset the stack.
@@ -337,6 +392,7 @@ export default function useProgressTracking(store) {
337392
// Used to ensure state is always saved when a session closes.
338393
force = false,
339394
} = {}) {
395+
const wasComplete = get(progress_state) >= 1;
340396
if (get(session_id) === null) {
341397
throw ReferenceError(noSessionErrorText);
342398
}
@@ -350,8 +406,9 @@ export default function useProgressTracking(store) {
350406
progress = _zeroToOne(progress);
351407
progress = threeDecimalPlaceRoundup(progress);
352408
if (get(progress_state) < progress) {
353-
const newProgressDelta =
354-
get(progress_delta) + threeDecimalPlaceRoundup(progress - get(progress_state));
409+
const newProgressDelta = _zeroToOne(
410+
threeDecimalPlaceRoundup(get(progress_delta) + progress - get(progress_state))
411+
);
355412
set(progress_delta, newProgressDelta);
356413
set(progress_state, progress);
357414
}
@@ -362,7 +419,10 @@ export default function useProgressTracking(store) {
362419
}
363420
progressDelta = _zeroToOne(progressDelta);
364421
progressDelta = threeDecimalPlaceRoundup(progressDelta);
365-
set(progress_delta, threeDecimalPlaceRoundup(get(progress_delta) + progressDelta));
422+
set(
423+
progress_delta,
424+
_zeroToOne(threeDecimalPlaceRoundup(get(progress_delta) + progressDelta))
425+
);
366426
set(
367427
progress_state,
368428
Math.min(threeDecimalPlaceRoundup(get(progress_state) + progressDelta), 1)
@@ -389,7 +449,7 @@ export default function useProgressTracking(store) {
389449
a => !a.id && a.item === interaction.item
390450
);
391451
if (unsavedInteraction) {
392-
for (let key in interaction) {
452+
for (const key in interaction) {
393453
set(unsavedInteraction, key, interaction[key]);
394454
}
395455
} else {
@@ -408,7 +468,8 @@ export default function useProgressTracking(store) {
408468
set(time_spent_delta, threeDecimalPlaceRoundup(get(time_spent_delta) + elapsedTime));
409469
}
410470

411-
immediate = (!isUndefined(interaction) && !interaction.id) || immediate;
471+
const completed = !wasComplete && get(progress_state) >= 1;
472+
immediate = (!isUndefined(interaction) && !interaction.id) || completed || immediate;
412473
forceSessionUpdate = forceSessionUpdate || force;
413474
// Logic for promise returning debounce vendored and modified from:
414475
// https://github.com/sindresorhus/p-debounce/blob/main/index.js
@@ -450,7 +511,7 @@ export default function useProgressTracking(store) {
450511
function stopTrackingProgress() {
451512
clearTrackingInterval();
452513
try {
453-
updateContentSession({ immediate: true, force: true }).catch(err => {
514+
return updateContentSession({ immediate: true, force: true }).catch(err => {
454515
logging.debug(err);
455516
});
456517
} catch (e) {
@@ -462,6 +523,7 @@ export default function useProgressTracking(store) {
462523
throw e;
463524
}
464525
}
526+
return Promise.resolve();
465527
}
466528

467529
onBeforeUnmount(stopTrackingProgress);

kolibri_explore_plugin/assets/src/modules/topicsTree/handlers.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,22 +128,18 @@ export function showTopicsChannel(store, id) {
128128
}
129129

130130
export function showTopicsContentInLightbox(store, id) {
131-
const promises = [
132-
ContentNodeResource.fetchModel({ id }),
133-
ContentNodeResource.fetchNextContent(id),
134-
store.dispatch('setChannelInfo'),
135-
];
131+
const promises = [ContentNodeResource.fetchModel({ id }), store.dispatch('setChannelInfo')];
136132
const shouldResolve = samePageCheckGenerator(store);
137133
Promise.all(promises).then(
138-
([content, nextContent]) => {
134+
([content]) => {
139135
if (shouldResolve()) {
140136
const currentChannel = store.getters.getChannelObject(content.channel_id);
141137
if (!currentChannel) {
142138
router.replace({ name: PageNames.CONTENT_UNAVAILABLE });
143139
return;
144140
}
145141
store.commit('topicsTree/SET_STATE', {
146-
content: contentState(content, nextContent),
142+
content: contentState(content),
147143
channel: currentChannel,
148144
});
149145

kolibri_explore_plugin/assets/src/views/ContentItem.vue

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
<AssessmentWrapper
1414
v-else
15-
:id="content.id"
1615
class="content-renderer"
1716
:class="{ 'without-fullscreen-bar': withoutFullscreenBar }"
1817
v-bind="exerciseProps"
@@ -70,7 +69,7 @@
7069
};
7170
},
7271
props: {
73-
content: {
72+
contentNode: {
7473
type: Object,
7574
required: true,
7675
},
@@ -91,10 +90,7 @@
9190
fullName: state => state.core.session.full_name,
9291
}),
9392
withoutFullscreenBar() {
94-
return GameAppIDs.includes(this.content.channel_id);
95-
},
96-
contentNode() {
97-
return this.content;
93+
return GameAppIDs.includes(this.contentNode.channel_id);
9894
},
9995
contentIsExercise() {
10096
return this.contentNode.kind === ContentNodeKinds.EXERCISE;
@@ -149,9 +145,6 @@
149145
totalattempts: this.totalattempts,
150146
};
151147
},
152-
contentNodeId() {
153-
return this.contentNode.id;
154-
},
155148
assessment() {
156149
if (this.contentNode.kind !== ContentNodeKinds.EXERCISE) {
157150
return null;
@@ -162,7 +155,7 @@
162155
},
163156
created() {
164157
return this.initContentSession({
165-
nodeId: this.contentNodeId,
158+
node: this.contentNode,
166159
}).then(() => {
167160
this.sessionReady = true;
168161
this.setWasIncomplete();
@@ -180,7 +173,7 @@
180173
* source of truth for referencing progress of content nodes.
181174
*/
182175
cacheProgress() {
183-
setContentNodeProgress({ content_id: this.content.content_id, progress: this.progress });
176+
setContentNodeProgress({ content_id: this.contentNode.id, progress: this.progress });
184177
},
185178
updateInteraction({ progress, interaction }) {
186179
this.updateContentSession({
@@ -207,6 +200,11 @@
207200
208201
@import '../styles';
209202
203+
.content-renderer {
204+
// Needs to be one less than the ScrollingHeader's z-index of 4
205+
z-index: 3;
206+
}
207+
210208
// Fix icon offset in the Kolibri plugins:
211209
.content-renderer::v-deep .button img,
212210
.content-renderer::v-deep .button svg {

kolibri_explore_plugin/assets/src/views/ContentModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
:title="content.title"
1111
headerCloseVariant="light"
1212
>
13-
<ContentItem isDark :content="content" />
13+
<ContentItem isDark :contentNode="content" />
1414
</b-modal>
1515

1616
</template>

kolibri_explore_plugin/collectionviews.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ def _enqueue_current_task(self, user):
507507

508508
def _call_task(task, user, **params):
509509
"""Create, validate and enqueue a job."""
510-
job = task.validate_job_data(user, params)
510+
job, _enqueue_args = task.validate_job_data(user, params)
511511
job_id = job_storage.enqueue_job(
512512
job, queue=DEFAULT_QUEUE, priority=Priority.HIGH
513513
)

0 commit comments

Comments
 (0)