Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion kolibri_explore_plugin/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"deep-object-diff": "^1.1.0",
"eos-components": "^0.1.0",
"lodash": "^4.17.21",
"vue-material-design-icons": "^4.12.1"
"vue-material-design-icons": "^4.12.1",
"kolibri-constants": "0.1.42"
},
"devDependencies": {
"vuex-router-sync": "^5.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import { ContentNodeProgressResource } from 'kolibri.resources';
const contentNodeProgressMap = reactive({});

export function setContentNodeProgress(progress) {
set(contentNodeProgressMap, progress.content_id, progress.progress);
// Avoid setting stale progress data - assume that progress increases monotonically
if (
!contentNodeProgressMap[progress.content_id] ||
progress.progress > contentNodeProgressMap[progress.content_id]
) {
set(contentNodeProgressMap, progress.content_id, progress.progress);
}
}

export default function useContentNodeProgress() {
Expand All @@ -32,7 +38,7 @@ export default function useContentNodeProgress() {
force: true,
}).then(progressData => {
const progresses = progressData ? progressData : [];
for (let progress of progresses) {
for (const progress of progresses) {
setContentNodeProgress(progress);
}
});
Expand All @@ -53,7 +59,7 @@ export default function useContentNodeProgress() {
id,
}).then(progressData => {
const progresses = progressData ? progressData : [];
for (let progress of progresses) {
for (const progress of progresses) {
setContentNodeProgress(progress);
}
});
Expand Down
104 changes: 83 additions & 21 deletions kolibri_explore_plugin/assets/src/composables/useProgressTracking.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import isNumber from 'lodash/isNumber';
import isPlainObject from 'lodash/isPlainObject';
import isUndefined from 'lodash/isUndefined';
import { diff } from 'deep-object-diff';
import Modalities from 'kolibri-constants/Modalities';
import client from 'kolibri.client';
import logger from 'kolibri.lib.logging';
import urls from 'kolibri.urls';
Expand Down Expand Up @@ -61,6 +62,12 @@ const replaceBlocklist = {
replace: true,
};

function clearObject(obj) {
for (const key in obj) {
delete obj[key];
}
}

export default function useProgressTracking(store) {
store = store || getCurrentInstance().proxy.$store;
const complete = ref(null);
Expand Down Expand Up @@ -145,33 +152,36 @@ export default function useProgressTracking(store) {
const data = response.data;
set(context, valOrNull(data.context));
set(complete, valOrNull(data.complete));
set(progress_state, threeDecimalPlaceRoundup(valOrNull(data.progress)));
set(progress_state, valOrNull(data.progress));
set(progress_delta, 0);
set(time_spent, valOrNull(data.time_spent));
set(time_spent_delta, 0);
set(session_id, valOrNull(data.session_id));
clearObject(extra_fields);
Object.assign(extra_fields, data.extra_fields || {});
set(mastery_criterion, valOrNull(data.mastery_criterion));
pastattempts.splice(0);
pastattempts.push(...(data.pastattempts || []));
clearObject(pastattemptMap);
Object.assign(
pastattemptMap,
data.pastattempts ? fromPairs(data.pastattempts.map(a => [a.id, a])) : {}
);
set(totalattempts, valOrNull(data.totalattempts));
set(unsaved_interactions, []);
unsaved_interactions.splice(0);
});
}

/**
* Initialize a content session for progress tracking
* To be called on page load for content renderers
*/
function initContentSession({ nodeId, lessonId, quizId } = {}) {
function initContentSession({ node, lessonId, quizId, repeat = false } = {}) {
const data = {};
if (!nodeId && !quizId) {
throw TypeError('Must define either nodeId or quizId');
if (!node && !quizId) {
throw TypeError('Must define either node or quizId');
}
if ((nodeId || lessonId) && quizId) {
if ((node || lessonId) && quizId) {
throw TypeError('quizId must be the only defined parameter if defined');
}
let sessionStarted = false;
Expand All @@ -181,16 +191,61 @@ export default function useProgressTracking(store) {
data.quiz_id = quizId;
}

if (nodeId) {
sessionStarted = get(context) && get(context).node_id === nodeId;
data.node_id = nodeId;
if (node) {
if (!node.id) {
throw TypeError('node must have id property');
}
if (!node.content_id) {
throw TypeError('node must have content_id property');
}
if (!node.channel_id) {
throw TypeError('node must have channel_id property');
}
if (!node.kind) {
throw TypeError('node must have kind property');
}
sessionStarted = get(context) && get(context).node_id === node.id;
data.node_id = node.id;
data.content_id = node.content_id;
data.channel_id = node.channel_id;
data.kind = node.kind;
if (lessonId) {
sessionStarted = sessionStarted && get(context) && get(context).lesson_id === lessonId;
data.lesson_id = lessonId;
}
if (node.kind === 'exercise') {
if (!node.assessmentmetadata) {
throw new TypeError('node must have assessmentmetadata property');
}
if (!node.assessmentmetadata.mastery_model) {
throw new TypeError(
'node must have assessmentmetadata property with mastery_model property'
);
}
if (!isPlainObject(node.assessmentmetadata.mastery_model)) {
throw new TypeError(
'node must have assessmentmetadata property with plain object mastery_model property'
);
}
if (!node.assessmentmetadata.mastery_model.type) {
throw new TypeError(
'node must have assessmentmetadata property with mastery_model property with type property'
);
}
data.mastery_model = node.assessmentmetadata.mastery_model;
if (node.options && node.options.modality === Modalities.QUIZ) {
// The mastery model and the modalities have different
// casing, so we don't reuse it here.
data.mastery_model = { type: 'quiz' };
}
}
}

if (sessionStarted) {
if (repeat) {
data.repeat = repeat;
}

if (sessionStarted && !repeat) {
return;
}

Expand All @@ -207,9 +262,8 @@ export default function useProgressTracking(store) {
);
Object.assign(nowSavedInteraction, interaction);
pastattemptMap[nowSavedInteraction.id] = nowSavedInteraction;
set(totalattempts, get(totalattempts) + 1);
} else {
for (let key in interaction) {
for (const key in interaction) {
if (!blocklist[key]) {
pastattemptMap[interaction.id][key] = interaction[key];
}
Expand All @@ -226,12 +280,13 @@ export default function useProgressTracking(store) {
data,
}).then(response => {
if (response.data.attempts) {
for (let attempt of response.data.attempts) {
for (const attempt of response.data.attempts) {
updateAttempt(attempt);
}
}
if (response.data.complete) {
set(complete, true);
set(progress_state, 1);
if (store.getters.isUserLoggedIn && !wasComplete) {
store.commit('INCREMENT_TOTAL_PROGRESS', 1);
}
Expand Down Expand Up @@ -307,7 +362,7 @@ export default function useProgressTracking(store) {
// If it is successful call all of the resolve functions that we have stored
// from all the Promises that have been returned while this specific debounce
// has been active.
for (let [resolve] of updateContentSessionResolveRejectStack) {
for (const [resolve] of updateContentSessionResolveRejectStack) {
resolve(result);
}
// Reset the stack for resolve/reject functions, so that future invocations
Expand All @@ -316,7 +371,7 @@ export default function useProgressTracking(store) {
})
.catch(err => {
// If there is an error call reject for all previously returned promises.
for (let [, reject] of updateContentSessionResolveRejectStack) {
for (const [, reject] of updateContentSessionResolveRejectStack) {
reject(err);
}
// Likewise reset the stack.
Expand All @@ -337,6 +392,7 @@ export default function useProgressTracking(store) {
// Used to ensure state is always saved when a session closes.
force = false,
} = {}) {
const wasComplete = get(progress_state) >= 1;
if (get(session_id) === null) {
throw ReferenceError(noSessionErrorText);
}
Expand All @@ -350,8 +406,9 @@ export default function useProgressTracking(store) {
progress = _zeroToOne(progress);
progress = threeDecimalPlaceRoundup(progress);
if (get(progress_state) < progress) {
const newProgressDelta =
get(progress_delta) + threeDecimalPlaceRoundup(progress - get(progress_state));
const newProgressDelta = _zeroToOne(
threeDecimalPlaceRoundup(get(progress_delta) + progress - get(progress_state))
);
set(progress_delta, newProgressDelta);
set(progress_state, progress);
}
Expand All @@ -362,7 +419,10 @@ export default function useProgressTracking(store) {
}
progressDelta = _zeroToOne(progressDelta);
progressDelta = threeDecimalPlaceRoundup(progressDelta);
set(progress_delta, threeDecimalPlaceRoundup(get(progress_delta) + progressDelta));
set(
progress_delta,
_zeroToOne(threeDecimalPlaceRoundup(get(progress_delta) + progressDelta))
);
set(
progress_state,
Math.min(threeDecimalPlaceRoundup(get(progress_state) + progressDelta), 1)
Expand All @@ -389,7 +449,7 @@ export default function useProgressTracking(store) {
a => !a.id && a.item === interaction.item
);
if (unsavedInteraction) {
for (let key in interaction) {
for (const key in interaction) {
set(unsavedInteraction, key, interaction[key]);
}
} else {
Expand All @@ -408,7 +468,8 @@ export default function useProgressTracking(store) {
set(time_spent_delta, threeDecimalPlaceRoundup(get(time_spent_delta) + elapsedTime));
}

immediate = (!isUndefined(interaction) && !interaction.id) || immediate;
const completed = !wasComplete && get(progress_state) >= 1;
immediate = (!isUndefined(interaction) && !interaction.id) || completed || immediate;
forceSessionUpdate = forceSessionUpdate || force;
// Logic for promise returning debounce vendored and modified from:
// https://github.com/sindresorhus/p-debounce/blob/main/index.js
Expand Down Expand Up @@ -450,7 +511,7 @@ export default function useProgressTracking(store) {
function stopTrackingProgress() {
clearTrackingInterval();
try {
updateContentSession({ immediate: true, force: true }).catch(err => {
return updateContentSession({ immediate: true, force: true }).catch(err => {
logging.debug(err);
});
} catch (e) {
Expand All @@ -462,6 +523,7 @@ export default function useProgressTracking(store) {
throw e;
}
}
return Promise.resolve();
}

onBeforeUnmount(stopTrackingProgress);
Expand Down
10 changes: 3 additions & 7 deletions kolibri_explore_plugin/assets/src/modules/topicsTree/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,22 +128,18 @@ export function showTopicsChannel(store, id) {
}

export function showTopicsContentInLightbox(store, id) {
const promises = [
ContentNodeResource.fetchModel({ id }),
ContentNodeResource.fetchNextContent(id),
store.dispatch('setChannelInfo'),
];
const promises = [ContentNodeResource.fetchModel({ id }), store.dispatch('setChannelInfo')];
const shouldResolve = samePageCheckGenerator(store);
Promise.all(promises).then(
([content, nextContent]) => {
([content]) => {
if (shouldResolve()) {
const currentChannel = store.getters.getChannelObject(content.channel_id);
if (!currentChannel) {
router.replace({ name: PageNames.CONTENT_UNAVAILABLE });
return;
}
store.commit('topicsTree/SET_STATE', {
content: contentState(content, nextContent),
content: contentState(content),
channel: currentChannel,
});

Expand Down
20 changes: 9 additions & 11 deletions kolibri_explore_plugin/assets/src/views/ContentItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

<AssessmentWrapper
v-else
:id="content.id"
class="content-renderer"
:class="{ 'without-fullscreen-bar': withoutFullscreenBar }"
v-bind="exerciseProps"
Expand Down Expand Up @@ -70,7 +69,7 @@
};
},
props: {
content: {
contentNode: {
type: Object,
required: true,
},
Expand All @@ -91,10 +90,7 @@
fullName: state => state.core.session.full_name,
}),
withoutFullscreenBar() {
return GameAppIDs.includes(this.content.channel_id);
},
contentNode() {
return this.content;
return GameAppIDs.includes(this.contentNode.channel_id);
},
contentIsExercise() {
return this.contentNode.kind === ContentNodeKinds.EXERCISE;
Expand Down Expand Up @@ -149,9 +145,6 @@
totalattempts: this.totalattempts,
};
},
contentNodeId() {
return this.contentNode.id;
},
assessment() {
if (this.contentNode.kind !== ContentNodeKinds.EXERCISE) {
return null;
Expand All @@ -162,7 +155,7 @@
},
created() {
return this.initContentSession({
nodeId: this.contentNodeId,
node: this.contentNode,
}).then(() => {
this.sessionReady = true;
this.setWasIncomplete();
Expand All @@ -180,7 +173,7 @@
* source of truth for referencing progress of content nodes.
*/
cacheProgress() {
setContentNodeProgress({ content_id: this.content.content_id, progress: this.progress });
setContentNodeProgress({ content_id: this.contentNode.id, progress: this.progress });
},
updateInteraction({ progress, interaction }) {
this.updateContentSession({
Expand All @@ -207,6 +200,11 @@

@import '../styles';

.content-renderer {
// Needs to be one less than the ScrollingHeader's z-index of 4
z-index: 3;
}

// Fix icon offset in the Kolibri plugins:
.content-renderer::v-deep .button img,
.content-renderer::v-deep .button svg {
Expand Down
2 changes: 1 addition & 1 deletion kolibri_explore_plugin/assets/src/views/ContentModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
:title="content.title"
headerCloseVariant="light"
>
<ContentItem isDark :content="content" />
<ContentItem isDark :contentNode="content" />
</b-modal>

</template>
Expand Down
2 changes: 1 addition & 1 deletion kolibri_explore_plugin/collectionviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ def _enqueue_current_task(self, user):

def _call_task(task, user, **params):
"""Create, validate and enqueue a job."""
job = task.validate_job_data(user, params)
job, _enqueue_args = task.validate_job_data(user, params)
job_id = job_storage.enqueue_job(
job, queue=DEFAULT_QUEUE, priority=Priority.HIGH
)
Expand Down
Loading