@@ -11,6 +11,7 @@ import isNumber from 'lodash/isNumber';
1111import isPlainObject from 'lodash/isPlainObject' ;
1212import isUndefined from 'lodash/isUndefined' ;
1313import { diff } from 'deep-object-diff' ;
14+ import Modalities from 'kolibri-constants/Modalities' ;
1415import client from 'kolibri.client' ;
1516import logger from 'kolibri.lib.logging' ;
1617import 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+
6471export 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 ) ;
0 commit comments