From f5ef14f54bb35fc9cea61c7d0581892ee3f516da Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 31 Jul 2024 16:50:36 +0200 Subject: [PATCH] chore: bundle assets --- views/css/creator.css.map | 2 +- views/css/new-test-runner.css.map | 2 +- views/css/test-runner.css.map | 2 +- views/js/loader/taoQtiTest.min.js.map | 2 +- views/js/loader/taoQtiTestRunner.min.js.map | 2 +- views/js/loader/taoQtiTestXMLEditor.min.js.map | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/views/css/creator.css.map b/views/css/creator.css.map index dd4fe77ed..78cb396e3 100644 --- a/views/css/creator.css.map +++ b/views/css/creator.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../scss/creator.scss","../../../tao/views/node_modules/@oat-sa/tao-core-ui/scss/inc/_functions.scss","../../../tao/views/node_modules/@oat-sa/tao-core-ui/scss/inc/_colors.scss","../../../tao/views/scss/inc/fonts/_tao-icon-vars.scss","../scss/inc/solar/_creator.scss","../../../tao/views/scss/inc/solar/mixins/_buttons.scss","../../../tao/views/scss/inc/solar/mixins/_forms.scss"],"names":[],"mappings":"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CA,WAEA,iFACI,aAEJ,iFCxWhB,uBACA,0BACA,kBDwWoB,iBEhcH,KFicG,YACA,sBACA,kBACA,gBAEA,2FACI,cAGR,qEACI,kBACA,WACA,SAMhB,mDAEI,sBC/XR,uBACA,0BACA,kBD+XQ,kBACA,sDACI,cAEJ,6DACI,kBACA,UACA,iBACA,oEACI,ME/dK,KFieT,gEACI,kBACA,YACA,iBACA,YACA,WACA,gFACK,iBEpeF,QFueC,oFACI,gBAKhB,wEACI,aACA,qBACA,YACA,iBACA,iBACA,0BACA,MEnfK,QFofL,yBACA,eChUR,eACA,iBAtGJ,uBACA,0BACA,kBA5DQ,mMDmeI,+EG1hBd,6BACA,YACA,kBACA,mBACA,oBACA,oBACA,cAGA,mCACA,kCAmGgB,YHibF,8EACI,yBCxeR,mMDyfJ,4CACI,cAEA,+CACI,iBACA,UAGJ,iEACI,WAKZ,oCACI,aACA,kBACA,MACA,QACA,UACA,kBACA,gBACA,aCpXA,0BACA,4BDsXA,iCACA,qBACA,0BACA,gBACA,iBAhCuB,KAkCvB,yGCzXA,eACA,iBD0XI,kBACA,QACA,MAxCqB,KA2CzB,gDACI,WACA,YACA,kBACA,eACA,SACA,qBACA,sBACA,iIACI,0BAEJ,sDACI,iBElmBT,QFsmBS,yBAHA,6IACI,MAvDW,KA2DnB,uDACI,yBAKZ,0BACI,+BACA,kBAEA,wCACI,qBACA,kBAEA,8CACI,eACA,yBAEJ,oDACI,eACA,UACA,yBACA,2CACA,mCAGJ,iHACI,WAGR,oCAGI,6BACA,oBACA,uBACA,gDACI,oBACA,qBACA,uBACA,sBAMZ,6BACI,kBACA,eC7iBJ,sBACA,kBACA,0BD6iBI,sBACA,kBACA,WACA,UACA,qBACA,iBACA,eACA,2FACI,kBACA,oBACA,qBACA,kBACA,WACA,kBACA,SAEJ,sDACI,6BACA,oBACA,uBACA,kEACI,oBACA,qBACA,uBACA,sBAGR,kDACI,uBAKR,iDACI,iBE5pBe,QF6pBf,sBACA,gBACA,eACA,iBAEA,6DACI,iBExrBI,QF6rBZ,oCACI,SACA,UACA,kBAEA,8CACI,eAEJ,4CACI,kBACA,yBACA,gCClmBR,uBACA,0BACA,kBDkmBQ,SACA,WACA,WACA,YACA,YAGJ,2CACI,iBE7rBW,QF8rBX,kBACA,YACA,UACA,SACA,WACA,YACA,QACA,YC1nBR,sBACA,kBACA,0BAIA,uBACA,0BACA,kBDsnBQ,gDACI,UACA,SACA,kBAEJ,uDACI,ME5tBJ,KF6tBI,kBACA,UACA,SACA,SACA,QACA,wBAEJ,6DACI,MEvtBK,QFytBT,gEGnnBS,YHsnBT,8DGrnBO,YH0nBX,2DACI,YAIR,gCACI,MErwBA,QFswBA,iBACA,aACA,iBAIR,kBACI,gCACA,oCACA,oCACI,yBIhxBI,sDACI,wBACA,kCACA,oCACA,iBACA,2BACA,gCACA,iBACA,SAEA,6DACI,UACA,QACA,2BACA,gCAEJ,+DACI,kBACA,eAEJ,uEACI,2BACA,gCAGR,iEACI,kBAEJ,6DACI,kCACA,4DAEA,gEACI,wBACA,kCACA,kCACA,iBACA,iBACA,0DACA,4DAGR,+DC9CR,iCACA,eAIA,2BACA,6CACA,iBACA,yBACA,mBAWA,iCACA,4BACA,+BD6BQ,oMCNR,uEACA,oCACA,oDACA,iBAEA,0cACI,oCACA,0DDGQ,4EACI,YAIJ,+DACI,wBACA,kCACA,kCACA,iBAIJ,gEACI,wBACA,kCACA,iCACA,iBAGA,8EACI,YACA,mCACA,wBACA,sCACA,YAEA,mGACI,YACA,gBACA,kCACA,UACA,kBAEA,qHACI,+BACA,yBACA,mCACA,0DACA,gBACA,YE/F5B,wgBACI,gBACA,iEACA,2EACA,aFkGgB,uFACI,UACA,QAKhB,qEACI,mCACA,+DAEA,wEACI,kCACA,iBAGA,gGACI,sDAGR,0FACI,oDACA,yBACA,0CACA,mCACA,+BAEA,iGACI,+BAEJ,gGACI,wBACA,0CAKhB,oDACI,kEAEA,uDACI,aAEJ,oEACI,6CAGR,oDACI,wBACA,kCAEA,gEACI,aAEJ,2DACI,6CAEJ,uDACI,SACA,UACA,kBACA,yBACA,kCACA,iBACA,qCACA,gCACA,wCACA,8BACA,sEAEA,8DACI,iCACA,iBACA,qCAGR,uDACI,wBACA,kCACA,kCACA,iBACA,YACA,kBAEJ,uDACI,wBACA,kCACA,kCACA,iBACA,2BACA,gCACA,SACA,cACA,oEAEA,qEACI,WACA,QACA,iBACA,oCACA,SAIJ,uRACI,kBACA,eACA,kBAEA,0qBACI,WACA,SACA,WAMJ,oaACI,WAKR,kFACI,iCAEJ,kFACI,kBACA,MACA,UAIJ,6JACI,aACA,mBACA,mBACA,kBACA,SAEA,wXACI,WACA,gBAEJ,2KACI,UAEJ,2KACI,UAEJ,2KACI,gBACA,0BAEA,+KACI,qBACA,0BACA,2BACA,gCACA,kBACA,SACA,yBAEA,2LACI,yBACA,sDACA,qBAIZ,6KACI,WACA,eAGR,yFCtRZ,iCACA,eAIA,2BACA,6CACA,iBACA,yBACA,mBAWA,iCACA,4BACA,+BAuBA,uEACA,oCACA,oDACA,iBD2OgB,iBACA,qBC1OhB,wMACI,oCACA,0DD0OY,+FACI,qBAEJ,mNACI,+BACA,sBAIZ,kNE3LR,2BACA,+BACA,iBACA,gCF0LY,cACA,mBACA,SACA,2BACA,gCE5LZ,ugBACI,+BACA,yBACA,WACA,uBAGJ,iOACI,yBFsLI,0DACI,kBACA,MACA,WAEA,qJACI,qBACA,0BACA,2BACA,gCACA,kBACA,SAEA,iKACI,sDAGR,iFACI,gBACA,kCACA,gBACA,kEACA,iDACA,gCACA,iBAGR,yDACI,yBACA,YACA,qBAGA,kxCEjUZ,0DACA,gBACA,2BACA,2BACA,+BACA,yBACA,eArBA,iwFACI,gBACA,iEACA,2EACA,aAKJ,++CACI,0CAeJ,myIACI,kCACA,4CACA,gDACA,qBFuUI,8DCvTR,uEACA,oCACA,oDACA,iBAEA,kJACI,oCACA,0DDoTQ,2LACI,kDACA,yCAGR,8DACI,kBACA,UAEA,sEACI,iCACA,gBACA,6BACA,uCACA,4EACA,yCACA,cACA,SAEJ,oEACI,iCACA,6BACA,uCACA,yEACA,yCACA,gBACA","file":"creator.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../scss/creator.scss","../../../tao/views/node_modules/@oat-sa/tao-core-ui/scss/inc/_functions.scss","../../../tao/views/node_modules/@oat-sa/tao-core-ui/scss/inc/_colors.scss","../../../tao/views/scss/inc/fonts/_tao-icon-vars.scss","../scss/inc/solar/_creator.scss","../../../tao/views/scss/inc/solar/mixins/_buttons.scss","../../../tao/views/scss/inc/solar/mixins/_forms.scss"],"names":[],"mappings":"CAwBA,cAgBI,kBACA,0BACA,WAfa,MCmCL,mDALA,4SDZR,sBCYQ,oCAyBR,wBACA,4BA1BQ,2IDRR,oCACI,kBACA,OAzBa,KA0Bb,iBEpBK,QFqBL,MEhCI,KFkCJ,uCAEI,OA/BS,KAiCT,0CACI,WACA,OAnCK,KAoCL,kBACA,yBACA,gBACA,kBCuJR,eACA,iBDrJQ,qHACG,cCmJX,eACA,eDlJW,MExDP,QF2DI,gDACG,eACA,MEtDH,KFuDG,iBEpET,QFqES,iIACI,eACA,ME1DP,KF2DO,iBExEb,QF2EM,mDACG,yBACC,uIACI,yBACA,mBACA,ME1EZ,QFiFR,oCACI,iBEzCC,QF0CD,ME7EI,KDoCA,sDD4CJ,YA9FJ,2CAyFA,oCA9E6B,MAaX,OArBlB,kEAsFA,oCA7E8B,MAWX,OAjBnB,2CAmFA,oCA5E4B,MASX,OA0Eb,oDCzCI,mDALA,iXDoDA,mJACI,kBACA,mBACA,eAKZ,kCACI,kBAEA,2BAEA,qCC8FA,eACA,iBD7FI,iBE5FC,QF6FD,MExGA,KFyGA,eACA,gBACA,YAEA,4CGhIV,6BACA,YACA,kBACA,mBACA,oBACA,oBACA,cAGA,mCACA,kCA+GiB,YHUH,iBAGR,kDACI,kBACA,yBACA,gBAIR,kCACI,kBACA,iBE/Ge,QFgHf,ME7HI,KF8HJ,2BACA,qCCoEA,eACA,iBDnEI,iBEtHC,QFuHD,MElIA,KFmIA,eACA,gBACA,YAEA,4CG1JV,6BACA,YACA,kBACA,mBACA,oBACA,oBACA,cAGA,mCACA,kCA2EqB,YFiIf,eACA,iBDzDQ,iBAIR,qCACI,YACA,cAGJ,qCC+CA,eACA,iBD9CI,gBACA,iBE5IC,QF6ID,MExJA,KFyJA,eACA,YACA,kBACA,WAEA,6CACI,eACA,mDACI,kBACA,WACA,QAMR,8DACI,eAIR,yCACI,yBACA,kBAGJ,wCACI,eAGJ,4CACI,iBACA,WAEA,kDACI,WACA,kBACA,kBACA,YAEJ,gEACI,YACA,+BACA,sBACA,0BAGJ,oDACI,iBEzLM,QF0LN,MEzMA,KF0MA,iBACA,cACA,eAGJ,kDACI,iBEzMK,KF0ML,MElNJ,KFmNI,eACA,cACA,WAGJ,yDACI,iBAGR,yCACI,WACA,kBACA,mBACA,+CACI,UAEJ,+FACI,kBACA,kBACA,kBACA,UAEJ,oEACI,kBACA,kBACA,UACA,WAEJ,4CC5CJ,eACA,iBD6CQ,gBAEJ,oDACI,YACA,iBAKZ,iCCtNQ,oCAyBR,wBACA,4BA1BQ,2IDwNJ,kBACA,WACA,YACA,iBEvPa,KFwPb,MEhQI,KFmQA,uIACI,kBACA,QACA,MACA,qBACA,WACA,gBACA,YACA,YACA,sJACI,qBACA,gBACA,iBC5EZ,eACA,iBD6EY,iLACI,2BACA,0BC/OZ,kNDsPJ,oCACG,kBACA,iBE/QY,QFgRZ,YACA,0BACA,gBACA,eC7FH,eACA,iBD8FG,iBACC,2CGtTV,6BACA,YACA,kBACA,mBACA,oBACA,oBACA,cAGA,mCACA,kCAgHiB,YH+LH,kBACA,UACA,QAEJ,6CACI,kBACA,gBAEJ,qDACI,qBACA,kBACA,gBACA,aAIR,6CACI,+BACA,cACA,iBACA,UACA,aACA,mBACA,kDACI,UACA,iBACA,iBAGR,+CACI,sBACA,yBACA,gBACA,oGACI,kBACA,YAMJ,0DACI,iBAIR,2CACI,MErVA,KFsVA,sBACA,UACA,cACA,oBC3PR,uBACA,0BACA,kBD4PQ,8CACI,iBACA,aACA,mBACA,iBEnVO,QFoVP,aACA,aACA,MEnWJ,KFoWI,iBACA,0BACA,sBACA,uDACI,kBACA,SACA,SACA,ME3WR,KF4WQ,qBACA,yBAEJ,sDACI,yBAGR,oDACI,QACA,WAGJ,+DACI,gBAIR,2CACI,mBAGJ,0CACI,MElYA,KFmYA,8BACA,qBACA,2BAEA,6CACI,iBACA,WACA,iBAGJ,qDACI,gBAGJ,mDACI,MACA,WAEJ,8DACI,gBAGR,gDACI,8BAGJ,6CAMI,8BACA,mBANA,gDACI,iBACA,WACA,iBAIJ,yDACI,kBAtZO,QA0Zf,2CACI,8BACA,wBACA,sBACA,8CACI,MA9ZD,QA+ZC,WACA,aAGJ,yDACI,WACA,kBACA,0IACG,kBACA,WAEH,4DACI,YACA,sBC/VhB,uBACA,0BACA,kBD+VgB,iBElbG,QFmbH,mBACA,WAEA,iFACI,aAEJ,iFCxWhB,uBACA,0BACA,kBDwWoB,iBEhcH,KFicG,YACA,sBACA,kBACA,gBAEA,2FACI,cAGR,qEACI,kBACA,WACA,SAMhB,mDAEI,sBC/XR,uBACA,0BACA,kBD+XQ,kBACA,sDACI,cAEJ,6DACI,kBACA,UACA,iBACA,oEACI,ME/dK,KFieT,gEACI,kBACA,YACA,iBACA,YACA,WACA,gFACK,iBEpeF,QFueC,oFACI,gBAKhB,wEACI,aACA,qBACA,YACA,iBACA,iBACA,0BACA,MEnfK,QFofL,yBACA,eChUR,eACA,iBAtGJ,uBACA,0BACA,kBA5DQ,mMDmeI,+EG1hBd,6BACA,YACA,kBACA,mBACA,oBACA,oBACA,cAGA,mCACA,kCAoGgB,YHgbF,8EACI,yBCxeR,mMDyfJ,4CACI,cAEA,+CACI,iBACA,UAGJ,iEACI,WAKZ,oCACI,aACA,kBACA,MACA,QACA,UACA,kBACA,gBACA,aCpXA,0BACA,4BDsXA,iCACA,qBACA,0BACA,gBACA,iBAhCuB,KAkCvB,yGCzXA,eACA,iBD0XI,kBACA,QACA,MAxCqB,KA2CzB,gDACI,WACA,YACA,kBACA,eACA,SACA,qBACA,sBACA,iIACI,0BAEJ,sDACI,iBElmBT,QFsmBS,yBAHA,6IACI,MAvDW,KA2DnB,uDACI,yBAKZ,0BACI,+BACA,kBAEA,wCACI,qBACA,kBAEA,8CACI,eACA,yBAEJ,oDACI,eACA,UACA,yBACA,2CACA,mCAGJ,iHACI,WAGR,oCAGI,6BACA,oBACA,uBACA,gDACI,oBACA,qBACA,uBACA,sBAMZ,6BACI,kBACA,eC7iBJ,sBACA,kBACA,0BD6iBI,sBACA,kBACA,WACA,UACA,qBACA,iBACA,eACA,2FACI,kBACA,oBACA,qBACA,kBACA,WACA,kBACA,SAEJ,sDACI,6BACA,oBACA,uBACA,kEACI,oBACA,qBACA,uBACA,sBAGR,kDACI,uBAKR,iDACI,iBE5pBe,QF6pBf,sBACA,gBACA,eACA,iBAEA,6DACI,iBExrBI,QF6rBZ,oCACI,SACA,UACA,kBAEA,8CACI,eAEJ,4CACI,kBACA,yBACA,gCClmBR,uBACA,0BACA,kBDkmBQ,SACA,WACA,WACA,YACA,YAGJ,2CACI,iBE7rBW,QF8rBX,kBACA,YACA,UACA,SACA,WACA,YACA,QACA,YC1nBR,sBACA,kBACA,0BAIA,uBACA,0BACA,kBDsnBQ,gDACI,UACA,SACA,kBAEJ,uDACI,ME5tBJ,KF6tBI,kBACA,UACA,SACA,SACA,QACA,wBAEJ,6DACI,MEvtBK,QFytBT,gEGlnBS,YHqnBT,8DGpnBO,YHynBX,2DACI,YAIR,gCACI,MErwBA,QFswBA,iBACA,aACA,iBAIR,kBACI,gCACA,oCACA,oCACI,yBIhxBI,sDACI,wBACA,kCACA,oCACA,iBACA,2BACA,gCACA,iBACA,SAEA,6DACI,UACA,QACA,2BACA,gCAEJ,+DACI,kBACA,eAEJ,uEACI,2BACA,gCAGR,iEACI,kBAEJ,6DACI,kCACA,4DAEA,gEACI,wBACA,kCACA,kCACA,iBACA,iBACA,0DACA,4DAGR,+DC9CR,iCACA,eAIA,2BACA,6CACA,iBACA,yBACA,mBAWA,iCACA,4BACA,+BD6BQ,oMCNR,uEACA,oCACA,oDACA,iBAEA,0cACI,oCACA,0DDGQ,4EACI,YAIJ,+DACI,wBACA,kCACA,kCACA,iBAIJ,gEACI,wBACA,kCACA,iCACA,iBAGA,8EACI,YACA,mCACA,wBACA,sCACA,YAEA,mGACI,YACA,gBACA,kCACA,UACA,kBAEA,qHACI,+BACA,yBACA,mCACA,0DACA,gBACA,YE/F5B,wgBACI,gBACA,iEACA,2EACA,aFkGgB,uFACI,UACA,QAKhB,qEACI,mCACA,+DAEA,wEACI,kCACA,iBAGA,gGACI,sDAGR,0FACI,oDACA,yBACA,0CACA,mCACA,+BAEA,iGACI,+BAEJ,gGACI,wBACA,0CAKhB,oDACI,kEAEA,uDACI,aAEJ,oEACI,6CAGR,oDACI,wBACA,kCAEA,gEACI,aAEJ,2DACI,6CAEJ,uDACI,SACA,UACA,kBACA,yBACA,kCACA,iBACA,qCACA,gCACA,wCACA,8BACA,sEAEA,8DACI,iCACA,iBACA,qCAGR,uDACI,wBACA,kCACA,kCACA,iBACA,YACA,kBAEJ,uDACI,wBACA,kCACA,kCACA,iBACA,2BACA,gCACA,SACA,cACA,oEAEA,qEACI,WACA,QACA,iBACA,oCACA,SAIJ,uRACI,kBACA,eACA,kBAEA,0qBACI,WACA,SACA,WAMJ,oaACI,WAKR,kFACI,iCAEJ,kFACI,kBACA,MACA,UAIJ,6JACI,aACA,mBACA,mBACA,kBACA,SAEA,wXACI,WACA,gBAEJ,2KACI,UAEJ,2KACI,UAEJ,2KACI,gBACA,0BAEA,+KACI,qBACA,0BACA,2BACA,gCACA,kBACA,SACA,yBAEA,2LACI,yBACA,sDACA,qBAIZ,6KACI,WACA,eAGR,yFCtRZ,iCACA,eAIA,2BACA,6CACA,iBACA,yBACA,mBAWA,iCACA,4BACA,+BAuBA,uEACA,oCACA,oDACA,iBD2OgB,iBACA,qBC1OhB,wMACI,oCACA,0DD0OY,+FACI,qBAEJ,mNACI,+BACA,sBAIZ,kNE3LR,2BACA,+BACA,iBACA,gCF0LY,cACA,mBACA,SACA,2BACA,gCE5LZ,ugBACI,+BACA,yBACA,WACA,uBAGJ,iOACI,yBFsLI,0DACI,kBACA,MACA,WAEA,qJACI,qBACA,0BACA,2BACA,gCACA,kBACA,SAEA,iKACI,sDAGR,iFACI,gBACA,kCACA,gBACA,kEACA,iDACA,gCACA,iBAGR,yDACI,yBACA,YACA,qBAGA,kxCEjUZ,0DACA,gBACA,2BACA,2BACA,+BACA,yBACA,eArBA,iwFACI,gBACA,iEACA,2EACA,aAKJ,++CACI,0CAeJ,myIACI,kCACA,4CACA,gDACA,qBFuUI,8DCvTR,uEACA,oCACA,oDACA,iBAEA,kJACI,oCACA,0DDoTQ,2LACI,kDACA,yCAGR,8DACI,kBACA,UAEA,sEACI,iCACA,gBACA,6BACA,uCACA,4EACA,yCACA,cACA,SAEJ,oEACI,iCACA,6BACA,uCACA,yEACA,yCACA,gBACA","file":"creator.css"} \ No newline at end of file diff --git a/views/css/new-test-runner.css.map b/views/css/new-test-runner.css.map index 02d2da2b8..f7b03141b 100644 --- a/views/css/new-test-runner.css.map +++ b/views/css/new-test-runner.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_navigator.scss","../../../tao/views/node_modules/@oat-sa/tao-core-ui/scss/inc/_functions.scss","../../../tao/views/scss/inc/fonts/_tao-icon-vars.scss","../../../tao/views/node_modules/@oat-sa/tao-core-ui/scss/inc/_colors.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_navigatorFizzy.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_document-viewer.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_answer-masking.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_area-masking.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_plugin-colors.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_connectivity.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_line-reader.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_magnifier.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_progressbar.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_apip-text-to-speech.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_test-delivery-override.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_test-layout.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_test-action-bars.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_offline-sync-modal-wait-content.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/rtl/_action-bar.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/rtl/_review-panel.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/rtl/_feedback.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/rtl/_title-bar.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_shortcuts.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_jumplinks.scss"],"names":[],"mappings":"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AKA,8CACI,aAIR,8CACI,iBb5NC,Qa6ND,kBACA,YACA,SACA,iBACA,Yf5KJ,sBACA,kBACA,0BAtDQ,2OemOJ,uDACI,cACA,aACA,YACA,YACA,YACA,kBACA,Yf3EJ,eACA,iBA5GJ,sBACA,kBACA,0Be2LA,8BACI,kBACA,iBC3SJ,2CACI,kBAGJ,yChByDQ,mDALA,8agBhDJ,sEACI,kBACA,WCFJ,6FACE,YACA,oBAEF,6FACE,WAME,+PACE,qBAGF,+OACE,iBAEE,qTACE,qBAIF,iTACE,qBAGA,6TACE,aAEF,6UACE,qBAKR,2OACE,UACA,WAIJ,+NACE,YACA,2OACE,kBAUE,i9EACE,qBACA,qBAIN,+OACE,iBCjEV,4GACI,cACA,kBAEJ,mEACI,cACA,kBACA,gBCTJ,mIAEI,0BACA,iBAEA,wUAEI,UACA,WCHJ,2JACE,gBACA,kBAEE,yLACE,gBACA,iBAMJ,qKACE,gBACA,kBAKF,mKACE,WAKI,kMACE,YAIF,iMACE,aCjChB,oCACE,WACA,YACA,eACA,MACA,OACA,aACA,mBACA,qBACA,uBACA,cACA,0BACA,cAEA,oDACE,WACA,gBACA,YACA,iBACA,wBACA,sBACA,4BACA,kBACA,iBnBDe,KmBEf,sBAEA,gFACE,YAGF,0EACE,eACA,aAGF,2EACI,eACA,gBAEA,iFACI,0BACA,mBAIR,0EACI,gBACA,SACA,UAEA,yFACE,oBACA,qBACA,aACA,WACA,SAEA,gOAEI,iBACA,qBACA,aACA,SACA,kBAGJ,+GACI,gBAKV,+DACI,+BACA,gBACA,YACA,UACA,kBACA,WACA,SACA,WAEA,2EACI,MnBrEF,KmBsEE,eACA,UAGJ,qEACI,0BACA,mBC1FZ,sCACE,cACA,QACA,SACA,kBAEA,sDACE,eACA,cACA,aAGF,iDACE,aACA,eACA,cACA,gBACA,SACA,UACA,QACA,qBACA,4BAEA,uDACE,WACA,YACA,wBACA,mBACA,sBACA,sBACA,aACA,4BAEF,6DACE,WpBPe,QoBQf","file":"new-test-runner.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_navigator.scss","../../../tao/views/node_modules/@oat-sa/tao-core-ui/scss/inc/_functions.scss","../../../tao/views/scss/inc/fonts/_tao-icon-vars.scss","../../../tao/views/node_modules/@oat-sa/tao-core-ui/scss/inc/_colors.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_navigatorFizzy.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_document-viewer.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_answer-masking.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_area-masking.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_plugin-colors.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_connectivity.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_line-reader.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_magnifier.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_progressbar.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_apip-text-to-speech.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_test-delivery-override.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_test-layout.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_test-action-bars.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_offline-sync-modal-wait-content.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/rtl/_action-bar.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/rtl/_review-panel.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/rtl/_feedback.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/rtl/_title-bar.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_shortcuts.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_jumplinks.scss"],"names":[],"mappings":"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AKA,8CACI,aAIR,8CACI,iBb5NC,Qa6ND,kBACA,YACA,SACA,iBACA,Yf5KJ,sBACA,kBACA,0BAtDQ,2OemOJ,uDACI,cACA,aACA,YACA,YACA,YACA,kBACA,Yf3EJ,eACA,iBA5GJ,sBACA,kBACA,0Be2LA,8BACI,kBACA,iBC3SJ,2CACI,kBAGJ,yChByDQ,mDALA,8agBhDJ,sEACI,kBACA,WCFJ,6FACE,YACA,oBAEF,6FACE,WAME,+PACE,qBAGF,+OACE,iBAEE,qTACE,qBAIF,iTACE,qBAGA,6TACE,aAEF,6UACE,qBAKR,2OACE,UACA,WAIJ,+NACE,YACA,2OACE,kBAUE,i9EACE,qBACA,qBAIN,+OACE,iBCjEV,4GACI,cACA,kBAEJ,mEACI,cACA,kBACA,gBCTJ,mIAEI,0BACA,iBAEA,wUAEI,UACA,WCHJ,2JACE,gBACA,kBAEE,yLACE,gBACA,iBAMJ,qKACE,gBACA,kBAKF,mKACE,WAKI,kMACE,YAIF,iMACE,aCjChB,oCACE,WACA,YACA,eACA,MACA,OACA,aACA,mBACA,qBACA,uBACA,cACA,0BACA,cAEA,oDACE,WACA,gBACA,YACA,iBACA,wBACA,sBACA,4BACA,kBACA,iBnBDe,KmBEf,sBAEA,gFACE,YAGF,0EACE,eACA,aAGF,2EACI,eACA,gBAEA,iFACI,0BACA,mBAIR,0EACI,gBACA,SACA,UAEA,yFACE,oBACA,qBACA,aACA,WACA,SAEA,gOAEI,iBACA,qBACA,aACA,SACA,kBAGJ,+GACI,gBAKV,+DACI,+BACA,gBACA,YACA,UACA,kBACA,WACA,SACA,WAEA,2EACI,MnBrEF,KmBsEE,eACA,UAGJ,qEACI,0BACA,mBC1FZ,sCACE,cACA,QACA,SACA,kBAEA,sDACE,eACA,cACA,aAGF,iDACE,aACA,eACA,cACA,gBACA,SACA,UACA,QACA,qBACA,4BAEA,uDACE,WACA,YACA,wBACA,mBACA,sBACA,sBACA,aACA,4BAEF,6DACE,WpBPe,QoBQf","file":"new-test-runner.css"} \ No newline at end of file diff --git a/views/css/test-runner.css.map b/views/css/test-runner.css.map index d2d987c80..28d116129 100644 --- a/views/css/test-runner.css.map +++ b/views/css/test-runner.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../../tao/views/node_modules/@oat-sa/tao-core-ui/scss/inc/_functions.scss","../../../tao/views/scss/inc/_loading-bar.scss","../../../tao/views/scss/inc/_section-container.scss","../../../tao/views/node_modules/@oat-sa/tao-core-ui/scss/inc/_colors.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_navigator.scss","../../../tao/views/scss/inc/fonts/_tao-icon-vars.scss","../scss/test-runner.scss"],"names":[],"mappings":"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","file":"test-runner.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../../tao/views/node_modules/@oat-sa/tao-core-ui/scss/inc/_functions.scss","../../../tao/views/scss/inc/_loading-bar.scss","../../../tao/views/scss/inc/_section-container.scss","../../../tao/views/node_modules/@oat-sa/tao-core-ui/scss/inc/_colors.scss","../node_modules/@oat-sa/tao-test-runner-qti/scss/inc/_navigator.scss","../../../tao/views/scss/inc/fonts/_tao-icon-vars.scss","../scss/test-runner.scss"],"names":[],"mappings":"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","file":"test-runner.css"} \ No newline at end of file diff --git a/views/js/loader/taoQtiTest.min.js.map b/views/js/loader/taoQtiTest.min.js.map index c8ef2fd29..c381563c2 100644 --- a/views/js/loader/taoQtiTest.min.js.map +++ b/views/js/loader/taoQtiTest.min.js.map @@ -1 +1 @@ -{"version":3,"names":["define","_","areaBroker","requireAreas","partial","module","config","arguments","length","defaults","baseTypeEnum","IDENTIFIER","BOOLEAN","INTEGER","FLOAT","STRING","POINT","PAIR","DIRECTED_PAIR","DURATION","FILE","URI","INT_OR_IDENTIFIER","COORDS","ANY","SAME","baseTypeHelper","asArray","getValid","type","defaultType","isFinite","getNameByConstant","getConstantByName","isUndefined","getValue","value","isString","toLowerCase","parseInt","parseFloat","isNaN","name","trim","constant","operator","assign","cardinalityEnum","SINGLE","MULTIPLE","ORDERED","RECORD","cardinalityHelper","cardinality","defaultCardinality","validateIdentifier","identifier","identifierValidator","test","validateOutcome","outcome","checkIdentifier","allowedTypes","validOutcome","isPlainObject","validIdentifier","isArray","indexOf","validateOutcomes","outcomes","valid","forEach","outcomeValidator","forceArray","qtiElementHelper","create","properties","element","\"qti-type\"","TypeError","find","collection","checkType","qtiElement","types","found","lookupElement","tree","path","nodeName","steps","split","nodeNames","len","i","key","has","lookupProperty","result","propertyName","pop","outcomeHelper","getProcessingRuleExpression","outcomeRule","getProcessingRuleProperty","getOutcomeIdentifier","isObject","getOutcomeDeclarations","testModel","outcomeDeclarations","getOutcomeProcessingRules","rules","outcomeProcessing","outcomeRules","eachOutcomeDeclarations","cb","eachOutcomeProcessingRules","eachOutcomeProcessingRuleExpressions","browseExpressions","processingRule","expression","expressions","listOutcomes","isFunction","push","removeOutcomes","declarations","check","keyBy","remove","createOutcome","views","interpretation","longInterpretation","normalMaximum","normalMinimum","masteryValue","baseType","addOutcomeProcessing","createOutcomeProcessing","addOutcome","replaceOutcomes","concat","isCategoryOption","category","eachCategories","getCategoriesRecursively","section","sectionParts","sectionPart","categories","testParts","testPart","assessmentSections","assessmentSection","listCategories","keys","listOptions","options","eventifier","statifier","categoryHelper","modelOverseerFactory","model","modelOverseer","getModel","setModel","newModel","trigger","getConfig","getOutcomesList","map","declaration","getOutcomesNames","getCategories","getOptions","$","areaBrokerFactory","testCreatorFactory","$creatorContainer","loadModelOverseer","loadAreaBroker","$container","creator","itemSelectorPanel","contentCreatorPanel","propertyPanel","elementPropertyPanel","testCreator","setTestModel","m","getAreaBroker","getModelOverseer","isTestHasErrors","__","urlUtil","request","defaultConfig","getItemClasses","url","route","getItems","getItemClassProperties","testItemProviderFactory","params","classUri","loggerFactory","resourceSelectorFactory","feedback","logger","testItemProvider","onError","err","error","message","ITEM_URI","itemView","filters","BRS","selectorConfig","selectionMode","selectionModes","multiple","selectAllPolicy","selectAllPolicies","visible","classes","label","uri","resourceSelector","on","clearSelection","then","items","update","catch","updateFilters","values","children","node","addClassNode","hb","template","Handlebars","depth0","helpers","partials","data","compilerInfo","merge","buffer","functionType","escapeExpression","helperMissing","stack1","helper","call","hash","title","href","dompurify","program1","program3","self","each","inverse","program","fn","lateSubmission","noop","program4","program6","modes","program7","program10","selected","program8","description","program12","showIdentifier","showTimeLimits","scoring","showOutcomeDeclarations","program5","equal","submissionMode","maxAttempts","itemSessionShowFeedback","itemSessionAllowComment","itemSessionAllowSkipping","program14","program15","navigationMode","submissionModeVisible","showItemSessionControl","program9","program11","program13","program17","program19","program21","program22","isSubsection","showVisible","showKeepTogether","hasBlueprint","hasSelectionWithReplacement","validateResponsesVisible","isLinear","program16","unless","program18","showReference","weightsVisible","orderIndex","classVisible","depth1","groupId","groupLabel","presets","programWithDepth","program2","depth2","qtiCategory","id","includes","presetGroups","testId","icon","rubricBlock","itemRef","testProps","testPartProps","sectionProps","itemRefProps","itemRefPropsWeight","rubricBlockProps","categoryPresets","subsection","menuButton","applyTemplateConfiguration","testpart","itemref","rubricblock","itemrefweight","categorypresets","ui","DataBinder","templates","propView","tmplName","propValidation","$view","e","isValid","$warningIconSelector","$test","parents","errors","currentTarget","currentTargetId","attr","slice","namespace","hasClass","first","css","groupValidator","events","open","propOpen","binderOptions","hide","appendTo","filter","startDomComponent","databinder","bind","$identifier","text","getView","propGetView","isOpen","propIsOpen","onOpen","propOnOpen","stopPropagation","onClose","propOnClose","destroy","propDestroy","toggle","propToggle","not","show","isFistLevelSubsection","$subsection","isNestedSubsection","getSubsections","$section","getSiblingSubsections","getSubsectionContainer","getParentSubsection","getParentSection","getParent","getSubsectionTitleIndex","$parentSection","index","sectionIndex","$parentSubsection","subsectionIndex","propertyView","subsectionsHelper","preventDefault","$elt","disabledClass","blur","addClass","activeClass","btnOnClass","removeClass","move","$actionContainer","containerClass","elementClass","$element","closest","click","$elements","is","fadeOut","insertBefore","fadeIn","insertAfter","movable","actionContainerElt","$moveUp","$moveDown","removable","$delete","disable","enable","displayItemWrapper","subsectionsCount","updateDeleteSelector","$deleteButton","deleteSelector","displayCategoryPresets","$authoringContainer","scope","$propertiesView","updateTitleIndex","$list","$indexSpan","$parent","features","addTestVisibilityProps","propertyNamespace","isVisible","addTestPartVisibilityProps","showNavigationWarnings","addSectionVisibilityProps","rubricBlocksClass","addItemRefVisibilityProps","filterVisiblePresets","level","categoryGroupNamespace","categoryPresetNamespace","filteredGroups","presetGroup","filteredGroup","filteredPresets","preset","confirmDialog","tooltip","featureVisibility","categorySelectorFactory","$presetsContainer","$customCategoriesSelect","categorySelector","updateCategories","presetSelected","toArray","categoryEl","presetIndeterminate","customSelected","siblings","textContent","customIndeterminate","selectedCategories","indeterminatedCategories","createForm","currentCategories","presetsTpl","customCategories","difference","allQtiCategoriesPresets","allPresets","append","$preset","target","$checkbox","prop","defer","select2","width","containerCssClass","tags","tokenSeparators","formatNoMatches","maximumInputLength","$choice","tag","lookup","updateFormState","indeterminate","$presetsCheckboxes","idx","input","categoryToPreset","checked","get","hasCategory","some","li","$li","content","Map","setPresets","Array","from","reduce","allCategories","group","all","altCategories","set","errorHandler","isValidSectionModel","setCategories","toRemove","toAdd","propagated","addCategories","removeCategories","itemCount","arrays","union","throw","_ns","getCategoriesRecursive","sectionModel","compact","createCategories","apply","intersection","mapValues","sort","validators","qtiIdentifier","idFormatValidator","invalidQtiIdMessage","validate","callback","qtiIdPattern","testidFormatValidator","qtiTestIdPattern","idAvailableValidator","toUpperCase","identifiers","extractIdentifiers","qtiTypesForUniqueIds","$idInUI","counts","countBy","Error","registerValidators","register","validateModel","nonUniqueIdentifiers","messageDetails","count","rubricBlocks","feedbackBlock","outcomeIdentifier","includesOnlyTypes","excludeTypes","extract","originalIdentifier","subElement","checkIfItemIdValid","pattern","qtiTestHelper","getIdentifiers","uniq","getIdentifiersOf","qtiType","filterQtiType","addMissingQtiType","parentType","isNumber","replace","consolidateModel","ordering","shuffle","part","getAvailableIdentifier","suggestion","glue","current","actions","sectionCategory","setUp","creatorContext","refModel","partModel","$itemRef","timeLimitsProperty","$minTimeContainer","$maxTimeContainer","$lockedTimeContainer","$locker","$durationFields","$minTimeField","$maxTimeField","minToMaxHandler","throttle","minToMax","off","val","maxToMinHandler","maxToMin","lockTimers","unlockTimers","toggleTimeContainers","guidedNavigation","timeLimits","minTime","maxTime","parent","categoriesProperty","$categoryField","join","weightsProperty","$weightList","weightTpl","defaultData","weights","propHandler","removePropHandler","$deletedNode","validIds","deletedNodeId","itemSessionControl","resize","$actions","innerWidth","outerWidth","listenActionState","document","$target","$refs","DOMPurify","getAttributes","object","omit","attrToStr","attributes","acc","isBoolean","isEmpty","normalizeNodeName","normalized","toLocaleLowerCase","normalizedNodes","feedbackblock","outcomeidentifier","showhide","printedvariable","powerform","mappingindicator","typedAttributes","printedVariable","powerForm","base","delimiter","field","mappingIndicator","encode","modelValue","startTag","decode","nodeValue","$nodeValue","elt","nodeType","sanitize","xmlBase","class","lang","transform","attrName","childNodes","uuid","qtiCommonRenderer","containerEditor","editorId","removePlugins","toolbar","setContext","getContentCreatorPanelArea","metadata","getOutcomes","change","resetRenderer","autofocus","hider","dialogAlert","namespaceHelper","Dom2QtiEncoder","qtiContentCreator","rubricModel","$rubricBlock","bindEvent","$el","eventName","namespaceAll","uid","ensureWrap","html","charAt","editorToModel","rubric","wrapper","modelToEditor","$rubricBlockContent","editorContent","updateFeedback","activated","matchValue","changeFeedback","$feedbackOutcomeLine","$feedbackMatchLine","changeHandler","changedModel","updateOutcomes","$feedbackOutcome","minimumResultsForSearch","$feedbackActivated","rbViews","$propContainer","$select","uniqueId","before","feedbackOutcome","changedRubricBlock","getPropertyPanelArea","CKEDITOR","getWindow","fire","eachItemInTest","testPartModel","eachItemInTestPart","eachItemInSection","sectionPartModel","testModelHelper","isValidTestPartModel","every","itemRefCategories","setBlueprint","blueprint","getBlueprint","getUrl","ajax","dataType","fail","itemRefView","rubricBlockView","sectionBlueprint","servicesFeatures","subsectionModel","$selectionSwitcher","$selectionSelect","$selectionWithRep","isSelectionFromServer","selection","switchSelection","incrementer","updateModel","$title","$titleWithActions","blueprintProperty","subsections","$sub2section","itemRefs","$itemRefsWrapper","labels","acceptItemRefs","$itemsPanel","$placeholder","$placeholders","size","defaultItemData","clone","defaultsConfigs","item","itemData","addItemRef","itemRefModel","$refList","$items","eq","$rubBlocks","addRubricBlock","adder","templateData","initBlueprint","routes","blueprintByTestSection","success","blueprintsById","delay","method","results","minimumInputLength","allowClear","placeholder","addSubsection","optionsConfirmDialog","buttons","ok","cancel","removeData","sectionTitlePrefix","checkAndCallAdd","executeAdd","confirmMessage","acceptFunction","setTimeout","empty","getDom","sub2sectionIndex","sub2sectionModel","$subsections","$AllNestedSubsections","testPartCategory","subsectionView","partCategories","$sections","$sectionsAndsubsections","$sectionsContainer","$newSection","sectionView","$testPart","sections","addSection","sectionIdPrefix","categoriesSummary","$testParts","testPartView","testView","changeScoring","noOptions","weightVisible","newScoringState","JSON","stringify","$cutScoreLine","$categoryScoreLine","$weightIdentifierLine","$descriptions","scoringState","$panel","$generate","removeAttr","addTestPart","testPartIndex","partIdPrefix","added","unaryOperator","processingRuleHelper","minOperands","maxOperands","addTypeAndCardinality","binaryOperator","left","right","acceptedCardinalities","acceptedBaseTypes","includeCategories","excludeCategories","addSectionIdentifier","sectionIdentifier","addWeightIdentifier","weightIdentifier","validateOutcomeList","setExpression","addExpression","setOutcomeValue","gte","lte","divide","sum","terms","testVariables","variableIdentifier","outcomeMaximum","numberPresented","baseValue","variable","match","isNull","outcomeCondition","outcomeIf","outcomeElse","outcomeElseIfs","instruction","format","_Mathmax","Math","max","addTotalScoreOutcomes","weight","scoreIdentifier","addMaxScoreOutcomes","addRatioOutcomes","identifierTotal","identifierMax","addCutScoreOutcomes","countIdentifier","cutScore","addGlobalCutScoreOutcomes","ratioIdentifier","addFeedbackScoreOutcomes","passed","notPassed","formatCategoryOutcome","belongToRecipe","recipe","onlyCategories","signature","weighted","matchRecipe","outcomeRecipe","matchRecipeOutcome","outcomeMatch","categoryIdentifier","categoryWeighted","categoryFeedback","include","outcomesRecipes","signatures","getSignatures","signatureMatch","getRecipes","descriptors","getOutcomesRecipe","mode","processingRecipe","listScoringModes","handleCategories","categoryScore","hasCategoryOutcome","categoryOutcomes","outcomeDeclaration","getCutScore","defaultCutScore","getWeightIdentifier","getOutcomeProcessing","processing","diff","included","detectScoring","processingModes","custom","removeScoring","scoringOutcomes","defaultScoreIdentifier","none","total","cut","clean","writer","scoreWeighted","feedbackOk","feedbackFailed","outcomesWriters","ratio","writerRatio","descriptor","writerTotal","writerMax","writerCut","totalModeOutcomes","whichOutcome","categoryOutcome","categoryOutcomeIdentifier","categoryScoreIdentifier","categoryCountIdentifier","scoringHelper","init","generate","recipes","dialog","changeTrackerFactory","container","wrapperSelector","eventNS","asking","originalTest","changeTracker","getSerializedTest","install","window","hasChanged","messages","leave","uninstall","contains","classList","stopImmediatePropagation","confirmBefore","whatToDo","ifWantSave","leavingStatus","warning","Promise","resolve","reject","confirmDlg","leaveWhenInvalid","buttonsYesNo","autoRender","autoDestroy","onDontsaveBtn","onCancelBtn","buttonsCancelSave","onSaveBtn","currentTest","serialized","preview","exit","close","DataBindController","qtiTestCreatorFactory","itemrefView","previewerFactory","Controller","start","$saver","$menu","$back","binder","categoriesPresets","previewId","createPreviewButton","translate","btnIdx","$button","Object","previewButtons","providers","isTestContainsItems","$previewer","isItemRef","isSection","encoders","dom2qti","beforeSave","takeControl","save","provider","saveUrl","testUri","decodeURIComponent","readOnly","fullPage","pluginsOptions","history","back","event","ckConfigurator","editor","toolbarType","underline","Creator","XmlEditor","edit","context","router","loadingBar","locale","providerLoader","runner","requiredOptions","exitReason","serviceCallId","plugins","preventFeedback","errorFeedback","reason","exitUrl","build","location","displayMessage","onFeedback","onWarning","typeMap","loggerByType","feedbackByType","stop","code","timeout","moduleConfig","dir","getLanguageDirection","option","extraRoutes","dispatch","bundle","testRunnerConfig","renderTo","proxy","getAvailableProviders","loadedProxies","debug","after","removeAllListeners","isValidConfig","toolconfig","hook","triggerItemLoaded","tool","itemLoaded","initQtiTool","$toolsContainer","testContext","testRunner","itemIsLoaded","tools","require","$existingBtn","isValidHook","clear","render","_appendInOrder","order","$after","$before","prepend","$btn","_order","$doc","actionBarHook","actionBarTools","registerTools","registeredQtiTools","getRegisteredTools","getRegistered","isRegistered","qtiTools","list","promises","forIn","getId","region","hidden","active","program29","answered","flagged","viewed","position","program24","program27","program25","itemId","parts","navigatorTpl","navigatorTreeTpl","capitalize","_cssCls","collapsed","collapsible","disabled","unseen","testSection","_selectors","component","filterBar","collapseHandle","linearState","infoAnswered","infoViewed","infoUnanswered","infoFlagged","infoPanel","infoPanelLabels","partLabels","sectionLabels","itemLabels","itemIcons","icons","linearStart","counters","actives","collapsiblePanels","notFlagged","notAnswered","_filterMap","unanswered","filtered","_optionsMap","reviewScope","reviewPreventsUnseen","canCollapse","_reviewScopes","testReview","initOptions","putOnRight","insertMethod","currentFilter","$component","_loadDOM","_initEvents","_updateDisplayOptions","$infoAnswered","$infoViewed","$infoUnanswered","$infoFlagged","$filterBar","$filters","$tree","$linearState","toggleClass","_openSelected","_togglePanel","_openOnly","$item","_mark","_select","_jump","_filter","criteria","_updateSectionCounters","jquery","hierarchy","parentsUntil","add","opened","root","panel","collapseSelector","_setItemIcon","_adjustItemIcon","defaultIcon","iconCls","cls","_toggleFlag","flag","itemPosition","$filtered","nb","_writeCount","scopeClass","$root","_updateOptions","optionKey","contextKey","_updateInfos","progression","$place","_getProgressionOfTest","numberItems","numberCompleted","numberFlagged","_getProgressionOfTestPart","numberItemsPart","numberCompletedPart","numberPresentedPart","numberFlaggedPart","_getProgressionOfTestSection","numberItemsSection","numberCompletedSection","numberPresentedSection","numberFlaggedSection","_updateTree","navigatorMap","reviewScopePart","reviewScopeSection","_partsFilter","preventsUnseen","setItemFlag","updateNumberFlagged","fields","currentPosition","currentFound","currentSection","currentPart","itemFound","itemSection","itemPart","getProgression","progressInfoMethod","dom","extraParameters","testReviewFactory","_Mathfloor","floor","_Mathmax2","progressUpdaters","progressBar","progressLabel","progressbar","write","progressIndicator","progressIndicatorMethod","percentageProgression","positionProgression","progressScope","progressIndicatorScope","progressScopeCounter","counter","progressUpdaterFactory","updater","testMetaDataFactory","_testServiceCallId","testServiceCallId","testMetaData","setData","getLocalStorageData","setLocalStorageData","currentKey","getLocalStorageKey","localStorage","setItem","domException","removed","removeItem","getItem","parse","_storageKeyPrefix","_data","SECTION_EXIT_CODE","COMPLETED_NORMALLY","QUIT","COMPLETE_TIMEOUT","TIMEOUT","FORCE_QUIT","IN_PROGRESS","ERROR","TEST_EXIT_CODE","COMPLETE","TERMINATED","INCOMPLETE","INCOMPLETE_QUIT","INACTIVE","CANDIDATE_DISAGREED_WITH_NDA","getTestServiceCallId","setTestServiceCallId","addData","overwrite","getData","clearData","progressUpdater","ServiceApi","UserInfoService","StateStorage","iframeNotifier","MathJax","deleter","moment","modal","_Mathfloor2","_Mathmax3","timerIds","currentTimes","lastDates","timeDiffs","waitingTime","optionNextSection","optionNextSectionWarning","optionReviewScreen","optionEndTestWarning","optionNoExitTimedSectionWarning","TestRunner","TEST_STATE_INITIAL","TEST_STATE_INTERACTING","TEST_STATE_MODAL_FEEDBACK","TEST_STATE_SUSPENDED","TEST_STATE_CLOSED","TEST_NAVIGATION_LINEAR","TEST_NAVIGATION_NONLINEAR","TEST_ITEM_STATE_INTERACTING","beforeTransition","disableGui","$controls","$itemFrame","$rubricBlocks","$timerWrapper","afterTransition","enableGui","ITEM","ITEM_START_TIME_CLIENT","Date","now","jump","action","isJumpOutOfSection","isCurrentItemActive","isTimedSection","exitTimedSection","killItemSession","actionCall","keepItemTimed","duration","markForReview","markForReviewUrl","cache","async","itemFlagged","updateTools","moveForward","doExitSection","isTimeout","exitSection","itemPositionSection","shouldDisplayEndTestWarning","displayEndTestWarning","isLast","hasOption","nextAction","confirmLabel","cancelLabel","showItemCount","displayExitMessage","moveBackward","jumpPosition","getCurrentSectionItems","isJumpToOtherSection","isValidPosition","hasOwnProperty","exitCode","SECTION","qtiRunner","getQtiRunner","doExitTimedSection","updateItemApi","keepTimerUpToTimeout","nextSection","doNextSection","scopeSuffixMap","scopeSuffix","$confirmBox","unansweredCount","flaggedCount","isCurrentItemAnswered","toString","ITEM_END_TIME_CLIENT","ITEM_TIMEZONE","utcOffset","itemServiceApi","kill","itemSessionState","getCurrentItemState","state","response","itemFrame","getElementById","itemWindow","contentWindow","itemContainerFrame","itemContainerWindow","timeConstraints","qtiClassName","partId","testPartId","navMap","sectionItems","partIndex","skip","doSkip","updateTimer","confirmBox","confirmBtn","setTestContext","eval","itemServiceApiCall","setHasBeenPaused","hasBeenPaused","initMetadata","getSessionStateService","sessionStateService","accuracy","$runner","restart","updateContext","updateProgress","updateNavigation","updateTestReview","updateInformation","updateRubrics","updateExitButton","resetCurrentItemState","$contentBox","adjustFrame","visibility","loadInto","itemIdentifier","showSkip","showSkipEnd","showNextSection","allowSkipping","$skip","$skipEnd","$nextSection","createTimer","cst","$timer","$label","$time","formatTime","seconds","hasTimers","clearTimeout","$topActionBar","allowLateSubmission","timerIndex","timerWarning","warnings","showed","point","closestPreviousWarning","setInterval","_Mathround","round","getTime","$timers","clearInterval","findLast","timeWarning","remaining","humanize","rubrics","prependTo","Hub","Queue","$exit","$moveForward","$moveEnd","$moveBackward","canMoveBackward","considerProgress","$progressBox","testTitle","sectionText","isDeepestSectionVisible","sectionTitle","$position","$titleGroup","$logout","logoutButton","exitButton","rubricHeight","outerHeight","finalHeight","innerHeight","$bottomActionBar","frameContentHeight","height","$sideBars","$sideBar","contents","$naviButtons","hideGui","showGui","totalSeconds","sec_num","hours","minutes","time","processError","serviceApi","finish","extraParams","metaData","TEST","setCurrentItemState","currentItemState","$skipButtons","$forwardButtons","$progressBar","$progressLabel","$contentPanel","ajaxError","jqxhr","status","onServiceApiReady","getDuration","reviewScreen","reviewRegion","reviewCanCollapse","animate","opacity","responseId","c","d","a","s","createElement","getElementsByTagName","appendChild","styleSheet","cssText","createTextNode"],"sources":["/Users/oat/Projects/terre/playground/files/delivery/tao/views/build/config-wrap-start-default.js","../controller/creator/areaBroker.js","../controller/creator/config/defaults.js","../controller/creator/helpers/baseType.js","../controller/creator/helpers/cardinality.js","../controller/creator/helpers/outcomeValidator.js","../controller/creator/helpers/qtiElement.js","../controller/creator/helpers/outcome.js","../controller/creator/helpers/category.js","../controller/creator/modelOverseer.js","../controller/creator/qtiTestCreator.js","../provider/testItems.js","../controller/creator/views/item.js","../controller/creator/templates/testpart!tpl","../controller/creator/templates/section!tpl","../controller/creator/templates/rubricblock!tpl","../controller/creator/templates/itemref!tpl","../controller/creator/templates/outcomes!tpl","tpl!taoQtiTest/controller/creator/templates/test-props","tpl!taoQtiTest/controller/creator/templates/testpart-props","tpl!taoQtiTest/controller/creator/templates/section-props","tpl!taoQtiTest/controller/creator/templates/itemref-props","tpl!taoQtiTest/controller/creator/templates/itemref-props-weight","tpl!taoQtiTest/controller/creator/templates/rubricblock-props","tpl!taoQtiTest/controller/creator/templates/category-presets","../controller/creator/templates/subsection!tpl","tpl!taoQtiTest/controller/creator/templates/menu-button","../controller/creator/templates/index.js","../controller/creator/views/property.js","../controller/creator/helpers/subsection.js","../controller/creator/views/actions.js","../controller/creator/helpers/featureVisibility.js","../controller/creator/helpers/categorySelector.js","../controller/creator/helpers/sectionCategory.js","../controller/creator/helpers/validators.js","../controller/creator/helpers/qtiTest.js","../controller/creator/views/itemref.js","../controller/creator/encoders/dom2qti.js","../controller/creator/qtiContentCreator.js","../controller/creator/views/rubricblock.js","../controller/creator/helpers/testModel.js","../controller/creator/helpers/testPartCategory.js","../controller/creator/helpers/sectionBlueprints.js","../controller/creator/views/subsection.js","../controller/creator/views/section.js","../controller/creator/views/testpart.js","../controller/creator/views/test.js","../controller/creator/helpers/processingRule.js","../controller/creator/helpers/scoring.js","../controller/creator/helpers/changeTracker.js","../controller/creator/creator.js","../controller/creator/helpers/ckConfigurator.js","../controller/routes.js","css!taoQtiTestCss/new-test-runner","../controller/runner/runner.js","../testRunner/actionBarHook.js","../testRunner/actionBarTools.js","../testRunner/tpl/navigator!tpl","../testRunner/tpl/navigatorTree!tpl","../testRunner/testReview.js","../testRunner/progressUpdater.js","../testRunner/testMetaData.js","../controller/runtime/testRunner.js","onLayerEnd0.js","module-create.js","/Users/oat/Projects/terre/playground/files/delivery/tao/views/build/config-wrap-end-default.js"],"sourcesContent":["\n","/*\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017 (original work) Open Assessment Technlogies SA\n *\n */\n\n/**\n *\n * Defines the test creator areas\n *\n * @author Christophe Noël \n */\ndefine('taoQtiTest/controller/creator/areaBroker',[\n 'lodash',\n 'ui/areaBroker'\n], function (_, areaBroker) {\n 'use strict';\n\n var requireAreas = [\n 'creator',\n 'itemSelectorPanel',\n 'contentCreatorPanel',\n 'propertyPanel',\n 'elementPropertyPanel'\n ];\n\n /**\n * Creates an area broker with the required areas for the item creator\n *\n * @see ui/areaBroker\n *\n * @param {jQueryElement|HTMLElement|String} $container - the main container\n * @param {Object} mapping - keys are the area names, values are jQueryElement\n * @returns {broker} the broker\n * @throws {TypeError} without a valid container\n */\n return _.partial(areaBroker, requireAreas);\n\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2020 (original work) Open Assessment Technologies SA;\n *\n * @author Sergei Mikhailov \n */\n\ndefine('taoQtiTest/controller/creator/config/defaults',[\n 'lodash',\n 'module',\n], function(\n _,\n module\n) {\n 'use strict';\n\n return (config = {}) => _.defaults({}, module.config(), config);\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;\n */\n/**\n * The BaseType enumeration (port of \\qtism\\common\\enums\\BaseType).\n *\n * From IMS QTI:\n *\n * A base-type is simply a description of a set of atomic values (atomic to this specification).\n * Note that several of the baseTypes used to define the runtime data model have identical\n * definitions to those of the basic data types used to define the values for attributes\n * in the specification itself. The use of an enumeration to define the set of baseTypes\n * used in the runtime model, as opposed to the use of classes with similar names, is\n * designed to help distinguish between these two distinct levels of modelling.\n *\n * @author Jérôme Bogaerts \n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/controller/creator/helpers/baseType',[\n 'lodash'\n], function (_) {\n 'use strict';\n\n /**\n * The list of QTI base types\n * @type {Object}\n */\n var baseTypeEnum = {\n /**\n * From IMS QTI:\n *\n * The set of identifier values is the same as the set of values\n * defined by the identifier class.\n *\n * @type {Number}\n */\n IDENTIFIER: 0,\n\n /**\n * From IMS QTI:\n *\n * The set of boolean values is the same as the set of values defined\n * by the boolean class.\n *\n * @type {Number}\n */\n BOOLEAN: 1,\n\n /**\n * From IMS QTI:\n *\n * The set of integer values is the same as the set of values defined\n * by the integer class.\n *\n * @type {Number}\n */\n INTEGER: 2,\n\n /**\n * From IMS QTI:\n *\n * The set of float values is the same as the set of values defined by the\n * float class.\n *\n * @type {Number}\n */\n FLOAT: 3,\n\n /**\n * From IMS QTI:\n *\n * The set of string values is the same as the set of values defined by the\n * string class.\n *\n * @type {Number}\n */\n STRING: 4,\n\n /**\n * From IMS QTI:\n *\n * A point value represents an integer tuple corresponding to a graphic point.\n * The two integers correspond to the horizontal (x-axis) and vertical (y-axis)\n * positions respectively. The up/down and left/right senses of the axes are\n * context dependent.\n *\n * @type {Number}\n */\n POINT: 5,\n\n /**\n * From IMS QTI:\n *\n * A pair value represents a pair of identifiers corresponding to an association\n * between two objects. The association is undirected so (A,B) and (B,A) are equivalent.\n *\n * @type {Number}\n */\n PAIR: 6,\n\n /**\n * From IMS QTI:\n *\n * A directedPair value represents a pair of identifiers corresponding to a directed\n * association between two objects. The two identifiers correspond to the source\n * and destination objects.\n *\n * @type {Number}\n */\n DIRECTED_PAIR: 7,\n\n /**\n * From IMS QTI:\n *\n * A duration value specifies a distance (in time) between two time points.\n * In other words, a time period as defined by [ISO8601], but represented as\n * a single float that records time in seconds. Durations may have a fractional\n * part. Durations are represented using the xsd:double datatype rather than\n * xsd:dateTime for convenience and backward compatibility.\n *\n * @type {Number}\n */\n DURATION: 8,\n\n /**\n * From IMS QTI:\n *\n * A file value is any sequence of octets (bytes) qualified by a content-type and an\n * optional filename given to the file (for example, by the candidate when uploading\n * it as part of an interaction). The content type of the file is one of the MIME\n * types defined by [RFC2045].\n *\n * @type {Number}\n */\n FILE: 9,\n\n /**\n * From IMS QTI:\n *\n * A URI value is a Uniform Resource Identifier as defined by [URI].\n *\n * @type {Number}\n */\n URI: 10,\n\n /**\n * From IMS QTI:\n *\n * An intOrIdentifier value is the union of the integer baseType and\n * the identifier baseType.\n *\n * @type {Number}\n */\n INT_OR_IDENTIFIER: 11,\n\n /**\n * In qtism, we consider an extra 'coords' baseType.\n *\n * @type {Number}\n */\n COORDS: 12,\n\n /**\n * Express that the operands can have any BaseType from the BaseType enumeration and\n * can be different.\n *\n * @type {Number}\n */\n ANY: 12,\n\n /**\n * Express that all the operands must have the same\n * baseType.\n *\n * @type {Number}\n */\n SAME: 13\n };\n\n var baseTypeHelper = _({\n /**\n * Gets the the list of QTI base types\n * @returns {Object}\n */\n asArray: function asArray() {\n return baseTypeEnum;\n },\n\n /**\n * Gets a valid type or the default\n * @param {String|Number} type\n * @param {String|Number} [defaultType]\n * @returns {*}\n */\n getValid: function getValid(type, defaultType) {\n if (_.isFinite(type)) {\n if (!baseTypeHelper.getNameByConstant(type)) {\n type = false;\n }\n } else {\n type = baseTypeHelper.getConstantByName(type);\n }\n\n if (false === type) {\n if (!_.isUndefined(defaultType) && defaultType !== -1) {\n type = baseTypeHelper.getValid(defaultType, -1);\n } else {\n type = -1;\n }\n }\n\n return type;\n },\n\n /**\n * Adjusts a value with respect to the type\n * @param {String|Number} type\n * @param {*} value\n * @returns {*}\n */\n getValue: function getValue(type, value) {\n if (_.isString(type)) {\n type = baseTypeHelper.getConstantByName(type);\n }\n\n switch (type) {\n case baseTypeEnum.URI:\n case baseTypeEnum.STRING:\n case baseTypeEnum.IDENTIFIER:\n return value + '';\n\n case baseTypeEnum.BOOLEAN:\n if (_.isString(value)) {\n switch (value.toLowerCase()) {\n case 'true':\n return true;\n case 'false':\n return false;\n }\n }\n return !!value;\n\n case baseTypeEnum.INTEGER:\n return parseInt(value, 10) || 0;\n\n case baseTypeEnum.FLOAT:\n return parseFloat(value) || 0;\n\n case baseTypeEnum.INT_OR_IDENTIFIER:\n if (!_.isNaN(parseInt(value, 10))) {\n return parseInt(value, 10) || 0;\n } else {\n return '' + value;\n }\n }\n\n return value;\n },\n\n /**\n * Get a constant value from the BaseType enumeration by baseType name.\n *\n * * 'identifier' -> baseTypes.IDENTIFIER\n * * 'boolean' -> baseTypes.BOOLEAN\n * * 'integer' -> baseTypes.INTEGER\n * * 'float' -> baseTypes.FLOAT\n * * 'string' -> baseTypes.STRING\n * * 'point' -> baseTypes.POINT\n * * 'pair' -> baseTypes.PAIR\n * * 'directedPair' -> baseTypes.DIRECTED_PAIR\n * * 'duration' -> baseTypes.DURATION\n * * 'file' -> baseTypes.FILE\n * * 'uri' -> baseTypes.URI\n * * 'intOrIdentifier' -> baseTypes.INT_OR_IDENTIFIER\n * * extra 'coords' -> baseTypes.COORDS\n *\n * @param {String} name The baseType name.\n * @return {Number|Boolean} The related enumeration value or `false` if the name could not be resolved.\n */\n getConstantByName: function getConstantByName(name) {\n switch (String(name).trim().toLowerCase()) {\n case 'identifier':\n return baseTypeEnum.IDENTIFIER;\n\n case 'boolean':\n return baseTypeEnum.BOOLEAN;\n\n case 'integer':\n return baseTypeEnum.INTEGER;\n\n case 'float':\n return baseTypeEnum.FLOAT;\n\n case 'string':\n return baseTypeEnum.STRING;\n\n case 'point':\n return baseTypeEnum.POINT;\n\n case 'pair':\n return baseTypeEnum.PAIR;\n\n case 'directedpair':\n return baseTypeEnum.DIRECTED_PAIR;\n\n case 'duration':\n return baseTypeEnum.DURATION;\n\n case 'file':\n return baseTypeEnum.FILE;\n\n case 'uri':\n return baseTypeEnum.URI;\n\n case 'intoridentifier':\n return baseTypeEnum.INT_OR_IDENTIFIER;\n\n case 'coords':\n return baseTypeEnum.COORDS;\n\n case 'any':\n return baseTypeEnum.ANY;\n\n case 'same':\n return baseTypeEnum.SAME;\n\n default:\n return false;\n }\n },\n\n /**\n * Get the QTI name of a BaseType.\n *\n * @param {Number} constant A constant value from the BaseType enumeration.\n * @param {Boolean} [operator] A flag that allow to switch between operator an value types to prevent duplicate name issue\n * @return {String|Boolean} The QTI name or false if not match.\n */\n getNameByConstant: function getNameByConstant(constant, operator) {\n switch (constant) {\n case baseTypeEnum.IDENTIFIER:\n return 'identifier';\n\n case baseTypeEnum.BOOLEAN:\n return 'boolean';\n\n case baseTypeEnum.INTEGER:\n return 'integer';\n\n case baseTypeEnum.FLOAT:\n return 'float';\n\n case baseTypeEnum.STRING:\n return 'string';\n\n case baseTypeEnum.POINT:\n return 'point';\n\n case baseTypeEnum.PAIR:\n return 'pair';\n\n case baseTypeEnum.DIRECTED_PAIR:\n return 'directedPair';\n\n case baseTypeEnum.DURATION:\n return 'duration';\n\n case baseTypeEnum.FILE:\n return 'file';\n\n case baseTypeEnum.URI:\n return 'uri';\n\n case baseTypeEnum.INT_OR_IDENTIFIER:\n return 'intOrIdentifier';\n\n case baseTypeEnum.COORDS:\n case baseTypeEnum.ANY:\n if (operator) {\n return 'any';\n } else {\n return 'coords';\n }\n\n case baseTypeEnum.SAME:\n return 'same';\n\n default:\n return false;\n }\n }\n }).assign(baseTypeEnum).value();\n\n return baseTypeHelper;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;\n */\n/**\n * The Cardinality enumeration (port of \\qtism\\common\\enums\\Cardinality).\n *\n * From IMS QTI:\n *\n * An expression or itemVariable can either be single-valued or multi-valued. A multi-valued\n * expression (or variable) is called a container. A container contains a list of values,\n * this list may be empty in which case it is treated as NULL. All the values in a multiple\n * or ordered container are drawn from the same value set, however, containers may contain\n * multiple occurrences of the same value. In other words, [A,B,B,C] is an acceptable value\n * for a container. A container with cardinality multiple and value [A,B,C] is equivalent\n * to a similar one with value [C,B,A] whereas these two values would be considered distinct\n * for containers with cardinality ordered. When used as the value of a response variable\n * this distinction is typified by the difference between selecting choices in a multi-response\n * multi-choice task and ranking choices in an order objects task. In the language of [ISO11404]\n * a container with multiple cardinality is a \"bag-type\", a container with ordered cardinality\n * is a \"sequence-type\" and a container with record cardinality is a \"record-type\".\n *\n * The record container type is a special container that contains a set of independent values\n * each identified by its own identifier and having its own base-type. This specification\n * does not make use of the record type directly however it is provided to enable\n * customInteractions to manipulate more complex responses and customOperators to\n * return more complex values, in addition to the use for detailed information about\n * numeric responses described in the stringInteraction abstract class.\n *\n * @author Jérôme Bogaerts \n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/controller/creator/helpers/cardinality',[\n 'lodash'\n], function (_) {\n 'use strict';\n\n /**\n * The list of QTI cardinalities\n * @type {Object}\n */\n var cardinalityEnum = {\n /**\n * Single term cardinality\n *\n * @type {Number}\n */\n SINGLE: 0,\n\n /**\n * Multiple terms cardinality\n *\n * @type {Number}\n */\n MULTIPLE: 1,\n\n /**\n * Ordered terms cardinality\n *\n * @type {Number}\n */\n ORDERED: 2,\n\n /**\n * Record term cardinality\n *\n * @type {Number}\n */\n RECORD: 3,\n\n /**\n * Express that all the expressions involved in an operator have\n * the same cardinality.\n *\n * @type {Number}\n */\n SAME: 4,\n\n /**\n * Express that all the expressions involved in an operator may\n * have any cardinality.\n *\n * @type {Number}\n */\n ANY: 5\n };\n\n var cardinalityHelper = _({\n /**\n * Gets the the list of QTI cardinalities\n * @returns {Object}\n */\n asArray: function asArray() {\n return cardinalityEnum;\n },\n\n /**\n * Gets a valid cardinality or the default\n * @param {String|Number} cardinality\n * @param {String|Number} [defaultCardinality]\n * @returns {*}\n */\n getValid: function getValid(cardinality, defaultCardinality) {\n if (_.isFinite(cardinality)) {\n if (!cardinalityHelper.getNameByConstant(cardinality)) {\n cardinality = false;\n }\n } else {\n cardinality = cardinalityHelper.getConstantByName(cardinality);\n }\n\n if (false === cardinality) {\n if (!_.isUndefined(defaultCardinality) && defaultCardinality !== cardinalityEnum.SINGLE) {\n cardinality = cardinalityHelper.getValid(defaultCardinality, cardinalityEnum.SINGLE);\n } else {\n cardinality = cardinalityEnum.SINGLE;\n }\n }\n\n return cardinality;\n },\n\n /**\n * Get a constant value from its name.\n *\n * @param {String} name The name of the constant, as per QTI spec.\n * @return {Number|Boolean} The constant value or `false` if not found.\n */\n getConstantByName: function getConstantByName(name) {\n switch (String(name).trim().toLowerCase()) {\n case 'single':\n return cardinalityEnum.SINGLE;\n\n case 'multiple':\n return cardinalityEnum.MULTIPLE;\n\n case 'ordered':\n return cardinalityEnum.ORDERED;\n\n case 'record':\n return cardinalityEnum.RECORD;\n\n case 'same':\n return cardinalityEnum.SAME;\n\n case 'any':\n return cardinalityEnum.ANY;\n\n default:\n return false;\n }\n },\n\n /**\n * Get the name of a constant from its value.\n *\n * @param {Number} constant The constant value to search the name for.\n * @return {String|Boolean} The name of the constant or false if not found.\n */\n getNameByConstant: function getNameByConstant(constant) {\n switch (constant) {\n case cardinalityEnum.SINGLE:\n return 'single';\n\n case cardinalityEnum.MULTIPLE:\n return 'multiple';\n\n case cardinalityEnum.ORDERED:\n return 'ordered';\n\n case cardinalityEnum.RECORD:\n return 'record';\n\n case cardinalityEnum.SAME:\n return 'same';\n\n case cardinalityEnum.ANY:\n return 'any';\n\n default:\n return false;\n }\n }\n }).assign(cardinalityEnum).value();\n\n return cardinalityHelper;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;\n */\n/**\n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/controller/creator/helpers/outcomeValidator',[\n 'lodash'\n], function (_) {\n 'use strict';\n\n /**\n * The RegEx that validates outcome identifiers\n * @type {RegExp}\n */\n var identifierValidator = /^[a-zA-Z_][a-zA-Z0-9_\\.-]*$/;\n\n /**\n * Checks the validity of an identifier\n * @param {String} identifier\n * @returns {Boolean}\n */\n function validateIdentifier(identifier) {\n return !!(identifier && _.isString(identifier) && identifierValidator.test(identifier));\n }\n\n /**\n * Checks if an object is a valid outcome\n * @param {Object} outcome\n * @param {Boolean} [checkIdentifier]\n * @param {String||String[]} [allowedTypes]\n * @returns {Boolean}\n */\n function validateOutcome(outcome, checkIdentifier, allowedTypes) {\n var validOutcome = _.isPlainObject(outcome) && validateIdentifier(outcome['qti-type']);\n var validIdentifier = !checkIdentifier || (outcome && validateIdentifier(outcome.identifier));\n\n if (allowedTypes) {\n allowedTypes = !_.isArray(allowedTypes) ? [allowedTypes] : allowedTypes;\n validOutcome = validOutcome && _.indexOf(allowedTypes, outcome['qti-type']) >= 0;\n }\n\n return !!(validOutcome && validIdentifier);\n }\n\n /**\n * Checks if an array contains valid outcomes\n * @param {Array} outcomes\n * @param {Boolean} [checkIdentifier]\n * @param {String||String[]} [allowedTypes]\n * @returns {Boolean}\n */\n function validateOutcomes(outcomes, checkIdentifier, allowedTypes) {\n var valid = _.isArray(outcomes);\n _.forEach(outcomes, function(outcome) {\n if (!validateOutcome(outcome, checkIdentifier, allowedTypes)) {\n valid = false;\n return false;\n }\n });\n return valid;\n }\n\n return {\n validateIdentifier: validateIdentifier,\n validateOutcomes: validateOutcomes,\n validateOutcome: validateOutcome\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;\n */\n/**\n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/controller/creator/helpers/qtiElement',[\n 'lodash',\n 'taoQtiTest/controller/creator/helpers/outcomeValidator'\n], function (_, outcomeValidator) {\n 'use strict';\n\n var qtiElementHelper = {\n /**\n * Creates a QTI element\n * @param {String} type - The QTI type of the element to create\n * @param {String|Object} [identifier] - An optional identifier, or a list of properties\n * @param {Object} [properties] - A list of additional properties\n * @returns {Object}\n * @throws {TypeError} if the type or the identifier is not valid\n */\n create: function create(type, identifier, properties) {\n var element = {\n 'qti-type': type\n };\n\n if (!outcomeValidator.validateIdentifier(type)) {\n throw new TypeError('You must provide a valid QTI type!');\n }\n\n if (_.isPlainObject(identifier)) {\n properties = identifier;\n identifier = null;\n }\n\n if (identifier) {\n if (!outcomeValidator.validateIdentifier(identifier)) {\n throw new TypeError('You must provide a valid identifier!');\n }\n element.identifier = identifier;\n }\n\n return _.assign(element, properties || {});\n },\n\n /**\n * Finds a QTI element in a collection, by its type.\n * The collection may also be a single object.\n * @param {Array|Object} collection\n * @param {Array|String} type\n * @returns {Object}\n */\n find: function find(collection, type) {\n var found = null;\n var types = forceArray(type);\n\n function checkType(qtiElement) {\n if (types.indexOf(qtiElement['qti-type']) >= 0) {\n found = qtiElement;\n return false;\n }\n }\n\n if (_.isArray(collection)) {\n _.forEach(collection, checkType);\n } else if (collection) {\n checkType(collection);\n }\n\n return found;\n },\n\n /**\n * Finds an element from a tree.\n * The path to the element is based on QTI types.\n * @param {Object} tree - The root of the tree from which get the property\n * @param {String|String[]} path - The path to the element, with QTI types separated by dot, like: \"setOutcomeValue.gte.baseValue\"\n * @param {String|String[]} nodeName - The name of the nodes that may contain subtrees\n * @returns {*}\n */\n lookupElement: function lookupElement(tree, path, nodeName) {\n var steps = _.isArray(path) ? path : path.split('.');\n var nodeNames = forceArray(nodeName);\n var len = steps.length;\n var i = 0;\n var key;\n\n while (tree && i < len) {\n tree = qtiElementHelper.find(tree, steps[i++]);\n if (tree && i < len) {\n key = _.find(nodeNames, _.partial(_.has, tree));\n tree = key && tree[key];\n }\n }\n\n return tree || null;\n },\n\n /**\n * Finds a property from a tree.\n * The path to the property is based on QTI types.\n * @param {Object} tree - The root of the tree from which get the property\n * @param {String|String[]} path - The path to the property, with QTI types separated by dot, like: \"setOutcomeValue.gte.baseValue.value\"\n * @param {String|String[]} nodeName - The name of the nodes that may contain subtrees\n * @returns {*}\n */\n lookupProperty: function lookupProperty(tree, path, nodeName) {\n var result = null;\n var steps = _.isArray(path) ? path : path.split('.');\n var propertyName = steps.pop();\n var element = qtiElementHelper.lookupElement(tree, steps, nodeName);\n\n if (element && element[propertyName]) {\n result = element[propertyName];\n }\n\n return result;\n }\n };\n\n /**\n * Ensures a value is an array\n * @param {*} value\n * @returns {Array}\n */\n function forceArray(value) {\n if (!value) {\n value = [];\n }\n if (!_.isArray(value)) {\n value = [value];\n }\n return value;\n }\n\n return qtiElementHelper;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;\n */\n/**\n * Basic helper that is intended to manage outcomes inside a test model.\n *\n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/controller/creator/helpers/outcome',[\n 'lodash',\n 'taoQtiTest/controller/creator/helpers/outcomeValidator',\n 'taoQtiTest/controller/creator/helpers/qtiElement',\n 'taoQtiTest/controller/creator/helpers/baseType',\n 'taoQtiTest/controller/creator/helpers/cardinality'\n], function (_, outcomeValidator, qtiElementHelper, baseTypeHelper, cardinalityHelper) {\n 'use strict';\n\n var outcomeHelper = {\n /**\n * Gets a property from an outcome rule expression.\n * The path to the property is based on QTI types.\n * @param {Object} outcomeRule - The outcome rule from which get the property\n * @param {String|String[]} path - The path to the property, with QTI types separated by dot, like: \"setOutcomeValue.gte.baseValue\"\n * @returns {*}\n */\n getProcessingRuleExpression: function getProcessingRuleExpression(outcomeRule, path) {\n return qtiElementHelper.lookupElement(outcomeRule, path, ['expression', 'expressions']);\n },\n\n /**\n * Gets a property from an outcome rule expression.\n * The path to the property is based on QTI types.\n * @param {Object} outcomeRule - The outcome rule from which get the property\n * @param {String|String[]} path - The path to the property, with QTI types separated by dot, like: \"setOutcomeValue.gte.baseValue.value\"\n * @returns {*}\n */\n getProcessingRuleProperty: function getProcessingRuleProperty(outcomeRule, path) {\n return qtiElementHelper.lookupProperty(outcomeRule, path, ['expression', 'expressions']);\n },\n\n /**\n * Gets the identifier of an outcome\n * @param {Object|String} outcome\n * @returns {String}\n */\n getOutcomeIdentifier: function getOutcomeIdentifier(outcome) {\n return String(_.isObject(outcome) ? outcome.identifier : outcome);\n },\n\n /**\n * Gets the list of outcome declarations\n * @param {Object} testModel\n * @returns {Array}\n */\n getOutcomeDeclarations: function getOutcomeDeclarations(testModel) {\n var outcomes = testModel && testModel.outcomeDeclarations;\n return outcomes || [];\n },\n\n /**\n * Gets the list of outcome processing rules\n * @param {Object} testModel\n * @returns {Array}\n */\n getOutcomeProcessingRules: function getOutcomeProcessingRules(testModel) {\n var rules = testModel && testModel.outcomeProcessing && testModel.outcomeProcessing.outcomeRules;\n return rules || [];\n },\n\n /**\n * Applies a function on each outcome declarations\n * @param {Object} testModel\n * @param {Function} cb\n */\n eachOutcomeDeclarations: function eachOutcomeDeclarations(testModel, cb) {\n _.forEach(outcomeHelper.getOutcomeDeclarations(testModel), cb);\n },\n\n /**\n * Applies a function on each outcome processing rules. Does not take care of sub-expressions.\n * @param {Object} testModel\n * @param {Function} cb\n */\n eachOutcomeProcessingRules: function eachOutcomeProcessingRules(testModel, cb) {\n _.forEach(outcomeHelper.getOutcomeProcessingRules(testModel), cb);\n },\n\n /**\n * Applies a function on each outcome processing rules, take care of each sub expression.\n * @param {Object} testModel\n * @param {Function} cb\n */\n eachOutcomeProcessingRuleExpressions: function eachOutcomeProcessingRuleExpressions(testModel, cb) {\n function browseExpressions(processingRule) {\n if (_.isArray(processingRule)) {\n _.forEach(processingRule, browseExpressions);\n } else if (processingRule) {\n cb(processingRule);\n\n if (processingRule.expression) {\n browseExpressions(processingRule.expression);\n } else if (processingRule.expressions) {\n browseExpressions(processingRule.expressions);\n }\n }\n }\n\n browseExpressions(outcomeHelper.getOutcomeProcessingRules(testModel));\n },\n\n /**\n * Lists all outcomes identifiers. An optional callback allows to filter the list\n * @param {Object} testModel\n * @param {Function} [cb]\n * @returns {Array}\n */\n listOutcomes: function listOutcomes(testModel, cb) {\n var outcomes = [];\n if (!_.isFunction(cb)) {\n cb = null;\n }\n outcomeHelper.eachOutcomeDeclarations(testModel, function (outcome) {\n if (!cb || cb(outcome)) {\n outcomes.push(outcomeHelper.getOutcomeIdentifier(outcome));\n }\n });\n return outcomes;\n },\n\n /**\n * Removes the specified outcomes from the provided test model\n * @param {Object} testModel - The test model to clean up\n * @param {Function|String[]} outcomes - The list of outcomes identifiers to remove,\n * or a callback that will match each outcome to remove\n */\n removeOutcomes: function removeOutcomes(testModel, outcomes) {\n var declarations = outcomeHelper.getOutcomeDeclarations(testModel);\n var rules = outcomeHelper.getOutcomeProcessingRules(testModel);\n var check;\n\n if (_.isFunction(outcomes)) {\n check = outcomes;\n } else {\n outcomes = _.keyBy(_.isArray(outcomes) ? outcomes : [outcomes], function (outcome) {\n return outcome;\n });\n\n check = function checkIdentifier(outcome) {\n return !!outcomes[outcomeHelper.getOutcomeIdentifier(outcome)];\n };\n }\n\n if (declarations) {\n _.remove(declarations, check);\n }\n\n if (rules) {\n _.remove(rules, check);\n }\n },\n\n /**\n * Creates an outcome declaration\n * @param {String} identifier\n * @param {String|Number|Boolean} [type] - The data type of the outcome, FLOAT by default\n * @param {Number} [cardinality] - The variable cardinality, default 0\n * @returns {Object}\n * @throws {TypeError} if the identifier is empty or is not a string\n */\n createOutcome: function createOutcome(identifier, type, cardinality) {\n\n if (!outcomeValidator.validateIdentifier(identifier)) {\n throw new TypeError('You must provide a valid identifier!');\n }\n\n return qtiElementHelper.create('outcomeDeclaration', identifier, {\n views: [],\n interpretation: '',\n longInterpretation: '',\n normalMaximum: false,\n normalMinimum: false,\n masteryValue: false,\n cardinality: cardinalityHelper.getValid(cardinality, cardinalityHelper.SINGLE),\n baseType: baseTypeHelper.getValid(type, baseTypeHelper.FLOAT)\n });\n },\n\n /**\n * Adds a processing rule into the test model\n *\n * @param {Object} testModel\n * @param {Object} processingRule\n * @returns {Object}\n * @throws {TypeError} if the processing rule is not valid\n */\n addOutcomeProcessing: function createOutcomeProcessing(testModel, processingRule) {\n var outcomeProcessing = testModel.outcomeProcessing;\n\n if (!outcomeValidator.validateOutcome(processingRule)) {\n throw new TypeError('You must provide a valid outcome processing rule!');\n }\n\n if (!outcomeProcessing) {\n outcomeProcessing = qtiElementHelper.create('outcomeProcessing', {\n outcomeRules: []\n });\n testModel.outcomeProcessing = outcomeProcessing;\n } else if (!outcomeProcessing.outcomeRules) {\n outcomeProcessing.outcomeRules = [];\n }\n\n outcomeProcessing.outcomeRules.push(processingRule);\n return processingRule;\n },\n\n /**\n * Creates an outcome and adds it to the declarations\n * @param {Object} testModel\n * @param {Object} outcome - The outcome to add\n * @param {Object} [processingRule] - The processing rule attached to the outcome\n * @returns {Object}\n * @throws {TypeError} if one of the outcome or the processing rule is not valid\n */\n addOutcome: function addOutcome(testModel, outcome, processingRule) {\n var declarations = testModel.outcomeDeclarations;\n\n if (!outcomeValidator.validateOutcome(outcome, true, 'outcomeDeclaration')) {\n throw new TypeError('You must provide a valid outcome!');\n }\n\n if (processingRule) {\n if (!outcomeValidator.validateOutcome(processingRule) || processingRule.identifier !== outcome.identifier) {\n throw new TypeError('You must provide a valid outcome processing rule!');\n }\n\n outcomeHelper.addOutcomeProcessing(testModel, processingRule);\n }\n\n if (!declarations) {\n declarations = [];\n testModel.outcomeDeclarations = declarations;\n }\n\n declarations.push(outcome);\n return outcome;\n },\n\n /**\n * Replaces the outcomes in a test model\n * @param {Object} testModel\n * @param {Object} outcomes\n * @throws {TypeError} if one of the outcomes or the processing rules are not valid\n */\n replaceOutcomes: function replaceOutcomes(testModel, outcomes) {\n if (_.isPlainObject(outcomes)) {\n if (_.isArray(outcomes.outcomeDeclarations)) {\n if (!outcomeValidator.validateOutcomes(outcomes.outcomeDeclarations, true, 'outcomeDeclaration')) {\n throw new TypeError('You must provide valid outcomes!');\n }\n\n testModel.outcomeDeclarations = [].concat(outcomes.outcomeDeclarations);\n }\n if (outcomes.outcomeProcessing && _.isArray(outcomes.outcomeProcessing.outcomeRules)) {\n if (!outcomeValidator.validateOutcomes(outcomes.outcomeProcessing.outcomeRules)) {\n throw new TypeError('You must provide valid processing rules!');\n }\n\n if (!testModel.outcomeProcessing) {\n testModel.outcomeProcessing = qtiElementHelper.create('outcomeProcessing');\n }\n testModel.outcomeProcessing.outcomeRules = [].concat(outcomes.outcomeProcessing.outcomeRules);\n }\n }\n }\n };\n\n return outcomeHelper;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017-2023 (original work) Open Assessment Technologies SA ;\n */\n/**\n * Helper that provides a way to browse all categories attached to a test model at the item level.\n *\n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/controller/creator/helpers/category',['lodash'], function (_) {\n 'use strict';\n\n /**\n * Checks if a category is an option\n *\n * @param {String} category\n * @returns {Boolean}\n */\n function isCategoryOption(category) {\n return category && category.indexOf('x-tao-') === 0;\n }\n\n /**\n * Calls a function for each category in the test model\n * @param {Object} testModel\n * @param {Function} cb\n */\n function eachCategories(testModel, cb) {\n const getCategoriesRecursively = section => {\n _.forEach(section.sectionParts, function (sectionPart) {\n if (sectionPart['qti-type'] === 'assessmentItemRef') {\n _.forEach(sectionPart.categories, function (category) {\n cb(category, sectionPart);\n });\n }\n if (sectionPart['qti-type'] === 'assessmentSection') {\n getCategoriesRecursively(sectionPart);\n }\n });\n };\n _.forEach(testModel.testParts, function (testPart) {\n _.forEach(testPart.assessmentSections, function (assessmentSection) {\n getCategoriesRecursively(assessmentSection);\n });\n });\n }\n\n return {\n /**\n * Calls a function for each category in the test model\n * @function eachCategories\n * @param {Object} testModel\n * @param {Function} cb\n */\n eachCategories: eachCategories,\n\n /**\n * Gets the list of categories assigned to the items.\n * Discards special purpose categories like 'x-tao-...'\n *\n * @param {Object} testModel\n * @returns {Array}\n */\n listCategories: function listCategories(testModel) {\n var categories = {};\n eachCategories(testModel, function (category) {\n if (!isCategoryOption(category)) {\n categories[category] = true;\n }\n });\n return _.keys(categories);\n },\n\n /**\n * Gets the list of options assigned to the items (special purpose categories like 'x-tao-...').\n *\n * @param {Object} testModel\n * @returns {Array}\n */\n listOptions: function listOptions(testModel) {\n var options = {};\n eachCategories(testModel, function (category) {\n if (isCategoryOption(category)) {\n options[category] = true;\n }\n });\n return _.keys(options);\n }\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;\n */\n/**\n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/controller/creator/modelOverseer',[\n 'lodash',\n 'core/eventifier',\n 'core/statifier',\n 'taoQtiTest/controller/creator/helpers/baseType',\n 'taoQtiTest/controller/creator/helpers/cardinality',\n 'taoQtiTest/controller/creator/helpers/outcome',\n 'taoQtiTest/controller/creator/helpers/category'\n], function (_, eventifier, statifier, baseTypeHelper, cardinalityHelper, outcomeHelper, categoryHelper) {\n 'use strict';\n\n /**\n * Wraps the test model in a manager, provides API to handle events and states\n * @param {Object} model\n * @param {Object} [config]\n * @returns {Object}\n */\n function modelOverseerFactory(model, config) {\n var modelOverseer = {\n /**\n * Gets the nested model\n * @returns {Object}\n */\n getModel: function getModel() {\n return model;\n },\n\n /**\n * Sets the nested model\n *\n * @param {Object} newModel\n * @returns {modelOverseer}\n * @fires setmodel\n */\n setModel: function setModel(newModel) {\n model = newModel;\n\n /**\n * @event modelOverseer#setmodel\n * @param {String} model\n */\n modelOverseer.trigger('setmodel', model);\n return this;\n },\n\n /**\n * Gets the config set\n * @returns {Object}\n */\n getConfig: function getConfig() {\n return config;\n },\n\n\n\n /**\n * Gets the list of defined outcomes for the nested model. A descriptor is built for each outcomes:\n * {\n * name: {String},\n * type: {String},\n * cardinality: {String}\n * }\n * @returns {Object[]}\n */\n getOutcomesList: function getOutcomesList() {\n return _.map(outcomeHelper.getOutcomeDeclarations(model), function(declaration) {\n return {\n name: declaration.identifier,\n type: baseTypeHelper.getNameByConstant(declaration.baseType),\n cardinality: cardinalityHelper.getNameByConstant(declaration.cardinality)\n };\n });\n },\n\n /**\n * Gets the names of the defined outcomes for the nested model\n * @returns {Array}\n */\n getOutcomesNames: function getOutcomesNames() {\n return _.map(outcomeHelper.getOutcomeDeclarations(model), function(declaration) {\n return declaration.identifier;\n });\n },\n\n /**\n * Gets the list of defined categories for the nested model\n * @returns {Array}\n */\n getCategories: function getCategories() {\n return categoryHelper.listCategories(model);\n },\n\n /**\n * Gets the list of defined options for the nested model\n * @returns {Array}\n */\n getOptions: function getOptions() {\n return categoryHelper.listOptions(model);\n }\n };\n\n config = _.isPlainObject(config) ? config : _.assign({}, config);\n\n return statifier(eventifier(modelOverseer));\n }\n\n return modelOverseerFactory;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017 (original work) Open Assessment Technologies SA;\n */\n/**\n * This object holds a shared context for all of the test creator modules and allow them to communicate via events.\n * Its lifecycle is bound to the creator controller.\n *\n * @author Christophe Noël \n */\ndefine('taoQtiTest/controller/creator/qtiTestCreator',[\n 'jquery',\n 'core/eventifier',\n 'taoQtiTest/controller/creator/areaBroker',\n 'taoQtiTest/controller/creator/modelOverseer'\n], function($, eventifier, areaBrokerFactory, modelOverseerFactory) {\n 'use strict';\n\n /**\n * @param {jQuery} $creatorContainer - root DOM element containing the creator\n * @param {Object} config - options that will be forwareded to the modelOverseer Factory\n * @returns {Object}\n */\n function testCreatorFactory($creatorContainer, config) {\n var testCreator;\n\n var $container,\n model,\n areaBroker,\n modelOverseer;\n\n /**\n * Create the model overseer with the given model\n * @returns {modelOverseer}\n */\n function loadModelOverseer() {\n if (! modelOverseer && model) {\n modelOverseer = modelOverseerFactory(model, config);\n }\n return modelOverseer;\n }\n\n /**\n * Set up the areaBroker mapping from the actual DOM\n * @returns {areaBroker} already mapped\n */\n function loadAreaBroker(){\n if (! areaBroker) {\n areaBroker = areaBrokerFactory($container, {\n 'creator': $container,\n 'itemSelectorPanel': $container.find('.test-creator-items'),\n 'contentCreatorPanel': $container.find('.test-creator-content'),\n 'propertyPanel': $container.find('.test-creator-props'),\n 'elementPropertyPanel': $container.find('.qti-widget-properties')\n });\n }\n return areaBroker;\n }\n\n if (! ($creatorContainer instanceof $)) {\n throw new TypeError('a valid $container must be given');\n }\n\n $container = $creatorContainer;\n\n testCreator = {\n setTestModel: function setTestModel(m) {\n model = m;\n },\n\n getAreaBroker: function getAreaBroker() {\n return loadAreaBroker();\n },\n\n getModelOverseer: function getModelOverseer() {\n return loadModelOverseer();\n },\n\n isTestHasErrors: function isTestHasErrors() {\n return $container.find('.test-creator-props').find('span.validate-error').length > 0;\n }\n };\n\n return eventifier(testCreator);\n }\n\n return testCreatorFactory;\n});\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017 (original work) Open Assessment Technologies SA;\n */\n\n/**\n * The testItem data provider\n *\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/provider/testItems',[\n 'lodash',\n 'i18n',\n 'util/url',\n 'core/dataProvider/request'\n], function (_, __, urlUtil, request) {\n 'use strict';\n\n /**\n * Per function requests configuration.\n */\n var defaultConfig = {\n getItemClasses : {\n url : urlUtil.route('getItemClasses', 'Items', 'taoQtiTest')\n },\n getItems : {\n url : urlUtil.route('getItems', 'Items', 'taoQtiTest')\n },\n getItemClassProperties : {\n url : urlUtil.route('create', 'RestFormItem', 'taoItems')\n }\n };\n\n /**\n * Creates a configured testItem provider\n *\n * @param {Object} [config] - to override the default config\n * @returns {testItemProvider} the new provider\n */\n return function testItemProviderFactory(config){\n\n config = _.defaults(config || {}, defaultConfig);\n\n /**\n * @typedef {testItemProvider}\n */\n return {\n\n /**\n * Get the list of Items classes and sub classes\n * @returns {Promise} that resolves with the classes\n */\n getItemClasses: function getItemClasses(){\n return request(config.getItemClasses.url);\n },\n\n /**\n * Get QTI Items in different formats\n * @param {Object} [params] - the parameters to pass through the request\n * @returns {Promise} that resolves with the classes\n */\n getItems : function getItems(params){\n return request(config.getItems.url, params);\n },\n\n /**\n * Get the properties of a the given item class\n * @param {String} classUri - the item class URI\n * @returns {Promise} that resolves with the classes\n */\n getItemClassProperties: function getItemClassProperties(classUri) {\n return request(config.getItemClassProperties.url, { classUri : classUri });\n }\n };\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2014 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\n\n/**\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/creator/views/item',[\n 'module',\n 'jquery',\n 'i18n',\n 'core/logger',\n 'taoQtiTest/provider/testItems',\n 'ui/resource/selector',\n 'ui/feedback'\n], function (module, $, __, loggerFactory, testItemProviderFactory, resourceSelectorFactory, feedback) {\n 'use strict';\n\n /**\n * Create a dedicated logger\n */\n const logger = loggerFactory('taoQtiTest/creator/views/item');\n\n /**\n * Let's you access the data\n */\n const testItemProvider = testItemProviderFactory();\n\n /**\n * Handles errors\n * @param {Error} err\n */\n const onError = function onError(err) {\n logger.error(err);\n feedback().error(err.message || __('An error occured while retrieving items'));\n };\n\n const ITEM_URI = 'http://www.tao.lu/Ontologies/TAOItem.rdf#Item';\n\n /**\n * The ItemView setup items related components\n * @exports taoQtiTest/controller/creator/views/item\n * @param {jQueryElement} $container - where to append the view\n */\n return function itemView($container) {\n const filters = module.config().BRS || false; // feature flag BRS (search by metadata) in Test Authoring\n const selectorConfig = {\n type: __('items'),\n selectionMode: resourceSelectorFactory.selectionModes.multiple,\n selectAllPolicy: resourceSelectorFactory.selectAllPolicies.visible,\n classUri: ITEM_URI,\n classes: [\n {\n label: 'Item',\n uri: ITEM_URI,\n type: 'class'\n }\n ],\n filters\n };\n\n //set up the resource selector with one root class Item in classSelector\n const resourceSelector = resourceSelectorFactory($container, selectorConfig)\n .on('render', function () {\n $container.on('itemselected.creator', () => {\n this.clearSelection();\n });\n })\n .on('query', function (params) {\n //ask the server the item from the component query\n testItemProvider\n .getItems(params)\n .then(items => {\n //and update the item list\n this.update(items, params);\n })\n .catch(onError);\n })\n .on('classchange', function (classUri) {\n //by changing the class we need to change the\n //properties filters\n testItemProvider\n .getItemClassProperties(classUri)\n .then(filters => {\n this.updateFilters(filters);\n })\n .catch(onError);\n })\n .on('change', function (values) {\n /**\n * We've got a selection, triggered on the view container\n *\n * TODO replace jquery events by the eventifier\n *\n * @event jQuery#itemselect.creator\n * @param {Object[]} values - the selection\n */\n $container.trigger('itemselect.creator', [values]);\n });\n\n //load the classes hierarchy\n testItemProvider\n .getItemClasses()\n .then(function (classes) {\n selectorConfig.classes = classes;\n selectorConfig.classUri = classes[0].uri;\n })\n .then(function () {\n //load the class properties\n return testItemProvider.getItemClassProperties(selectorConfig.classUri);\n })\n .then(function (filters) {\n //set the filters from the properties\n selectorConfig.filters = filters;\n })\n .then(function () {\n // add classes in classSelector\n selectorConfig.classes[0].children.forEach(node => {\n resourceSelector.addClassNode(node, selectorConfig.classUri);\n });\n resourceSelector.updateFilters(selectorConfig.filters);\n })\n .catch(onError);\n };\n});\n\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/testpart', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, options, functionType=\"function\", escapeExpression=this.escapeExpression, helperMissing=helpers.helperMissing;\n\n\n buffer += \"
\\n

\\n \\n \";\n if (helper = helpers.identifier) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.identifier); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n \\n
\\n
\\n
\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n
\\n
\\n
\\n

\\n
\\n\\n \\n
\\n\\n \\n
\\n
\";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/section', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, options, functionType=\"function\", escapeExpression=this.escapeExpression, helperMissing=helpers.helperMissing;\n\n\n buffer += \"
\\n\\n\\n

\";\n if (helper = helpers.title) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.title); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n \\n
\\n
\\n
\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n
\\n
\\n
\\n

\\n\\n
\\n

\\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Rubric Blocks\", options) : helperMissing.call(depth0, \"__\", \"Rubric Blocks\", options)))\n + \"\\n

\\n
    \\n \\n
    \\n
    \\n

    \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Items\", options) : helperMissing.call(depth0, \"__\", \"Items\", options)))\n + \"\\n

    \\n
      \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Add selected item(s) here.\", options) : helperMissing.call(depth0, \"__\", \"Add selected item(s) here.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n
      \";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/rubricblock', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", helper, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;\n\n\n buffer += \"
    1. \\n
      \\n
      \\n
      \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n
      \\n
      \\n
      \\n
      \\n
    2. \";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/itemref', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, options, functionType=\"function\", escapeExpression=this.escapeExpression, helperMissing=helpers.helperMissing;\n\n\n buffer += \"
    3. \\n \";\n stack1 = (helper = helpers.dompurify || (depth0 && depth0.dompurify),options={hash:{},data:data},helper ? helper.call(depth0, (depth0 && depth0.label), options) : helperMissing.call(depth0, \"dompurify\", (depth0 && depth0.label), options));\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n \\n
      \\n
      \\n
      \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n
      \\n
      \\n
      \\n
    4. \";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/outcomes', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, options, functionType=\"function\", escapeExpression=this.escapeExpression, helperMissing=helpers.helperMissing, self=this;\n\nfunction program1(depth0,data) {\n \n var buffer = \"\", stack1, helper;\n buffer += \"\\n
      \\n
      \";\n if (helper = helpers.name) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.name); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"
      \\n
      \";\n if (helper = helpers.type) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.type); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"
      \\n
      \";\n if (helper = helpers.cardinality) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.cardinality); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"
      \\n
      \\n\";\n return buffer;\n }\n\nfunction program3(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n
      \\n
      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"no outcome declaration found\", options) : helperMissing.call(depth0, \"__\", \"no outcome declaration found\", options)))\n + \"
      \\n
      \\n\";\n return buffer;\n }\n\n buffer += \"
      \\n
      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Identifier\", options) : helperMissing.call(depth0, \"__\", \"Identifier\", options)))\n + \"
      \\n
      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Type\", options) : helperMissing.call(depth0, \"__\", \"Type\", options)))\n + \"
      \\n
      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Cardinality\", options) : helperMissing.call(depth0, \"__\", \"Cardinality\", options)))\n + \"
      \\n
      \\n\";\n stack1 = helpers.each.call(depth0, (depth0 && depth0.outcomes), {hash:{},inverse:self.program(3, program3, data),fn:self.program(1, program1, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/test-props', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, functionType=\"function\", self=this;\n\nfunction program1(depth0,data) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n \";\n if (helper = helpers.identifier) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.identifier); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The principle identifier of the test.\", options) : helperMissing.call(depth0, \"__\", \"The principle identifier of the test.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program3(depth0,data) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Time Limits\", options) : helperMissing.call(depth0, \"__\", \"Time Limits\", options)))\n + \"

      \\n\\n \\n
      \\n\\n \\n\\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Maximum duration for the all test.\", options) : helperMissing.call(depth0, \"__\", \"Maximum duration for the all test.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.lateSubmission), {hash:{},inverse:self.noop,fn:self.program(4, program4, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
      \\n \";\n return buffer;\n }\nfunction program4(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Late submission allowed\", options) : helperMissing.call(depth0, \"__\", \"Late submission allowed\", options)))\n + \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Whether a candidate's response that is beyond the maximum duration should still be accepted.\", options) : helperMissing.call(depth0, \"__\", \"Whether a candidate's response that is beyond the maximum duration should still be accepted.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program6(depth0,data) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n
      \\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Select the way the responses of your test should be processed\", options) : helperMissing.call(depth0, \"__\", \"Select the way the responses of your test should be processed\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Also compute the score per categories\", options) : helperMissing.call(depth0, \"__\", \"Also compute the score per categories\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Set the cut score (or pass score ratio) associated to the test. It must be a float between 0 and 1.\", options) : helperMissing.call(depth0, \"__\", \"Set the cut score (or pass score ratio) associated to the test. It must be a float between 0 and 1.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Set the weight identifier used to process the score\", options) : helperMissing.call(depth0, \"__\", \"Set the weight identifier used to process the score\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n\\n
      \\n
      \\n \";\n stack1 = helpers.each.call(depth0, (depth0 && depth0.modes), {hash:{},inverse:self.noop,fn:self.program(10, program10, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
      \\n
      \\n
      \\n\";\n return buffer;\n }\nfunction program7(depth0,data) {\n \n var buffer = \"\", stack1, helper;\n buffer += \"\\n \\n \";\n return buffer;\n }\nfunction program8(depth0,data) {\n \n \n return \"selected=\\\"selected\\\"\";\n }\n\nfunction program10(depth0,data) {\n \n var buffer = \"\", stack1, helper;\n buffer += \"\\n
      \\n \\n \";\n if (helper = helpers.description) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.description); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n
      \\n \";\n return buffer;\n }\n\nfunction program12(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Outcome declarations\", options) : helperMissing.call(depth0, \"__\", \"Outcome declarations\", options)))\n + \"

      \\n\\n \\n
      \\n
      \\n
      \\n \\n
      \\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\n buffer += \"
      \\n\\n \\n

      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showIdentifier), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The test title.\", options) : helperMissing.call(depth0, \"__\", \"The test title.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showTimeLimits), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Scoring\", options) : helperMissing.call(depth0, \"__\", \"Scoring\", options)))\n + \"

      \\n\\n\\n\";\n stack1 = helpers['with'].call(depth0, (depth0 && depth0.scoring), {hash:{},inverse:self.noop,fn:self.program(6, program6, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showOutcomeDeclarations), {hash:{},inverse:self.noop,fn:self.program(12, program12, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
      \";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/testpart-props', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, functionType=\"function\", self=this;\n\nfunction program1(depth0,data) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n \";\n if (helper = helpers.identifier) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.identifier); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The test part identifier.\", options) : helperMissing.call(depth0, \"__\", \"The test part identifier.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program3(depth0,data) {\n \n \n return \"checked\";\n }\n\nfunction program5(depth0,data) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Submission\", options) : helperMissing.call(depth0, \"__\", \"Submission\", options)))\n + \" *\\n
      \\n
      \\n \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The submission mode determines when the candidate's responses are submitted for response processing. A testPart in individual mode requires the candidate to submit their responses on an item-by-item basis. In simultaneous mode the candidate's responses are all submitted together at the end of the testPart.\", options) : helperMissing.call(depth0, \"__\", \"The submission mode determines when the candidate's responses are submitted for response processing. A testPart in individual mode requires the candidate to submit their responses on an item-by-item basis. In simultaneous mode the candidate's responses are all submitted together at the end of the testPart.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program7(depth0,data) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Item Session Control\", options) : helperMissing.call(depth0, \"__\", \"Item Session Control\", options)))\n + \"

      \\n\\n\\n \\n
      \\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Controls the maximum number of attempts allowed. 0 means unlimited.\", options) : helperMissing.call(depth0, \"__\", \"Controls the maximum number of attempts allowed. 0 means unlimited.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.itemSessionShowFeedback), {hash:{},inverse:self.noop,fn:self.program(8, program8, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n\\n\\n\\n\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.itemSessionAllowComment), {hash:{},inverse:self.noop,fn:self.program(10, program10, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.itemSessionAllowSkipping), {hash:{},inverse:self.noop,fn:self.program(12, program12, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The candidate is not allowed to submit invalid responses.\", options) : helperMissing.call(depth0, \"__\", \"The candidate is not allowed to submit invalid responses.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n
      \\n \";\n return buffer;\n }\nfunction program8(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"This constraint affects the visibility of feedback after the end of the last attempt.\", options) : helperMissing.call(depth0, \"__\", \"This constraint affects the visibility of feedback after the end of the last attempt.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program10(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"This constraint controls whether or not the candidate is allowed to provide a comment on the item during the session. Comments are not part of the assessed responses.\", options) : helperMissing.call(depth0, \"__\", \"This constraint controls whether or not the candidate is allowed to provide a comment on the item during the session. Comments are not part of the assessed responses.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program12(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"If the candidate can skip the item, without submitting a response (default is true).\", options) : helperMissing.call(depth0, \"__\", \"If the candidate can skip the item, without submitting a response (default is true).\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program14(depth0,data) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Time Limits\", options) : helperMissing.call(depth0, \"__\", \"Time Limits\", options)))\n + \"

      \\n\\n \\n
      \\n\\n \\n\\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Maximum duration for this test part.\", options) : helperMissing.call(depth0, \"__\", \"Maximum duration for this test part.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.lateSubmission), {hash:{},inverse:self.noop,fn:self.program(15, program15, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
      \\n \";\n return buffer;\n }\nfunction program15(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Whether a candidate's response that is beyond the maximum duration of the test part should still be accepted.\", options) : helperMissing.call(depth0, \"__\", \"Whether a candidate's response that is beyond the maximum duration of the test part should still be accepted.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\n buffer += \"
      \\n

      \";\n if (helper = helpers.identifier) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.identifier); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"

      \\n\\n
      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showIdentifier), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n
      \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Navigation\", options) : helperMissing.call(depth0, \"__\", \"Navigation\", options)))\n + \" *\\n
      \\n
      \\n \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The navigation mode determines the general paths that the candidate may take. A linear mode restricts the candidate to attempt each item in turn. Non Linear removes this restriction.\", options) : helperMissing.call(depth0, \"__\", \"The navigation mode determines the general paths that the candidate may take. A linear mode restricts the candidate to attempt each item in turn. Non Linear removes this restriction.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.submissionModeVisible), {hash:{},inverse:self.noop,fn:self.program(5, program5, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n
      \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Test part level category enables configuring the categories of its composing items all at once. A category in gray means that all items have that category. A category in white means that only a few items have that category.\", options) : helperMissing.call(depth0, \"__\", \"Test part level category enables configuring the categories of its composing items all at once. A category in gray means that all items have that category. A category in white means that only a few items have that category.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n \\n
      \\n
      \\n\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showItemSessionControl), {hash:{},inverse:self.noop,fn:self.program(7, program7, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showTimeLimits), {hash:{},inverse:self.noop,fn:self.program(14, program14, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
      \\n
      \";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/section-props', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, functionType=\"function\", self=this;\n\nfunction program1(depth0,data) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n \";\n if (helper = helpers.identifier) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.identifier); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The identifier of the section.\", options) : helperMissing.call(depth0, \"__\", \"The identifier of the section.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program3(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"If required it must appear (at least once) in the selection.\", options) : helperMissing.call(depth0, \"__\", \"If required it must appear (at least once) in the selection.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\";\n return buffer;\n }\n\nfunction program5(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"A visible section is one that is identifiable by the candidate.\", options) : helperMissing.call(depth0, \"__\", \"A visible section is one that is identifiable by the candidate.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program7(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n
      \\n\\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"An invisible section with a parent that is subject to shuffling can specify whether or not its children, which will appear to the candidate as if they were part of the parent, are shuffled as a block or mixed up with the other children of the parent section.\", options) : helperMissing.call(depth0, \"__\", \"An invisible section with a parent that is subject to shuffling can specify whether or not its children, which will appear to the candidate as if they were part of the parent, are shuffled as a block or mixed up with the other children of the parent section.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program9(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n
      \\n
      \\n \\n
      \\n\\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Associate a blueprint to a section allow you to validate this section against the specified blueprint.\", options) : helperMissing.call(depth0, \"__\", \"Associate a blueprint to a section allow you to validate this section against the specified blueprint.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program11(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n\\n
      \\n
      \\n \\n
      \\n\\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"When selecting child elements each element is normally eligible for selection once only.\", options) : helperMissing.call(depth0, \"__\", \"When selecting child elements each element is normally eligible for selection once only.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program13(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"This constraint affects the visibility of feedback after the end of the last attempt.\", options) : helperMissing.call(depth0, \"__\", \"This constraint affects the visibility of feedback after the end of the last attempt.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program15(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"This constraint controls whether or not the candidate is allowed to provide a comment on the item during the session. Comments are not part of the assessed responses.\", options) : helperMissing.call(depth0, \"__\", \"This constraint controls whether or not the candidate is allowed to provide a comment on the item during the session. Comments are not part of the assessed responses.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program17(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"If the candidate can skip the item, without submitting a response (default is true).\", options) : helperMissing.call(depth0, \"__\", \"If the candidate can skip the item, without submitting a response (default is true).\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program19(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The candidate is not allowed to submit invalid responses.\", options) : helperMissing.call(depth0, \"__\", \"The candidate is not allowed to submit invalid responses.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program21(depth0,data) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Time Limits\", options) : helperMissing.call(depth0, \"__\", \"Time Limits\", options)))\n + \"

      \\n\\n \\n
      \\n\\n\\n \\n\\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Maximum duration for this section.\", options) : helperMissing.call(depth0, \"__\", \"Maximum duration for this section.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.lateSubmission), {hash:{},inverse:self.noop,fn:self.program(22, program22, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
      \\n \";\n return buffer;\n }\nfunction program22(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Whether a candidate's response that is beyond the maximum duration of the section should still be accepted.\", options) : helperMissing.call(depth0, \"__\", \"Whether a candidate's response that is beyond the maximum duration of the section should still be accepted.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\n buffer += \"
      \\n

      \";\n if (helper = helpers.title) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.title); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"

      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showIdentifier), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The section title.\", options) : helperMissing.call(depth0, \"__\", \"The section title.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n\";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.isSubsection), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showVisible), {hash:{},inverse:self.noop,fn:self.program(5, program5, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showKeepTogether), {hash:{},inverse:self.noop,fn:self.program(7, program7, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.hasBlueprint), {hash:{},inverse:self.noop,fn:self.program(9, program9, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n
      \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Section level category enables configuring the categories of its composing items all at once. A category in gray means that all items have that category. A category in white means that only a few items have that category.\", options) : helperMissing.call(depth0, \"__\", \"Section level category enables configuring the categories of its composing items all at once. A category in gray means that all items have that category. A category in white means that only a few items have that category.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n \\n
      \\n
      \\n\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Selection\", options) : helperMissing.call(depth0, \"__\", \"Selection\", options)))\n + \"

      \\n\\n\\n
      \\n\\n
      \\n
      \\n \\n
      \\n\\n
      \\n \\n
      \\n
      \\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The number of child elements to be selected.\", options) : helperMissing.call(depth0, \"__\", \"The number of child elements to be selected.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.hasSelectionWithReplacement), {hash:{},inverse:self.noop,fn:self.program(11, program11, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
      \\n\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Ordering\", options) : helperMissing.call(depth0, \"__\", \"Ordering\", options)))\n + \"

      \\n\\n\\n
      \\n\\n
      \\n
      \\n \\n
      \\n\\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"If set, it causes the order of the child elements to be randomized, otherwise it uses the order in which the child elements are defined.\", options) : helperMissing.call(depth0, \"__\", \"If set, it causes the order of the child elements to be randomized, otherwise it uses the order in which the child elements are defined.\", options)))\n + \"\\n
      \\n
      \\n
      \\n
      \\n\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Item Session Control\", options) : helperMissing.call(depth0, \"__\", \"Item Session Control\", options)))\n + \"

      \\n\\n\\n
      \\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Controls the maximum number of attempts allowed. 0 means unlimited.\", options) : helperMissing.call(depth0, \"__\", \"Controls the maximum number of attempts allowed. 0 means unlimited.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.itemSessionShowFeedback), {hash:{},inverse:self.noop,fn:self.program(13, program13, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n\\n\\n\\n\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.itemSessionAllowComment), {hash:{},inverse:self.noop,fn:self.program(15, program15, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.itemSessionAllowSkipping), {hash:{},inverse:self.noop,fn:self.program(17, program17, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.validateResponsesVisible), {hash:{},inverse:self.noop,fn:self.program(19, program19, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n
      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showTimeLimits), {hash:{},inverse:self.noop,fn:self.program(21, program21, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
      \";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/itemref-props', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, functionType=\"function\", self=this;\n\nfunction program1(depth0,data) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n \";\n if (helper = helpers.identifier) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.identifier); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The identifier of the item reference.\", options) : helperMissing.call(depth0, \"__\", \"The identifier of the item reference.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program3(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The reference.\", options) : helperMissing.call(depth0, \"__\", \"The reference.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program5(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Weights\", options) : helperMissing.call(depth0, \"__\", \"Weights\", options)))\n + \"

      \\n\\n
      \\n
      \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Identifier\", options) : helperMissing.call(depth0, \"__\", \"Identifier\", options)))\n + \"\\n
      \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Value\", options) : helperMissing.call(depth0, \"__\", \"Value\", options)))\n + \"\\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Controls the contribution of an individual item score to the overall test score.\", options) : helperMissing.call(depth0, \"__\", \"Controls the contribution of an individual item score to the overall test score.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \\n
      \\n \\n
      \\n \";\n return buffer;\n }\n\nfunction program7(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"This constraint affects the visibility of feedback after the end of the last attempt.\", options) : helperMissing.call(depth0, \"__\", \"This constraint affects the visibility of feedback after the end of the last attempt.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program9(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"This constraint controls whether or not the candidate is allowed to provide a comment on the item during the session. Comments are not part of the assessed responses.\", options) : helperMissing.call(depth0, \"__\", \"This constraint controls whether or not the candidate is allowed to provide a comment on the item during the session. Comments are not part of the assessed responses.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program11(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"If the candidate can skip the item, without submitting a response (default is true).\", options) : helperMissing.call(depth0, \"__\", \"If the candidate can skip the item, without submitting a response (default is true).\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program13(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The candidate is not allowed to submit invalid responses.\", options) : helperMissing.call(depth0, \"__\", \"The candidate is not allowed to submit invalid responses.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program15(depth0,data) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Time Limits\", options) : helperMissing.call(depth0, \"__\", \"Time Limits\", options)))\n + \"

      \\n\\n\\n
      \\n\\n
      \\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Minimum duration : enforces the test taker to stay on the item for the given duration.\", options) : helperMissing.call(depth0, \"__\", \"Minimum duration : enforces the test taker to stay on the item for the given duration.\", options)))\n + \"
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Maximum duration : the items times out when the duration reaches 0.\", options) : helperMissing.call(depth0, \"__\", \"Maximum duration : the items times out when the duration reaches 0.\", options)))\n + \"
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Locked duration : guided navigation. The test transition to the next item once the duration reaches 0.\", options) : helperMissing.call(depth0, \"__\", \"Locked duration : guided navigation. The test transition to the next item once the duration reaches 0.\", options)))\n + \"
      \\n
      \\n
      \\n
      \\n
      \\n \\n
      \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n
      \\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Maximum duration for this item.\", options) : helperMissing.call(depth0, \"__\", \"Maximum duration for this item.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Minimum duration for this item.\", options) : helperMissing.call(depth0, \"__\", \"Minimum duration for this item.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.lateSubmission), {hash:{},inverse:self.noop,fn:self.program(18, program18, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
      \\n \";\n return buffer;\n }\nfunction program16(depth0,data) {\n \n \n return \"hidden\";\n }\n\nfunction program18(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Whether a candidate's response that is beyond the maximum duration of the item should still be accepted.\", options) : helperMissing.call(depth0, \"__\", \"Whether a candidate's response that is beyond the maximum duration of the item should still be accepted.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\n buffer += \"
      \\n\\n

      \";\n stack1 = (helper = helpers.dompurify || (depth0 && depth0.dompurify),options={hash:{},data:data},helper ? helper.call(depth0, (depth0 && depth0.label), options) : helperMissing.call(depth0, \"dompurify\", (depth0 && depth0.label), options));\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"

      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showIdentifier), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showReference), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"If required it must appear (at least once) in the selection.\", options) : helperMissing.call(depth0, \"__\", \"If required it must appear (at least once) in the selection.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Not shuffled, the position remains fixed.\", options) : helperMissing.call(depth0, \"__\", \"Not shuffled, the position remains fixed.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n
      \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Items can optionally be assigned to one or more categories.\", options) : helperMissing.call(depth0, \"__\", \"Items can optionally be assigned to one or more categories.\", options)))\n + \"\\n
      \\n
      \\n
      \\n\\n \\n \\n\\n \\n
      \\n
      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.weightsVisible), {hash:{},inverse:self.noop,fn:self.program(5, program5, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Item Session Control\", options) : helperMissing.call(depth0, \"__\", \"Item Session Control\", options)))\n + \"

      \\n\\n\\n
      \\n\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Controls the maximum number of attempts allowed. 0 means unlimited.\", options) : helperMissing.call(depth0, \"__\", \"Controls the maximum number of attempts allowed. 0 means unlimited.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.itemSessionShowFeedback), {hash:{},inverse:self.noop,fn:self.program(7, program7, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n\\n\\n\\n\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.itemSessionAllowComment), {hash:{},inverse:self.noop,fn:self.program(9, program9, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.itemSessionAllowSkipping), {hash:{},inverse:self.noop,fn:self.program(11, program11, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.validateResponsesVisible), {hash:{},inverse:self.noop,fn:self.program(13, program13, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n
      \\n\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.showTimeLimits), {hash:{},inverse:self.noop,fn:self.program(15, program15, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
      \";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/itemref-props-weight', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, functionType=\"function\", escapeExpression=this.escapeExpression;\n\n\n buffer += \"
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n \\n \\n
      \\n
      \";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/rubricblock-props', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, functionType=\"function\", self=this;\n\nfunction program1(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Set the XHTML-QTI class of the rubric block.\", options) : helperMissing.call(depth0, \"__\", \"Set the XHTML-QTI class of the rubric block.\", options)))\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\n\nfunction program3(depth0,data) {\n \n var buffer = \"\", helper, options;\n buffer += \"\\n \\n \";\n return buffer;\n }\n\n buffer += \"
      \\n\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Rubric Block\", options) : helperMissing.call(depth0, \"__\", \"Rubric Block\", options)))\n + \": \";\n if (helper = helpers.orderIndex) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.orderIndex); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"

      \\n\\n \\n \\n \\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.classVisible), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\\n \\n \\n\\n

      \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Feedback block\", options) : helperMissing.call(depth0, \"__\", \"Feedback block\", options)))\n + \"

      \\n\\n \\n \";\n stack1 = helpers['with'].call(depth0, (depth0 && depth0.feedback), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
      \";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/category-presets', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var stack1, functionType=\"function\", escapeExpression=this.escapeExpression, self=this, helperMissing=helpers.helperMissing;\n\nfunction program1(depth0,data,depth1) {\n \n var buffer = \"\", stack1, helper;\n buffer += \"\\n

      \";\n if (helper = helpers.groupLabel) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.groupLabel); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"

      \\n\\n
      \\n \";\n stack1 = helpers.each.call(depth0, (depth0 && depth0.presets), {hash:{},inverse:self.noop,fn:self.programWithDepth(2, program2, data, depth1),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
      \\n\\n\";\n return buffer;\n }\nfunction program2(depth0,data,depth2) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n
      \\n \\n
      \\n \";\n if (helper = helpers.description) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.description); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n
      \\n
      \\n
      \\n \";\n return buffer;\n }\nfunction program3(depth0,data) {\n \n \n return \"checked\";\n }\n\n stack1 = helpers.each.call(depth0, (depth0 && depth0.presetGroups), {hash:{},inverse:self.noop,fn:self.programWithDepth(1, program1, data, depth0),data:data});\n if(stack1 || stack1 === 0) { return stack1; }\n else { return ''; }\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/subsection', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, options, functionType=\"function\", escapeExpression=this.escapeExpression, helperMissing=helpers.helperMissing;\n\n\n buffer += \"
      \\n\\n

      \";\n if (helper = helpers.title) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.title); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n \\n
      \\n
      \\n
      \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n
      \\n
      \\n
      \\n

      \\n\\n
      \\n

      \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Rubric Blocks\", options) : helperMissing.call(depth0, \"__\", \"Rubric Blocks\", options)))\n + \"\\n

      \\n
        \\n \\n
        \\n
        \\n

        \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Items\", options) : helperMissing.call(depth0, \"__\", \"Items\", options)))\n + \"\\n

        \\n
          \\n
          \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Add selected item(s) here.\", options) : helperMissing.call(depth0, \"__\", \"Add selected item(s) here.\", options)))\n + \"\\n
          \\n
          \\n
          \\n
          \";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/menu-button', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, functionType=\"function\", escapeExpression=this.escapeExpression;\n\n\n buffer += \"
        1. \\n \\n \\n \";\n if (helper = helpers.label) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.label); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n \\n
        2. \";\n return buffer;\n }); });\n","\n/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2014-2021 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\n/**\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/creator/templates/index',[\n 'taoQtiTest/controller/creator/config/defaults',\n 'tpl!taoQtiTest/controller/creator/templates/testpart',\n 'tpl!taoQtiTest/controller/creator/templates/section',\n 'tpl!taoQtiTest/controller/creator/templates/rubricblock',\n 'tpl!taoQtiTest/controller/creator/templates/itemref',\n 'tpl!taoQtiTest/controller/creator/templates/outcomes',\n 'tpl!taoQtiTest/controller/creator/templates/test-props',\n 'tpl!taoQtiTest/controller/creator/templates/testpart-props',\n 'tpl!taoQtiTest/controller/creator/templates/section-props',\n 'tpl!taoQtiTest/controller/creator/templates/itemref-props',\n 'tpl!taoQtiTest/controller/creator/templates/itemref-props-weight',\n 'tpl!taoQtiTest/controller/creator/templates/rubricblock-props',\n 'tpl!taoQtiTest/controller/creator/templates/category-presets',\n 'tpl!taoQtiTest/controller/creator/templates/subsection',\n 'tpl!taoQtiTest/controller/creator/templates/menu-button'\n],\nfunction(\n defaults,\n testPart,\n section,\n rubricBlock,\n itemRef,\n outcomes,\n testProps,\n testPartProps,\n sectionProps,\n itemRefProps,\n itemRefPropsWeight,\n rubricBlockProps,\n categoryPresets,\n subsection,\n menuButton\n){\n 'use strict';\n\n const applyTemplateConfiguration = (template) => (config) => template(defaults(config));\n\n /**\n * Expose all the templates used by the test creator\n * @exports taoQtiTest/controller/creator/templates/index\n */\n return {\n testpart : applyTemplateConfiguration(testPart),\n section : applyTemplateConfiguration(section),\n itemref : applyTemplateConfiguration(itemRef),\n rubricblock : applyTemplateConfiguration(rubricBlock),\n outcomes : applyTemplateConfiguration(outcomes),\n subsection : applyTemplateConfiguration(subsection),\n menuButton : applyTemplateConfiguration(menuButton),\n properties : {\n test : applyTemplateConfiguration(testProps),\n testpart : applyTemplateConfiguration(testPartProps),\n section : applyTemplateConfiguration(sectionProps),\n itemref : applyTemplateConfiguration(itemRefProps),\n itemrefweight : applyTemplateConfiguration(itemRefPropsWeight),\n rubricblock : applyTemplateConfiguration(rubricBlockProps),\n categorypresets : applyTemplateConfiguration(categoryPresets),\n subsection : applyTemplateConfiguration(sectionProps)\n\n }\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2014-2022 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\n\n/**\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/creator/views/property',['jquery', 'uikitLoader', 'core/databinder', 'taoQtiTest/controller/creator/templates/index'], function (\n $,\n ui,\n DataBinder,\n templates\n) {\n 'use strict';\n\n /**\n * @callback PropertyViewCallback\n * @param {propertyView} propertyView - the view object\n */\n\n /**\n * The PropertyView setup the property panel component\n * @param {String} tmplName\n * @param {Object} model\n * @exports taoQtiTest/controller/creator/views/property\n * @returns {Object}\n */\n const propView = function propView(tmplName, model) {\n const $container = $('.test-creator-props');\n const template = templates.properties[tmplName];\n let $view;\n\n /**\n * Opens the view for the 1st time\n */\n const open = function propOpen() {\n const binderOptions = {\n templates: templates.properties\n };\n $container.children('.props').hide().trigger('propclose.propview');\n $view = $(template(model)).appendTo($container).filter('.props');\n\n //start listening for DOM compoenents inside the view\n ui.startDomComponent($view);\n\n //start the data binding\n const databinder = new DataBinder($view, model, binderOptions);\n databinder.bind();\n\n propValidation();\n\n $view.trigger('propopen.propview');\n\n // contains identifier from model, needed for validation on keyup for identifiers\n // jQuesy selector for Id with dots don't work\n // dots are allowed for id by default see taoQtiItem/qtiCreator/widgets/helpers/qtiIdentifier\n // need to use attr\n const $identifier = $view.find(`[id=\"props-${model.identifier}\"]`);\n $view.on('change.binder', function (e) {\n if (e.namespace === 'binder' && $identifier.length) {\n $identifier.text(model.identifier);\n }\n });\n };\n\n /**\n * Get the view container element\n * @returns {jQueryElement}\n */\n const getView = function propGetView() {\n return $view;\n };\n\n /**\n * Check wheter the view is displayed\n * @returns {boolean} true id opened\n */\n const isOpen = function propIsOpen() {\n return $view.css('display') !== 'none';\n };\n\n /**\n * Bind a callback on view open\n * @param {PropertyViewCallback} cb\n */\n const onOpen = function propOnOpen(cb) {\n $view.on('propopen.propview', function (e) {\n e.stopPropagation();\n cb();\n });\n };\n\n /**\n * Bind a callback on view close\n * @param {PropertyViewCallback} cb\n */\n const onClose = function propOnClose(cb) {\n $view.on('propclose.propview', function (e) {\n e.stopPropagation();\n cb();\n });\n };\n\n /**\n * Removes the property view\n */\n const destroy = function propDestroy() {\n $view.remove();\n };\n\n /**\n * Toggles the property view display\n */\n const toggle = function propToggle() {\n $container.children('.props').not($view).hide().trigger('propclose.propview');\n if (isOpen()) {\n $view.hide().trigger('propclose.propview');\n } else {\n $view.show().trigger('propopen.propview');\n }\n };\n\n /**\n * Set up the validation on the property view\n * @private\n */\n function propValidation() {\n $view.on('validated.group', function(e, isValid){\n const $warningIconSelector = $('span.icon-warning');\n const $test = $('.tlb-button-on').parents('.test-creator-test');\n\n // finds error current element if any\n let errors = $(e.currentTarget).find('span.validate-error');\n let currentTargetId = `[id=\"${$(e.currentTarget).find('span[data-bind=\"identifier\"]').attr('id').slice(6)}\"]`;\n \n if(e.namespace === 'group'){\n if (isValid && errors.length === 0) {\n //remove warning icon if validation fails\n if($(e.currentTarget).hasClass('test-props')){\n $($test).find($warningIconSelector).first().css('display', 'none');\n }\n $(currentTargetId).find($warningIconSelector).first().css('display', 'none');\n } else {\n //add warning icon if validation fails\n if($(e.currentTarget).hasClass('test-props')){\n $($test).find($warningIconSelector).first().css('display', 'inline');\n }\n $(currentTargetId).find($warningIconSelector).first().css('display', 'inline');\n }\n }\n });\n\n $view.groupValidator({ events: ['keyup', 'change', 'blur'] });\n }\n\n return {\n open: open,\n getView: getView,\n isOpen: isOpen,\n onOpen: onOpen,\n onClose: onClose,\n destroy: destroy,\n toggle: toggle\n };\n };\n\n return propView;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2021-2022 (original work) Open Assessment Technologies SA;\n */\ndefine('taoQtiTest/controller/creator/helpers/subsection',['jquery', 'lodash'], function ($, _) {\n 'use strict';\n\n /**\n * Check if this is first level of subsections\n *\n * @param {JQueryElement} $subsection\n * @returns {boolean}\n */\n function isFistLevelSubsection($subsection) {\n return $subsection.parents('.subsection').length === 0;\n }\n /**\n * Check if this is nested subsections (2nd level)\n *\n * @param {JQueryElement} $subsection\n * @returns {boolean}\n */\n function isNestedSubsection($subsection) {\n return $subsection.parents('.subsection').length > 0;\n }\n /**\n * Get subsections of this section/subsection (without nesting subsection)\n *\n * @param {JQueryElement} $section\n * @returns {boolean}\n */\n function getSubsections($section) {\n return $section.children('.subsections').children('.subsection');\n }\n /**\n * Get siblings subsections of this subsection\n *\n * @param {JQueryElement} $subsection\n * @returns {boolean}\n */\n function getSiblingSubsections($subsection) {\n return getSubsectionContainer($subsection).children('.subsection');\n }\n /**\n * Get parent subsection of this nested subsection\n *\n * @param {JQueryElement} $subsection\n * @returns {boolean}\n */\n function getParentSubsection($subsection) {\n return $subsection.parents('.subsection').first();\n }\n /**\n * Get parent section of this subsection\n *\n * @param {JQueryElement} $subsection\n * @returns {boolean}\n */\n function getParentSection($subsection) {\n return $subsection.parents('.section');\n }\n /**\n * Get parent section/subsection\n *\n * @param {JQueryElement} $subsection\n * @returns {boolean}\n */\n function getParent($subsection) {\n if (isFistLevelSubsection($subsection)) {\n return getParentSection($subsection);\n }\n return getParentSubsection($subsection);\n }\n /**\n * Get parent container('.subsections') for this subsection\n *\n * @param {JQueryElement} $subsection\n * @returns {boolean}\n */\n function getSubsectionContainer($subsection) {\n return $subsection.hasClass('subsections') ? $subsection : $subsection.parents('.subsections').first();\n }\n\n /**\n * Get index for this subsection\n *\n * @param {JQueryElement} $subsection\n * @returns {boolean}\n */\n function getSubsectionTitleIndex($subsection) {\n const $parentSection = getParentSection($subsection);\n const index = getSiblingSubsections($subsection).index($subsection);\n const sectionIndex = $parentSection.parents('.sections').children('.section').index($parentSection);\n if (isFistLevelSubsection($subsection)) {\n return `${sectionIndex + 1}.${index + 1}.`;\n } else {\n const $parentSubsection = getParentSubsection($subsection);\n const subsectionIndex = getSiblingSubsections($parentSubsection).index($parentSubsection);\n return `${sectionIndex + 1}.${subsectionIndex + 1}.${index + 1}.`;\n }\n }\n\n return {\n isFistLevelSubsection,\n isNestedSubsection,\n getSubsections,\n getSubsectionContainer,\n getSiblingSubsections,\n getParentSubsection,\n getParentSection,\n getParent,\n getSubsectionTitleIndex\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2014-2022 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\n\n/**\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/creator/views/actions',[\n 'jquery',\n 'taoQtiTest/controller/creator/views/property',\n 'taoQtiTest/controller/creator/helpers/subsection'\n], function ($, propertyView, subsectionsHelper) {\n 'use strict';\n\n const disabledClass = 'disabled';\n const activeClass = 'active';\n const btnOnClass = 'tlb-button-on';\n\n /**\n * Set up the property view for an element\n * @param {jQueryElement} $container - that contains the property opener\n * @param {String} template - the name of the template to give to the propertyView\n * @param {Object} model - the model to bind\n * @param {PropertyViewCallback} cb - execute at view setup phase\n */\n function properties($container, template, model, cb) {\n let propView = null;\n $container.find('.property-toggler').on('click', function (e) {\n e.preventDefault();\n const $elt = $(this);\n if (!$(this).hasClass(disabledClass)) {\n $elt.blur(); //to remove the focus\n\n if (propView === null) {\n $container.addClass(activeClass);\n $elt.addClass(btnOnClass);\n\n propView = propertyView(template, model);\n propView.open();\n\n propView.onOpen(function () {\n $container.addClass(activeClass);\n $elt.addClass(btnOnClass);\n });\n propView.onClose(function () {\n $container.removeClass(activeClass);\n $elt.removeClass(btnOnClass);\n });\n\n if (typeof cb === 'function') {\n cb(propView);\n }\n } else {\n propView.toggle();\n }\n }\n });\n }\n\n /**\n * Enable to move an element\n * @param {jQueryElement} $actionContainer - where the mover is\n * @param {String} containerClass - the cssClass of the element container\n * @param {String} elementClass - the cssClass to identify elements\n */\n function move($actionContainer, containerClass, elementClass) {\n const $element = $actionContainer.closest(`.${elementClass}`);\n const $container = $element.closest(`.${containerClass}`);\n\n //move up an element\n $('.move-up', $actionContainer).click(function (e) {\n let $elements, index;\n\n //prevent default and click during animation and on disabled icon\n e.preventDefault();\n if ($element.is(':animated') && $element.hasClass('disabled')) {\n return false;\n }\n\n //get the position\n $elements = $container.children(`.${elementClass}`);\n index = $elements.index($element);\n if (index > 0) {\n $element.fadeOut(200, () => {\n $element\n .insertBefore($container.children(`.${elementClass}:eq(${index - 1})`))\n .fadeIn(400, () => $container.trigger('change'));\n });\n }\n });\n\n //move down an element\n $('.move-down', $actionContainer).click(function (e) {\n let $elements, index;\n\n //prevent default and click during animation and on disabled icon\n e.preventDefault();\n if ($element.is(':animated') && $element.hasClass('disabled')) {\n return false;\n }\n\n //get the position\n $elements = $container.children(`.${elementClass}`);\n index = $elements.index($element);\n if (index < $elements.length - 1 && $elements.length > 1) {\n $element.fadeOut(200, () => {\n $element\n .insertAfter($container.children(`.${elementClass}:eq(${index + 1})`))\n .fadeIn(400, () => $container.trigger('change'));\n });\n }\n });\n }\n\n /**\n * Update the movable state of an element\n * @param {jQueryElement} $container - the movable elements (scopped)\n * @param {String} elementClass - the cssClass to identify elements\n * @param {String} actionContainerElt - the element name that contains the actions\n */\n function movable($container, elementClass, actionContainerElt) {\n $container.each(function () {\n const $elt = $(this);\n const $actionContainer = $elt.children(actionContainerElt);\n\n const index = $container.index($elt);\n const $moveUp = $('.move-up', $actionContainer);\n const $moveDown = $('.move-down', $actionContainer);\n\n //only one test part, no moving\n if ($container.length === 1) {\n $moveUp.addClass(disabledClass);\n $moveDown.addClass(disabledClass);\n\n //testpart is the first, only moving down\n } else if (index === 0) {\n $moveUp.addClass(disabledClass);\n $moveDown.removeClass(disabledClass);\n\n //testpart is the lasst, only moving up\n } else if (index >= $container.length - 1) {\n $moveDown.addClass(disabledClass);\n $moveUp.removeClass(disabledClass);\n\n //or enable moving top/bottom\n } else {\n $moveUp.removeClass(disabledClass);\n $moveDown.removeClass(disabledClass);\n }\n });\n }\n\n /**\n * Update the removable state of an element\n * @param {jQueryElement} $container - that contains the removable action\n * @param {String} actionContainerElt - the element name that contains the actions\n */\n function removable($container, actionContainerElt) {\n $container.each(function () {\n const $elt = $(this);\n const $actionContainer = $elt.children(actionContainerElt);\n const $delete = $('[data-delete]', $actionContainer);\n\n if ($container.length <= 1 && !$elt.hasClass('subsection')) {\n $delete.addClass(disabledClass);\n } else {\n $delete.removeClass(disabledClass);\n }\n });\n }\n\n /**\n * Disable all the actions of the target\n * @param {jQueryElement} $container - that contains the the actions\n * @param {String} actionContainerElt - the element name that contains the actions\n */\n function disable($container, actionContainerElt) {\n\n if ($container.length <= 2){\n $container.children(actionContainerElt).find('[data-delete]').addClass(disabledClass);\n }\n }\n\n /**\n * Enable all the actions of the target\n * @param {jQueryElement} $container - that contains the the actions\n * @param {String} actionContainerElt - the element name that contains the actions\n */\n function enable($container, actionContainerElt) {\n $container.children(actionContainerElt).find('[data-delete],.move-up,.move-down').removeClass(disabledClass);\n }\n\n /**\n * Hides/shows container for adding items inside a section checking if there is at least\n * one subsection inside of it. As delete subsection event is triggered before subsection\n * container is actually removed from section container, we need to have conditional flow\n * @param {jQueryElement} $section - section jquery container\n */\n function displayItemWrapper($section) {\n const $elt = $('.itemrefs-wrapper:first', $section);\n const subsectionsCount = subsectionsHelper.getSubsections($section).length;\n if (subsectionsCount) {\n $elt.hide();\n } else {\n $elt.show();\n }\n }\n\n /**\n * Update delete selector for 2nd level subsections\n *@param {jQueryElement} $actionContainer - action's container\n */\n function updateDeleteSelector($actionContainer) {\n const $deleteButton = $actionContainer.find('.delete-subsection');\n if ($deleteButton.parents('.subsection').length > 1) {\n const deleteSelector = $deleteButton.data('delete');\n $deleteButton.attr('data-delete', `${deleteSelector} .subsection`);\n }\n }\n\n /**\n * Hides/shows category-presets (Test Navigation, Navigation Warnings, Test-Taker Tools)\n * Hide category-presets for section that contains subsections\n * @param {jQueryElement} $authoringContainer\n * @param {string} [scope='section'] // can also be 'testpart'\n * @fires propertiesView#set-default-categories\n */\n function displayCategoryPresets($authoringContainer, scope = 'section') {\n const id = $authoringContainer.attr('id');\n const $propertiesView = $(`.test-creator-props #${scope}-props-${id}`);\n if (!$propertiesView.length) {\n // property view is not setup\n return;\n }\n const $elt = $propertiesView.find('.category-presets');\n switch (scope) {\n case 'testpart':\n $elt.show();\n break;\n\n case 'section':\n const subsectionsCount = subsectionsHelper.getSubsections($authoringContainer).length;\n if (subsectionsCount) {\n $elt.hide();\n $propertiesView.trigger('set-default-categories');\n } else {\n $elt.show();\n }\n break;\n }\n }\n\n /**\n * Update the index of an section/subsection\n * @param {jQueryElement} $list - list of elements\n */\n function updateTitleIndex($list) {\n $list.each(function () {\n const $elt = $(this);\n const $indexSpan = $('.title-index', $elt.children('h2'));\n\n if ($elt.hasClass('section')) {\n const $parent = $elt.parents('.sections');\n const index = $('.section', $parent).index($elt);\n $indexSpan.text(`${index + 1}.`);\n } else {\n $indexSpan.text(subsectionsHelper.getSubsectionTitleIndex($elt));\n }\n });\n }\n\n /**\n * The actions gives you shared behavior for some actions.\n *\n * @exports taoQtiTest/controller/creator/views/actions\n */\n return {\n properties,\n move,\n removable,\n movable,\n disable,\n enable,\n displayItemWrapper,\n updateDeleteSelector,\n displayCategoryPresets,\n updateTitleIndex\n };\n});\n\n","/*\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2022 (original work) Open Assessment Technologies SA\n *\n */\n\ndefine('taoQtiTest/controller/creator/helpers/featureVisibility',['services/features'], function (features) {\n 'use strict';\n\n /**\n * Adds visibility properties for test model which allow to toggle test properties presence in interface\n * @param {Object} model\n */\n function addTestVisibilityProps(model) {\n const propertyNamespace = 'taoQtiTest/creator/test/property/';\n if (features.isVisible(`${propertyNamespace}timeLimits`)) {\n model.showTimeLimits = true;\n }\n if (features.isVisible('taoQtiTest/creator/test/property/identifier')) {\n model.showIdentifier = true;\n }\n if (features.isVisible('taoQtiTest/creator/test/property/lateSubmission')) {\n model.lateSubmission = true;\n }\n if (features.isVisible('taoQtiTest/creator/test/property/outcomeDeclarations')) {\n model.showOutcomeDeclarations = true;\n }\n }\n\n /**\n * Adds visibility properties for testPart model which allow to toggle testPart properties presence in interface\n * @param {Object} model\n */\n function addTestPartVisibilityProps(model) {\n const propertyNamespace = 'taoQtiTest/creator/testPart/property/';\n if (features.isVisible(`${propertyNamespace}timeLimits`)) {\n model.showTimeLimits = true;\n }\n if (features.isVisible(`${propertyNamespace}identifier`)) {\n model.showIdentifier = true;\n }\n if (features.isVisible(`${propertyNamespace}submissionMode`)) {\n model.submissionModeVisible = true;\n }\n if (features.isVisible(`${propertyNamespace}lateSubmission`)) {\n model.lateSubmission = true;\n }\n if (features.isVisible(`${propertyNamespace}itemSessionControl`)) {\n model.showItemSessionControl = true;\n }\n if (features.isVisible(`${propertyNamespace}navigationWarnings`)) {\n model.showNavigationWarnings = true;\n }\n if (features.isVisible(`${propertyNamespace}itemSessionControl/showFeedback`)) {\n model.itemSessionShowFeedback = true;\n }\n if (features.isVisible(`${propertyNamespace}itemSessionControl/allowComment`)) {\n model.itemSessionAllowComment = true;\n }\n if (features.isVisible(`${propertyNamespace}itemSessionControl/allowSkipping`)) {\n model.itemSessionAllowSkipping = true;\n }\n }\n\n /**\n * Adds visibility properties for section model which allow to toggle section properties presence in interface\n * @param {Object} model\n */\n function addSectionVisibilityProps(model) {\n const propertyNamespace = 'taoQtiTest/creator/section/property/';\n if (features.isVisible(`${propertyNamespace}timeLimits`)) {\n model.showTimeLimits = true;\n }\n if (features.isVisible(`${propertyNamespace}identifier`)) {\n model.showIdentifier = true;\n }\n if (features.isVisible(`${propertyNamespace}visible`)) {\n model.showVisible = true;\n }\n if (features.isVisible(`${propertyNamespace}keepTogether`)) {\n model.showKeepTogether = true;\n }\n if (features.isVisible(`${propertyNamespace}lateSubmission`)) {\n model.lateSubmission = true;\n }\n if (features.isVisible(`${propertyNamespace}itemSessionControl/validateResponses`)) {\n model.validateResponsesVisible = true;\n }\n if (features.isVisible(`${propertyNamespace}itemSessionControl/showFeedback`)) {\n model.itemSessionShowFeedback = true;\n }\n if (features.isVisible(`${propertyNamespace}itemSessionControl/allowComment`)) {\n model.itemSessionAllowComment = true;\n }\n if (features.isVisible(`${propertyNamespace}itemSessionControl/allowSkipping`)) {\n model.itemSessionAllowSkipping = true;\n }\n if (features.isVisible(`${propertyNamespace}rubricBlocks/class`)) {\n model.rubricBlocksClass = true;\n }\n }\n\n /**\n * Adds visibility properties for item model which allow to toggle item properties presence in interface\n * @param {Object} model\n */\n function addItemRefVisibilityProps(model) {\n const propertyNamespace = 'taoQtiTest/creator/itemRef/property/';\n if (features.isVisible(`${propertyNamespace}timeLimits`)) {\n model.showTimeLimits = true;\n }\n if (features.isVisible(`${propertyNamespace}identifier`)) {\n model.showIdentifier = true;\n }\n if (features.isVisible(`${propertyNamespace}reference`)) {\n model.showReference = true;\n }\n if (features.isVisible(`${propertyNamespace}lateSubmission`)) {\n model.lateSubmission = true;\n }\n if (features.isVisible(`${propertyNamespace}itemSessionControl/showFeedback`)) {\n model.itemSessionShowFeedback = true;\n }\n if (features.isVisible(`${propertyNamespace}itemSessionControl/allowComment`)) {\n model.itemSessionAllowComment = true;\n }\n if (features.isVisible(`${propertyNamespace}itemSessionControl/allowSkipping`)) {\n model.itemSessionAllowSkipping = true;\n }\n if (features.isVisible(`${propertyNamespace}itemSessionControl/validateResponses`)) {\n model.validateResponsesVisible = true;\n }\n if (features.isVisible(`${propertyNamespace}weights`)) {\n model.weightsVisible = true;\n }\n }\n\n /**\n * Filters the presets and preset groups based on visibility config\n * @param {Array} presetGroups array of presetGroups\n * @param {string} [level='all'] testPart, section of itemRef\n * @returns {Array} filtered presetGroups array\n */\n function filterVisiblePresets(presetGroups, level = 'all') {\n const categoryGroupNamespace = `taoQtiTest/creator/${level}/category/presetGroup/`;\n const categoryPresetNamespace = `taoQtiTest/creator/${level}/category/preset/`;\n let filteredGroups;\n if (presetGroups && presetGroups.length) {\n filteredGroups = presetGroups.filter(presetGroup => {\n return features.isVisible(`${categoryGroupNamespace}${presetGroup.groupId}`);\n });\n if (filteredGroups.length) {\n filteredGroups.forEach(filteredGroup => {\n if (filteredGroup.presets && filteredGroup.presets.length) {\n const filteredPresets = filteredGroup.presets.filter(preset => {\n return features.isVisible(`${categoryPresetNamespace}${preset.id}`);\n });\n filteredGroup.presets = filteredPresets;\n }\n });\n }\n }\n return filteredGroups;\n }\n\n return {\n addTestVisibilityProps,\n addTestPartVisibilityProps,\n addSectionVisibilityProps,\n addItemRefVisibilityProps,\n filterVisiblePresets\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017-2024 (original work) Open Assessment Technologies SA;\n */\n/**\n * This helper manages the category selection UI:\n * - either via a text entry field that allow to enter any custom categories\n * - either via displaying grouped checkboxes that allow to select any categories presets\n * All categories are then grouped and given to this object's listeners, as they will later end up in the same model field.\n *\n * @author Christophe Noël \n */\ndefine('taoQtiTest/controller/creator/helpers/categorySelector',[\n 'jquery',\n 'lodash',\n 'i18n',\n 'core/eventifier',\n 'ui/dialog/confirm',\n 'ui/tooltip',\n 'taoQtiTest/controller/creator/templates/index',\n 'taoQtiTest/controller/creator/helpers/featureVisibility',\n 'select2'\n], function ($, _, __, eventifier, confirmDialog, tooltip, templates, featureVisibility) {\n 'use strict';\n\n let allPresets = [];\n let allQtiCategoriesPresets = [];\n let categoryToPreset = new Map();\n\n function categorySelectorFactory($container) {\n const $presetsContainer = $container.find('.category-presets');\n const $customCategoriesSelect = $container.find('[name=category-custom]');\n\n const categorySelector = {\n /**\n * Read the form state from the DOM and trigger an event with the result, so the listeners can update the item/section model\n * @fires categorySelector#category-change\n */\n updateCategories() {\n const presetSelected = $container\n .find('.category-preset input:checked')\n .toArray()\n .map(categoryEl => categoryEl.value),\n presetIndeterminate = $container\n .find('.category-preset input:indeterminate')\n .toArray()\n .map(categoryEl => categoryEl.value),\n customSelected = $customCategoriesSelect\n .siblings('.select2-container')\n .find('.select2-search-choice')\n .not('.partial')\n .toArray()\n .map(categoryEl => categoryEl.textContent && categoryEl.textContent.trim()),\n customIndeterminate = $customCategoriesSelect\n .siblings('.select2-container')\n .find('.select2-search-choice.partial')\n .toArray()\n .map(categoryEl => categoryEl.textContent && categoryEl.textContent.trim());\n\n const selectedCategories = presetSelected.concat(customSelected);\n const indeterminatedCategories = presetIndeterminate.concat(customIndeterminate);\n\n /**\n * @event categorySelector#category-change\n * @param {String[]} allCategories\n * @param {String[]} indeterminate\n */\n this.trigger('category-change', selectedCategories, indeterminatedCategories);\n },\n\n /**\n * Create the category selection form\n *\n * @param {Array} [currentCategories] - all categories currently associated to the item. If applied to a section,\n * contains all the categories applied to at least one item of the section.\n * @param {string} [level] one of the values `testPart`, `section` or `itemRef`\n */\n createForm(currentCategories, level) {\n const presetsTpl = templates.properties.categorypresets;\n const customCategories = _.difference(currentCategories, allQtiCategoriesPresets);\n\n const filteredPresets = featureVisibility.filterVisiblePresets(allPresets, level);\n // add preset checkboxes\n $presetsContainer.append(presetsTpl({ presetGroups: filteredPresets }));\n\n $presetsContainer.on('click', e => {\n const $preset = $(e.target).closest('.category-preset');\n if ($preset.length) {\n const $checkbox = $preset.find('input');\n $checkbox.prop('indeterminate', false);\n\n _.defer(() => this.updateCategories());\n }\n });\n\n // init custom categories field\n $customCategoriesSelect\n .select2({\n width: '100%',\n containerCssClass: 'custom-categories',\n tags: customCategories,\n multiple: true,\n tokenSeparators: [',', ' ', ';'],\n formatNoMatches: function () {\n return __('Enter a custom category');\n },\n maximumInputLength: 32\n })\n .on('change', () => this.updateCategories());\n\n // when clicking on a partial category, ask the user if it wants to apply it to all items\n $container.find('.custom-categories').on('click', '.partial', e => {\n const $choice = $(e.target).closest('.select2-search-choice');\n const tag = $choice.text().trim();\n\n confirmDialog(__('Do you want to apply the category \"%s\" to all included items?', tag), () => {\n $choice.removeClass('partial');\n this.updateCategories();\n });\n });\n\n // enable help tooltips\n tooltip.lookup($container);\n },\n\n /**\n * Check/Uncheck boxes and fill the custom category field to match the new model\n * @param {String[]} selected - categories associated with an item, or with all the items of the same section\n * @param {String[]} [indeterminate] - categories in an indeterminate state at a section level\n */\n updateFormState(selected, indeterminate) {\n indeterminate = indeterminate || [];\n\n const customCategories = _.difference(selected.concat(indeterminate), allQtiCategoriesPresets);\n\n // Preset categories\n\n const $presetsCheckboxes = $container.find('.category-preset input');\n $presetsCheckboxes.each((idx, input) => {\n const qtiCategory = input.value;\n if (!categoryToPreset.has(qtiCategory)) {\n // Unlikely to happen, but better safe than sorry...\n input.indeterminate = indeterminate.includes(qtiCategory);\n input.checked = selected.includes(qtiCategory);\n return;\n }\n // Check if one category declared for the preset is selected.\n // Usually, only one exists, but it may happen that alternatives are present.\n // In any case, only the main declared category (qtiCategory) will be saved.\n // The concept is as follows: read all, write one.\n const preset = categoryToPreset.get(qtiCategory);\n const hasCategory = category => preset.categories.includes(category);\n input.indeterminate = indeterminate.some(hasCategory);\n input.checked = selected.some(hasCategory);\n });\n\n // Custom categories\n\n $customCategoriesSelect.select2('val', customCategories);\n\n $customCategoriesSelect\n .siblings('.select2-container')\n .find('.select2-search-choice')\n .each((idx, li) => {\n const $li = $(li);\n const content = $li.find('div').text();\n if (indeterminate.indexOf(content) !== -1) {\n $li.addClass('partial');\n }\n });\n }\n };\n\n eventifier(categorySelector);\n\n return categorySelector;\n }\n\n /**\n * @param {Object[]} presets - expected format:\n * [\n * {\n * groupId: 'navigation',\n * groupLabel: 'Test Navigation',\n * presets: [\n * {\n * id: 'nextPartWarning',\n * label: 'Next Part Warning',\n * qtiCategory: 'x-tao-option-nextPartWarning',\n * altCategories: [x-tao-option-nextPartWarningMessage]\n * description: 'Displays a warning before the user finishes a part'\n * ...\n * },\n * ...\n * ]\n * },\n * ...\n * ]\n */\n categorySelectorFactory.setPresets = function setPresets(presets) {\n if (Array.isArray(presets)) {\n allPresets = Array.from(presets);\n categoryToPreset = new Map();\n allQtiCategoriesPresets = allPresets.reduce((allCategories, group) => {\n return group.presets.reduce((all, preset) => {\n const categories = [preset.qtiCategory].concat(preset.altCategories || []);\n categories.forEach(category => categoryToPreset.set(category, preset));\n preset.categories = categories;\n return all.concat(categories);\n }, allCategories);\n }, []);\n }\n };\n\n return categorySelectorFactory;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2015-2023 (original work) Open Assessment Technologies SA;\n */\ndefine('taoQtiTest/controller/creator/helpers/sectionCategory',['lodash', 'i18n', 'core/errorHandler'], function (_, __, errorHandler) {\n 'use strict';\n\n const _ns = '.sectionCategory';\n\n /**\n * Check if the given object is a valid assessmentSection model object\n *\n * @param {object} model\n * @returns {boolean}\n */\n function isValidSectionModel(model) {\n return _.isObject(model) && model['qti-type'] === 'assessmentSection' && _.isArray(model.sectionParts);\n }\n\n /**\n * Set an array of categories to the section model (affect the childen itemRef)\n *\n * @param {object} model\n * @param {array} selected - all categories active for the whole section\n * @param {array} partial - only categories in an indeterminate state\n * @returns {undefined}\n */\n function setCategories(model, selected, partial) {\n const currentCategories = getCategories(model);\n\n partial = partial || [];\n\n //the categories that are no longer in the new list of categories should be removed\n const toRemove = _.difference(currentCategories.all, selected.concat(partial));\n\n //the categories that are not in the current categories collection should be added to the children\n const toAdd = _.difference(selected, currentCategories.propagated);\n\n model.categories = _.difference(model.categories, toRemove);\n model.categories = model.categories.concat(toAdd);\n\n //process the modification\n addCategories(model, toAdd);\n removeCategories(model, toRemove);\n }\n\n /**\n * Get the categories assign to the section model, infered by its interal itemRefs\n *\n * @param {object} model\n * @returns {object}\n */\n function getCategories(model) {\n let categories= [],\n arrays,\n union,\n propagated,\n partial,\n itemCount = 0;\n\n if (!isValidSectionModel(model)) {\n return errorHandler.throw(_ns, 'invalid tool config format');\n }\n\n const getCategoriesRecursive = sectionModel => _.forEach(sectionModel.sectionParts, function (sectionPart) {\n if (\n sectionPart['qti-type'] === 'assessmentItemRef' &&\n ++itemCount &&\n _.isArray(sectionPart.categories)\n ) {\n categories.push(_.compact(sectionPart.categories));\n }\n if (sectionPart['qti-type'] === 'assessmentSection' && _.isArray(sectionPart.sectionParts)) {\n getCategoriesRecursive(sectionPart);\n }\n });\n\n getCategoriesRecursive(model);\n\n if (!itemCount) {\n return createCategories(model.categories, model.categories);\n }\n\n //array of categories\n arrays = _.values(categories);\n union = _.union.apply(null, arrays);\n\n //categories that are common to all itemRef\n propagated = _.intersection.apply(null, arrays);\n\n //the categories that are only partially covered on the section level : complementary of \"propagated\"\n partial = _.difference(union, propagated);\n\n return createCategories(union, propagated, partial);\n }\n\n /**\n * Add an array of categories to a section model (affect the childen itemRef)\n *\n * @param {object} model\n * @param {array} categories\n * @returns {undefined}\n */\n function addCategories(model, categories) {\n if (isValidSectionModel(model)) {\n _.forEach(model.sectionParts, function (sectionPart) {\n if (sectionPart['qti-type'] === 'assessmentItemRef') {\n if (!_.isArray(sectionPart.categories)) {\n sectionPart.categories = [];\n }\n sectionPart.categories = _.union(sectionPart.categories, categories);\n }\n if (sectionPart['qti-type'] === 'assessmentSection') {\n addCategories(sectionPart, categories);\n }\n });\n } else {\n errorHandler.throw(_ns, 'invalid tool config format');\n }\n }\n\n /**\n * Remove an array of categories from a section model (affect the childen itemRef)\n *\n * @param {object} model\n * @param {array} categories\n * @returns {undefined}\n */\n function removeCategories(model, categories) {\n if (isValidSectionModel(model)) {\n _.forEach(model.sectionParts, function (sectionPart) {\n if (sectionPart['qti-type'] === 'assessmentItemRef' && _.isArray(sectionPart.categories)) {\n sectionPart.categories = _.difference(sectionPart.categories, categories);\n }\n if (sectionPart['qti-type'] === 'assessmentSection') {\n removeCategories(sectionPart, categories);\n }\n });\n } else {\n errorHandler.throw(_ns, 'invalid tool config format');\n }\n }\n\n function createCategories(all = [], propagated = [], partial = []) {\n return _.mapValues(\n {\n all: all,\n propagated: propagated,\n partial: partial\n },\n function (categories) {\n return categories.sort();\n }\n );\n }\n\n return {\n isValidSectionModel: isValidSectionModel,\n setCategories: setCategories,\n getCategories: getCategories,\n addCategories: addCategories,\n removeCategories: removeCategories\n };\n});\n\n","/*\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2022 (original work) Open Assessment Technologies SA\n *\n */\ndefine('taoQtiTest/controller/creator/helpers/validators',[\n 'ui/validator/validators',\n 'jquery',\n 'lodash',\n 'i18n',\n 'taoQtiTest/controller/creator/helpers/outcome',\n 'taoQtiTest/controller/creator/helpers/qtiElement',\n 'taoQtiItem/qtiCreator/widgets/helpers/qtiIdentifier'\n], function (validators, $, _, __, outcomeHelper, qtiElementHelper, qtiIdentifier) {\n 'use strict';\n\n const qtiIdPattern = qtiIdentifier.pattern;\n //Identifiers must be unique across\n //those QTI types\n const qtiTypesForUniqueIds = ['assessmentTest', 'testPart', 'assessmentSection', 'assessmentItemRef'];\n\n /**\n * Gives you a validator that check QTI id format\n * @returns {Object} the validator\n */\n function idFormatValidator() {\n return {\n name: 'idFormat',\n message: qtiIdentifier.invalidQtiIdMessage,\n validate: function (value, callback) {\n if (typeof callback === 'function') {\n callback(qtiIdPattern.test(value));\n }\n }\n };\n }\n\n /**\n * Gives you a validator that check QTI id format of the test (it is different from the others...)\n * @returns {Object} the validator\n */\n function testidFormatValidator() {\n const qtiTestIdPattern = /^\\S+$/;\n return {\n name: 'testIdFormat',\n message: __('is not a valid identifier (everything except spaces)'),\n validate: function (value, callback) {\n if (typeof callback === 'function') {\n callback(qtiTestIdPattern.test(value));\n }\n }\n };\n }\n\n /**\n * Gives you a validator that check if a QTI id is available\n * @param {Object} modelOverseer - let's you get the data model\n * @returns {Object} the validator\n */\n function idAvailableValidator(modelOverseer) {\n return {\n name: 'testIdAvailable',\n message: __('is already used in the test.'),\n validate: function (value, callback, options) {\n if (options.identifier) {\n const key = value.toUpperCase();\n const identifiers = extractIdentifiers(modelOverseer.getModel(), qtiTypesForUniqueIds);\n // jQuesy selector for Id with dots don't work\n // dots are allowed for id by default see taoQtiItem/qtiCreator/widgets/helpers/qtiIdentifier\n // need to use attr\n const $idInUI = $(`[id=\"props-${options.identifier}\"]:contains(\"${value}\")`);\n if (typeof callback === 'function') {\n const counts = _.countBy(identifiers, 'identifier');\n //the identifier list contains itself after change on input\n //on keyup $idInUI.length === 0\n //on change and blur $idInUI.length === 1 and text equal value\n callback(\n typeof counts[key] === 'undefined' ||\n $idInUI.length === 1 && $idInUI.text() === value && counts[key] === 1\n );\n }\n } else {\n throw new Error('missing required option \"identifier\"');\n }\n }\n };\n }\n\n /**\n * Gives you a validator that check if a QTI id is available\n * @param {Object} modelOverseer - let's you get the data model\n */\n function registerValidators(modelOverseer) {\n //register validators\n validators.register('idFormat', idFormatValidator());\n validators.register('testIdFormat', testidFormatValidator());\n validators.register('testIdAvailable', idAvailableValidator(modelOverseer), true);\n }\n\n /**\n * Validates the provided model\n * @param {Object} model\n * @throws {Error} if the model is not valid\n */\n function validateModel(model) {\n const identifiers = extractIdentifiers(model, qtiTypesForUniqueIds);\n let nonUniqueIdentifiers = 0;\n const outcomes = _.keyBy(outcomeHelper.listOutcomes(model));\n let messageDetails = '';\n\n _(identifiers)\n .countBy('identifier')\n .forEach(function (count, id) {\n if (count > 1) {\n nonUniqueIdentifiers++;\n messageDetails += `\\n${id}`;\n }\n });\n if (nonUniqueIdentifiers >= 1) {\n throw new Error(__('The following identifiers are not unique accross the test : %s', messageDetails));\n }\n\n _.forEach(model.testParts, function (testPart) {\n _.forEach(testPart.assessmentSections, function (assessmentSection) {\n _.forEach(assessmentSection.rubricBlocks, function (rubricBlock) {\n const feedbackBlock = qtiElementHelper.lookupElement(\n rubricBlock,\n 'rubricBlock.div.feedbackBlock',\n 'content'\n );\n if (feedbackBlock && !outcomes[feedbackBlock.outcomeIdentifier]) {\n throw new Error(\n __(\n 'The outcome \"%s\" does not exist, but it is referenced by a feedback block!',\n feedbackBlock.outcomeIdentifier\n )\n );\n }\n });\n });\n });\n }\n /**\n * Extracts the identifiers from a QTI model\n * @param {Object|Object[]} model - the JSON QTI model\n * @param {String[]} [includesOnlyTypes] - list of qti-type to include, exclusively\n * @param {String[]} [excludeTypes] - list of qti-type to exclude, it excludes the children too\n * @returns {Object[]} a collection of identifiers (with some meta), if the id is not unique it will appear multiple times, as extracted.\n */\n function extractIdentifiers(model, includesOnlyTypes, excludeTypes) {\n const identifiers = [];\n\n const extract = function extract(element) {\n if (element && _.has(element, 'identifier') && _.isString(element.identifier)) {\n if (!includesOnlyTypes.length || _.includes(includesOnlyTypes, element['qti-type'])) {\n identifiers.push({\n identifier: element.identifier.toUpperCase(),\n originalIdentifier: element.identifier,\n type: element['qti-type'],\n label: element.title || element.identifier\n });\n }\n }\n _.forEach(element, function (subElement) {\n if (_.isPlainObject(subElement) || _.isArray(subElement)) {\n if (!excludeTypes.length || !_.includes(excludeTypes, subElement['qti-type'])) {\n extract(subElement);\n }\n }\n });\n };\n\n if (_.isPlainObject(model) || _.isArray(model)) {\n excludeTypes = excludeTypes || [];\n includesOnlyTypes = includesOnlyTypes || [];\n\n extract(model);\n }\n return identifiers;\n }\n\n /**\n * Gives you a validator that check QTI id format\n * @param {String} value of identifier\n * @param {Object} modelOverseer - let's you get the data model\n * @returns {Boolean} isValid\n */\n function checkIfItemIdValid(value, modelOverseer) {\n const identifiers = extractIdentifiers(modelOverseer.getModel(), qtiTypesForUniqueIds);\n const key = value.toUpperCase();\n const counts = _.countBy(identifiers, 'identifier');\n return qtiIdPattern.test(value) && counts[key] === 1;\n }\n return {\n qtiTypesForUniqueIds,\n extractIdentifiers,\n registerValidators,\n validateModel,\n checkIfItemIdValid\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2015-2022 (original work) Open Assessment Technologies SA ;\n */\n\n/**\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/creator/helpers/qtiTest',['jquery', 'lodash', 'taoQtiTest/controller/creator/helpers/validators'], function ($, _, validators) {\n 'use strict';\n\n /**\n * Utility to manage the QTI Test model\n * @exports taoQtiTest/controller/creator/qtiTestHelper\n */\n const qtiTestHelper = {\n /**\n * Get the list of unique identifiers for the given model.\n * @param {Object|Object[]} model - the JSON QTI model\n * @param {String[]} [includesOnlyTypes] - list of qti-type to include, exclusively\n * @param {String[]} [excludeTypes] - list of qti-type to exclude, it excludes the children too\n * @returns {String[]} the list of unique identifiers\n */\n getIdentifiers: function getIdentifiers(model, includesOnlyTypes, excludeTypes) {\n return _.uniq(_.map(validators.extractIdentifiers(model, includesOnlyTypes, excludeTypes), 'identifier'));\n },\n\n /**\n * Get the list of identifiers for a given QTI type, only.\n * @param {Object|Object[]} model - the JSON QTI model\n * @param {String} qtiType - the type of QTI element to get the identifiers.\n * @returns {String[]} the list of unique identifiers\n */\n getIdentifiersOf: function getIdentifiersOf(model, qtiType) {\n return this.getIdentifiers(model, [qtiType]);\n },\n\n /**\n * Does the value contains the type type\n * @param {Object} value\n * @param {string} type\n * @returns {boolean}\n */\n filterQtiType: function filterQtiType(value, type) {\n return value['qti-type'] && value['qti-type'] === type;\n },\n\n /**\n * Add the 'qti-type' properties to object that miss it, using the parent key name\n * @param {Object|Array} collection\n * @param {string} parentType\n */\n addMissingQtiType: function addMissingQtiType(collection, parentType) {\n _.forEach(collection, (value, key) => {\n if (_.isObject(value) && !_.isArray(value) && !_.has(value, 'qti-type')) {\n if (_.isNumber(key)) {\n if (parentType) {\n value['qti-type'] = parentType;\n }\n } else {\n value['qti-type'] = key;\n }\n }\n if (_.isArray(value)) {\n this.addMissingQtiType(value, key.replace(/s$/, ''));\n } else if (_.isObject(value)) {\n this.addMissingQtiType(value);\n }\n });\n },\n\n /**\n * Applies consolidation rules to the model\n * @param {Object} model\n * @returns {Object}\n */\n consolidateModel: function consolidateModel(model) {\n if (model && model.testParts && _.isArray(model.testParts)) {\n _.forEach(model.testParts, function (testPart) {\n if (testPart.assessmentSections && _.isArray(testPart.assessmentSections)) {\n _.forEach(testPart.assessmentSections, function (assessmentSection) {\n //remove ordering is shuffle is false\n if (\n assessmentSection.ordering &&\n typeof assessmentSection.ordering.shuffle !== 'undefined' &&\n assessmentSection.ordering.shuffle === false\n ) {\n delete assessmentSection.ordering;\n }\n\n // clean categories (QTI identifier can't be empty string)\n if (assessmentSection.sectionParts && _.isArray(assessmentSection.sectionParts)) {\n _.forEach(assessmentSection.sectionParts, function (part) {\n if (\n part.categories &&\n _.isArray(part.categories) &&\n (part.categories.length === 0 || part.categories[0].length === 0)\n ) {\n part.categories = [];\n }\n });\n }\n\n if (assessmentSection.rubricBlocks && _.isArray(assessmentSection.rubricBlocks)) {\n //remove rubric blocks if empty\n if (\n assessmentSection.rubricBlocks.length === 0 ||\n (assessmentSection.rubricBlocks.length === 1 &&\n assessmentSection.rubricBlocks[0].content.length === 0)\n ) {\n delete assessmentSection.rubricBlocks;\n } else if (assessmentSection.rubricBlocks.length > 0) {\n //ensure the view attribute is present\n _.forEach(assessmentSection.rubricBlocks, function (rubricBlock) {\n rubricBlock.views = ['candidate'];\n //change once views are supported\n //if(rubricBlock && rubricBlock.content && (!rubricBlock.views || (_.isArray(rubricBlock.views) && rubricBlock.views.length === 0))){\n //rubricBlock.views = ['candidate'];\n //}\n });\n }\n }\n });\n }\n });\n }\n return model;\n },\n /**\n * Get a valid and available QTI identifier for the given type\n * @param {Object|Object[]} model - the JSON QTI model to check the existing IDs\n * @param {String} qtiType - the type of element you want an id for\n * @param {String} [suggestion] - the default pattern body, we use the type otherwise\n * @returns {String} the generated identifier\n */\n getAvailableIdentifier: function getAvailableIdentifier(model, qtiType, suggestion) {\n let index = 1;\n const glue = '-';\n let identifier;\n let current;\n if (_.includes(validators.qtiTypesForUniqueIds, qtiType)) {\n current = this.getIdentifiers(model, validators.qtiTypesForUniqueIds);\n } else {\n current = this.getIdentifiersOf(model, qtiType);\n }\n\n suggestion = suggestion || qtiType;\n\n do {\n identifier = suggestion + glue + index++;\n } while (\n _.includes(current, identifier.toUpperCase()) || // identifier exist in model\n $(`#${identifier}`).length // identifier was in model but still exist in DOM\n );\n\n return identifier;\n }\n };\n\n return qtiTestHelper;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2014 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\n\n/**\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/creator/views/itemref',[\n 'jquery',\n 'lodash',\n 'i18n',\n 'taoQtiTest/controller/creator/views/actions',\n 'taoQtiTest/controller/creator/helpers/categorySelector',\n 'taoQtiTest/controller/creator/helpers/sectionCategory',\n 'taoQtiTest/controller/creator/helpers/qtiTest',\n 'taoQtiTest/controller/creator/helpers/featureVisibility',\n 'taoQtiTest/controller/creator/templates/index'\n], function (\n $,\n _,\n __,\n actions,\n categorySelectorFactory,\n sectionCategory,\n qtiTestHelper,\n featureVisibility,\n templates\n) {\n ('use strict');\n\n /**\n * We need to resize the itemref in case of long labels\n */\n var resize = _.throttle(function resize() {\n var $refs = $('.itemrefs').first();\n var $actions = $('.itemref .actions').first();\n var width = $refs.innerWidth() - $actions.outerWidth();\n $('.itemref > .title').width(width);\n }, 100);\n\n /**\n * Set up an item ref: init action behaviors. Called for each one.\n *\n * @param {Object} creatorContext\n * @param {Object} refModel - the data model to bind to the item ref\n * @param {Object} sectionModel - the parent data model to inherit\n * @param {Object} partModel - the model of the parent's test part\n * @param {jQueryElement} $itemRef - the itemRef element to set up\n */\n function setUp(creatorContext, refModel, sectionModel, partModel, $itemRef) {\n var modelOverseer = creatorContext.getModelOverseer();\n var config = modelOverseer.getConfig() || {};\n var $actionContainer = $('.actions', $itemRef);\n\n // set item session control to use test part options if section level isn't set\n if (!refModel.itemSessionControl) {\n refModel.itemSessionControl = {};\n }\n _.defaults(refModel.itemSessionControl, sectionModel.itemSessionControl);\n\n refModel.isLinear = partModel.navigationMode === 0;\n\n //add feature visibility properties to itemRef model\n featureVisibility.addItemRefVisibilityProps(refModel);\n\n actions.properties($actionContainer, 'itemref', refModel, propHandler);\n actions.move($actionContainer, 'itemrefs', 'itemref');\n\n /**\n * We need to resize the itemref in case of long labels\n */\n _.throttle(function resize() {\n var $actions = $itemRef.find('.actions').first();\n var width = $itemRef.innerWidth() - $actions.outerWidth();\n $('.itemref > .title').width(width);\n }, 100);\n\n /**\n * Set up the time limits behaviors :\n * - linear test part: display the minTime field\n * - linear + guided nav option : display the minTime field + the lock\n * - otherwise only the maxTime field\n * @param {propView} propView - the view object\n */\n function timeLimitsProperty(propView) {\n var $view = propView.getView();\n\n //target elements\n var $minTimeContainer = $('.mintime-container', $view);\n var $maxTimeContainer = $('.maxtime-container', $view);\n var $lockedTimeContainer = $('.lockedtime-container', $view);\n var $locker = $('.locker button', $lockedTimeContainer);\n var $durationFields = $(':text[data-duration]', $lockedTimeContainer);\n var $minTimeField = $(':text[name=\"min-time\"]', $lockedTimeContainer);\n var $maxTimeField = $(':text[name=\"max-time\"]', $lockedTimeContainer);\n\n /**\n * Sync min value to max value, trigger change to sync the component.\n * Need to temporally remove the other handler to prevent infinite loop\n */\n var minToMaxHandler = _.throttle(function minToMax() {\n $maxTimeField.off('change.sync');\n $maxTimeField.val($minTimeField.val()).trigger('change');\n _.defer(function () {\n $maxTimeField.on('change.sync', minToMaxHandler);\n });\n }, 200);\n\n /**\n * Sync max value to min value, trigger change to sync the component.\n * Need to temporally remove the other handler to prevent infinite loop\n */\n var maxToMinHandler = _.throttle(function maxToMin() {\n $minTimeField.off('change.sync');\n $minTimeField.val($maxTimeField.val()).trigger('change');\n _.defer(function () {\n $minTimeField.on('change.sync', minToMaxHandler);\n });\n }, 200);\n\n /**\n * Lock the timers\n */\n var lockTimers = function lockTimers() {\n $locker\n .removeClass('unlocked')\n .addClass('locked')\n .attr('title', __('Unlink to use separated durations'));\n\n //sync min to max\n $minTimeField.val($maxTimeField.val()).trigger('change');\n\n //keep both in sync\n $minTimeField.on('change.sync', minToMaxHandler);\n $maxTimeField.on('change.sync', maxToMinHandler);\n };\n\n /**\n * Unlock the timers\n */\n var unlockTimers = function unlockTimers() {\n $locker\n .removeClass('locked')\n .addClass('unlocked')\n .attr('title', __('Link durations to activate the guided navigation'));\n\n $durationFields.off('change.sync');\n $minTimeField.val('00:00:00').trigger('change');\n };\n\n /**\n * Toggle the timelimits modes max, min + max, min + max + locked\n */\n var toggleTimeContainers = function toggleTimeContainers() {\n refModel.isLinear = partModel.navigationMode === 0;\n if (refModel.isLinear && config.guidedNavigation) {\n $minTimeContainer.addClass('hidden');\n $maxTimeContainer.addClass('hidden');\n $lockedTimeContainer.removeClass('hidden');\n if ($minTimeField.val() === $maxTimeField.val() && $maxTimeField.val() !== '00:00:00') {\n lockTimers();\n }\n $locker.on('click', function (e) {\n e.preventDefault();\n\n if ($locker.hasClass('locked')) {\n unlockTimers();\n } else {\n lockTimers();\n }\n });\n } else if (refModel.isLinear) {\n $lockedTimeContainer.addClass('hidden');\n $minTimeContainer.removeClass('hidden');\n $maxTimeContainer.removeClass('hidden');\n } else {\n $lockedTimeContainer.addClass('hidden');\n $minTimeContainer.addClass('hidden');\n $maxTimeContainer.removeClass('hidden');\n }\n };\n\n //if the testpart changes it's navigation mode\n modelOverseer.on('testpart-change', function () {\n toggleTimeContainers();\n });\n\n toggleTimeContainers();\n\n //check if min <= maw\n $durationFields.on('change.check', function () {\n if (\n refModel.timeLimits.minTime > 0 &&\n refModel.timeLimits.maxTime > 0 &&\n refModel.timeLimits.minTime > refModel.timeLimits.maxTime\n ) {\n $minTimeField.parent('div').find('.duration-ctrl-wrapper').addClass('brd-danger');\n } else {\n $minTimeField.parent('div').find('.duration-ctrl-wrapper').removeClass('brd-danger');\n }\n });\n }\n\n /**\n * Set up the category property\n * @private\n * @param {jQueryElement} $view - the $view object containing the $select\n */\n function categoriesProperty($view) {\n var categorySelector = categorySelectorFactory($view),\n $categoryField = $view.find('[name=\"itemref-category\"]');\n\n categorySelector.createForm([], 'itemRef');\n categorySelector.updateFormState(refModel.categories);\n\n $view.on('propopen.propview', function () {\n categorySelector.updateFormState(refModel.categories);\n });\n\n categorySelector.on('category-change', function (selected) {\n // Let the binder update the model by going through the category hidden field\n $categoryField.val(selected.join(','));\n $categoryField.trigger('change');\n\n modelOverseer.trigger('category-change', selected);\n });\n }\n\n /**\n * Setup the weights properties\n */\n function weightsProperty(propView) {\n var $view = propView.getView(),\n $weightList = $view.find('[data-bind-each=\"weights\"]'),\n weightTpl = templates.properties.itemrefweight;\n\n $view.find('.itemref-weight-add').on('click', function (e) {\n var defaultData = {\n value: 1,\n 'qti-type': 'weight',\n identifier:\n refModel.weights.length === 0\n ? 'WEIGHT'\n : qtiTestHelper.getAvailableIdentifier(refModel, 'weight', 'WEIGHT')\n };\n e.preventDefault();\n\n $weightList.append(weightTpl(defaultData));\n refModel.weights.push(defaultData);\n $weightList.trigger('add.internalbinder'); // trigger model update\n\n $view.groupValidator();\n });\n }\n\n /**\n * Perform some binding once the property view is create\n * @private\n * @param {propView} propView - the view object\n */\n function propHandler(propView) {\n const removePropHandler = function removePropHandler(e, $deletedNode) {\n const validIds = [\n $itemRef.attr('id'),\n $itemRef.parents('.section').attr('id'),\n $itemRef.parents('.testpart').attr('id')\n ];\n const deletedNodeId = $deletedNode.attr('id');\n\n if (propView !== null && validIds.includes(deletedNodeId)) {\n propView.destroy();\n }\n };\n\n categoriesProperty(propView.getView());\n weightsProperty(propView);\n timeLimitsProperty(propView);\n\n $itemRef.parents('.testparts').on('deleted.deleter', removePropHandler);\n }\n }\n\n /**\n * Listen for state changes to enable/disable . Called globally.\n */\n function listenActionState() {\n $('.itemrefs').each(function () {\n actions.movable($('.itemref', $(this)), 'itemref', '.actions');\n });\n\n $(document)\n .on('delete', function (e) {\n var $parent;\n var $target = $(e.target);\n if ($target.hasClass('itemref')) {\n $parent = $target.parents('.itemrefs');\n }\n })\n .on('add change undo.deleter deleted.deleter', '.itemrefs', function (e) {\n var $parent;\n var $target = $(e.target);\n if ($target.hasClass('itemref') || $target.hasClass('itemrefs')) {\n $parent = $('.itemref', $target.hasClass('itemrefs') ? $target : $target.parents('.itemrefs'));\n actions.enable($parent, '.actions');\n actions.movable($parent, 'itemref', '.actions');\n }\n });\n }\n\n /**\n * The itemrefView setup itemref related components and behavior\n *\n * @exports taoQtiTest/controller/creator/views/itemref\n */\n return {\n setUp: setUp,\n listenActionState: listenActionState\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2014 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\n\n/**\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/creator/encoders/dom2qti',[\n 'jquery',\n 'lodash',\n 'taoQtiTest/controller/creator/helpers/qtiElement',\n 'taoQtiTest/controller/creator/helpers/baseType',\n 'lib/dompurify/purify'\n], function ($, _, qtiElementHelper, baseType, DOMPurify) {\n 'use strict';\n\n /**\n * A mapping of QTI-XML node and attributes names in order to keep the camel case form\n * @type {Object}\n */\n const normalizedNodes = {\n feedbackblock: 'feedbackBlock',\n outcomeidentifier: 'outcomeIdentifier',\n showhide: 'showHide',\n printedvariable: 'printedVariable',\n powerform: 'powerForm',\n mappingindicator: 'mappingIndicator'\n };\n\n /**\n * Some Nodes have attributes that needs typing during decoding.\n * @type {Object}\n */\n const typedAttributes = {\n printedVariable: {\n identifier: baseType.getConstantByName('identifier'),\n powerForm: baseType.getConstantByName('boolean'),\n base: baseType.getConstantByName('intOrIdentifier'),\n index: baseType.getConstantByName('intOrIdentifier'),\n delimiter: baseType.getConstantByName('string'),\n field: baseType.getConstantByName('string'),\n mappingIndicator: baseType.getConstantByName('string')\n }\n };\n\n /**\n * Get the list of objects attributes to encode\n * @param {Object} object\n * @returns {Array}\n */\n function getAttributes(object) {\n return _.omit(object, [\n 'qti-type',\n 'content',\n 'xmlBase',\n 'lang',\n 'label'\n ]);\n }\n\n /**\n * Encode object's properties to xml/html string attributes\n * @param {Object} attributes\n * @returns {String}\n */\n function attrToStr(attributes) {\n return _.reduce(attributes, function (acc, value, key) {\n if (_.isNumber(value) || _.isBoolean(value) || (_.isString(value) && !_.isEmpty(value))) {\n return acc + ' ' + key + '=\"' + value + '\" ';\n }\n return acc;\n }, '');\n }\n\n /**\n * Ensures the nodeName has a normalized form:\n * - standard HTML tags are in lower case\n * - QTI-XML tags are in the right form\n * @param {String} nodeName\n * @returns {String}\n */\n function normalizeNodeName(nodeName) {\n const normalized = (nodeName) ? nodeName.toLocaleLowerCase() : '';\n return normalizedNodes[normalized] || normalized;\n }\n\n /**\n * This encoder is used to transform DOM to JSON QTI and JSON QTI to DOM.\n * It works now for the rubricBlocks components.\n * @exports creator/encoders/dom2qti\n */\n return {\n\n /**\n * Encode an object to a dom string\n * @param {Object} modelValue\n * @returns {String}\n */\n encode: function (modelValue) {\n let self = this,\n startTag;\n\n if (_.isArray(modelValue)) {\n return _.reduce(modelValue, function (result, value) {\n return result + self.encode(value);\n }, '');\n } else if (_.isObject(modelValue) && modelValue['qti-type']) {\n if (modelValue['qti-type'] === 'textRun') {\n return modelValue.content;\n }\n startTag = '<' + modelValue['qti-type'] + attrToStr(getAttributes(modelValue));\n if (modelValue.content) {\n return startTag + '>' + self.encode(modelValue.content) + '';\n } else {\n return startTag + '/>';\n }\n }\n return '' + modelValue;\n },\n\n /**\n * Decode a string that represents a DOM to a QTI formatted object\n * @param {String} nodeValue\n * @returns {Array}\n */\n decode: function (nodeValue) {\n const self = this;\n const $nodeValue = (nodeValue instanceof $) ? nodeValue : $(nodeValue);\n const result = [];\n let nodeName;\n\n _.forEach($nodeValue, function (elt) {\n let object;\n if (elt.nodeType === 3) {\n if (!_.isEmpty($.trim(elt.nodeValue))) {\n result.push(qtiElementHelper.create('textRun', {\n 'content': DOMPurify.sanitize(elt.nodeValue),\n 'xmlBase': ''\n }));\n }\n } else if (elt.nodeType === 1) {\n nodeName = normalizeNodeName(elt.nodeName);\n\n object = _.merge(qtiElementHelper.create(nodeName, {\n 'id': '',\n 'class': '',\n 'xmlBase': '',\n 'lang': '',\n 'label': ''\n }),\n _.transform(elt.attributes, function (acc, value) {\n const attrName = normalizeNodeName(value.nodeName);\n if (attrName) {\n if (typedAttributes[nodeName] && typedAttributes[nodeName][attrName]) {\n acc[attrName] = baseType.getValue(typedAttributes[nodeName][attrName], value.nodeValue);\n } else {\n acc[attrName] = value.nodeValue;\n }\n }\n return acc;\n }, {})\n );\n if (elt.childNodes.length > 0) {\n object.content = self.decode(elt.childNodes);\n }\n result.push(object);\n }\n });\n return result;\n }\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017 (original work) Open Assessment Technologies SA;\n */\n/**\n * Instanciate a Wysiwyg editor to create QTI content.\n *\n * @author Christophe Noël \n */\ndefine('taoQtiTest/controller/creator/qtiContentCreator',[\n 'lodash',\n 'jquery',\n 'lib/uuid',\n 'taoQtiItem/qtiCreator/helper/commonRenderer',\n 'taoQtiItem/qtiCreator/editor/containerEditor'\n], function(_, $, uuid, qtiCommonRenderer, containerEditor) {\n 'use strict';\n\n return {\n create: function create(creatorContext, $container, options) {\n var self = this,\n editorId = uuid(),\n areaBroker = creatorContext.getAreaBroker(),\n modelOverseer = creatorContext.getModelOverseer();\n\n var removePlugins = [\n 'magicline',\n 'taoqtiimage',\n 'taoqtimedia',\n 'taoqtimaths',\n 'taoqtiinclude',\n 'taoqtitable',\n 'sharedspace', // That Ck instance still use floatingspace to position the toolbar, whereas the sharedspace plugin is used by the Item creator\n 'taofurigana' // furiganaPlugin currently unsupported on test authoring\n ].join(','),\n\n toolbar = [\n {\n name : 'basicstyles',\n items : ['Bold', 'Italic', 'Subscript', 'Superscript']\n }, {\n name : 'insert',\n items : ['SpecialChar', 'TaoQtiPrintedVariable']\n }, {\n name : 'links',\n items : ['Link']\n },\n '/',\n {\n name : 'styles',\n items : ['Format']\n }, {\n name : 'paragraph',\n items : ['NumberedList', 'BulletedList', '-', 'Blockquote', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock']\n }\n ];\n\n qtiCommonRenderer.setContext(areaBroker.getContentCreatorPanelArea());\n\n containerEditor.create($container, {\n areaBroker: areaBroker,\n removePlugins: removePlugins,\n toolbar: toolbar,\n metadata: {\n getOutcomes: function getOutcomes() {\n return modelOverseer.getOutcomesNames();\n }\n },\n change: options.change || _.noop,\n resetRenderer: true,\n autofocus: false\n });\n\n // destroying ckInstance on editor close\n creatorContext.on('creatorclose.' + editorId, function() {\n self.destroy(creatorContext, $container);\n });\n\n $container.data('editorId', editorId);\n },\n\n /**\n * @returns {Promise} - when editor is destroyed\n */\n destroy: function destroy(creatorContext, $container) {\n var editorId = $container.data('editorId');\n if (editorId) {\n creatorContext.off('.' + editorId);\n }\n return containerEditor.destroy($container);\n }\n };\n});\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2014 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\n\n/**\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/creator/views/rubricblock',[\n 'jquery',\n 'lodash',\n 'i18n',\n 'ui/hider',\n 'ui/dialog/alert',\n 'util/namespace',\n 'taoQtiTest/controller/creator/views/actions',\n 'helpers',\n 'taoQtiTest/controller/creator/encoders/dom2qti',\n 'taoQtiTest/controller/creator/helpers/qtiElement',\n 'taoQtiTest/controller/creator/qtiContentCreator',\n 'ckeditor',\n], function ($, _, __, hider, dialogAlert, namespaceHelper, actions, helpers, Dom2QtiEncoder, qtiElementHelper, qtiContentCreator) {\n 'use strict';\n\n /**\n * The rubriclockView setup RB related components and behavior\n *\n * @exports taoQtiTest/controller/creator/views/rubricblock\n */\n return {\n /**\n * Set up a rubric block: init action behaviors. Called for each one.\n *\n * @param {Object} creatorContext\n * @param {Object} rubricModel - the rubric block data\n * @param {jQueryElement} $rubricBlock - the rubric block to set up\n */\n setUp: function setUp(creatorContext, rubricModel, $rubricBlock) {\n var modelOverseer = creatorContext.getModelOverseer();\n var areaBroker = creatorContext.getAreaBroker();\n var $rubricBlockContent = $('.rubricblock-content', $rubricBlock);\n\n /**\n * Bind a listener only related to this rubric.\n * @param {jQuery} $el\n * @param {String} eventName\n * @param {Function} cb\n * @returns {jQuery}\n */\n function bindEvent($el, eventName, cb) {\n eventName = namespaceHelper.namespaceAll(eventName, rubricModel.uid);\n return $el.off(eventName).on(eventName, cb);\n }\n\n /**\n * Ensures an html content is wrapped by a container tag.\n * @param {String} html\n * @returns {String}\n */\n function ensureWrap(html) {\n html = (html || '').trim();\n if (html.charAt(0) !== '<' || html.charAt(html.length - 1) !== '>') {\n html = '
          ' + html + '
          ';\n }\n if ($(html).length > 1) {\n html = '
          ' + html + '
          ';\n }\n return html;\n }\n\n /**\n * Forwards the editor content into the model\n */\n function editorToModel(html) {\n var rubric = qtiElementHelper.lookupElement(rubricModel, 'rubricBlock', 'content');\n var wrapper = qtiElementHelper.lookupElement(rubricModel, 'rubricBlock.div.feedbackBlock', 'content');\n var content = Dom2QtiEncoder.decode(ensureWrap(html));\n\n if (wrapper) {\n wrapper.content = content;\n } else {\n rubric.content = content;\n }\n }\n\n /**\n * Forwards the model content into the editor\n */\n function modelToEditor() {\n var rubric = qtiElementHelper.lookupElement(rubricModel, 'rubricBlock', 'content') || {};\n var wrapper = qtiElementHelper.lookupElement(rubricModel, 'rubricBlock.div.feedbackBlock', 'content');\n var content = wrapper ? wrapper.content : rubric.content;\n var html = ensureWrap(Dom2QtiEncoder.encode(content));\n\n // Destroy any existing CKEditor instance\n qtiContentCreator.destroy(creatorContext, $rubricBlockContent).then(function() {\n // update the editor content\n $rubricBlockContent.html(html);\n\n // Re-create the Qti-ckEditor instance\n qtiContentCreator.create(creatorContext, $rubricBlockContent, {\n change: function change(editorContent) {\n editorToModel(editorContent);\n }\n });\n });\n }\n\n /**\n * Wrap/unwrap the rubric block in a feedback according to the user selection\n * @param {Object} feedback\n * @returns {Boolean}\n */\n function updateFeedback(feedback) {\n var activated = feedback && feedback.activated;\n var wrapper = qtiElementHelper.lookupElement(rubricModel, 'rubricBlock.div.feedbackBlock', 'content');\n\n if (activated) {\n // wrap the actual content into a feedbackBlock if needed\n if (!wrapper) {\n rubricModel.content = [qtiElementHelper.create('div', {\n content: [qtiElementHelper.create('feedbackBlock', {\n outcomeIdentifier: feedback.outcome,\n identifier: feedback.matchValue,\n content: rubricModel.content\n })]\n })];\n } else {\n wrapper.outcomeIdentifier = feedback.outcome;\n wrapper.identifier = feedback.matchValue;\n }\n modelToEditor();\n } else {\n // remove the feedbackBlock wrapper, just keep the actual content\n if (wrapper) {\n rubricModel.content = wrapper.content;\n modelToEditor();\n }\n }\n\n return activated;\n }\n\n /**\n * Perform some binding once the property view is created\n * @private\n * @param {propView} propView - the view object\n */\n function propHandler(propView) {\n var $view = propView.getView();\n var $feedbackOutcomeLine = $('.rubric-feedback-outcome', $view);\n var $feedbackMatchLine = $('.rubric-feedback-match-value', $view);\n var $feedbackOutcome = $('[name=feedback-outcome]', $view);\n var $feedbackActivated = $('[name=activated]', $view);\n\n // toggle the feedback panel\n function changeFeedback(activated) {\n hider.toggle($feedbackOutcomeLine, activated);\n hider.toggle($feedbackMatchLine, activated);\n }\n\n // should be called when the properties panel is removed\n function removePropHandler() {\n rubricModel.feedback = {};\n if (propView !== null) {\n propView.destroy();\n }\n }\n\n // take care of changes in the properties view\n function changeHandler(e, changedModel) {\n if (e.namespace === 'binder' && changedModel['qti-type'] === 'rubricBlock') {\n changeFeedback(updateFeedback(changedModel.feedback));\n }\n }\n\n // update the list of outcomes the feedback can target\n function updateOutcomes() {\n var activated = rubricModel.feedback && rubricModel.feedback.activated;\n // build the list of outcomes in a way select2 can understand\n var outcomes = _.map(modelOverseer.getOutcomesNames(), function(name) {\n return {\n id: name,\n text: name\n };\n });\n\n // create/update the select field\n $feedbackOutcome.select2({\n minimumResultsForSearch: -1,\n width: '100%',\n data: outcomes\n });\n\n // update the UI to reflect the data\n if (!activated) {\n $feedbackActivated.prop('checked', false);\n }\n changeFeedback(activated);\n }\n\n $('[name=type]', $view).select2({\n minimumResultsForSearch: -1,\n width: '100%'\n });\n\n $view.on('change.binder', changeHandler);\n bindEvent($rubricBlock.parents('.testpart'), 'delete', removePropHandler);\n bindEvent($rubricBlock.parents('.section'), 'delete', removePropHandler);\n bindEvent($rubricBlock, 'delete', removePropHandler);\n bindEvent($rubricBlock, 'outcome-removed', function() {\n $feedbackOutcome.val('');\n updateOutcomes();\n });\n bindEvent($rubricBlock, 'outcome-updated', function() {\n updateFeedback(rubricModel.feedback);\n updateOutcomes();\n });\n\n changeFeedback(rubricModel.feedback);\n updateOutcomes();\n rbViews($view);\n }\n\n /**\n * Set up the views select box\n * @private\n * @param {jQueryElement} $propContainer - the element container\n */\n function rbViews($propContainer) {\n var $select = $('[name=view]', $propContainer);\n\n bindEvent($select.select2({'width': '100%'}), \"select2-removed\", function () {\n if ($select.select2('val').length === 0) {\n $select.select2('val', [1]);\n }\n });\n\n if ($select.select2('val').length === 0) {\n $select.select2('val', [1]);\n }\n }\n\n rubricModel.orderIndex = (rubricModel.index || 0) + 1;\n rubricModel.uid = _.uniqueId('rb');\n rubricModel.feedback = {\n activated: !!qtiElementHelper.lookupElement(rubricModel, 'rubricBlock.div.feedbackBlock', 'content'),\n outcome: qtiElementHelper.lookupProperty(rubricModel, 'rubricBlock.div.feedbackBlock.outcomeIdentifier', 'content'),\n matchValue: qtiElementHelper.lookupProperty(rubricModel, 'rubricBlock.div.feedbackBlock.identifier', 'content')\n };\n\n modelOverseer\n .before('scoring-write.' + rubricModel.uid, function() {\n var feedbackOutcome = rubricModel.feedback && rubricModel.feedback.outcome;\n if (feedbackOutcome && _.indexOf(modelOverseer.getOutcomesNames(), feedbackOutcome) < 0) {\n // the targeted outcome has been removed, so remove the feedback\n modelOverseer.changedRubricBlock = (modelOverseer.changedRubricBlock || 0) + 1;\n rubricModel.feedback.activated = false;\n rubricModel.feedback.outcome = '';\n updateFeedback(rubricModel.feedback);\n $rubricBlock.trigger('outcome-removed');\n } else {\n // the tageted outcome is still here, just notify the properties panel to update the list\n $rubricBlock.trigger('outcome-updated');\n }\n })\n .on('scoring-write.' + rubricModel.uid, function() {\n // will notify the user of any removed feedbacks\n if (modelOverseer.changedRubricBlock) {\n /** @todo: provide a way to cancel changes */\n dialogAlert(__('Some rubric blocks have been updated to reflect the changes in the list of outcomes.'));\n modelOverseer.changedRubricBlock = 0;\n }\n });\n\n actions.properties($rubricBlock, 'rubricblock', rubricModel, propHandler);\n\n modelToEditor();\n\n // destroy CK instance on rubric bloc deletion.\n // todo: find a way to destroy CK upon destroying rubric bloc parent section/part\n bindEvent($rubricBlock, 'delete', function() {\n qtiContentCreator.destroy(creatorContext, $rubricBlockContent);\n });\n\n $rubricBlockContent.on('editorfocus', function() {\n // close all properties forms and turn off their related button\n areaBroker.getPropertyPanelArea().children('.props').hide().trigger('propclose.propview');\n });\n\n //change position of CKeditor toolbar on scroll\n areaBroker.getContentCreatorPanelArea().find('.test-content').on('scroll', function () {\n CKEDITOR.document.getWindow().fire('scroll');\n });\n }\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2022 (original work) Open Assessment Technologies SA ;\n */\n\n/**\n * Helper for iterating on nested collections within a test model.\n * It's recommended to use a validator on the model before calling these functions.\n *\n * @example\nconst testModel = {\n 'qti-type': 'test',\n testParts: [{\n 'qti-type': 'testPart',\n identifier: 'testPart-1',\n assessmentSections: [{\n 'qti-type': 'assessmentSection',\n identifier: 'assessmentSection-1',\n sectionParts: [{\n 'qti-type': 'assessmentSection',\n identifier: 'subsection-1',\n sectionParts: [{\n 'qti-type': 'assessmentItemRef',\n identifier: 'item-1',\n categories: ['math', 'history']\n }]\n }]\n }]\n }]\n};\neachItemInTest(testModel, itemRef => {\n console.log(itemRef.categories);\n});\n */\ndefine('taoQtiTest/controller/creator/helpers/testModel',[\n 'lodash',\n 'core/errorHandler'\n], function (_, errorHandler) {\n 'use strict';\n\n const _ns = '.testModel';\n\n /**\n * Calls a function for each itemRef in the test model. Handles nested subsections.\n * @param {Object} testModel\n * @param {Function} cb - takes itemRef as only param\n */\n function eachItemInTest(testModel, cb) {\n _.forEach(testModel.testParts, testPartModel => {\n eachItemInTestPart(testPartModel, cb);\n });\n }\n\n /**\n * Calls a function for each itemRef in the testPart model. Handles nested subsections.\n * @param {Object} testPartModel\n * @param {Function} cb - takes itemRef as only param\n */\n function eachItemInTestPart(testPartModel, cb) {\n _.forEach(testPartModel.assessmentSections, sectionModel => {\n eachItemInSection(sectionModel, cb);\n });\n }\n\n /**\n * Calls a function for each itemRef in the section model. Handles nested subsections.\n * @param {Object} sectionModel\n * @param {Function} cb - takes itemRef as only param\n */\n function eachItemInSection(sectionModel, cb) {\n _.forEach(sectionModel.sectionParts, sectionPartModel => {\n // could be item, could be subsection\n if (sectionPartModel['qti-type'] === 'assessmentSection') {\n // recursion to handle any amount of subsection levels\n eachItemInSection(sectionPartModel, cb);\n } else if (sectionPartModel['qti-type'] === 'assessmentItemRef') {\n const itemRef = sectionPartModel;\n if (typeof cb === 'function') {\n cb(itemRef);\n } else {\n errorHandler.throw(_ns, 'cb must be a function');\n }\n }\n });\n }\n\n return {\n eachItemInTest,\n eachItemInTestPart,\n eachItemInSection\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2022 (original work) Open Assessment Technologies SA;\n */\ndefine('taoQtiTest/controller/creator/helpers/testPartCategory',[\n 'lodash',\n 'i18n',\n 'taoQtiTest/controller/creator/helpers/sectionCategory',\n 'taoQtiTest/controller/creator/helpers/testModel',\n 'core/errorHandler'\n], function(_, __, sectionCategory, testModelHelper, errorHandler) {\n 'use strict';\n\n const _ns = '.testPartCategory';\n\n /**\n * Check if the given object is a valid testPart model object\n *\n * @param {object} model\n * @returns {boolean}\n */\n function isValidTestPartModel(model) {\n return _.isObject(model)\n && model['qti-type'] === 'testPart'\n && _.isArray(model.assessmentSections)\n && model.assessmentSections.every(section => sectionCategory.isValidSectionModel(section));\n }\n\n /**\n * Set an array of categories to the testPart model (affects the children itemRef, and after propagation, the section models)\n *\n * @param {object} model\n * @param {array} selected - all categories active for the whole testPart\n * @param {array} partial - only categories in an indeterminate state\n * @returns {undefined}\n */\n function setCategories(model, selected, partial = []) {\n\n const currentCategories = getCategories(model);\n\n // partial = partial || [];\n\n //the categories that are no longer in the new list of categories should be removed\n const toRemove = _.difference(currentCategories.all, selected.concat(partial));\n\n //the categories that are not in the current categories collection should be added to the children\n const toAdd = _.difference(selected, currentCategories.propagated);\n\n model.categories = _.difference(model.categories, toRemove);\n model.categories = model.categories.concat(toAdd);\n\n //process the modification\n addCategories(model, toAdd);\n removeCategories(model, toRemove);\n }\n\n /**\n * @typedef {object} CategoriesSummary\n * @property {string[]} all - array of all categories of itemRef descendents\n * @property {string[]} propagated - array of categories propagated to every itemRef descendent\n * @property {string[]} partial - array of categories propagated to a partial set of itemRef descendents\n */\n\n /**\n * Get the categories assigned to the testPart model, inferred by its internal itemRefs\n *\n * @param {object} model\n * @returns {CategoriesSummary}\n */\n function getCategories(model) {\n if (!isValidTestPartModel(model)) {\n return errorHandler.throw(_ns, 'invalid tool config format');\n }\n\n let itemCount = 0;\n\n /**\n * List of lists of categories of each itemRef in the testPart\n * @type {string[][]}\n */\n const itemRefCategories = [];\n testModelHelper.eachItemInTestPart(model, itemRef => {\n if (++itemCount && _.isArray(itemRef.categories)) {\n itemRefCategories.push(_.compact(itemRef.categories));\n }\n });\n\n if (!itemCount) {\n return createCategories(model.categories, model.categories);\n }\n\n //all item categories\n const union = _.union.apply(null, itemRefCategories);\n //categories that are common to all itemRefs\n const propagated = _.intersection.apply(null, itemRefCategories);\n //the categories that are only partially covered on the section level : complementary of \"propagated\"\n const partial = _.difference(union, propagated);\n\n return createCategories(union, propagated, partial);\n }\n\n /**\n * Add an array of categories to a testPart model (affects the children itemRef, and after propagation, the section models)\n *\n * @param {object} model\n * @param {array} categories\n * @returns {undefined}\n */\n function addCategories(model, categories) {\n if (isValidTestPartModel(model)) {\n testModelHelper.eachItemInTestPart(model, itemRef => {\n if (!_.isArray(itemRef.categories)) {\n itemRef.categories = [];\n }\n itemRef.categories = _.union(itemRef.categories, categories);\n });\n } else {\n errorHandler.throw(_ns, 'invalid tool config format');\n }\n }\n\n /**\n * Remove an array of categories from a testPart model (affects the children itemRef, and after propagation, the section models)\n *\n * @param {object} model\n * @param {array} categories\n * @returns {undefined}\n */\n function removeCategories(model, categories) {\n if (isValidTestPartModel(model)) {\n testModelHelper.eachItemInTestPart(model, itemRef => {\n if (_.isArray(itemRef.categories)) {\n itemRef.categories = _.difference(itemRef.categories, categories);\n }\n });\n } else {\n errorHandler.throw(_ns, 'invalid tool config format');\n }\n }\n\n /**\n * Assigns input category arrays to output object, while sorting each one\n * @param {string[]} all\n * @param {string[]} propagated\n * @param {string[]} partial\n * @returns {CategoriesSummary}\n */\n function createCategories(all = [], propagated = [], partial = []) {\n return _.mapValues({\n all: all,\n propagated: propagated,\n partial: partial\n }, function(categories) {\n return categories.sort();\n });\n }\n\n return {\n isValidTestPartModel : isValidTestPartModel,\n setCategories : setCategories,\n getCategories : getCategories,\n addCategories : addCategories,\n removeCategories : removeCategories\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2016 (original work) Open Assessment Technologies SA;\n */\ndefine('taoQtiTest/controller/creator/helpers/sectionBlueprints',[\n 'lodash',\n 'i18n',\n 'core/errorHandler'\n], function (_, __, errorHandler){\n\n 'use strict';\n\n var _ns = '.sectionBlueprint';\n\n\n /**\n * Set an array of categories to the section model (affect the childen itemRef)\n *\n * @param {object} model\n * @param {string} blueprint\n * @returns {undefined}\n */\n function setBlueprint(model, blueprint){\n model.blueprint = blueprint;\n }\n\n /**\n * Get the categories assign to the section model, infered by its interal itemRefs\n *\n * @param {string} getUrl\n * @param {object} model\n * @returns {object}\n */\n function getBlueprint(getUrl, model){\n\n return $.ajax({\n url: getUrl,\n type: 'GET',\n data: {\n section: model.identifier\n },\n dataType: 'json'\n\n })\n .fail(function () {\n errorHandler.throw(_ns, 'invalid tool config format');\n });\n\n }\n\n return {\n setBlueprint : setBlueprint,\n getBlueprint : getBlueprint\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2021-2022 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\n\ndefine('taoQtiTest/controller/creator/views/subsection',[\n 'jquery',\n 'lodash',\n 'uri',\n 'i18n',\n 'taoQtiTest/controller/creator/config/defaults',\n 'taoQtiTest/controller/creator/views/actions',\n 'taoQtiTest/controller/creator/views/itemref',\n 'taoQtiTest/controller/creator/views/rubricblock',\n 'taoQtiTest/controller/creator/templates/index',\n 'taoQtiTest/controller/creator/helpers/qtiTest',\n 'taoQtiTest/controller/creator/helpers/categorySelector',\n 'taoQtiTest/controller/creator/helpers/sectionCategory',\n 'taoQtiTest/controller/creator/helpers/sectionBlueprints',\n 'ui/dialog/confirm',\n 'taoQtiTest/controller/creator/helpers/subsection',\n 'taoQtiTest/controller/creator/helpers/validators',\n 'services/features'\n], function (\n $,\n _,\n uri,\n __,\n defaults,\n actions,\n itemRefView,\n rubricBlockView,\n templates,\n qtiTestHelper,\n categorySelectorFactory,\n sectionCategory,\n sectionBlueprint,\n confirmDialog,\n subsectionsHelper,\n validators,\n servicesFeatures\n) {\n 'use strict';\n /**\n * Set up a section: init action behaviors. Called for each section.\n *\n * @param {Object} creatorContext\n * @param {Object} subsectionModel - the data model to bind to the test section\n * @param {Object} sectionModel - the parent data model to inherit\n * @param {jQuery} $subsection - the subsection to set up\n */\n function setUp(creatorContext, subsectionModel, sectionModel, $subsection) {\n const defaultsConfigs = defaults();\n // select elements for subsection, to avoid selecting the same elements in nested subsections\n const $itemRefsWrapper = $subsection.children('.itemrefs-wrapper');\n const $rubBlocks = $subsection.children('.rublocks');\n const $titleWithActions = $subsection.children('h2');\n\n const modelOverseer = creatorContext.getModelOverseer();\n const config = modelOverseer.getConfig();\n\n // set item session control to use test part options if section level isn't set\n if (!subsectionModel.itemSessionControl) {\n subsectionModel.itemSessionControl = {};\n }\n if (!subsectionModel.categories) {\n subsectionModel.categories = defaultsConfigs.categories;\n }\n _.defaults(subsectionModel.itemSessionControl, sectionModel.itemSessionControl);\n\n if (!_.isEmpty(config.routes.blueprintsById)) {\n subsectionModel.hasBlueprint = true;\n }\n subsectionModel.isSubsection = true;\n sectionModel.hasSelectionWithReplacement = servicesFeatures.isVisible(\n 'taoQtiTest/creator/properties/selectionWithReplacement',\n false\n );\n\n actions.properties($titleWithActions, 'section', subsectionModel, propHandler);\n actions.move($titleWithActions, 'subsections', 'subsection');\n actions.displayItemWrapper($subsection);\n actions.updateDeleteSelector($titleWithActions);\n\n subsections();\n itemRefs();\n acceptItemRefs();\n rubricBlocks();\n addRubricBlock();\n\n if (subsectionsHelper.isNestedSubsection($subsection)) {\n // prevent adding a third subsection level\n $('.add-subsection', $subsection).hide();\n $('.add-subsection + .tlb-separator', $subsection).hide();\n } else {\n addSubsection();\n }\n\n /**\n * Perform some binding once the property view is create\n * @param {propView} propView - the view object\n */\n function propHandler(propView) {\n const $view = propView.getView();\n\n //enable/disable selection\n const $selectionSwitcher = $('[name=section-enable-selection]', $view);\n const $selectionSelect = $('[name=section-select]', $view);\n const $selectionWithRep = $('[name=section-with-replacement]', $view);\n\n // subsectionModel.selection will be filled by binded values from template section-props.tpl\n // if subsectionModel.selection from server response it has 'qti-type'\n const isSelectionFromServer = !!(subsectionModel.selection && subsectionModel.selection['qti-type']);\n\n const switchSelection = function switchSelection() {\n if ($selectionSwitcher.prop('checked') === true) {\n $selectionSelect.incrementer('enable');\n $selectionWithRep.removeClass('disabled');\n } else {\n $selectionSelect.incrementer('disable');\n $selectionWithRep.addClass('disabled');\n }\n };\n $selectionSwitcher.on('change', switchSelection);\n $selectionSwitcher.on('change', function updateModel() {\n if (!$selectionSwitcher.prop('checked')) {\n $selectionSelect.val(0);\n $selectionWithRep.prop('checked', false);\n delete subsectionModel.selection;\n }\n });\n\n $selectionSwitcher.prop('checked', isSelectionFromServer).trigger('change');\n\n //listen for databinder change to update the test part title\n const $title = $('[data-bind=title]', $titleWithActions);\n $view.on('change.binder', function (e) {\n if (e.namespace === 'binder' && subsectionModel['qti-type'] === 'assessmentSection') {\n $title.text(subsectionModel.title);\n }\n });\n\n // deleted.deleter event fires only on the parent nodes (testparts, sections, etc)\n // Since it \"bubles\" we can subsctibe only to the highest parent node\n $subsection.parents('.testparts').on('deleted.deleter', removePropHandler);\n\n //section level category configuration\n categoriesProperty($view);\n\n if (typeof subsectionModel.hasBlueprint !== 'undefined') {\n blueprintProperty($view);\n }\n\n actions.displayCategoryPresets($subsection);\n\n function removePropHandler(e, $deletedNode) {\n const validIds = [$subsection.parents('.testpart').attr('id'), $subsection.attr('id')];\n\n const deletedNodeId = $deletedNode.attr('id');\n // We have to check id of a deleted node, because\n // 1. Event fires after child node was deleted, but e.stopPropagation doesn't help\n // because currentTarget is always document\n // 2. We have to subscribe to the parent node and it's posiible that another section was removed even from another testpart\n // Subscription to the .sections selector event won't help because sections element might contain several children.\n\n if (propView !== null && validIds.includes(deletedNodeId)) {\n propView.destroy();\n }\n }\n }\n /**\n * Set up subsections that already belongs to the section\n * @private\n */\n function subsections() {\n if (!subsectionModel.sectionParts) {\n subsectionModel.sectionParts = [];\n }\n\n subsectionsHelper.getSubsections($subsection).each(function () {\n const $sub2section = $(this);\n const index = $sub2section.data('bind-index');\n if (!subsectionModel.sectionParts[index]) {\n subsectionModel.sectionParts[index] = {};\n }\n\n setUp(creatorContext, subsectionModel.sectionParts[index], subsectionModel, $sub2section);\n });\n }\n /**\n * Set up the item refs that already belongs to the section\n * @private\n */\n function itemRefs() {\n if (!subsectionModel.sectionParts) {\n subsectionModel.sectionParts = [];\n }\n $('.itemref', $itemRefsWrapper).each(function () {\n const $itemRef = $(this);\n const index = $itemRef.data('bind-index');\n if (!subsectionModel.sectionParts[index]) {\n subsectionModel.sectionParts[index] = {};\n }\n\n itemRefView.setUp(\n creatorContext,\n subsectionModel.sectionParts[index],\n subsectionModel,\n sectionModel,\n $itemRef\n );\n $itemRef.find('.title').text(config.labels[uri.encode($itemRef.data('uri'))]);\n });\n }\n\n /**\n * Make the section to accept the selected items\n * @private\n * @fires modelOverseer#item-add\n */\n function acceptItemRefs() {\n const $itemsPanel = $('.test-creator-items .item-selection');\n\n //the item selector trigger a select event\n $itemsPanel.on('itemselect.creator', function (e, selection) {\n const $placeholder = $('.itemref-placeholder', $itemRefsWrapper);\n const $placeholders = $('.itemref-placeholder');\n\n if (_.size(selection) > 0) {\n $placeholder\n .show()\n .off('click')\n .on('click', function () {\n //prepare the item data\n const defaultItemData = {};\n\n if (\n subsectionModel.itemSessionControl &&\n !_.isUndefined(subsectionModel.itemSessionControl.maxAttempts)\n ) {\n //for a matter of consistency, the itemRef will \"inherit\" the itemSessionControl configuration from its parent section\n defaultItemData.itemSessionControl = _.clone(subsectionModel.itemSessionControl);\n }\n\n //the itemRef should also \"inherit\" default categories set at the item level\n defaultItemData.categories = _.clone(defaultsConfigs.categories) || [];\n\n _.forEach(selection, function (item) {\n const itemData = _.defaults(\n {\n href: item.uri,\n label: item.label,\n 'qti-type': 'assessmentItemRef'\n },\n defaultItemData\n );\n\n if (_.isArray(item.categories)) {\n itemData.categories = item.categories.concat(itemData.categories);\n }\n\n addItemRef($('.itemrefs', $itemRefsWrapper), null, itemData);\n });\n\n $itemsPanel.trigger('itemselected.creator');\n\n $placeholders.hide().off('click');\n });\n } else {\n $placeholders.hide().off('click');\n }\n });\n\n //we listen the event not from the adder but from the data binder to be sure the model is up to date\n // jquesry issue to select id with dot by '#ab.cd', should be used [id=\"ab.cd\"]\n $(document)\n .off('add.binder', `[id=\"${$subsection.attr('id')}\"] > .itemrefs-wrapper .itemrefs`)\n .on(\n 'add.binder',\n `[id=\"${$subsection.attr('id')}\"] > .itemrefs-wrapper .itemrefs`,\n function (e, $itemRef) {\n if (\n e.namespace === 'binder' &&\n $itemRef.hasClass('itemref') &&\n $itemRef.closest('.subsection').attr('id') === $subsection.attr('id')\n ) {\n const index = $itemRef.data('bind-index');\n const itemRefModel = subsectionModel.sectionParts[index];\n\n //initialize the new item ref\n itemRefView.setUp(creatorContext, itemRefModel, subsectionModel, sectionModel, $itemRef);\n\n /**\n * @event modelOverseer#item-add\n * @param {Object} itemRefModel\n */\n modelOverseer.trigger('item-add', itemRefModel);\n }\n }\n );\n }\n\n /**\n * Add a new item ref to the section\n * @param {jQuery} $refList - the element to add the item to\n * @param {Number} [index] - the position of the item to add\n * @param {Object} [itemData] - the data to bind to the new item ref\n */\n function addItemRef($refList, index, itemData) {\n const $items = $refList.children('li');\n index = index || $items.length;\n itemData.identifier = qtiTestHelper.getAvailableIdentifier(\n modelOverseer.getModel(),\n 'assessmentItemRef',\n 'item'\n );\n itemData.index = index + 1;\n const $itemRef = $(templates.itemref(itemData));\n if (index > 0) {\n $itemRef.insertAfter($items.eq(index - 1));\n } else {\n $itemRef.appendTo($refList);\n }\n $refList.trigger('add', [$itemRef, itemData]);\n }\n\n /**\n * Set up the rubric blocks that already belongs to the section\n * @private\n */\n function rubricBlocks() {\n if (!subsectionModel.rubricBlocks) {\n subsectionModel.rubricBlocks = [];\n }\n $('.rubricblock', $rubBlocks).each(function () {\n const $rubricBlock = $(this);\n const index = $rubricBlock.data('bind-index');\n if (!subsectionModel.rubricBlocks[index]) {\n subsectionModel.rubricBlocks[index] = {};\n }\n\n rubricBlockView.setUp(creatorContext, subsectionModel.rubricBlocks[index], $rubricBlock);\n });\n\n //opens the rubric blocks section if they are there.\n if (subsectionModel.rubricBlocks.length > 0) {\n $('.rub-toggler', $titleWithActions).trigger('click');\n }\n }\n\n /**\n * Enable to add new rubric block\n * @private\n * @fires modelOverseer#rubric-add\n */\n function addRubricBlock() {\n $('.rublock-adder', $rubBlocks).adder({\n target: $('.rubricblocks', $rubBlocks),\n content: templates.rubricblock,\n templateData: function (cb) {\n cb({\n 'qti-type': 'rubricBlock',\n index: $('.rubricblock', $rubBlocks).length,\n content: [],\n views: [1]\n });\n }\n });\n\n //we listen the event not from the adder but from the data binder to be sure the model is up to date\n // jquesry issue to select id with dot by '#ab.cd', should be used [id=\"ab.cd\"]\n $(document)\n .off('add.binder', `[id=\"${$subsection.attr('id')}\"] > .rublocks .rubricblocks`)\n .on(\n 'add.binder',\n `[id=\"${$subsection.attr('id')}\"] > .rublocks .rubricblocks`,\n function (e, $rubricBlock) {\n if (\n e.namespace === 'binder' &&\n $rubricBlock.hasClass('rubricblock') &&\n $rubricBlock.closest('.subsection').attr('id') === $subsection.attr('id')\n ) {\n const index = $rubricBlock.data('bind-index');\n const rubricModel = subsectionModel.rubricBlocks[index] || {};\n\n $('.rubricblock-binding', $rubricBlock).html('

           

          ');\n rubricBlockView.setUp(creatorContext, rubricModel, $rubricBlock);\n\n /**\n * @event modelOverseer#rubric-add\n * @param {Object} rubricModel\n */\n modelOverseer.trigger('rubric-add', rubricModel);\n }\n }\n );\n }\n\n /**\n * Set up the category property\n * @private\n * @param {jQuery} $view - the $view object containing the $select\n * @fires modelOverseer#category-change\n */\n function categoriesProperty($view) {\n const categories = sectionCategory.getCategories(subsectionModel),\n categorySelector = categorySelectorFactory($view);\n\n categorySelector.createForm(categories.all);\n updateFormState(categorySelector);\n\n $view.on('propopen.propview', function () {\n updateFormState(categorySelector);\n });\n\n $view.on('set-default-categories', function () {\n subsectionModel.categories = defaultsConfigs.categories;\n updateFormState(categorySelector);\n });\n\n categorySelector.on('category-change', function (selected, indeterminate) {\n sectionCategory.setCategories(subsectionModel, selected, indeterminate);\n\n modelOverseer.trigger('category-change');\n });\n }\n\n function updateFormState(categorySelector) {\n const categories = sectionCategory.getCategories(subsectionModel);\n categorySelector.updateFormState(categories.propagated, categories.partial);\n }\n\n /**\n * Set up the Blueprint property\n * @private\n * @param {jQuery} $view - the $view object containing the $select\n */\n function blueprintProperty($view) {\n const $select = $('[name=section-blueprint]', $view);\n $select\n .select2({\n ajax: {\n url: config.routes.blueprintsById,\n dataType: 'json',\n delay: 350,\n method: 'POST',\n data: function (params) {\n return {\n identifier: params // search term\n };\n },\n results: function (data) {\n return data;\n }\n },\n minimumInputLength: 3,\n width: '100%',\n multiple: false,\n allowClear: true,\n placeholder: __('Select a blueprint'),\n formatNoMatches: function () {\n return __('Enter a blueprint');\n },\n maximumInputLength: 32\n })\n .on('change', function (e) {\n setBlueprint(e.val);\n });\n\n initBlueprint();\n $view.on('propopen.propview', function () {\n initBlueprint();\n });\n\n /**\n * Start the blueprint editing\n * @private\n */\n function initBlueprint() {\n if (typeof subsectionModel.blueprint === 'undefined') {\n sectionBlueprint\n .getBlueprint(config.routes.blueprintByTestSection, subsectionModel)\n .success(function (data) {\n if (!_.isEmpty(data)) {\n if (subsectionModel.blueprint !== '') {\n subsectionModel.blueprint = data.uri;\n $select.select2('data', { id: data.uri, text: data.text });\n $select.trigger('change');\n }\n }\n });\n }\n }\n\n /**\n * save the categories into the model\n * @param {Object} blueprint\n * @private\n */\n function setBlueprint(blueprint) {\n sectionBlueprint.setBlueprint(subsectionModel, blueprint);\n }\n }\n\n function addSubsection() {\n const optionsConfirmDialog = {\n buttons: {\n labels: {\n ok: __('Yes'),\n cancel: __('No')\n }\n }\n };\n\n $('.add-subsection', $titleWithActions).adder({\n target: $subsection.children('.subsections'),\n content: templates.subsection,\n templateData: function (cb) {\n //create a new subsection model object to be bound to the template\n let sectionParts = [];\n if ($subsection.data('movedItems')) {\n sectionParts = $subsection.data('movedItems');\n $subsection.removeData('movedItems');\n }\n cb({\n 'qti-type': 'assessmentSection',\n identifier: qtiTestHelper.getAvailableIdentifier(\n modelOverseer.getModel(),\n 'assessmentSection',\n 'subsection'\n ),\n title: defaultsConfigs.sectionTitlePrefix,\n index: 0,\n sectionParts,\n visible: true,\n itemSessionControl: {\n maxAttempts: defaultsConfigs.maxAttempts\n }\n });\n },\n checkAndCallAdd: function (executeAdd) {\n if (\n subsectionModel.sectionParts[0] &&\n qtiTestHelper.filterQtiType(subsectionModel.sectionParts[0], 'assessmentItemRef')\n ) {\n // subsection has item(s)\n const subsectionIndex = subsectionsHelper.getSubsectionTitleIndex($subsection);\n const confirmMessage = __(\n 'The items contained in %s %s will be moved into the new %s %s. Do you wish to proceed?',\n subsectionIndex,\n subsectionModel.title,\n `${subsectionIndex}1.`,\n defaultsConfigs.sectionTitlePrefix\n );\n const acceptFunction = () => {\n // trigger deleted event for each itemfer to run removePropHandler and remove propView\n $('.itemrefs .itemref', $itemRefsWrapper).each(function () {\n $subsection.parents('.testparts').trigger('deleted.deleter', [$(this)]);\n });\n setTimeout(() => {\n // remove all itemrefs\n $('.itemrefs', $itemRefsWrapper).empty();\n // check itemrefs identifiers\n // because validation is build on \n // and each item should have valid and unique id\n subsectionModel.sectionParts.forEach(itemRef => {\n if (!validators.checkIfItemIdValid(itemRef.identifier, modelOverseer)) {\n itemRef.identifier = qtiTestHelper.getAvailableIdentifier(\n modelOverseer.getModel(),\n 'assessmentItemRef',\n 'item'\n );\n }\n });\n $subsection.data('movedItems', _.clone(subsectionModel.sectionParts));\n subsectionModel.sectionParts = [];\n executeAdd();\n }, 0);\n };\n confirmDialog(confirmMessage, acceptFunction, () => {}, optionsConfirmDialog)\n .getDom()\n .find('.buttons')\n .css('display', 'flex')\n .css('flex-direction', 'row-reverse');\n } else {\n if (!subsectionModel.sectionParts.length && subsectionModel.categories.length) {\n $subsection.data('movedCategories', _.clone(subsectionModel.categories));\n }\n executeAdd();\n }\n }\n });\n\n //we listen the event not from the adder but from the data binder to be sure the model is up to date\n // jquesry issue to select id with dot by '#ab.cd', should be used [id=\"ab.cd\"]\n $(document)\n .off('add.binder', `[id=\"${$subsection.attr('id')}\"] > .subsections`)\n .on('add.binder', `[id=\"${$subsection.attr('id')}\"] > .subsections`, function (e, $sub2section) {\n if (\n e.namespace === 'binder' &&\n $sub2section.hasClass('subsection') &&\n $sub2section.parents('.subsection').length\n ) {\n // second level of subsection){\n const sub2sectionIndex = $sub2section.data('bind-index');\n const sub2sectionModel = subsectionModel.sectionParts[sub2sectionIndex];\n\n if ($subsection.data('movedCategories')) {\n sub2sectionModel.categories = $subsection.data('movedCategories');\n $subsection.removeData('movedCategories');\n }\n\n // initialize the new subsection\n setUp(creatorContext, sub2sectionModel, subsectionModel, $sub2section);\n // hide 'Items' block and category-presets for current subsection\n actions.displayItemWrapper($subsection);\n actions.displayCategoryPresets($subsection);\n // set index for new subsection\n actions.updateTitleIndex($sub2section);\n\n /**\n * @event modelOverseer#section-add\n * @param {Object} sub2sectionModel\n */\n modelOverseer.trigger('section-add', sub2sectionModel);\n }\n });\n }\n }\n\n /**\n * Listen for state changes to enable/disable . Called globally.\n */\n function listenActionState() {\n $('.subsections').each(function () {\n const $subsections = $('.subsection', $(this));\n\n actions.removable($subsections, 'h2');\n actions.movable($subsections, 'subsection', 'h2');\n actions.updateTitleIndex($subsections);\n });\n\n $(document)\n .on('delete', function (e) {\n const $target = $(e.target);\n if ($target.hasClass('subsection')) {\n const $parent = subsectionsHelper.getParent($target);\n setTimeout(() => {\n actions.displayItemWrapper($parent);\n actions.displayCategoryPresets($parent);\n // element detached after event\n const $subsections = $('.subsection', $parent);\n actions.updateTitleIndex($subsections);\n }, 100);\n }\n })\n .on('add change undo.deleter deleted.deleter', function (e) {\n const $target = $(e.target);\n if ($target.hasClass('subsection') || $target.hasClass('subsections')) {\n const $subsections = subsectionsHelper.getSiblingSubsections($target);\n actions.removable($subsections, 'h2');\n actions.movable($subsections, 'subsection', 'h2');\n\n if (e.type === 'undo' || e.type === 'change') {\n const $parent = subsectionsHelper.getParent($target);\n const $AllNestedSubsections = $('.subsection', $parent);\n actions.updateTitleIndex($AllNestedSubsections);\n actions.displayItemWrapper($parent);\n actions.displayCategoryPresets($parent);\n }\n }\n });\n }\n /**\n * The sectionView setup section related components and beahvior\n *\n * @exports taoQtiTest/controller/creator/views/section\n */\n return {\n setUp: setUp,\n listenActionState: listenActionState\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2014-2022 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\n\n/**\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/creator/views/section',[\n 'jquery',\n 'lodash',\n 'uri',\n 'i18n',\n 'taoQtiTest/controller/creator/config/defaults',\n 'taoQtiTest/controller/creator/views/actions',\n 'taoQtiTest/controller/creator/views/itemref',\n 'taoQtiTest/controller/creator/views/rubricblock',\n 'taoQtiTest/controller/creator/templates/index',\n 'taoQtiTest/controller/creator/helpers/qtiTest',\n 'taoQtiTest/controller/creator/helpers/categorySelector',\n 'taoQtiTest/controller/creator/helpers/testPartCategory',\n 'taoQtiTest/controller/creator/helpers/sectionCategory',\n 'taoQtiTest/controller/creator/helpers/sectionBlueprints',\n 'taoQtiTest/controller/creator/views/subsection',\n 'ui/dialog/confirm',\n 'taoQtiTest/controller/creator/helpers/subsection',\n 'taoQtiTest/controller/creator/helpers/validators',\n 'taoQtiTest/controller/creator/helpers/featureVisibility',\n 'services/features'\n], function (\n $,\n _,\n uri,\n __,\n defaults,\n actions,\n itemRefView,\n rubricBlockView,\n templates,\n qtiTestHelper,\n categorySelectorFactory,\n testPartCategory,\n sectionCategory,\n sectionBlueprint,\n subsectionView,\n confirmDialog,\n subsectionsHelper,\n validators,\n featureVisibility,\n servicesFeatures\n) {\n ('use strict');\n\n /**\n * Set up a section: init action behaviors. Called for each section.\n *\n * @param {Object} creatorContext\n * @param {Object} sectionModel - the data model to bind to the test section\n * @param {Object} partModel - the parent data model to inherit\n * @param {jQuery} $section - the section to set up\n */\n function setUp(creatorContext, sectionModel, partModel, $section) {\n const defaultsConfigs = defaults();\n // select elements for section, to avoid selecting the same elements in subsections\n const $itemRefsWrapper = $section.children('.itemrefs-wrapper');\n const $rubBlocks = $section.children('.rublocks');\n const $titleWithActions = $section.children('h2');\n\n const modelOverseer = creatorContext.getModelOverseer();\n const config = modelOverseer.getConfig();\n\n // set item session control to use test part options if section level isn't set\n if (!sectionModel.itemSessionControl) {\n sectionModel.itemSessionControl = {};\n }\n if (!sectionModel.categories) {\n // inherit the parent testPart's propagated categories\n const partCategories = testPartCategory.getCategories(partModel);\n sectionModel.categories = _.clone(partCategories.propagated || defaultsConfigs.categories);\n }\n _.defaults(sectionModel.itemSessionControl, partModel.itemSessionControl);\n\n if (!_.isEmpty(config.routes.blueprintsById)) {\n sectionModel.hasBlueprint = true;\n }\n sectionModel.isSubsection = false;\n sectionModel.hasSelectionWithReplacement = servicesFeatures.isVisible(\n 'taoQtiTest/creator/properties/selectionWithReplacement',\n false\n );\n\n //add feature visibility properties to sectionModel\n featureVisibility.addSectionVisibilityProps(sectionModel);\n\n actions.properties($titleWithActions, 'section', sectionModel, propHandler);\n actions.move($titleWithActions, 'sections', 'section');\n actions.displayItemWrapper($section);\n\n subsections();\n itemRefs();\n acceptItemRefs();\n rubricBlocks();\n addRubricBlock();\n addSubsection();\n\n /**\n * Perform some binding once the property view is create\n * @param {propView} propView - the view object\n */\n function propHandler(propView) {\n const $view = propView.getView();\n\n //enable/disable selection\n const $selectionSwitcher = $('[name=section-enable-selection]', $view);\n const $selectionSelect = $('[name=section-select]', $view);\n const $selectionWithRep = $('[name=section-with-replacement]', $view);\n\n // sectionModel.selection will be filled by bound values from template section-props.tpl\n // if sectionModel.selection from server response it has 'qti-type'\n const isSelectionFromServer = !!(sectionModel.selection && sectionModel.selection['qti-type']);\n\n const switchSelection = function switchSelection() {\n if ($selectionSwitcher.prop('checked') === true) {\n $selectionSelect.incrementer('enable');\n $selectionWithRep.removeClass('disabled');\n } else {\n $selectionSelect.incrementer('disable');\n $selectionWithRep.addClass('disabled');\n }\n };\n $selectionSwitcher.on('change', switchSelection);\n $selectionSwitcher.on('change', function updateModel() {\n if (!$selectionSwitcher.prop('checked')) {\n $selectionSelect.val(0);\n $selectionWithRep.prop('checked', false);\n delete sectionModel.selection;\n }\n });\n\n $selectionSwitcher.prop('checked', isSelectionFromServer).trigger('change');\n\n //listen for databinder change to update the test part title\n const $title = $('[data-bind=title]', $titleWithActions);\n $view.on('change.binder', function (e) {\n if (e.namespace === 'binder' && sectionModel['qti-type'] === 'assessmentSection') {\n $title.text(sectionModel.title);\n }\n });\n\n // deleted.deleter event fires only on the parent nodes (testparts, sections, etc)\n // Since it \"bubles\" we can subscribe only to the highest parent node\n $section.parents('.testparts').on('deleted.deleter', removePropHandler);\n\n //section level category configuration\n categoriesProperty($view);\n\n if (typeof sectionModel.hasBlueprint !== 'undefined') {\n blueprintProperty($view);\n }\n\n actions.displayCategoryPresets($section);\n\n function removePropHandler(e, $deletedNode) {\n const validIds = [$section.parents('.testpart').attr('id'), $section.attr('id')];\n\n const deletedNodeId = $deletedNode.attr('id');\n // We have to check id of a deleted node, because\n // 1. Event fires after child node was deleted, but e.stopPropagation doesn't help\n // because currentTarget is always document\n // 2. We have to subscribe to the parent node and it's possible that another section was removed even from another testpart\n // Subscription to the .sections selector event won't help because sections element might contain several children.\n\n if (propView !== null && validIds.includes(deletedNodeId)) {\n propView.destroy();\n }\n }\n }\n\n /**\n * Set up subsections that already belongs to the section\n * @private\n */\n function subsections() {\n if (!sectionModel.sectionParts) {\n sectionModel.sectionParts = [];\n }\n\n subsectionsHelper.getSubsections($section).each(function () {\n const $subsection = $(this);\n const index = $subsection.data('bind-index');\n if (!sectionModel.sectionParts[index]) {\n sectionModel.sectionParts[index] = {};\n }\n\n subsectionView.setUp(creatorContext, sectionModel.sectionParts[index], sectionModel, $subsection);\n });\n }\n /**\n * Set up the item refs that already belongs to the section\n * @private\n */\n function itemRefs() {\n if (!sectionModel.sectionParts) {\n sectionModel.sectionParts = [];\n }\n $('.itemref', $itemRefsWrapper).each(function () {\n const $itemRef = $(this);\n const index = $itemRef.data('bind-index');\n if (!sectionModel.sectionParts[index]) {\n sectionModel.sectionParts[index] = {};\n }\n\n itemRefView.setUp(creatorContext, sectionModel.sectionParts[index], sectionModel, partModel, $itemRef);\n $itemRef.find('.title').text(config.labels[uri.encode($itemRef.data('uri'))]);\n });\n }\n\n /**\n * Make the section to accept the selected items\n * @private\n * @fires modelOverseer#item-add\n */\n function acceptItemRefs() {\n const $itemsPanel = $('.test-creator-items .item-selection');\n\n //the item selector trigger a select event\n $itemsPanel.on('itemselect.creator', function (e, selection) {\n const $placeholder = $('.itemref-placeholder', $itemRefsWrapper);\n const $placeholders = $('.itemref-placeholder');\n\n if (_.size(selection) > 0) {\n $placeholder\n .show()\n .off('click')\n .on('click', function () {\n //prepare the item data\n const defaultItemData = {};\n\n if (\n sectionModel.itemSessionControl &&\n !_.isUndefined(sectionModel.itemSessionControl.maxAttempts)\n ) {\n //for a matter of consistency, the itemRef will \"inherit\" the itemSessionControl configuration from its parent section\n defaultItemData.itemSessionControl = _.clone(sectionModel.itemSessionControl);\n }\n\n //the itemRef should also \"inherit\" default categories set at the item level\n defaultItemData.categories = _.clone(defaultsConfigs.categories) || [];\n\n _.forEach(selection, function (item) {\n const itemData = _.defaults(\n {\n href: item.uri,\n label: item.label,\n 'qti-type': 'assessmentItemRef'\n },\n defaultItemData\n );\n\n if (_.isArray(item.categories)) {\n itemData.categories = item.categories.concat(itemData.categories);\n }\n\n addItemRef($('.itemrefs', $itemRefsWrapper), null, itemData);\n });\n\n $itemsPanel.trigger('itemselected.creator');\n\n $placeholders.hide().off('click');\n });\n } else {\n $placeholders.hide().off('click');\n }\n });\n\n //we listen the event not from the adder but from the data binder to be sure the model is up to date\n // jquery issue to select id with dot by '#ab.cd', should be used [id=\"ab.cd\"]\n $(document)\n .off('add.binder', `[id=\"${$section.attr('id')}\"] > .itemrefs-wrapper .itemrefs`)\n .on(\n 'add.binder',\n `[id=\"${$section.attr('id')}\"] > .itemrefs-wrapper .itemrefs`,\n function (e, $itemRef) {\n if (\n e.namespace === 'binder' &&\n $itemRef.hasClass('itemref') &&\n !$itemRef.parents('.subsection').length\n ) {\n const index = $itemRef.data('bind-index');\n const itemRefModel = sectionModel.sectionParts[index];\n\n //initialize the new item ref\n itemRefView.setUp(creatorContext, itemRefModel, sectionModel, partModel, $itemRef);\n\n /**\n * @event modelOverseer#item-add\n * @param {Object} itemRefModel\n */\n modelOverseer.trigger('item-add', itemRefModel);\n }\n }\n );\n }\n\n /**\n * Add a new item ref to the section\n * @param {jQuery} $refList - the element to add the item to\n * @param {Number} [index] - the position of the item to add\n * @param {Object} [itemData] - the data to bind to the new item ref\n */\n function addItemRef($refList, index, itemData) {\n const $items = $refList.children('li');\n index = index || $items.length;\n itemData.identifier = qtiTestHelper.getAvailableIdentifier(\n modelOverseer.getModel(),\n 'assessmentItemRef',\n 'item'\n );\n itemData.index = index + 1;\n const $itemRef = $(templates.itemref(itemData));\n if (index > 0) {\n $itemRef.insertAfter($items.eq(index - 1));\n } else {\n $itemRef.appendTo($refList);\n }\n $refList.trigger('add', [$itemRef, itemData]);\n }\n\n /**\n * Set up the rubric blocks that already belongs to the section\n * @private\n */\n function rubricBlocks() {\n if (!sectionModel.rubricBlocks) {\n sectionModel.rubricBlocks = [];\n }\n $('.rubricblock', $rubBlocks).each(function () {\n const $rubricBlock = $(this);\n const index = $rubricBlock.data('bind-index');\n if (!sectionModel.rubricBlocks[index]) {\n sectionModel.rubricBlocks[index] = {};\n }\n\n rubricBlockView.setUp(creatorContext, sectionModel.rubricBlocks[index], $rubricBlock);\n });\n\n //opens the rubric blocks section if they are there.\n if (sectionModel.rubricBlocks.length > 0) {\n $('.rub-toggler', $titleWithActions).trigger('click');\n }\n }\n\n /**\n * Enable to add new rubric block\n * @private\n * @fires modelOverseer#rubric-add\n */\n function addRubricBlock() {\n $('.rublock-adder', $rubBlocks).adder({\n target: $('.rubricblocks', $rubBlocks),\n content: templates.rubricblock,\n templateData: function (cb) {\n cb({\n 'qti-type': 'rubricBlock',\n index: $('.rubricblock', $rubBlocks).length,\n content: [],\n views: [1]\n });\n }\n });\n\n //we listen the event not from the adder but from the data binder to be sure the model is up to date\n // jquery issue to select id with dot by '#ab.cd', should be used [id=\"ab.cd\"]\n $(document)\n .off('add.binder', `[id=\"${$section.attr('id')}\"] > .rublocks .rubricblocks`)\n .on(\n 'add.binder',\n `[id=\"${$section.attr('id')}\"] > .rublocks .rubricblocks`,\n function (e, $rubricBlock) {\n if (\n e.namespace === 'binder' &&\n $rubricBlock.hasClass('rubricblock') &&\n !$rubricBlock.parents('.subsection').length\n ) {\n const index = $rubricBlock.data('bind-index');\n const rubricModel = sectionModel.rubricBlocks[index] || {};\n rubricModel.classVisible = sectionModel.rubricBlocksClass;\n\n $('.rubricblock-binding', $rubricBlock).html('

           

          ');\n rubricBlockView.setUp(creatorContext, rubricModel, $rubricBlock);\n\n /**\n * @event modelOverseer#rubric-add\n * @param {Object} rubricModel\n */\n modelOverseer.trigger('rubric-add', rubricModel);\n }\n }\n );\n }\n\n /**\n * Set up the category property\n * @private\n * @param {jQuery} $view - the $view object containing the $select\n * @fires modelOverseer#category-change\n */\n function categoriesProperty($view) {\n const categories = sectionCategory.getCategories(sectionModel),\n categorySelector = categorySelectorFactory($view);\n\n categorySelector.createForm(categories.all, 'section');\n updateFormState(categorySelector);\n\n $view.on('propopen.propview', function () {\n updateFormState(categorySelector);\n });\n\n $view.on('set-default-categories', function () {\n sectionModel.categories = defaultsConfigs.categories;\n updateFormState(categorySelector);\n });\n\n categorySelector.on('category-change', function (selected, indeterminate) {\n sectionCategory.setCategories(sectionModel, selected, indeterminate);\n\n modelOverseer.trigger('category-change');\n });\n }\n\n function updateFormState(categorySelector) {\n const categories = sectionCategory.getCategories(sectionModel);\n categorySelector.updateFormState(categories.propagated, categories.partial);\n }\n\n /**\n * Set up the Blueprint property\n * @private\n * @param {jQuery} $view - the $view object containing the $select\n */\n function blueprintProperty($view) {\n const $select = $('[name=section-blueprint]', $view);\n $select\n .select2({\n ajax: {\n url: config.routes.blueprintsById,\n dataType: 'json',\n delay: 350,\n method: 'POST',\n data: function (params) {\n return {\n identifier: params // search term\n };\n },\n results: function (data) {\n return data;\n }\n },\n minimumInputLength: 3,\n width: '100%',\n multiple: false,\n allowClear: true,\n placeholder: __('Select a blueprint'),\n formatNoMatches: function () {\n return __('Enter a blueprint');\n },\n maximumInputLength: 32\n })\n .on('change', function (e) {\n setBlueprint(e.val);\n });\n\n initBlueprint();\n $view.on('propopen.propview', function () {\n initBlueprint();\n });\n\n /**\n * Start the blueprint editing\n * @private\n */\n function initBlueprint() {\n if (typeof sectionModel.blueprint === 'undefined') {\n sectionBlueprint\n .getBlueprint(config.routes.blueprintByTestSection, sectionModel)\n .success(function (data) {\n if (!_.isEmpty(data)) {\n if (sectionModel.blueprint !== '') {\n sectionModel.blueprint = data.uri;\n $select.select2('data', { id: data.uri, text: data.text });\n $select.trigger('change');\n }\n }\n });\n }\n }\n\n /**\n * save the categories into the model\n * @param {Object} blueprint\n * @private\n */\n function setBlueprint(blueprint) {\n sectionBlueprint.setBlueprint(sectionModel, blueprint);\n }\n }\n\n function addSubsection() {\n const optionsConfirmDialog = {\n buttons: {\n labels: {\n ok: __('Yes'),\n cancel: __('No')\n }\n }\n };\n\n $('.add-subsection', $titleWithActions).adder({\n target: $section.children('.subsections'),\n content: templates.subsection,\n templateData: function (cb) {\n //create a new subsection model object to be bound to the template\n let sectionParts = [];\n if ($section.data('movedItems')) {\n sectionParts = $section.data('movedItems');\n $section.removeData('movedItems');\n }\n cb({\n 'qti-type': 'assessmentSection',\n identifier: qtiTestHelper.getAvailableIdentifier(\n modelOverseer.getModel(),\n 'assessmentSection',\n 'subsection'\n ),\n title: defaultsConfigs.sectionTitlePrefix,\n index: 0,\n sectionParts,\n visible: true,\n itemSessionControl: {\n maxAttempts: defaultsConfigs.maxAttempts\n }\n });\n },\n checkAndCallAdd: function (executeAdd) {\n if (\n sectionModel.sectionParts[0] &&\n qtiTestHelper.filterQtiType(sectionModel.sectionParts[0], 'assessmentItemRef')\n ) {\n // section has item(s)\n const $parent = $section.parents('.sections');\n const index = $('.section', $parent).index($section);\n const confirmMessage = __(\n 'The items contained in %s %s will be moved into the new %s %s. Do you wish to proceed?',\n `${index + 1}.`,\n sectionModel.title,\n `${index + 1}.1.`,\n defaultsConfigs.sectionTitlePrefix\n );\n const acceptFunction = () => {\n // trigger deleted event for each itemfer to run removePropHandler and remove propView\n $('.itemrefs .itemref', $itemRefsWrapper).each(function () {\n $section.parents('.testparts').trigger('deleted.deleter', [$(this)]);\n });\n setTimeout(() => {\n // remove all itemrefs\n $('.itemrefs', $itemRefsWrapper).empty();\n // check itemrefs identifiers\n // because validation is build on \n // and each item should have valid and unique id\n sectionModel.sectionParts.forEach(itemRef => {\n if (!validators.checkIfItemIdValid(itemRef.identifier, modelOverseer)) {\n itemRef.identifier = qtiTestHelper.getAvailableIdentifier(\n modelOverseer.getModel(),\n 'assessmentItemRef',\n 'item'\n );\n }\n });\n $section.data('movedItems', _.clone(sectionModel.sectionParts));\n sectionModel.sectionParts = [];\n executeAdd();\n }, 0);\n };\n confirmDialog(confirmMessage, acceptFunction, () => {}, optionsConfirmDialog)\n .getDom()\n .find('.buttons')\n .css('display', 'flex')\n .css('flex-direction', 'row-reverse');\n } else {\n if (!sectionModel.sectionParts.length && sectionModel.categories.length) {\n $section.data('movedCategories', _.clone(sectionModel.categories));\n }\n executeAdd();\n }\n }\n });\n\n //we listen the event not from the adder but from the data binder to be sure the model is up to date\n // jquery issue to select id with dot by '#ab.cd', should be used [id=\"ab.cd\"]\n $(document)\n .off('add.binder', `[id=\"${$section.attr('id')}\"] > .subsections`)\n .on('add.binder', `[id=\"${$section.attr('id')}\"] > .subsections`, function (e, $subsection) {\n if (\n e.namespace === 'binder' &&\n $subsection.hasClass('subsection') &&\n !$subsection.parents('.subsection').length\n ) {\n // first level of subsection\n const subsectionIndex = $subsection.data('bind-index');\n const subsectionModel = sectionModel.sectionParts[subsectionIndex];\n\n if ($section.data('movedCategories')) {\n subsectionModel.categories = $section.data('movedCategories');\n $section.removeData('movedCategories');\n }\n\n //initialize the new subsection\n subsectionView.setUp(creatorContext, subsectionModel, sectionModel, $subsection);\n // hide 'Items' block and category-presets for current section\n actions.displayItemWrapper($section);\n actions.displayCategoryPresets($section);\n // set index for new subsection\n actions.updateTitleIndex($subsection);\n\n /**\n * @event modelOverseer#section-add\n * @param {Object} subsectionModel\n */\n modelOverseer.trigger('section-add', subsectionModel);\n }\n });\n }\n }\n\n /**\n * Listen for state changes to enable/disable . Called globally.\n */\n function listenActionState() {\n $('.sections').each(function () {\n const $sections = $('.section', $(this));\n\n actions.removable($sections, 'h2');\n actions.movable($sections, 'section', 'h2');\n actions.updateTitleIndex($sections);\n });\n\n $(document)\n .on('delete', function (e) {\n let $parent;\n const $target = $(e.target);\n if ($target.hasClass('section')) {\n $parent = $target.parents('.sections');\n actions.disable($parent.find('.section'), 'h2');\n setTimeout(() => {\n // element detached after event\n const $sectionsAndsubsections = $('.section,.subsection', $parent);\n actions.updateTitleIndex($sectionsAndsubsections);\n }, 100);\n }\n })\n .on('add change undo.deleter deleted.deleter', function (e) {\n const $target = $(e.target);\n if ($target.hasClass('section') || $target.hasClass('sections')) {\n const $sectionsContainer = $target.hasClass('sections') ? $target : $target.parents('.sections');\n const $sections = $('.section', $sectionsContainer);\n actions.removable($sections, 'h2');\n actions.movable($sections, 'section', 'h2');\n if (e.type === 'undo' || e.type === 'change') {\n const $sectionsAndsubsections = $('.section,.subsection', $sectionsContainer);\n actions.updateTitleIndex($sectionsAndsubsections);\n } else if (e.type === 'add') {\n const $newSection = $('.section:last', $sectionsContainer);\n actions.updateTitleIndex($newSection);\n }\n }\n })\n .on('open.toggler', '.rub-toggler', function (e) {\n if (e.namespace === 'toggler') {\n $(this).parents('h2').addClass('active');\n }\n })\n .on('close.toggler', '.rub-toggler', function (e) {\n if (e.namespace === 'toggler') {\n $(this).parents('h2').removeClass('active');\n }\n });\n }\n\n /**\n * The sectionView setup section related components and behavior\n *\n * @exports taoQtiTest/controller/creator/views/section\n */\n return {\n setUp: setUp,\n listenActionState: listenActionState\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2014-2022 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\n\n/**\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/creator/views/testpart',[\n 'jquery',\n 'lodash',\n 'taoQtiTest/controller/creator/config/defaults',\n 'taoQtiTest/controller/creator/views/actions',\n 'taoQtiTest/controller/creator/views/section',\n 'taoQtiTest/controller/creator/templates/index',\n 'taoQtiTest/controller/creator/helpers/qtiTest',\n 'taoQtiTest/controller/creator/helpers/testPartCategory',\n 'taoQtiTest/controller/creator/helpers/categorySelector',\n 'taoQtiTest/controller/creator/helpers/featureVisibility'\n], function (\n $,\n _,\n defaults,\n actions,\n sectionView,\n templates,\n qtiTestHelper,\n testPartCategory,\n categorySelectorFactory,\n featureVisibility\n) {\n ('use strict');\n\n /**\n * Set up a test part: init action behaviors. Called for each test part.\n *\n * @param {Object} creatorContext\n * @param {Object} partModel - the data model to bind to the test part\n * @param {jQuery} $testPart - the testpart container to set up\n */\n function setUp(creatorContext, partModel, $testPart) {\n const defaultsConfigs = defaults();\n const $actionContainer = $('h1', $testPart);\n const $titleWithActions = $testPart.children('h1');\n const modelOverseer = creatorContext.getModelOverseer();\n\n //add feature visibility properties to testPartModel\n featureVisibility.addTestPartVisibilityProps(partModel);\n\n //run setup methods\n actions.properties($actionContainer, 'testpart', partModel, propHandler);\n actions.move($actionContainer, 'testparts', 'testpart');\n sections();\n addSection();\n\n /**\n * Perform some binding once the property view is created\n * @private\n * @param {propView} propView - the view object\n */\n function propHandler(propView) {\n const $view = propView.getView();\n\n //listen for databinder change to update the test part title\n const $identifier = $('[data-bind=identifier]', $titleWithActions);\n $view.on('change.binder', function (e, model) {\n if (e.namespace === 'binder' && model['qti-type'] === 'testPart') {\n $identifier.text(model.identifier);\n\n /**\n * @event modelOverseer#section-add\n * @param {Object} sectionModel\n */\n modelOverseer.trigger('testpart-change', partModel);\n }\n });\n\n //destroy it when it's testpart is removed\n $testPart.parents('.testparts').on('deleted.deleter', function (e, $deletedNode) {\n if (propView !== null && $deletedNode.attr('id') === $testPart.attr('id')) {\n propView.destroy();\n }\n });\n\n //testPart level category configuration\n categoriesProperty($view);\n\n actions.displayCategoryPresets($testPart, 'testpart');\n }\n\n /**\n * Set up sections that already belongs to the test part\n * @private\n */\n function sections() {\n if (!partModel.assessmentSections) {\n partModel.assessmentSections = [];\n }\n $('.section', $testPart).each(function () {\n const $section = $(this);\n const index = $section.data('bind-index');\n if (!partModel.assessmentSections[index]) {\n partModel.assessmentSections[index] = {};\n }\n\n sectionView.setUp(creatorContext, partModel.assessmentSections[index], partModel, $section);\n });\n }\n\n /**\n * Enable to add new sections\n * @private\n * @fires modelOverseer#section-add\n */\n function addSection() {\n $('.section-adder', $testPart).adder({\n target: $('.sections', $testPart),\n content: templates.section,\n templateData: function (cb) {\n //create a new section model object to be bound to the template\n cb({\n 'qti-type': 'assessmentSection',\n identifier: qtiTestHelper.getAvailableIdentifier(\n modelOverseer.getModel(),\n 'assessmentSection',\n defaultsConfigs.sectionIdPrefix\n ),\n title: defaultsConfigs.sectionTitlePrefix,\n index: 0,\n sectionParts: [],\n visible: true,\n itemSessionControl: {\n maxAttempts: defaultsConfigs.maxAttempts\n }\n });\n }\n });\n\n //we listen the event not from the adder but from the data binder to be sure the model is up to date\n // jquery issue to select id with dot by '#ab.cd', should be used [id=\"ab.cd\"]\n $(document)\n .off('add.binder', `[id=${$testPart.attr('id')}] .sections`)\n .on('add.binder', `[id=${$testPart.attr('id')}] .sections`, function (e, $section) {\n if (e.namespace === 'binder' && $section.hasClass('section')) {\n const index = $section.data('bind-index');\n const sectionModel = partModel.assessmentSections[index];\n\n //initialize the new section\n sectionView.setUp(creatorContext, sectionModel, partModel, $section);\n\n /**\n * @event modelOverseer#section-add\n * @param {Object} sectionModel\n */\n modelOverseer.trigger('section-add', sectionModel);\n }\n });\n }\n\n /**\n * Set up the category property\n * @private\n * @param {jQuery} $view - the $view object containing the $select\n * @fires modelOverseer#category-change\n */\n function categoriesProperty($view) {\n const categoriesSummary = testPartCategory.getCategories(partModel);\n const categorySelector = categorySelectorFactory($view);\n\n categorySelector.createForm(categoriesSummary.all, 'testPart');\n updateFormState(categorySelector);\n\n $view.on('propopen.propview', function () {\n updateFormState(categorySelector);\n });\n\n $view.on('set-default-categories', function () {\n partModel.categories = defaultsConfigs.categories;\n updateFormState(categorySelector);\n });\n\n categorySelector.on('category-change', function (selected, indeterminate) {\n testPartCategory.setCategories(partModel, selected, indeterminate);\n\n modelOverseer.trigger('category-change');\n });\n }\n\n function updateFormState(categorySelector) {\n const categoriesSummary = testPartCategory.getCategories(partModel);\n categorySelector.updateFormState(categoriesSummary.propagated, categoriesSummary.partial);\n }\n }\n\n /**\n * Listen for state changes to enable/disable . Called globally.\n */\n function listenActionState() {\n let $testParts = $('.testpart');\n\n actions.removable($testParts, 'h1');\n actions.movable($testParts, 'testpart', 'h1');\n\n $('.testparts')\n .on('delete', function (e) {\n const $target = $(e.target);\n if ($target.hasClass('testpart')) {\n actions.disable($(e.target.id), 'h1');\n }\n })\n .on('add change undo.deleter deleted.deleter', function (e) {\n const $target = $(e.target);\n\n if ($target.hasClass('testpart') || $target.hasClass('testparts')) {\n //refresh\n $testParts = $('.testpart');\n\n //check state\n actions.removable($testParts, 'h1');\n actions.movable($testParts, 'testpart', 'h1');\n }\n });\n }\n\n /**\n * The testPartView setup testpart related components and behavior\n *\n * @exports taoQtiTest/controller/creator/views/testpart\n */\n return {\n setUp: setUp,\n listenActionState: listenActionState\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2014 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\n/**\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/creator/views/test',[\n 'jquery',\n 'lodash',\n 'i18n',\n 'ui/hider',\n 'ui/feedback',\n 'services/features',\n 'taoQtiTest/controller/creator/config/defaults',\n 'taoQtiTest/controller/creator/views/actions',\n 'taoQtiTest/controller/creator/views/testpart',\n 'taoQtiTest/controller/creator/templates/index',\n 'taoQtiTest/controller/creator/helpers/qtiTest',\n 'taoQtiTest/controller/creator/helpers/featureVisibility'\n], function (\n $,\n _,\n __,\n hider,\n feedback,\n features,\n defaults,\n actions,\n testPartView,\n templates,\n qtiTestHelper,\n featureVisibility\n) {\n ('use strict');\n\n /**\n * The TestView setup test related components and behavior\n *\n * @exports taoQtiTest/controller/creator/views/test\n * @param {Object} creatorContext\n */\n function testView(creatorContext) {\n const defaultsConfigs = defaults();\n var modelOverseer = creatorContext.getModelOverseer();\n var testModel = modelOverseer.getModel();\n\n //add feature visibility properties to testModel\n featureVisibility.addTestVisibilityProps(testModel);\n\n actions.properties($('.test-creator-test > h1'), 'test', testModel, propHandler);\n testParts();\n addTestPart();\n\n //add feature visibility props to model\n\n /**\n * set up the existing test part views\n * @private\n */\n function testParts() {\n if (!testModel.testParts) {\n testModel.testParts = [];\n }\n $('.testpart').each(function () {\n var $testPart = $(this);\n var index = $testPart.data('bind-index');\n if (!testModel.testParts[index]) {\n testModel.testParts[index] = {};\n }\n\n testPartView.setUp(creatorContext, testModel.testParts[index], $testPart);\n });\n }\n\n /**\n * Perform some binding once the property view is created\n * @private\n * @param {propView} propView - the view object\n * @fires modelOverseer#scoring-change\n */\n function propHandler(propView) {\n var $view = propView.getView();\n var $categoryScoreLine = $('.test-category-score', $view);\n var $cutScoreLine = $('.test-cut-score', $view);\n var $weightIdentifierLine = $('.test-weight-identifier', $view);\n var $descriptions = $('.test-outcome-processing-description', $view);\n var $generate = $('[data-action=\"generate-outcomes\"]', $view);\n var $title = $('.test-creator-test > h1 [data-bind=title]');\n var scoringState = JSON.stringify(testModel.scoring);\n const weightVisible = features.isVisible('taoQtiTest/creator/test/property/scoring/weight')\n\n function changeScoring(scoring) {\n var noOptions = !!scoring && ['none', 'custom'].indexOf(scoring.outcomeProcessing) === -1 && weightVisible;\n var newScoringState = JSON.stringify(scoring);\n\n hider.toggle($cutScoreLine, !!scoring && scoring.outcomeProcessing === 'cut');\n hider.toggle($categoryScoreLine, noOptions);\n hider.toggle($weightIdentifierLine, noOptions);\n hider.hide($descriptions);\n hider.show($descriptions.filter('[data-key=\"' + scoring.outcomeProcessing + '\"]'));\n\n if (scoringState !== newScoringState) {\n /**\n * @event modelOverseer#scoring-change\n * @param {Object} testModel\n */\n modelOverseer.trigger('scoring-change', testModel);\n }\n scoringState = newScoringState;\n }\n\n function updateOutcomes() {\n var $panel = $('.outcome-declarations', $view);\n\n $panel.html(templates.outcomes({ outcomes: modelOverseer.getOutcomesList() }));\n }\n\n $('[name=test-outcome-processing]', $view).select2({\n minimumResultsForSearch: -1,\n width: '100%'\n });\n\n $generate.on('click', function () {\n $generate.addClass('disabled').attr('disabled', true);\n modelOverseer\n .on('scoring-write.regenerate', function () {\n modelOverseer.off('scoring-write.regenerate');\n feedback()\n .success(__('The outcomes have been regenerated!'))\n .on('destroy', function () {\n $generate.removeClass('disabled').removeAttr('disabled');\n });\n })\n .trigger('scoring-change');\n });\n\n $view.on('change.binder', function (e, model) {\n if (e.namespace === 'binder' && model['qti-type'] === 'assessmentTest') {\n changeScoring(model.scoring);\n\n //update the test part title when the databinder has changed it\n $title.text(model.title);\n }\n });\n\n modelOverseer.on('scoring-write', updateOutcomes);\n\n changeScoring(testModel.scoring);\n updateOutcomes();\n }\n\n /**\n * Enable to add new test parts\n * @private\n * @fires modelOverseer#part-add\n */\n function addTestPart() {\n $('.testpart-adder').adder({\n target: $('.testparts'),\n content: templates.testpart,\n templateData: function (cb) {\n //create an new testPart model object to be bound to the template\n var testPartIndex = $('.testpart').length;\n cb({\n 'qti-type': 'testPart',\n identifier: qtiTestHelper.getAvailableIdentifier(\n modelOverseer.getModel(),\n 'testPart',\n defaultsConfigs.partIdPrefix\n ),\n index: testPartIndex,\n navigationMode: defaultsConfigs.navigationMode,\n submissionMode: defaultsConfigs.submissionMode,\n assessmentSections: [\n {\n 'qti-type': 'assessmentSection',\n identifier: qtiTestHelper.getAvailableIdentifier(\n modelOverseer.getModel(),\n 'assessmentSection',\n defaultsConfigs.sectionIdPrefix\n ),\n title: defaultsConfigs.sectionTitlePrefix,\n index: 0,\n sectionParts: [],\n visible: true,\n itemSessionControl: {\n maxAttempts: defaultsConfigs.maxAttempts\n }\n }\n ]\n });\n }\n });\n\n //we listen the event not from the adder but from the data binder to be sure the model is up to date\n $(document)\n .off('add.binder', '.testparts')\n .on('add.binder', '.testparts', function (e, $testPart, added) {\n var partModel;\n if (e.namespace === 'binder' && $testPart.hasClass('testpart')) {\n partModel = testModel.testParts[added.index];\n\n //initialize the new test part\n testPartView.setUp(creatorContext, partModel, $testPart);\n // set index for new section\n actions.updateTitleIndex($('.section', $testPart));\n\n /**\n * @event modelOverseer#part-add\n * @param {Object} partModel\n */\n modelOverseer.trigger('part-add', partModel);\n }\n });\n }\n }\n\n return testView;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;\n */\n/**\n * Basic helper that is intended to generate outcomes processing rules for a test model.\n *\n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/controller/creator/helpers/processingRule',[\n 'lodash',\n 'taoQtiTest/controller/creator/helpers/outcomeValidator',\n 'taoQtiTest/controller/creator/helpers/qtiElement',\n 'taoQtiTest/controller/creator/helpers/baseType',\n 'taoQtiTest/controller/creator/helpers/cardinality'\n], function (_, outcomeValidator, qtiElementHelper, baseTypeHelper, cardinalityHelper) {\n 'use strict';\n\n var processingRuleHelper = {\n /**\n * Creates a basic processing rule\n * @param {String} type\n * @param {String} [identifier]\n * @param {Array|Object} [expression]\n * @returns {Object}\n * @throws {TypeError} if the type is empty or is not a string\n * @throws {TypeError} if the identifier is not valid\n * @throws {TypeError} if the expression does not contain valid QTI elements\n */\n create: function create(type, identifier, expression) {\n var processingRule = qtiElementHelper.create(type, identifier && validateIdentifier(identifier));\n\n if (expression) {\n processingRuleHelper.setExpression(processingRule, expression);\n }\n\n return processingRule;\n },\n\n /**\n * Sets an expression to a processing rule\n * @param {Object} processingRule\n * @param {Object|Array} expression\n * @throws {TypeError} if the expression does not contain valid QTI elements\n */\n setExpression: function setExpression(processingRule, expression) {\n if (processingRule) {\n if (_.isArray(expression)) {\n if (processingRule.expression) {\n processingRule.expression = null;\n }\n processingRule.expressions = validateOutcomeList(expression);\n } else {\n if (processingRule.expressions) {\n processingRule.expressions = null;\n }\n if (expression) {\n processingRule.expression = validateOutcome(expression);\n }\n }\n }\n },\n\n /**\n * Adds an expression to a processing rule\n * @param {Object} processingRule\n * @param {Object|Array} expression\n * @throws {TypeError} if the expression does not contain valid QTI elements\n */\n addExpression: function addExpression(processingRule, expression) {\n if (processingRule && expression) {\n if (processingRule.expression) {\n processingRule.expressions = forceArray(processingRule.expression);\n processingRule.expression = null;\n }\n\n if (_.isArray(expression)) {\n processingRule.expressions = forceArray(processingRule.expressions).concat(validateOutcomeList(expression));\n } else {\n if (processingRule.expressions) {\n processingRule.expressions.push(expression);\n } else {\n processingRule.expression = validateOutcome(expression);\n }\n }\n }\n },\n\n /**\n * Creates a `setOutcomeValue` rule\n * @param {String} identifier\n * @param {Object|Array} [expression]\n * @returns {Object}\n * @throws {TypeError} if the identifier is empty or is not a string\n * @throws {TypeError} if the expression does not contain valid QTI elements\n */\n setOutcomeValue: function setOutcomeValue(identifier, expression) {\n return processingRuleHelper.create('setOutcomeValue', validateIdentifier(identifier), expression);\n },\n\n /**\n * Creates a `gte` rule\n * @param {Object|Array} left - the left operand\n * @param {Object|Array} right - the right operand\n * @returns {Object}\n * @throws {TypeError} if the left and right operands are not valid QTI elements\n */\n gte: function gte(left, right) {\n return binaryOperator('gte', left, right);\n },\n\n /**\n * Creates a `lte` rule\n * @param {Object|Array} left - the left operand\n * @param {Object|Array} right - the right operand\n * @returns {Object}\n * @throws {TypeError} if the left and right operands are not valid QTI elements\n */\n lte: function lte(left, right) {\n return binaryOperator('lte', left, right);\n },\n\n /**\n * Creates a `divide` rule\n * @param {Object|Array} left - the left operand\n * @param {Object|Array} right - the right operand\n * @returns {Object}\n * @throws {TypeError} if the left and right operands are not valid QTI elements\n */\n divide: function divide(left, right) {\n return binaryOperator('divide', left, right);\n },\n\n /**\n * Creates a `sum` rule\n * @param {Object|Array} terms\n * @returns {Object}\n * @throws {TypeError} if the terms are not valid QTI elements\n */\n sum: function sum(terms) {\n var processingRule = processingRuleHelper.create('sum', null, forceArray(terms));\n\n processingRule.minOperands = 1;\n processingRule.maxOperands = -1;\n processingRule.acceptedCardinalities = [cardinalityHelper.SINGLE, cardinalityHelper.MULTIPLE, cardinalityHelper.ORDERED];\n processingRule.acceptedBaseTypes = [baseTypeHelper.INTEGER, baseTypeHelper.FLOAT];\n\n return processingRule;\n },\n\n /**\n * Creates a `testVariables` rule\n * @param {String} identifier\n * @param {String|Number} [type]\n * @param {String} weightIdentifier\n * @param {String|String[]} [includeCategories]\n * @param {String|String[]} [excludeCategories]\n * @returns {Object}\n * @throws {TypeError} if the identifier is empty or is not a string\n */\n testVariables: function testVariables(identifier, type, weightIdentifier, includeCategories, excludeCategories) {\n var processingRule = processingRuleHelper.create('testVariables');\n\n processingRule.variableIdentifier = validateIdentifier(identifier);\n processingRule.baseType = baseTypeHelper.getValid(type);\n addWeightIdentifier(processingRule, weightIdentifier);\n addSectionIdentifier(processingRule, '');\n addCategories(processingRule, includeCategories, excludeCategories);\n\n return processingRule;\n },\n\n /**\n * Creates a `outcomeMaximum` rule\n * @param {String} identifier\n * @param {String} weightIdentifier\n * @param {String|String[]} [includeCategories]\n * @param {String|String[]} [excludeCategories]\n * @returns {Object}\n * @throws {TypeError} if the identifier is empty or is not a string\n */\n outcomeMaximum: function outcomeMaximum(identifier, weightIdentifier, includeCategories, excludeCategories) {\n var processingRule = processingRuleHelper.create('outcomeMaximum');\n\n processingRule.outcomeIdentifier = validateIdentifier(identifier);\n\n addWeightIdentifier(processingRule, weightIdentifier);\n addSectionIdentifier(processingRule, '');\n addCategories(processingRule, includeCategories, excludeCategories);\n\n return processingRule;\n },\n\n /**\n * Creates a `numberPresented` rule\n * @param {String|String[]} [includeCategories]\n * @param {String|String[]} [excludeCategories]\n * @returns {Object}\n */\n numberPresented: function numberPresented(includeCategories, excludeCategories) {\n var processingRule = processingRuleHelper.create('numberPresented');\n\n addSectionIdentifier(processingRule, '');\n addCategories(processingRule, includeCategories, excludeCategories);\n\n return processingRule;\n },\n\n /**\n * Creates a `baseValue` rule\n * @param {*} [value]\n * @param {String|Number} [type]\n * @returns {Object}\n */\n baseValue: function baseValue(value, type) {\n var processingRule = processingRuleHelper.create('baseValue');\n\n processingRule.baseType = baseTypeHelper.getValid(type, baseTypeHelper.FLOAT);\n processingRule.value = baseTypeHelper.getValue(processingRule.baseType, value);\n\n return processingRule;\n },\n\n /**\n ** Creates a `variable` rule\n * @param {String} identifier\n * @param {String} [weightIdentifier]\n * @returns {Object}\n * @throws {TypeError} if the identifier is not valid\n * @throws {TypeError} if the weight identifier is not valid\n */\n variable: function variable(identifier, weightIdentifier) {\n var processingRule = processingRuleHelper.create('variable', validateIdentifier(identifier));\n\n addWeightIdentifier(processingRule, weightIdentifier);\n\n return processingRule;\n },\n\n /**\n * Creates a `match` rule\n * @param {Object|Array} left - the left operand\n * @param {Object|Array} right - the right operand\n * @returns {Object}\n * @throws {TypeError} if the left and right operands are not valid QTI elements\n */\n match: function match(left, right) {\n return binaryOperator('match', left, right, cardinalityHelper.SAME, cardinalityHelper.SAME);\n },\n\n /**\n * Creates a `isNull` rule\n * @param {Object|Array} expression - the operand\n * @returns {Object}\n * @throws {TypeError} if the operand is not valid QTI element\n */\n isNull: function isNull(expression) {\n return unaryOperator('isNull', expression, baseTypeHelper.ANY, cardinalityHelper.ANY);\n },\n\n /**\n * Creates a `outcomeCondition` rule\n * @param {Object} outcomeIf\n * @param {Object} outcomeElse\n * @returns {Object}\n * @throws {TypeError} if the outcomeIf and outcomeElse operands are not valid QTI elements\n */\n outcomeCondition: function outcomeCondition(outcomeIf, outcomeElse) {\n var processingRule = processingRuleHelper.create('outcomeCondition');\n\n if (!outcomeValidator.validateOutcome(outcomeIf, false, 'outcomeIf')) {\n throw new TypeError('You must provide a valid outcomeIf element!');\n }\n\n if (outcomeElse && !outcomeValidator.validateOutcome(outcomeElse, false, 'outcomeElse')) {\n throw new TypeError('You must provide a valid outcomeElse element!');\n }\n\n processingRule.outcomeIf = outcomeIf;\n processingRule.outcomeElseIfs = [];\n\n if (outcomeElse) {\n processingRule.outcomeElse = outcomeElse;\n }\n\n return processingRule;\n },\n\n /**\n * Creates a `outcomeIf` rule\n * @param {Object} expression\n * @param {Object|Object[]} instruction\n * @returns {Object}\n * @throws {TypeError} if the expression and instruction operands are not valid QTI elements\n */\n outcomeIf: function outcomeIf(expression, instruction) {\n var processingRule = processingRuleHelper.create('outcomeIf');\n\n if (!_.isArray(instruction)) {\n instruction = [instruction];\n }\n\n processingRule.expression = validateOutcome(expression);\n processingRule.outcomeRules = validateOutcomeList(instruction);\n\n return processingRule;\n },\n\n /**\n * Creates a `outcomeElse` rule\n * @param {Object|Object[]} instruction\n * @returns {Object}\n * @throws {TypeError} if the instruction is not a valid QTI element\n */\n outcomeElse: function outcomeElse(instruction) {\n var processingRule = processingRuleHelper.create('outcomeElse');\n\n if (!_.isArray(instruction)) {\n instruction = [instruction];\n }\n\n processingRule.outcomeRules = validateOutcomeList(instruction);\n\n return processingRule;\n }\n\n };\n\n /**\n * Creates a unary operator rule\n * @param {String} type - The rule type\n * @param {Object|Array} expression - The operand\n * @param {Number|Array} [baseType] - The accepted base type\n * @param {Number|Array} [cardinality] - The accepted cardinality\n * @returns {Object}\n * @throws {TypeError} if the type is empty or is not a string\n * @throws {TypeError} if the operand is not valid QTI element\n */\n function unaryOperator(type, expression, baseType, cardinality) {\n var processingRule = processingRuleHelper.create(type, null, [expression]);\n\n processingRule.minOperands = 1;\n processingRule.maxOperands = 1;\n\n addTypeAndCardinality(processingRule, baseType, cardinality);\n\n return processingRule;\n }\n\n /**\n * Creates a binary operator rule\n * @param {String} type - The rule type\n * @param {Object|Array} left - The left operand\n * @param {Object|Array} right - The right operand\n * @param {Number|Array} [baseType] - The accepted base type\n * @param {Number|Array} [cardinality] - The accepted cardinality\n * @returns {Object}\n * @throws {TypeError} if the type is empty or is not a string\n * @throws {TypeError} if the left and right operands are not valid QTI elements\n */\n function binaryOperator(type, left, right, baseType, cardinality) {\n var processingRule = processingRuleHelper.create(type, null, [left, right]);\n\n processingRule.minOperands = 2;\n processingRule.maxOperands = 2;\n\n addTypeAndCardinality(processingRule, baseType, cardinality);\n\n return processingRule;\n }\n\n /**\n * Appends the base type and the cardinality on a processing rule\n * @param {Object} processingRule\n * @param {Number|Array} [baseType] - The accepted base type\n * @param {Number|Array} [cardinality] - The accepted cardinality\n * @returns {Object}\n */\n function addTypeAndCardinality(processingRule, baseType, cardinality) {\n if (_.isUndefined(baseType)) {\n baseType = [baseTypeHelper.INTEGER, baseTypeHelper.FLOAT];\n }\n\n if (_.isUndefined(cardinality)) {\n cardinality = [cardinalityHelper.SINGLE];\n }\n\n processingRule.acceptedCardinalities = forceArray(cardinality);\n processingRule.acceptedBaseTypes = forceArray(baseType);\n\n return processingRule;\n }\n\n /**\n * Extends a processing rule with categories\n * @param {Object} processingRule\n * @param {Array|String} [includeCategories]\n * @param {Array|String} [excludeCategories]\n * @returns {Object}\n */\n function addCategories(processingRule, includeCategories, excludeCategories) {\n processingRule.includeCategories = forceArray(includeCategories);\n processingRule.excludeCategories = forceArray(excludeCategories);\n\n return processingRule;\n }\n\n /**\n * Appends the section identifier on a processing rule\n * @param {Object} processingRule\n * @param {String} [sectionIdentifier]\n * @returns {Object}\n * @throws {TypeError} if the weight identifier is not valid\n */\n function addSectionIdentifier(processingRule, sectionIdentifier) {\n if (sectionIdentifier) {\n if (!outcomeValidator.validateIdentifier(sectionIdentifier)) {\n throw new TypeError('You must provide a valid weight identifier!');\n }\n processingRule.sectionIdentifier = sectionIdentifier;\n } else {\n processingRule.sectionIdentifier = '';\n }\n\n return processingRule;\n }\n\n /**\n * Appends the weight identifier on a processing rule\n * @param {Object} processingRule\n * @param {String} [weightIdentifier]\n * @returns {Object}\n * @throws {TypeError} if the weight identifier is not valid\n */\n function addWeightIdentifier(processingRule, weightIdentifier) {\n if (weightIdentifier) {\n if (!outcomeValidator.validateIdentifier(weightIdentifier)) {\n throw new TypeError('You must provide a valid weight identifier!');\n }\n processingRule.weightIdentifier = weightIdentifier;\n } else {\n processingRule.weightIdentifier = '';\n }\n\n return processingRule;\n }\n\n /**\n * Validates an identifier\n * @param {String} identifier\n * @returns {String}\n * @throws {TypeError} if the identifier is not valid\n */\n function validateIdentifier(identifier) {\n if (!outcomeValidator.validateIdentifier(identifier)) {\n throw new TypeError('You must provide a valid identifier!');\n }\n return identifier;\n }\n\n /**\n * Validates an outcome\n * @param {Object} outcome\n * @returns {Object}\n * @throws {TypeError} if the outcome is not valid\n */\n function validateOutcome(outcome) {\n if (!outcomeValidator.validateOutcome(outcome)) {\n throw new TypeError('You must provide a valid QTI element!');\n }\n return outcome;\n }\n\n /**\n * Validates a list of outcomes\n * @param {Array} outcomes\n * @returns {Array}\n * @throws {TypeError} if an outcome is not valid\n */\n function validateOutcomeList(outcomes) {\n if (!outcomeValidator.validateOutcomes(outcomes)) {\n throw new TypeError('You must provide a valid list of QTI elements!');\n }\n return outcomes;\n }\n\n /**\n * Ensures a value is an array\n * @param {*} value\n * @returns {Array}\n */\n function forceArray(value) {\n if (!value) {\n value = [];\n }\n if (!_.isArray(value)) {\n value = [value];\n }\n return value;\n }\n\n return processingRuleHelper;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2017-2023 (original work) Open Assessment Technologies SA ;\n */\n/**\n * Basic helper that is intended to manage the score processing declaration in a test model.\n *\n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/controller/creator/helpers/scoring',[\n 'lodash',\n 'i18n',\n 'core/format',\n 'taoQtiTest/controller/creator/helpers/baseType',\n 'taoQtiTest/controller/creator/helpers/outcome',\n 'taoQtiTest/controller/creator/helpers/processingRule',\n 'services/features'\n], function (_, __, format, baseTypeHelper, outcomeHelper, processingRuleHelper, features) {\n 'use strict';\n\n /**\n * The default cut score\n * @todo Move this to a config file\n * @type {Number}\n */\n var defaultCutScore = 0.5;\n\n /**\n * The name of the variable containing the score\n * @type {String}\n */\n var defaultScoreIdentifier = 'SCORE';\n\n /**\n * The list of supported processing modes, indexed by mode identifier\n * @type {Object}\n */\n var processingModes = {\n none: {\n key: 'none',\n label: __('None'),\n description: __('No outcome processing. Erase the existing rules, if any.')\n },\n custom: {\n key: 'custom',\n label: __('Custom'),\n description: __('Custom outcome processing. No changes will be made to the existing rules.')\n },\n total: {\n key: 'total',\n label: __('Total score'),\n description: __('The score will be processed for the entire test. A sum of all SCORE outcomes will be computed, the result will take place in the SCORE_TOTAL outcome.')\n + ' ' +\n __('If the category option is set, the score will also be processed per categories, and each results will take place in the SCORE_xxx outcome, where xxx is the name of the category.')\n },\n cut: {\n key: 'cut',\n label: __('Cut score'),\n description: __('The score will be processed for the entire test. A sum of all SCORE outcomes will be computed and divided by the sum of MAX SCORE, the result will be compared to the cut score (or pass ratio), then the PASS_TOTAL outcome will be set accordingly.')\n + ' ' +\n __('If the category option is set, the score will also be processed per categories, and each results will take place in the PASS_xxx outcome, where xxx is the name of the category.')\n }\n };\n\n /**\n * The list of recipes to generate the outcomes\n * @type {Object}\n */\n var outcomesRecipes = {\n none: {\n key: 'none',\n clean: true\n },\n custom: {\n key: 'custom',\n clean: false\n },\n total: {\n key: 'total',\n signature: /^SCORE_([a-zA-Z][a-zA-Z0-9_\\.-]*)$/,\n outcomes: [\n {\n writer: 'total',\n identifier: 'SCORE_TOTAL',\n weighted: 'SCORE_TOTAL_WEIGHTED',\n categoryIdentifier: 'SCORE_CATEGORY_%s',\n categoryWeighted: 'SCORE_CATEGORY_WEIGHTED_%s'\n },\n {\n writer: 'max',\n identifier: 'SCORE_TOTAL_MAX',\n weighted: 'SCORE_TOTAL_MAX_WEIGHTED',\n categoryIdentifier: 'SCORE_CATEGORY_MAX_%s',\n categoryWeighted: 'SCORE_CATEGORY_WEIGHTED_MAX_%s'\n },\n {\n writer: 'ratio',\n identifier: 'SCORE_RATIO',\n weighted: 'SCORE_RATIO_WEIGHTED',\n scoreIdentifier: {\n total: 'SCORE_TOTAL',\n max: 'SCORE_TOTAL_MAX'\n },\n scoreWeighted: {\n total: 'SCORE_TOTAL_WEIGHTED',\n max: 'SCORE_TOTAL_MAX_WEIGHTED'\n }\n }\n ],\n clean: true\n },\n cut: {\n key: 'cut',\n include: 'total',\n signature: /^PASS_([a-zA-Z][a-zA-Z0-9_\\.-]*)$/,\n outcomes: [\n {\n writer: 'cut',\n identifier: 'PASS_ALL',\n feedback: 'PASS_ALL_RENDERING',\n feedbackOk: 'passed',\n feedbackFailed: 'not_passed',\n categoryIdentifier: 'PASS_CATEGORY_%s',\n categoryFeedback: 'PASS_CATEGORY_%s_RENDERING'\n }\n ],\n clean: true\n }\n };\n\n /**\n * List of writers that provide the outcomes for each score processing mode.\n * @type {Object}\n */\n var outcomesWriters = {\n /**\n * Generates the outcomes that compute the \"Score ratio\"\n * @param {Object} descriptor\n * @param {Object} scoring\n * @param {Object} outcomes\n */\n ratio: function writerRatio(descriptor, scoring, outcomes) {\n addRatioOutcomes(\n outcomes,\n descriptor.identifier,\n descriptor.scoreIdentifier.total,\n descriptor.scoreIdentifier.max\n );\n if (scoring.weightIdentifier) {\n //add weighted ratio outcome only when the scoring outcome processing rule uses a weight\n addRatioOutcomes(\n outcomes,\n descriptor.weighted,\n descriptor.scoreWeighted.total,\n descriptor.scoreWeighted.max\n );\n }\n },\n\n /**\n * Generates the outcomes that compute the \"Total score\"\n * @param {Object} descriptor\n * @param {Object} scoring\n * @param {Object} outcomes\n * @param {Array} [categories]\n */\n total: function writerTotal(descriptor, scoring, outcomes, categories) {\n // create the outcome and the rule that process the overall score\n addTotalScoreOutcomes(outcomes, scoring, descriptor.identifier, false);\n if (descriptor.weighted && scoring.weightIdentifier) {\n addTotalScoreOutcomes(outcomes, scoring, descriptor.weighted, true);\n }\n\n // create an outcome per categories\n if (descriptor.categoryIdentifier && categories) {\n _.forEach(categories, function (category) {\n addTotalScoreOutcomes(\n outcomes,\n scoring,\n formatCategoryOutcome(category, descriptor.categoryIdentifier),\n false,\n category\n );\n if (descriptor.categoryWeighted && scoring.weightIdentifier) {\n addTotalScoreOutcomes(\n outcomes,\n scoring,\n formatCategoryOutcome(category, descriptor.categoryWeighted),\n true,\n category\n );\n }\n });\n }\n },\n\n /**\n * Generates the outcomes that compute the \"Max score\"\n * @param {Object} descriptor\n * @param {Object} scoring\n * @param {Object} outcomes\n * @param {Array} [categories]\n */\n max: function writerMax(descriptor, scoring, outcomes, categories) {\n // create the outcome and the rule that process the maximum overall score\n addMaxScoreOutcomes(outcomes, scoring, descriptor.identifier, false);\n if (descriptor.weighted && scoring.weightIdentifier) {\n addMaxScoreOutcomes(outcomes, scoring, descriptor.weighted, true);\n }\n\n // create an outcome per categories\n if (descriptor.categoryIdentifier && categories) {\n _.forEach(categories, function (category) {\n addMaxScoreOutcomes(\n outcomes,\n scoring,\n formatCategoryOutcome(category, descriptor.categoryIdentifier),\n false,\n category\n );\n if (descriptor.categoryWeighted && scoring.weightIdentifier) {\n addMaxScoreOutcomes(\n outcomes,\n scoring,\n formatCategoryOutcome(category, descriptor.categoryWeighted),\n true,\n category\n );\n }\n });\n }\n },\n\n /**\n * Generates the outcomes that compute the \"Cut score\"\n * @param {Object} descriptor\n * @param {Object} scoring\n * @param {Object} outcomes\n * @param {Array} [categories]\n * @returns {Object} outcomes\n */\n cut: function writerCut(descriptor, scoring, outcomes, categories) {\n var cutScore = scoring.cutScore;\n var totalModeOutcomes = outcomesRecipes.total.outcomes;\n var total = _.find(totalModeOutcomes, { writer: 'total' });\n var max = _.find(totalModeOutcomes, { writer: 'max' });\n var ratio = _.find(totalModeOutcomes, { writer: 'ratio' });\n var whichOutcome = scoring.weightIdentifier ? 'weighted' : 'identifier';\n var ratioIdentifier = ratio[whichOutcome];\n\n // create the outcome and the rule that process the overall score\n addGlobalCutScoreOutcomes(outcomes, descriptor.identifier, ratioIdentifier, cutScore);\n\n // create the outcome and the rule that process the score feedback\n if (descriptor.feedback) {\n addFeedbackScoreOutcomes(\n outcomes,\n descriptor.feedback,\n descriptor.identifier,\n descriptor.feedbackOk,\n descriptor.feedbackFailed\n );\n }\n\n // create an outcome per category\n if (descriptor.categoryIdentifier && categories) {\n _.forEach(categories, function (category) {\n var categoryOutcome = scoring.weightIdentifier ? 'categoryWeighted' : 'categoryIdentifier';\n var categoryOutcomeIdentifier = formatCategoryOutcome(category, descriptor.categoryIdentifier);\n var categoryScoreIdentifier = formatCategoryOutcome(category, total[categoryOutcome]);\n var categoryCountIdentifier = formatCategoryOutcome(category, max[categoryOutcome]);\n\n addCutScoreOutcomes(\n outcomes,\n categoryOutcomeIdentifier,\n categoryScoreIdentifier,\n categoryCountIdentifier,\n cutScore\n );\n\n if (descriptor.categoryFeedback) {\n addFeedbackScoreOutcomes(\n outcomes,\n formatCategoryOutcome(category, descriptor.categoryFeedback),\n categoryOutcomeIdentifier,\n descriptor.feedbackOk,\n descriptor.feedbackFailed\n );\n }\n });\n }\n\n return outcomes;\n }\n };\n\n var scoringHelper = {\n /**\n * Checks the test model against outcome processing mode.\n * Initializes the scoring property accordingly.\n *\n * @param {modelOverseer} modelOverseer\n * @throws {TypeError} if the modelOverseer is invalid\n * @fires modelOverseer#scoring-init\n * @fires modelOverseer#scoring-generate\n * @fires modelOverseer#scoring-write\n */\n init: function init(modelOverseer) {\n var model;\n\n if (!modelOverseer || !_.isFunction(modelOverseer.getModel)) {\n throw new TypeError('You must provide a valid modelOverseer');\n }\n\n model = modelOverseer.getModel();\n\n // detect the score processing mode and build the descriptor used to manage the UI\n model.scoring = detectScoring(modelOverseer);\n\n modelOverseer\n .on('scoring-change category-change delete', function () {\n /**\n * Regenerates the outcomes on any significant changes.\n * After the outcomes have been generated a write is needed to actually apply the data.\n * Other component can listen to this event and eventually prevent the write to happen.\n * @event modelOverseer#scoring-generate\n * @param {Object} outcomes\n */\n modelOverseer.trigger('scoring-generate', scoringHelper.generate(modelOverseer));\n })\n .on('scoring-generate', function (outcomes) {\n outcomeHelper.replaceOutcomes(model, outcomes);\n\n /**\n * The generated outcome have just been applied on the model.\n * @event modelOverseer#scoring-write\n * @param {Object} testModel\n */\n modelOverseer.trigger('scoring-write', model);\n })\n\n /**\n * @event modelOverseer#scoring-init\n * @param {Object} testModel\n */\n .trigger('scoring-init', model);\n },\n\n /**\n * If the processing mode has been set, generates the outcomes that define the scoring.\n *\n * @param {modelOverseer} modelOverseer\n * @returns {Object}\n * @throws {TypeError} if the modelOverseer is invalid or the processing mode is unknown\n */\n generate: function generate(modelOverseer) {\n var model, scoring, outcomes, outcomeRecipe, recipes, categories;\n\n if (!modelOverseer || !_.isFunction(modelOverseer.getModel)) {\n throw new TypeError('You must provide a valid modelOverseer');\n }\n\n model = modelOverseer.getModel();\n scoring = model.scoring;\n outcomes = getOutcomes(model);\n\n // write the score processing mode by generating the outcomes variables, but only if the mode has been set\n if (scoring) {\n outcomeRecipe = outcomesRecipes[scoring.outcomeProcessing];\n if (outcomeRecipe) {\n if (outcomeRecipe.clean) {\n // erase the existing rules, they will be replaced by those that are defined here\n removeScoring(outcomes);\n }\n\n // get the recipes that define the outcomes, include sub-recipes if any\n recipes = getRecipes(outcomeRecipe);\n\n // only get the categories if requested\n if (handleCategories(modelOverseer)) {\n categories = modelOverseer.getCategories();\n }\n\n // will generate outcomes based of the defined recipe\n _.forEach(recipes, function (recipe) {\n var writer = outcomesWriters[recipe.writer];\n writer(recipe, scoring, outcomes, categories);\n });\n } else {\n throw new Error(`Unknown score processing mode: ${scoring.outcomeProcessing}`);\n }\n }\n\n return outcomes;\n }\n };\n\n /**\n * Creates an outcome and the rule that process the total score\n *\n * @param {Object} model\n * @param {Object} scoring\n * @param {String} identifier\n * @param {Boolean} [weight]\n * @param {String} [category]\n */\n function addTotalScoreOutcomes(model, scoring, identifier, weight, category) {\n var outcome = outcomeHelper.createOutcome(identifier, baseTypeHelper.FLOAT);\n var processingRule = processingRuleHelper.setOutcomeValue(\n identifier,\n processingRuleHelper.sum(\n processingRuleHelper.testVariables(\n scoring.scoreIdentifier,\n -1,\n weight && scoring.weightIdentifier,\n category\n )\n )\n );\n\n outcomeHelper.addOutcome(model, outcome, processingRule);\n }\n\n /**\n * Creates an outcome and the rule that process the maximum score\n *\n * @param {Object} model\n * @param {Object} scoring\n * @param {String} identifier\n * @param {Boolean} [weight]\n * @param {String} [category]\n */\n function addMaxScoreOutcomes(model, scoring, identifier, weight, category) {\n var outcome = outcomeHelper.createOutcome(identifier, baseTypeHelper.FLOAT);\n var processingRule = processingRuleHelper.setOutcomeValue(\n identifier,\n processingRuleHelper.sum(\n processingRuleHelper.testVariables('MAXSCORE', -1, weight && scoring.weightIdentifier, category)\n )\n );\n outcomeHelper.addOutcome(model, outcome, processingRule);\n }\n\n /**\n * Create an outcome and the rule that process the score ratio\n *\n * @param {Object} model\n * @param {String} identifier\n * @param {String} identifierTotal\n * @param {String} identifierMax\n */\n function addRatioOutcomes(model, identifier, identifierTotal, identifierMax) {\n var outcome = outcomeHelper.createOutcome(identifier, baseTypeHelper.FLOAT);\n var outcomeCondition = processingRuleHelper.outcomeCondition(\n processingRuleHelper.outcomeIf(\n processingRuleHelper.isNull(processingRuleHelper.variable(identifierMax)),\n processingRuleHelper.setOutcomeValue(\n identifier,\n processingRuleHelper.baseValue(0, baseTypeHelper.FLOAT)\n )\n ),\n processingRuleHelper.outcomeElse(\n processingRuleHelper.setOutcomeValue(\n identifier,\n processingRuleHelper.divide(\n processingRuleHelper.variable(identifierTotal),\n processingRuleHelper.variable(identifierMax)\n )\n )\n )\n );\n\n outcomeHelper.addOutcome(model, outcome);\n outcomeHelper.addOutcomeProcessing(model, outcomeCondition);\n }\n\n /**\n * Creates an outcome and the rule that process the cut score by category\n *\n * @param {Object} model\n * @param {String} identifier\n * @param {String} scoreIdentifier\n * @param {String} countIdentifier\n * @param {String|Number} cutScore\n */\n function addCutScoreOutcomes(model, identifier, scoreIdentifier, countIdentifier, cutScore) {\n var outcome = outcomeHelper.createOutcome(identifier, baseTypeHelper.BOOLEAN);\n var processingRule = processingRuleHelper.setOutcomeValue(\n identifier,\n processingRuleHelper.gte(\n processingRuleHelper.divide(\n processingRuleHelper.variable(scoreIdentifier),\n processingRuleHelper.variable(countIdentifier)\n ),\n processingRuleHelper.baseValue(cutScore, baseTypeHelper.FLOAT)\n )\n );\n\n outcomeHelper.addOutcome(model, outcome, processingRule);\n }\n\n /**\n * Creates an outcome and the rule that process the global cut score\n *\n * @param {Object} model\n * @param {String} identifier\n * @param {String} ratioIdentifier\n * @param {String|Number} cutScore\n */\n function addGlobalCutScoreOutcomes(model, identifier, ratioIdentifier, cutScore) {\n var outcome = outcomeHelper.createOutcome(identifier, baseTypeHelper.BOOLEAN);\n var processingRule = processingRuleHelper.setOutcomeValue(\n identifier,\n processingRuleHelper.gte(\n processingRuleHelper.variable(ratioIdentifier),\n processingRuleHelper.baseValue(cutScore, baseTypeHelper.FLOAT)\n )\n );\n\n outcomeHelper.addOutcome(model, outcome, processingRule);\n }\n\n /**\n * Creates an outcome and the rule that process the score feedback\n *\n * @param {Object} model\n * @param {String} identifier\n * @param {String} variable\n * @param {String} passed\n * @param {String} notPassed\n */\n function addFeedbackScoreOutcomes(model, identifier, variable, passed, notPassed) {\n var type = baseTypeHelper.IDENTIFIER;\n var outcome = outcomeHelper.createOutcome(identifier, type);\n var processingRule = processingRuleHelper.outcomeCondition(\n processingRuleHelper.outcomeIf(\n processingRuleHelper.match(\n processingRuleHelper.variable(variable),\n processingRuleHelper.baseValue(true, baseTypeHelper.BOOLEAN)\n ),\n processingRuleHelper.setOutcomeValue(identifier, processingRuleHelper.baseValue(passed, type))\n ),\n processingRuleHelper.outcomeElse(\n processingRuleHelper.setOutcomeValue(identifier, processingRuleHelper.baseValue(notPassed, type))\n )\n );\n\n outcomeHelper.addOutcome(model, outcome);\n outcomeHelper.addOutcomeProcessing(model, processingRule);\n }\n\n /**\n * Formats the identifier of a category outcome\n * @param {String} category\n * @param {String} template\n * @returns {String}\n */\n function formatCategoryOutcome(category, template) {\n return format(template, category.toUpperCase());\n }\n\n /**\n * Checks whether an identifier belongs to a particular recipe\n * @param {String} identifier\n * @param {Object} recipe\n * @param {Boolean} [onlyCategories]\n * @returns {Boolean}\n */\n function belongToRecipe(identifier, recipe, onlyCategories) {\n var match = false;\n if (recipe.signature && recipe.signature.test(identifier)) {\n match = true;\n if (onlyCategories) {\n _.forEach(recipe.outcomes, function (outcome) {\n if (\n outcome.identifier === identifier ||\n (outcome.weighted && outcome.weighted === identifier) ||\n (outcome.feedback && outcome.feedback === identifier)\n ) {\n match = false;\n return false;\n }\n });\n }\n }\n return match;\n }\n\n /**\n * Checks if all the outcomes are related to the recipe\n * @param {Object} outcomeRecipe\n * @param {Array} outcomes\n * @param {Array} categories\n * @returns {Boolean}\n */\n function matchRecipe(outcomeRecipe, outcomes, categories) {\n var signatures = getSignatures(outcomeRecipe);\n var match = true;\n\n // check the outcomes definitions against the provided identifier\n function matchRecipeOutcome(recipe, identifier) {\n var outcomeMatch = false;\n\n // first level, the signature must match\n if (recipe.signature && recipe.signature.test(identifier)) {\n _.forEach(recipe.outcomes, function (outcome) {\n // second level, the main identifier must match\n if (\n outcome.identifier !== identifier &&\n (!outcome.weighted || (outcome.weighted && outcome.weighted !== identifier)) &&\n (!outcome.feedback || (outcome.feedback && outcome.feedback !== identifier))\n ) {\n if (categories) {\n // third level, a category must match\n _.forEach(categories, function (category) {\n if (\n outcome.categoryIdentifier &&\n identifier === formatCategoryOutcome(category, outcome.categoryIdentifier)\n ) {\n outcomeMatch = true;\n } else if (\n outcome.categoryWeighted &&\n identifier === formatCategoryOutcome(category, outcome.categoryWeighted)\n ) {\n outcomeMatch = true;\n } else if (\n outcome.categoryFeedback &&\n identifier === formatCategoryOutcome(category, outcome.categoryFeedback)\n ) {\n outcomeMatch = true;\n }\n // found something?\n if (outcomeMatch) {\n return false;\n }\n });\n }\n } else {\n outcomeMatch = true;\n }\n\n // found something?\n if (outcomeMatch) {\n return false;\n }\n });\n }\n\n if (!outcomeMatch && recipe.include) {\n outcomeMatch = matchRecipeOutcome(outcomesRecipes[recipe.include], identifier);\n }\n\n return outcomeMatch;\n }\n\n // only check the outcomes that are related to the scoring mode\n _.forEach(outcomes, function (identifier) {\n var signatureMatch = false;\n _.forEach(signatures, function (signature) {\n if (signature.test(identifier)) {\n signatureMatch = true;\n return false;\n }\n });\n\n if (signatureMatch) {\n match = matchRecipeOutcome(outcomeRecipe, identifier);\n\n if (!match) {\n return false;\n }\n }\n });\n\n return match;\n }\n\n /**\n * Gets all the outcomes signatures related to a scoring mode\n * @param {Object} recipe\n * @returns {Array}\n */\n function getSignatures(recipe) {\n var signatures = [];\n\n // list the signatures for each processing mode, taking care of includes\n while (recipe) {\n if (recipe.signature) {\n signatures.push(recipe.signature);\n }\n recipe = recipe.include && outcomesRecipes[recipe.include];\n }\n\n return signatures;\n }\n\n /**\n * Gets all the outcomes recipes related to a scoring mode\n * @param {Object} recipe\n * @returns {Array}\n */\n function getRecipes(recipe) {\n var descriptors = [];\n\n // get the recipes that define the outcomes, include sub-recipes if any\n while (recipe) {\n if (recipe.outcomes) {\n descriptors = [].concat(recipe.outcomes, descriptors);\n }\n recipe = recipe.include && outcomesRecipes[recipe.include];\n }\n\n return descriptors;\n }\n\n /**\n * Checks if an outcome is related to the outcomes recipe,\n * then returns the recipe descriptor.\n * @param {Object|String} outcome\n * @returns {Object}\n */\n function getOutcomesRecipe(outcome) {\n var identifier = outcomeHelper.getOutcomeIdentifier(outcome);\n var mode = null;\n _.forEach(outcomesRecipes, function (processingRecipe) {\n if (belongToRecipe(identifier, processingRecipe)) {\n mode = processingRecipe;\n return false;\n }\n });\n return mode;\n }\n\n /**\n * Gets the score processing modes from a list of outcomes\n * @param {Array} outcomes\n * @returns {Array}\n */\n function listScoringModes(outcomes) {\n var modes = {};\n _.forEach(outcomes, function (outcome) {\n var recipe = getOutcomesRecipe(outcome);\n if (recipe) {\n modes[recipe.key] = true;\n }\n });\n return _.keys(modes);\n }\n\n /**\n * Checks whether the categories have to be taken into account\n * @param {modelOverseer} modelOverseer\n * @returns {Boolean}\n */\n function handleCategories(modelOverseer) {\n var model = modelOverseer.getModel();\n return !!(model.scoring && model.scoring.categoryScore);\n }\n\n /**\n * Checks if the test model contains outcomes for categories\n * @param {Object} model\n * @returns {Boolean}\n */\n function hasCategoryOutcome(model) {\n var categoryOutcomes = false;\n _.forEach(outcomeHelper.getOutcomeDeclarations(model), function (outcomeDeclaration) {\n var identifier = outcomeHelper.getOutcomeIdentifier(outcomeDeclaration);\n _.forEach(outcomesRecipes, function (processingRecipe) {\n if (belongToRecipe(identifier, processingRecipe, true)) {\n categoryOutcomes = true;\n return false;\n }\n });\n });\n return categoryOutcomes;\n }\n\n /**\n * Gets the defined cut score from the outcome rules\n * @param {Object} model\n * @returns {Number}\n */\n function getCutScore(model) {\n var values = _(outcomeHelper.getOutcomeProcessingRules(model))\n .map(function (outcome) {\n return outcomeHelper.getProcessingRuleProperty(outcome, 'setOutcomeValue.gte.baseValue.value');\n })\n .compact()\n .uniq()\n .value();\n if (_.isEmpty(values)) {\n values = [defaultCutScore];\n }\n return Math.max(0, _.max(values));\n }\n\n /**\n * Gets the defined weight identifier from the outcome rules\n * @param {Object} model\n * @returns {String}\n */\n function getWeightIdentifier(model) {\n var values = [];\n outcomeHelper.eachOutcomeProcessingRuleExpressions(model, function (processingRule) {\n if (processingRule['qti-type'] === 'testVariables' && processingRule.weightIdentifier) {\n values.push(processingRule.weightIdentifier);\n }\n });\n values = _(values).compact().uniq().value();\n\n return values.length ? values[0] : '';\n }\n\n /**\n * Detects the outcome processing mode for the scoring\n * @param {modelOverseer} modelOverseer\n * @returns {String}\n */\n function getOutcomeProcessing(modelOverseer) {\n var model = modelOverseer.getModel();\n var outcomeDeclarations = outcomeHelper.getOutcomeDeclarations(model);\n var outcomeRules = outcomeHelper.getOutcomeProcessingRules(model);\n\n // walk through each outcome declaration, and tries to identify the score processing mode\n var declarations = listScoringModes(outcomeDeclarations);\n var processing = listScoringModes(outcomeRules);\n var diff = _.difference(declarations, processing);\n var count = _.size(declarations);\n var included;\n\n // default fallback, applied when several modes are detected at the same time\n var outcomeProcessing = 'custom';\n\n // set the score processing mode with respect to the found outcomes\n if (count === _.size(processing)) {\n if (!count) {\n // no mode detected, set the mode to none\n outcomeProcessing = 'none';\n } else if (_.isEmpty(diff)) {\n if (count > 1) {\n // several modes detected, try to reduce the list by detecting includes\n included = [];\n _.forEach(declarations, function (mode) {\n if (outcomesRecipes[mode] && outcomesRecipes[mode].include) {\n included.push(outcomesRecipes[mode].include);\n }\n });\n processing = _.difference(processing, included);\n count = _.size(processing);\n }\n\n if (count === 1) {\n // single mode detected, keep the last got key\n outcomeProcessing = processing[0];\n\n // check if all outcomes are strictly related to the detected mode\n if (\n !matchRecipe(\n outcomesRecipes[outcomeProcessing],\n modelOverseer.getOutcomesNames(),\n modelOverseer.getCategories()\n )\n ) {\n outcomeProcessing = 'custom';\n }\n }\n }\n }\n\n return outcomeProcessing;\n }\n\n /**\n * Detects the score processing mode and builds the descriptor used to manage the UI.\n * @param {modelOverseer} modelOverseer\n * @returns {Object}\n */\n function detectScoring(modelOverseer) {\n var model = modelOverseer.getModel();\n let modes = processingModes;\n if(!features.isVisible('taoQtiTest/creator/test/property/scoring/custom')) {\n delete modes.custom;\n }\n return {\n modes: processingModes,\n scoreIdentifier: defaultScoreIdentifier,\n weightIdentifier: getWeightIdentifier(model),\n cutScore: getCutScore(model),\n categoryScore: hasCategoryOutcome(model),\n outcomeProcessing: getOutcomeProcessing(modelOverseer)\n };\n }\n\n /**\n * Removes all scoring outcomes\n * @param {Object} model\n */\n function removeScoring(model) {\n var scoringOutcomes = _.keyBy(outcomeHelper.listOutcomes(model, getOutcomesRecipe), function (outcome) {\n return outcome;\n });\n\n outcomeHelper.removeOutcomes(model, function (outcome) {\n var match = false;\n\n function browseExpressions(processingRule) {\n if (_.isArray(processingRule)) {\n _.forEach(processingRule, browseExpressions);\n } else if (processingRule) {\n if (scoringOutcomes[outcomeHelper.getOutcomeIdentifier(processingRule)]) {\n match = true;\n }\n\n if (!match && processingRule.expression) {\n browseExpressions(processingRule.expression);\n }\n if (!match && processingRule.expressions) {\n browseExpressions(processingRule.expressions);\n }\n if (!match && processingRule.outcomeRules) {\n browseExpressions(processingRule.outcomeRules);\n }\n if (!match && processingRule.outcomeIf) {\n browseExpressions(processingRule.outcomeIf);\n }\n if (!match && processingRule.outcomeElse) {\n browseExpressions(processingRule.outcomeElse);\n }\n }\n }\n\n if (outcome['qti-type'] === 'outcomeCondition') {\n browseExpressions(outcome);\n } else {\n match = !!scoringOutcomes[outcomeHelper.getOutcomeIdentifier(outcome)];\n }\n return match;\n });\n }\n\n /**\n * Gets a copy of the list of outcomes, provides the same structure as the model\n * @param {Object} model\n * @returns {Object}\n */\n function getOutcomes(model) {\n return {\n outcomeDeclarations: [].concat(outcomeHelper.getOutcomeDeclarations(model)),\n outcomeProcessing: {\n outcomeRules: [].concat(outcomeHelper.getOutcomeProcessingRules(model))\n }\n };\n }\n\n return scoringHelper;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2020 Open Assessment Technologies SA ;\n */\n/**\n * Track the change within the testCreator\n * @author Juan Luis Gutierrez Dos Santos \n */\ndefine('taoQtiTest/controller/creator/helpers/changeTracker',[\n 'jquery',\n 'lodash',\n 'i18n',\n 'lib/uuid',\n 'core/eventifier',\n 'ui/dialog',\n 'ui/feedback',\n], function (\n $,\n _,\n __,\n uuid,\n eventifier,\n dialog,\n feedback\n) {\n 'use strict';\n\n /**\n * The messages asking to save\n */\n const messages = {\n preview: __('The test needs to be saved before it can be previewed.'),\n leave: __('The test has unsaved changes, are you sure you want to leave?'),\n exit: __('The test has unsaved changes, would you like to save it?'),\n leaveWhenInvalid: __('If you leave the test, your changes will not be saved due to invalid test settings. Are you sure you wish to leave?')\n };\n const buttonsYesNo = [{\n id: 'dontsave',\n type: 'regular',\n label: __('YES'),\n close: true\n }, {\n id: 'cancel',\n type: 'regular',\n label: __('NO'),\n close: true\n }];\n const buttonsCancelSave = [{\n id: 'dontsave',\n type: 'regular',\n label: __('Don\\'t save'),\n close: true\n }, {\n id: 'cancel',\n type: 'regular',\n label: __('Cancel'),\n close: true\n }, {\n id: 'save',\n type: 'info',\n label: __('Save'),\n close: true\n }];\n\n /**\n *\n * @param {HTMLElement} container\n * @param {testCreator} testCreator\n * @param {String} [wrapperSelector]\n * @returns {changeTracker}\n */\n function changeTrackerFactory(container, testCreator, wrapperSelector = 'body') {\n\n // internal namespace for global registered events\n const eventNS = `.ct-${uuid(8, 16)}`;\n\n // keep the value of the test before changes\n let originalTest;\n\n // are we in the middle of the confirm process?\n let asking = false;\n\n /**\n * @typedef {Object} changeTracker\n */\n const changeTracker = eventifier({\n /**\n * Initialized the changed state\n * @returns {changeTracker}\n */\n init() {\n originalTest = this.getSerializedTest();\n\n return this;\n },\n\n /**\n * Installs the change tracker, registers listeners\n * @returns {changeTracker}\n */\n install() {\n this.init();\n asking = false;\n\n // add a browser popup to prevent leaving the browser\n $(window)\n .on(`beforeunload${eventNS}`, () => {\n if (!asking && this.hasChanged()) {\n return messages.leave;\n }\n })\n // since we don't know how to prevent history based events, we just stop the handling\n .on('popstate', this.uninstall);\n\n // every click outside the authoring\n $(wrapperSelector)\n .on(`click${eventNS}`, e => {\n if (!$.contains(container, e.target) && this.hasChanged() && e.target.classList[0] !== 'icon-close') {\n e.stopImmediatePropagation();\n e.preventDefault();\n\n if (testCreator.isTestHasErrors()) {\n this.confirmBefore('leaveWhenInvalid')\n .then(whatToDo => {\n this.ifWantSave(whatToDo);\n this.uninstall();\n e.target.click();\n })\n .catch(() => {});\n } else {\n this.confirmBefore('exit')\n .then(whatToDo => {\n this.ifWantSave(whatToDo);\n this.uninstall();\n e.target.click();\n })\n //do nothing but prevent uncaught error\n .catch(() => {});\n }\n }\n });\n\n testCreator\n .on(`ready${eventNS} saved${eventNS}`, () => this.init())\n .before(`creatorclose${eventNS}`, () => {\n let leavingStatus = 'leave';\n if(testCreator.isTestHasErrors()) {\n leavingStatus ='leaveWhenInvalid';\n }\n return this.confirmBefore(leavingStatus).then(whatToDo => {\n this.ifWantSave(whatToDo);\n })})\n .before(`preview${eventNS}`, () => this.confirmBefore('preview').then(whatToDo => {\n if(testCreator.isTestHasErrors()){\n feedback().warning(`${__('The test cannot be saved because it currently contains invalid settings.\\n' +\n 'Please fix the invalid settings and try again.')}`);\n } else {\n this.ifWantSave(whatToDo);\n }\n }))\n .before(`exit${eventNS}`, () => this.uninstall());\n\n return this;\n },\n\n /**\n * Check if we need to trigger save\n * @param {Object} whatToDo\n * @fires {save}\n */\n ifWantSave(whatToDo) {\n if (whatToDo && whatToDo.ifWantSave) {\n testCreator.trigger('save');\n }\n },\n\n\n /**\n * Uninstalls the change tracker, unregisters listeners\n * @returns {changeTracker}\n */\n uninstall() {\n // remove all global handlers\n $(window.document)\n .off(eventNS);\n $(window).off(eventNS);\n $(wrapperSelector).off(eventNS);\n testCreator.off(eventNS);\n\n return this;\n },\n\n /**\n * Displays a confirmation dialog,\n * The \"ok\" button will save and resolve.\n * The \"cancel\" button will reject.\n *\n * @param {String} message - the confirm message to display\n * @returns {Promise} resolves once saved\n */\n confirmBefore(message) {\n // if a key is given load the related message\n message = messages[message] || message;\n\n return new Promise((resolve, reject) => {\n if (asking) {\n return reject();\n }\n\n if (!this.hasChanged()) {\n return resolve();\n }\n\n asking = true;\n let confirmDlg;\n\n // chosses what buttons to display depending on the message\n if(message === messages.leaveWhenInvalid) {\n confirmDlg = dialog({\n message: message,\n buttons: buttonsYesNo,\n autoRender: true,\n autoDestroy: true,\n onDontsaveBtn: () => resolve({ ifWantSave: false }),\n onCancelBtn: () => {\n confirmDlg.hide();\n reject({ cancel: true });\n }\n });\n } else {\n confirmDlg = dialog({\n message: message,\n buttons: buttonsCancelSave,\n autoRender: true,\n autoDestroy: true,\n onSaveBtn: () => resolve({ ifWantSave: true }),\n onDontsaveBtn: () => resolve({ ifWantSave: false }),\n onCancelBtn: () => {\n confirmDlg.hide();\n reject({ cancel: true });\n }\n });\n }\n confirmDlg.on('closed.modal', () => asking = false);\n });\n },\n\n /**\n * Does the test have changed?\n * @returns {Boolean}\n */\n hasChanged() {\n const currentTest = this.getSerializedTest();\n return originalTest !== currentTest || (null === currentTest && null === originalTest);\n },\n\n /**\n * Get a string representation of the current test, used for comparison\n * @returns {String} the test\n */\n getSerializedTest() {\n let serialized = '';\n try {\n // create a string from the test content\n serialized = JSON.stringify(testCreator.getModelOverseer().getModel());\n\n // sometimes the creator strip spaces between tags, so we do the same\n serialized = serialized.replace(/ {2,}/g, ' ');\n } catch (err) {\n serialized = null;\n }\n return serialized;\n }\n });\n\n return changeTracker.install();\n }\n\n return changeTrackerFactory;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2014-2024 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\n/**\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/creator/creator',[\n 'module',\n 'jquery',\n 'lodash',\n 'helpers',\n 'i18n',\n 'services/features',\n 'ui/feedback',\n 'core/databindcontroller',\n 'taoQtiTest/controller/creator/qtiTestCreator',\n 'taoQtiTest/controller/creator/views/item',\n 'taoQtiTest/controller/creator/views/test',\n 'taoQtiTest/controller/creator/views/testpart',\n 'taoQtiTest/controller/creator/views/section',\n 'taoQtiTest/controller/creator/views/itemref',\n 'taoQtiTest/controller/creator/encoders/dom2qti',\n 'taoQtiTest/controller/creator/templates/index',\n 'taoQtiTest/controller/creator/helpers/qtiTest',\n 'taoQtiTest/controller/creator/helpers/scoring',\n 'taoQtiTest/controller/creator/helpers/categorySelector',\n 'taoQtiTest/controller/creator/helpers/validators',\n 'taoQtiTest/controller/creator/helpers/changeTracker',\n 'taoQtiTest/controller/creator/helpers/featureVisibility',\n 'taoTests/previewer/factory',\n 'core/logger',\n 'taoQtiTest/controller/creator/views/subsection'\n], function (\n module,\n $,\n _,\n helpers,\n __,\n features,\n feedback,\n DataBindController,\n qtiTestCreatorFactory,\n itemView,\n testView,\n testPartView,\n sectionView,\n itemrefView,\n Dom2QtiEncoder,\n templates,\n qtiTestHelper,\n scoringHelper,\n categorySelector,\n validators,\n changeTracker,\n featureVisibility,\n previewerFactory,\n loggerFactory,\n subsectionView\n) {\n ('use strict');\n const logger = loggerFactory('taoQtiTest/controller/creator');\n\n /**\n * We assume the ClientLibConfigRegistry is filled up with something like this:\n * 'taoQtiTest/controller/creator/creator' => [\n * 'provider' => 'qtiTest',\n * ],\n *\n * Or, with something like this for allowing multiple buttons in case of several providers are available:\n * 'taoQtiTest/controller/creator/creator' => [\n * 'provider' => 'qtiTest',\n * 'providers' => [\n * ['id' => 'qtiTest', 'label' => 'Preview'],\n * ['id' => 'xxxx', 'label' => 'xxxx'],\n * ...\n * ],\n * ],\n */\n\n /**\n * The test creator controller is the main entry point\n * and orchestrates data retrieval and view/components loading.\n * @exports creator/controller\n */\n const Controller = {\n routes: {},\n\n /**\n * Start the controller, main entry method.\n * @public\n * @param {Object} options\n * @param {Object} options.labels - the list of item's labels to give to the ItemView\n * @param {Object} options.routes - action's urls\n * @param {Object} options.categoriesPresets - predefined category that can be set at the item or section level\n * @param {Boolean} [options.guidedNavigation=false] - feature flag for the guided navigation\n */\n start(options) {\n const $container = $('#test-creator');\n const $saver = $('#saver');\n const $menu = $('ul.test-editor-menu');\n const $back = $('#authoringBack');\n\n let creatorContext;\n let binder;\n let binderOptions;\n let modelOverseer;\n\n this.identifiers = [];\n\n options = _.merge(module.config(), options || {});\n options.routes = options.routes || {};\n options.labels = options.labels || {};\n options.categoriesPresets = featureVisibility.filterVisiblePresets(options.categoriesPresets) || {};\n options.guidedNavigation = options.guidedNavigation === true;\n\n categorySelector.setPresets(options.categoriesPresets);\n\n //back button\n $back.on('click', e => {\n e.preventDefault();\n creatorContext.trigger('creatorclose');\n });\n\n //preview button\n let previewId = 0;\n const createPreviewButton = ({ id, label } = {}) => {\n // configured labels will need to to be registered elsewhere for the translations\n const translate = text => text && __(text);\n\n const btnIdx = previewId ? `-${previewId}` : '';\n const $button = $(\n templates.menuButton({\n id: `previewer${btnIdx}`,\n testId: `preview-test${btnIdx}`,\n icon: 'preview',\n label: translate(label) || __('Preview')\n })\n ).on('click', e => {\n e.preventDefault();\n if (!$(e.currentTarget).hasClass('disabled')) {\n creatorContext.trigger('preview', id, previewId);\n }\n });\n if (!Object.keys(options.labels).length) {\n $button.attr('disabled', true).addClass('disabled');\n }\n $menu.append($button);\n previewId++;\n return $button;\n };\n const previewButtons = options.providers\n ? options.providers.map(createPreviewButton)\n : [createPreviewButton()];\n\n const isTestContainsItems = () => {\n if ($container.find('.test-content').find('.itemref').length) {\n previewButtons.forEach($previewer => $previewer.attr('disabled', false).removeClass('disabled'));\n return true;\n } else {\n previewButtons.forEach($previewer => $previewer.attr('disabled', true).addClass('disabled'));\n return false;\n }\n };\n\n //set up the ItemView, give it a configured loadItems ref\n itemView($('.test-creator-items .item-selection', $container));\n\n // forwards some binder events to the model overseer\n $container.on('change.binder delete.binder', (e, model) => {\n if (e.namespace === 'binder' && model && modelOverseer) {\n modelOverseer.trigger(e.type, model);\n }\n });\n\n //Data Binding options\n binderOptions = _.merge(options.routes, {\n filters: {\n isItemRef: value => qtiTestHelper.filterQtiType(value, 'assessmentItemRef'),\n isSection: value => qtiTestHelper.filterQtiType(value, 'assessmentSection')\n },\n encoders: {\n dom2qti: Dom2QtiEncoder\n },\n templates: templates,\n beforeSave(model) {\n //ensure the qti-type is present\n qtiTestHelper.addMissingQtiType(model);\n\n //apply consolidation rules\n qtiTestHelper.consolidateModel(model);\n\n //validate the model\n try {\n validators.validateModel(model);\n } catch (err) {\n $saver.attr('disabled', false).removeClass('disabled');\n feedback().error(`${__('The test has not been saved.')} + ${err}`);\n return false;\n }\n return true;\n }\n });\n\n //set up the databinder\n binder = DataBindController.takeControl($container, binderOptions).get(model => {\n creatorContext = qtiTestCreatorFactory($container, {\n uri: options.uri,\n labels: options.labels,\n routes: options.routes,\n guidedNavigation: options.guidedNavigation\n });\n creatorContext.setTestModel(model);\n modelOverseer = creatorContext.getModelOverseer();\n\n //detect the scoring mode\n scoringHelper.init(modelOverseer);\n\n //register validators\n validators.registerValidators(modelOverseer);\n\n //once model is loaded, we set up the test view\n testView(creatorContext);\n\n //listen for changes to update available actions\n testPartView.listenActionState();\n sectionView.listenActionState();\n subsectionView.listenActionState();\n itemrefView.listenActionState();\n\n changeTracker($container.get()[0], creatorContext, '.content-wrap');\n\n creatorContext.on('save', function () {\n if (!$saver.hasClass('disabled')) {\n $saver.prop('disabled', true).addClass('disabled');\n binder.save(\n function () {\n $saver.prop('disabled', false).removeClass('disabled');\n feedback().success(__('Test Saved'));\n isTestContainsItems();\n creatorContext.trigger('saved');\n },\n function () {\n $saver.prop('disabled', false).removeClass('disabled');\n }\n );\n }\n });\n\n creatorContext.on('preview', provider => {\n if (isTestContainsItems() && !creatorContext.isTestHasErrors()) {\n const saveUrl = options.routes.save;\n const testUri = saveUrl.slice(saveUrl.indexOf('uri=') + 4);\n const config = module.config();\n const type = provider || config.provider || 'qtiTest';\n return previewerFactory(type, decodeURIComponent(testUri), {\n readOnly: false,\n fullPage: true,\n pluginsOptions: config.pluginsOptions\n }).catch(err => {\n logger.error(err);\n feedback().error(\n __('Test Preview is not installed, please contact to your administrator.')\n );\n });\n }\n });\n\n creatorContext.on('creatorclose', () => {\n creatorContext.trigger('exit');\n window.history.back();\n });\n });\n\n //the save button triggers binder's save action.\n $saver.on('click', function (event) {\n if (creatorContext.isTestHasErrors()) {\n event.preventDefault();\n feedback().warning(\n __(\n 'The test cannot be saved because it currently contains invalid settings.\\nPlease fix the invalid settings and try again.'\n )\n );\n } else {\n creatorContext.trigger('save');\n }\n });\n }\n };\n\n return Controller;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2015 (original work) Open Assessment Technologies SA ;\n */\n/**\n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/controller/creator/helpers/ckConfigurator',['ui/ckeditor/ckConfigurator', 'mathJax'], function(ckConfigurator) {\n 'use strict';\n\n /**\n * Generate a configuration object for CKEDITOR\n *\n * @param editor instance of ckeditor\n * @param toolbarType block | inline | flow | qtiBlock | qtiInline | qtiFlow | reset to get back to normal\n * @param {Object} [options] - is based on the CKEDITOR config object with some additional sugar\n * Note that it's here you need to add parameters for the resource manager.\n * Some options are not covered in http://docs.ckeditor.com/#!/api/CKEDITOR.config\n * @param [options.dtdOverrides] - @see dtdOverrides which pre-defines them\n * @param {Object} [options.positionedPlugins] - @see ckConfig.positionedPlugins\n * @param {Boolean} [options.qtiImage] - enables the qtiImage plugin\n * @param {Boolean} [options.qtiInclude] - enables the qtiInclude plugin\n * @param {Boolean} [options.underline] - enables the underline plugin\n * @param {Boolean} [options.mathJax] - enables the mathJax plugin\n *\n * @see http://docs.ckeditor.com/#!/api/CKEDITOR.config\n */\n var getConfig = function(editor, toolbarType, options){\n options = options || {};\n\n options.underline = true;\n\n return ckConfigurator.getConfig(editor, toolbarType, options);\n };\n\n return {\n getConfig : getConfig\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2014 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n *\n *\n */\n\n//@see http://forge.taotesting.com/projects/tao/wiki/Front_js\ndefine('taoQtiTest/controller/routes',[],function () {\n 'use strict';\n\n return {\n Creator: {\n css: 'creator',\n actions: {\n index: 'controller/creator/creator'\n }\n },\n XmlEditor: {\n actions: {\n edit: 'controller/content/edit'\n }\n }\n };\n});\n\n","\ndefine('css!taoQtiTestCss/new-test-runner',[],function(){});\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2016-2019 (original work) Open Assessment Technologies SA ;\n */\n\n/**\n * Test runner controller entry\n *\n * @author Bertrand Chevrier \n */\ndefine('taoQtiTest/controller/runner/runner',[\n 'jquery',\n 'lodash',\n 'i18n',\n 'context',\n 'module',\n 'core/router',\n 'core/logger',\n 'layout/loading-bar',\n 'ui/feedback',\n 'util/url',\n 'util/locale',\n 'taoTests/runner/providerLoader',\n 'taoTests/runner/runner',\n 'css!taoQtiTestCss/new-test-runner'\n], function (\n $,\n _,\n __,\n context,\n module,\n router,\n loggerFactory,\n loadingBar,\n feedback,\n urlUtil,\n locale,\n providerLoader,\n runner\n) {\n 'use strict';\n\n /**\n * List of options required by the controller\n * @type {String[]}\n */\n const requiredOptions = [\n 'testDefinition',\n 'testCompilation',\n 'serviceCallId',\n 'bootstrap',\n 'options',\n 'providers'\n ];\n\n /**\n * The runner controller\n */\n return {\n\n /**\n * Controller entry point\n *\n * @param {Object} config - the testRunner config\n * @param {String} config.testDefinition - the test definition id\n * @param {String} config.testCompilation - the test compilation id\n * @param {String} config.serviceCallId - the service call id\n * @param {Object} config.bootstrap - contains the extension and the controller to call\n * @param {Object} config.options - the full URL where to return at the final end of the test\n * @param {Object[]} config.providers - the collection of providers to load\n */\n start(config) {\n let exitReason;\n const $container = $('.runner');\n\n const logger = loggerFactory('controller/runner', {\n serviceCallId : config.serviceCallId,\n plugins : config && config.providers && Object.keys(config.providers.plugins)\n });\n\n let preventFeedback = false;\n let errorFeedback = null;\n\n /**\n * Exit the test runner using the configured exitUrl\n * @param {String} [reason] - to add a warning once left\n * @param {String} [level] - error level\n */\n const exit = function exit(reason, level){\n let url = config.options.exitUrl;\n const params = {};\n if (reason) {\n if (!level) {\n level = 'warning';\n }\n params[level] = reason;\n url = urlUtil.build(url, params);\n }\n window.location = url;\n };\n\n /**\n * Handles errors\n * @param {Error} err - the thrown error\n * @param {String} [displayMessage] - an alternate message to display\n */\n const onError = function onError(err, displayMessage) {\n onFeedback(err, displayMessage, \"error\");\n };\n\n /**\n * Handles warnings\n * @param {Error} err - the thrown error\n * @param {String} [displayMessage] - an alternate message to display\n */\n const onWarning = function onWarning(err, displayMessage) {\n onFeedback(err, displayMessage, \"warning\");\n };\n\n /**\n * Handles errors & warnings\n * @param {Error} err - the thrown error\n * @param {String} [displayMessage] - an alternate message to display\n * @param {String} [type] - \"error\" or \"warning\"\n */\n const onFeedback = function onFeedback(err, displayMessage, type) {\n const typeMap = {\n warning: {\n logger: \"warn\",\n feedback: \"warning\"\n },\n error: {\n logger: \"error\",\n feedback: \"error\"\n }\n };\n const loggerByType = logger[typeMap[type].logger];\n const feedbackByType = feedback()[typeMap[type].feedback];\n\n displayMessage = displayMessage || err.message;\n\n if(!_.isString(displayMessage)){\n displayMessage = JSON.stringify(displayMessage);\n }\n loadingBar.stop();\n\n loggerByType({ displayMessage : displayMessage }, err);\n\n if(type === \"error\" && (err.code === 403 || err.code === 500)) {\n displayMessage = `${__('An error occurred during the test, please content your administrator.')} ${displayMessage}`;\n return exit(displayMessage, 'error');\n }\n if (!preventFeedback) {\n errorFeedback = feedbackByType(displayMessage, {timeout: -1});\n }\n };\n\n const moduleConfig = module.config();\n\n loadingBar.start();\n\n // adding attr for RTL languages\n $('.delivery-scope').attr({dir: locale.getLanguageDirection(context.locale)});\n\n // verify required config\n if ( ! requiredOptions.every( option => typeof config[option] !== 'undefined') ) {\n return onError(new TypeError(__('Missing required configuration option %s', name)));\n }\n\n // dispatch any extra registered routes\n if (moduleConfig && _.isArray(moduleConfig.extraRoutes) && moduleConfig.extraRoutes.length) {\n router.dispatch(moduleConfig.extraRoutes);\n }\n\n //for the qti provider to be selected here\n config.provider = Object.assign( config.provider || {}, { runner: 'qti' });\n\n //load the plugins and the proxy provider\n providerLoader(config.providers, context.bundle)\n .then(function (results) {\n\n const testRunnerConfig = _.omit(config, ['providers']);\n testRunnerConfig.renderTo = $container;\n\n if (results.proxy && typeof results.proxy.getAvailableProviders === 'function') {\n const loadedProxies = results.proxy.getAvailableProviders();\n testRunnerConfig.provider.proxy = loadedProxies[0];\n }\n\n logger.debug({\n config: testRunnerConfig,\n providers : config.providers\n }, 'Start test runner');\n\n //instantiate the QtiTestRunner\n runner(config.provider.runner, results.plugins, testRunnerConfig)\n .on('error', onError)\n .on('warning', onWarning)\n .on('ready', function () {\n _.defer(function () {\n $container.removeClass('hidden');\n });\n })\n .on('pause', function(data) {\n if (data && data.reason) {\n exitReason = data.reason;\n }\n })\n .after('destroy', function () {\n this.removeAllListeners();\n\n // at the end, we are redirected to the exit URL\n exit(exitReason);\n })\n\n //FIXME this event should not be triggered on the test runner\n .on('disablefeedbackalerts', function() {\n if (errorFeedback) {\n errorFeedback.close();\n }\n preventFeedback = true;\n })\n\n //FIXME this event should not be triggered on the test runner\n .on('enablefeedbackalerts', function() {\n preventFeedback = false;\n })\n .init();\n })\n .catch(function(err){\n onError(err, __('An error occurred during the test initialization!'));\n });\n }\n };\n});\n\n","/*\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2015 (original work) Open Assessment Technologies SA ;\n *\n */\n\n/**\n * This module allows adding extra buttons in the action bar of the test runner\n *\n */\ndefine('taoQtiTest/testRunner/actionBarHook',[\n 'jquery',\n 'lodash',\n 'core/errorHandler',\n 'core/promise'\n], function ($, _, errorHandler, Promise) {\n\n 'use strict';\n\n /**\n * Events namespace\n * @type {String}\n * @private\n */\n var _ns = '.actionBarHook';\n\n /**\n * We need to access the root document to listen for some events\n * @type {jQuery}\n * @private\n */\n var $doc = $(document);\n\n /**\n * List of loaded and visible hooks\n * @type {Object}\n * @private\n */\n var tools = {};\n\n /**\n * Flag set to true when the item is loaded\n * @type {Boolean}\n * @private\n */\n var itemIsLoaded = false;\n\n // catch the item loaded event\n $doc.off(_ns).on('serviceloaded' + _ns, function() {\n itemIsLoaded = true;\n _.forEach(tools, function(tool) {\n triggerItemLoaded(tool);\n });\n });\n\n /**\n * Check that the toolConfig is correct\n *\n * @param {Object} toolconfig\n * @param {String} toolconfig.hook - the amd module to be loaded to initialize the button\n * @param {String} [toolconfig.label] - the label to be displayed in the button\n * @param {String} [toolconfig.icon] - the icon to be displayed in the button\n * @param {String} [toolconfig.title] - the title to be displayed in the button\n * @param {Array} [toolconfig.items] - an optional list of menu items\n * @returns {Boolean}\n */\n function isValidConfig(toolconfig) {\n return !!(_.isObject(toolconfig) && toolconfig.hook);\n }\n\n /**\n * Triggers the itemLoaded event inside the provided actionBar hook\n * @param {Object} tool\n */\n function triggerItemLoaded(tool) {\n if (tool && tool.itemLoaded) {\n tool.itemLoaded();\n }\n }\n\n /**\n * Init a test runner button from its config\n *\n * @param {String} id\n * @param {Object|String} toolconfig\n * @param {String} toolconfig.hook - the amd module to be loaded to initialize the button\n * @param {String} [toolconfig.label] - the label to be displayed in the button\n * @param {String} [toolconfig.icon] - the icon to be displayed in the button\n * @param {String} [toolconfig.title] - the title to be displayed in the button\n * @param {Array} [toolconfig.items] - an optional list of menu items\n * @param {Object} testContext - the complete state of the test\n * @param {Object} testRunner - the test runner instance\n * @fires ready.actionBarHook when the hook has been initialized\n * @returns {Promise}\n */\n function initQtiTool($toolsContainer, id, toolconfig, testContext, testRunner) {\n\n // the tool is always initialized before the item is loaded, so we can safely false the flag\n itemIsLoaded = false;\n tools[id] = null;\n\n if (_.isString(toolconfig)) {\n toolconfig = {\n hook: toolconfig\n };\n }\n\n return new Promise(function(resolve) {\n if (isValidConfig(toolconfig)) {\n\n require([toolconfig.hook], function (hook) {\n\n var $button;\n var $existingBtn;\n\n if (isValidHook(hook)) {\n //init the control\n hook.init(id, toolconfig, testContext, testRunner);\n\n //if an instance of the tool is already attached, remove it:\n $existingBtn = $toolsContainer.children('[data-control=\"' + id + '\"]');\n if ($existingBtn.length) {\n hook.clear($existingBtn);\n $existingBtn.remove();\n }\n\n //check if the tool is to be available\n if (hook.isVisible()) {\n //keep access to the tool\n tools[id] = hook;\n\n // renders the button from the config\n $button = hook.render();\n\n //only attach the button to the dom when everything is ready\n _appendInOrder($toolsContainer, $button);\n\n //ready !\n $button.trigger('ready' + _ns, [hook]);\n\n //fires the itemLoaded event if the item has already been loaded\n if (itemIsLoaded) {\n triggerItemLoaded(hook);\n }\n }\n\n resolve(hook);\n } else {\n errorHandler.throw(_ns, 'invalid hook format');\n resolve(null);\n }\n\n }, function (e) {\n errorHandler.throw(_ns, 'the hook amd module cannot be found');\n resolve(null);\n });\n\n } else {\n errorHandler.throw(_ns, 'invalid tool config format');\n resolve(null);\n }\n });\n }\n\n /**\n * Append a dom element $button to a $container in a specific order\n * The orders are provided by data-order attribute set to the $button\n *\n * @param {JQuery} $container\n * @param {JQuery} $button\n */\n function _appendInOrder($container, $button) {\n\n var $after, $before;\n var order = $button.data('order');\n\n if ('last' === order) {\n\n $container.append($button);\n\n } else if ('first' === order) {\n\n $container.prepend($button);\n\n } else {\n\n order = _.parseInt(order);\n if (!_.isNaN(order)) {\n\n $container.children('.action').each(function () {\n\n var $btn = $(this),\n _order = $btn.data('order');\n\n if ('last' === _order) {\n\n $before = $btn;\n $after = null;\n\n } else if ('first' === _order) {\n\n $before = null;\n $after = $btn;\n\n } else {\n\n _order = _.parseInt(_order);\n\n if (_.isNaN(_order) || _order > order) {\n $before = $btn;\n $after = null;\n //stops here because $container children returns the dom elements in the dom order\n return false;\n } else if (_order === order) {\n $after = $btn;\n } else if (_order < order) {\n $after = $btn;\n $before = null;\n }\n\n }\n\n });\n\n if ($after) {\n $after.after($button);\n } else if ($before) {\n $before.before($button);\n } else {\n $container.append($button);\n }\n\n } else {\n //unordered buttons are append at the end (including when order equals 0)\n $container.append($button);\n }\n }\n }\n\n /**\n * Check if the hook object is valid\n *\n * @param {Object} hook\n * @param {Function} hook.init\n * @param {Function} hook.clear\n * @param {Function} hook.isVisible\n * @returns {Boolean}\n */\n function isValidHook(hook) {\n return (_.isObject(hook) && _(['init', 'render', 'clear', 'isVisible']).reduce(function (result, method) {\n return result && _.isFunction(hook[method]);\n }, true));\n }\n\n return {\n isValid: isValidConfig,\n initQtiTool: initQtiTool\n };\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2015 (original work) Open Assessment Technologies SA ;\n */\n/**\n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/testRunner/actionBarTools',[\n 'jquery',\n 'lodash',\n 'core/eventifier',\n 'core/promise',\n 'taoQtiTest/testRunner/actionBarHook'\n], function ($, _, eventifier, Promise, actionBarHook) {\n 'use strict';\n\n /**\n * The list of registered actionBar tools\n * @type {Object}\n */\n var registeredQtiTools;\n\n /**\n * The list of actionBar tools instances\n * @type {Object}\n */\n var qtiTools;\n\n /**\n * Manages the actionBar tools\n * @type {Object}\n */\n var actionBarTools = {\n /**\n * Registers the actionBar tools\n * @param {Object} tools\n */\n register : function register(tools) {\n var registerTools = tools || {};\n\n /**\n * @event actionBarTools#beforeregister\n * @param {Object} tools\n * @param {actionBarTools} this\n */\n this.trigger('beforeregister', registerTools, this);\n\n registeredQtiTools = registerTools;\n\n /**\n * @event actionBarTools#afterregister\n * @param {Object} tools\n * @param {actionBarTools} this\n */\n this.trigger('afterregister', registerTools, this);\n },\n\n /**\n * Gets the list of registered tools\n * @returns {Object}\n */\n getRegisteredTools : function getRegisteredTools() {\n return registeredQtiTools || {};\n },\n\n /**\n * Gets a particular tool config\n * @param {String} id\n * @returns {Object}\n */\n getRegistered : function getRegistered(id) {\n return registeredQtiTools && registeredQtiTools[id];\n },\n\n /**\n * Checks if a particular tool is registered\n * @param {String} id\n * @returns {Boolean}\n */\n isRegistered : function isRegistered(id) {\n return !!(registeredQtiTools && registeredQtiTools[id]);\n },\n\n /**\n * Gets a particular tool\n * @param {String} id\n * @returns {Object}\n */\n get : function get(id) {\n return qtiTools && qtiTools[id];\n },\n\n /**\n * Gets the list of tools instances\n * @returns {Array}\n */\n list : function list() {\n return _.values(qtiTools || {});\n },\n\n /**\n * Renders the actionBar\n * @param {String|jQuery|HTMLElement} container - The container in which renders the tools\n * @param {Object} testContext - The assessment test context\n * @param {Object} testRunner - The assessment test runner\n * @param {Function} [callback] - An optional callback fired when all tools have been rendered\n */\n render : function render(container, testContext, testRunner, callback) {\n var self = this;\n var $container = $(container);\n var promises = [];\n\n /**\n * @event actionBarTools#beforerender\n * @param {jQuery} $container\n * @param {Object} testContext\n * @param {Object} testRunner\n * @param {actionBarTools} this\n */\n this.trigger('beforerender', $container, testContext, testRunner, this);\n\n _.forIn(this.getRegisteredTools(), function(toolconfig, id){\n promises.push(actionBarHook.initQtiTool($container, id, toolconfig, testContext, testRunner));\n });\n\n Promise.all(promises).then(function(values) {\n var tools = [];\n qtiTools = {};\n\n _.forEach(values, function(tool) {\n if (tool) {\n tools.push(tool);\n qtiTools[tool.getId()] = tool;\n }\n });\n\n if (_.isFunction(callback)) {\n callback.call(self, tools, $container, testContext, testRunner, self);\n }\n\n /**\n * @event actionBarTools#afterrender\n * @param {Array} tools\n * @param {jQuery} $container\n * @param {Object} testContext\n * @param {Object} testRunner\n * @param {actionBarTools} this\n */\n self.trigger('afterrender', tools, $container, testContext, testRunner, self);\n });\n }\n };\n\n return eventifier(actionBarTools);\n});\n\n","\ndefine('tpl!taoQtiTest/testRunner/tpl/navigator', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, helper, options, functionType=\"function\", escapeExpression=this.escapeExpression, self=this, helperMissing=helpers.helperMissing;\n\nfunction program1(depth0,data) {\n \n \n return \" hidden\";\n }\n\n buffer += \"\";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/testRunner/tpl/navigatorTree', ['handlebars'], function(hb){ return hb.template(function (Handlebars,depth0,helpers,partials,data) {\n this.compilerInfo = [4,'>= 1.0.0'];\nhelpers = this.merge(helpers, Handlebars.helpers); data = data || {};\n var buffer = \"\", stack1, self=this, functionType=\"function\", escapeExpression=this.escapeExpression, helperMissing=helpers.helperMissing;\n\nfunction program1(depth0,data) {\n \n var buffer = \"\", stack1, helper;\n buffer += \"\\n
        3. \\n \\n \";\n if (helper = helpers.label) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.label); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n \\n \\n \\n \";\n stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.sections)),stack1 == null || stack1 === false ? stack1 : stack1.length), {hash:{},inverse:self.program(29, program29, data),fn:self.program(6, program6, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
        4. \\n \";\n return buffer;\n }\nfunction program2(depth0,data) {\n \n \n return \"active\";\n }\n\nfunction program4(depth0,data) {\n \n \n return \"collapsed\";\n }\n\nfunction program6(depth0,data) {\n \n var buffer = \"\", stack1;\n buffer += \"\\n
            \\n \";\n stack1 = helpers.each.call(depth0, (depth0 && depth0.sections), {hash:{},inverse:self.noop,fn:self.program(7, program7, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
          \\n \";\n return buffer;\n }\nfunction program7(depth0,data) {\n \n var buffer = \"\", stack1, helper;\n buffer += \"\\n
        5. \\n \\n \";\n if (helper = helpers.label) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.label); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n \";\n if (helper = helpers.answered) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.answered); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"/\"\n + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.items)),stack1 == null || stack1 === false ? stack1 : stack1.length)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))\n + \"\\n \\n
            \\n \";\n stack1 = helpers.each.call(depth0, (depth0 && depth0.items), {hash:{},inverse:self.noop,fn:self.program(8, program8, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
          \\n
        6. \\n \";\n return buffer;\n }\nfunction program8(depth0,data) {\n \n var buffer = \"\", stack1, helper;\n buffer += \"\\n
        7. \\n \\n \\n \"\n + escapeExpression(((stack1 = (data == null || data === false ? data : data.index)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))\n + \"\\n \";\n if (helper = helpers.label) { stack1 = helper.call(depth0, {hash:{},data:data}); }\n else { helper = (depth0 && depth0.label); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }\n buffer += escapeExpression(stack1)\n + \"\\n \\n
        8. \\n \";\n return buffer;\n }\nfunction program9(depth0,data) {\n \n \n return \" active\";\n }\n\nfunction program11(depth0,data) {\n \n \n return \" flagged\";\n }\n\nfunction program13(depth0,data) {\n \n \n return \" answered\";\n }\n\nfunction program15(depth0,data) {\n \n \n return \" viewed\";\n }\n\nfunction program17(depth0,data) {\n \n \n return \" unseen\";\n }\n\nfunction program19(depth0,data) {\n \n \n return \"flagged\";\n }\n\nfunction program21(depth0,data) {\n \n var stack1;\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.answered), {hash:{},inverse:self.program(24, program24, data),fn:self.program(22, program22, data),data:data});\n if(stack1 || stack1 === 0) { return stack1; }\n else { return ''; }\n }\nfunction program22(depth0,data) {\n \n \n return \"answered\";\n }\n\nfunction program24(depth0,data) {\n \n var stack1;\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.viewed), {hash:{},inverse:self.program(27, program27, data),fn:self.program(25, program25, data),data:data});\n if(stack1 || stack1 === 0) { return stack1; }\n else { return ''; }\n }\nfunction program25(depth0,data) {\n \n \n return \"viewed\";\n }\n\nfunction program27(depth0,data) {\n \n \n return \"unseen\";\n }\n\nfunction program29(depth0,data) {\n \n var buffer = \"\", stack1, helper, options;\n buffer += \"\\n
          \\n \\n

          \\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"In this part of the test navigation is not allowed.\", options) : helperMissing.call(depth0, \"__\", \"In this part of the test navigation is not allowed.\", options)))\n + \"\\n

          \\n

          \\n \\n

          \\n
          \\n \";\n return buffer;\n }\n\n buffer += \"
            \\n \";\n stack1 = helpers.each.call(depth0, (depth0 && depth0.parts), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
          \";\n return buffer;\n }); });\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2015 (original work) Open Assessment Technologies SA ;\n */\n/**\n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/testRunner/testReview',[\n 'jquery',\n 'lodash',\n 'i18n',\n 'tpl!taoQtiTest/testRunner/tpl/navigator',\n 'tpl!taoQtiTest/testRunner/tpl/navigatorTree',\n 'util/capitalize'\n], function ($, _, __, navigatorTpl, navigatorTreeTpl, capitalize) {\n 'use strict';\n\n /**\n * List of CSS classes\n * @type {Object}\n * @private\n */\n var _cssCls = {\n active : 'active',\n collapsed : 'collapsed',\n collapsible : 'collapsible',\n hidden : 'hidden',\n disabled : 'disabled',\n flagged : 'flagged',\n answered : 'answered',\n viewed : 'viewed',\n unseen : 'unseen',\n icon : 'qti-navigator-icon',\n scope : {\n test : 'scope-test',\n testPart : 'scope-test-part',\n testSection : 'scope-test-section'\n }\n };\n\n /**\n * List of common CSS selectors\n * @type {Object}\n * @private\n */\n var _selectors = {\n component : '.qti-navigator',\n filterBar : '.qti-navigator-filters',\n tree : '.qti-navigator-tree',\n collapseHandle : '.qti-navigator-collapsible',\n linearState : '.qti-navigator-linear',\n infoAnswered : '.qti-navigator-answered .qti-navigator-counter',\n infoViewed : '.qti-navigator-viewed .qti-navigator-counter',\n infoUnanswered : '.qti-navigator-unanswered .qti-navigator-counter',\n infoFlagged : '.qti-navigator-flagged .qti-navigator-counter',\n infoPanel : '.qti-navigator-info',\n infoPanelLabels : '.qti-navigator-info > .qti-navigator-label',\n parts : '.qti-navigator-part',\n partLabels : '.qti-navigator-part > .qti-navigator-label',\n sections : '.qti-navigator-section',\n sectionLabels : '.qti-navigator-section > .qti-navigator-label',\n items : '.qti-navigator-item',\n itemLabels : '.qti-navigator-item > .qti-navigator-label',\n itemIcons : '.qti-navigator-item > .qti-navigator-icon',\n icons : '.qti-navigator-icon',\n linearStart : '.qti-navigator-linear-part button',\n counters : '.qti-navigator-counter',\n actives : '.active',\n collapsible : '.collapsible',\n collapsiblePanels : '.collapsible-panel',\n unseen : '.unseen',\n answered : '.answered',\n flagged : '.flagged',\n notFlagged : ':not(.flagged)',\n notAnswered : ':not(.answered)',\n hidden : '.hidden'\n };\n\n /**\n * Maps the filter mode to filter criteria.\n * Each filter criteria is a CSS selector used to find and mask the items to be discarded by the filter.\n * @type {Object}\n * @private\n */\n var _filterMap = {\n all : \"\",\n unanswered : _selectors.answered,\n flagged : _selectors.notFlagged,\n answered : _selectors.notAnswered,\n filtered : _selectors.hidden\n };\n\n /**\n * Maps of config options translated from the context object to the local options\n * @type {Object}\n * @private\n */\n var _optionsMap = {\n 'reviewScope' : 'reviewScope',\n 'reviewPreventsUnseen' : 'preventsUnseen',\n 'canCollapse' : 'canCollapse'\n };\n\n /**\n * Maps the handled review scopes\n * @type {Object}\n * @private\n */\n var _reviewScopes = {\n test : 'test',\n testPart : 'testPart',\n testSection : 'testSection'\n };\n\n /**\n * Provides a test review manager\n * @type {Object}\n */\n var testReview = {\n /**\n * Initializes the component\n * @param {String|jQuery|HTMLElement} element The element on which install the component\n * @param {Object} [options] A list of extra options\n * @param {String} [options.region] The region on which put the component: left or right\n * @param {String} [options.reviewScope] Limit the review screen to a particular scope:\n * the whole test, the current test part or the current test section)\n * @param {Boolean} [options.preventsUnseen] Prevents the test taker to access unseen items\n * @returns {testReview}\n */\n init: function init(element, options) {\n var initOptions = _.isObject(options) && options || {};\n var putOnRight = 'right' === initOptions.region;\n var insertMethod = putOnRight ? 'append' : 'prepend';\n\n this.options = initOptions;\n this.disabled = false;\n this.hidden = !!initOptions.hidden;\n this.currentFilter = 'all';\n\n // clean the DOM if the init method is called after initialisation\n if (this.$component) {\n this.$component.remove();\n }\n\n // build the component structure and inject it into the DOM\n this.$container = $(element);\n insertMethod = this.$container[insertMethod];\n if (insertMethod) {\n insertMethod.call(this.$container, navigatorTpl({\n region: putOnRight ? 'right' : 'left',\n hidden: this.hidden\n }));\n } else {\n throw new Error(\"Unable to inject the component structure into the DOM\");\n }\n\n // install the component behaviour\n this._loadDOM();\n this._initEvents();\n this._updateDisplayOptions();\n\n return this;\n },\n\n /**\n * Links the component to the underlying DOM elements\n * @private\n */\n _loadDOM: function() {\n this.$component = this.$container.find(_selectors.component);\n\n // access to info panel displaying counters\n this.$infoAnswered = this.$component.find(_selectors.infoAnswered);\n this.$infoViewed = this.$component.find(_selectors.infoViewed);\n this.$infoUnanswered = this.$component.find(_selectors.infoUnanswered);\n this.$infoFlagged = this.$component.find(_selectors.infoFlagged);\n\n // access to filter switches\n this.$filterBar = this.$component.find(_selectors.filterBar);\n this.$filters = this.$filterBar.find('li');\n\n // access to the tree of parts/sections/items\n this.$tree = this.$component.find(_selectors.tree);\n\n // access to the panel displayed when a linear part is reached\n this.$linearState = this.$component.find(_selectors.linearState);\n },\n\n /**\n * Installs the event handlers on the underlying DOM elements\n * @private\n */\n _initEvents: function() {\n var self = this;\n\n // click on the collapse handle: collapse/expand the review panel\n this.$component.on('click' + _selectors.component, _selectors.collapseHandle, function() {\n if (self.disabled) {\n return;\n }\n\n self.$component.toggleClass(_cssCls.collapsed);\n if (self.$component.hasClass(_cssCls.collapsed)) {\n self._openSelected();\n }\n });\n\n // click on the info panel title: toggle the related panel\n this.$component.on('click' + _selectors.component, _selectors.infoPanelLabels, function() {\n if (self.disabled) {\n return;\n }\n\n var $panel = $(this).closest(_selectors.infoPanel);\n self._togglePanel($panel, _selectors.infoPanel);\n });\n\n // click on a part title: toggle the related panel\n this.$tree.on('click' + _selectors.component, _selectors.partLabels, function() {\n if (self.disabled) {\n return;\n }\n\n var $panel = $(this).closest(_selectors.parts);\n var open = self._togglePanel($panel, _selectors.parts);\n\n if (open) {\n if ($panel.hasClass(_cssCls.active)) {\n self._openSelected();\n } else {\n self._openOnly($panel.find(_selectors.sections).first(), $panel);\n }\n }\n });\n\n // click on a section title: toggle the related panel\n this.$tree.on('click' + _selectors.component, _selectors.sectionLabels, function() {\n if (self.disabled) {\n return;\n }\n\n var $panel = $(this).closest(_selectors.sections);\n\n self._togglePanel($panel, _selectors.sections);\n });\n\n // click on an item: jump to the position\n this.$tree.on('click' + _selectors.component, _selectors.itemLabels, function(event) {\n if (self.disabled) {\n return;\n }\n\n var $item = $(this).closest(_selectors.items);\n var $target;\n\n if (!$item.hasClass(_cssCls.disabled)) {\n $target = $(event.target);\n if ($target.is(_selectors.icons) && !self.$component.hasClass(_cssCls.collapsed)) {\n if (!$item.hasClass(_cssCls.unseen)) {\n self._mark($item);\n }\n } else {\n self._select($item);\n self._jump($item);\n }\n }\n });\n\n // click on the start button inside a linear part: jump to the position\n this.$tree.on('click' + _selectors.component, _selectors.linearStart, function() {\n if (self.disabled) {\n return;\n }\n\n var $btn = $(this);\n\n if (!$btn.hasClass(_cssCls.disabled)) {\n $btn.addClass(_cssCls.disabled);\n self._jump($btn);\n }\n });\n\n // click on a filter button\n this.$filterBar.on('click' + _selectors.component, 'li', function() {\n if (self.disabled) {\n return;\n }\n\n var $btn = $(this);\n var mode = $btn.data('mode');\n\n self.$filters.removeClass(_cssCls.active);\n self.$component.removeClass(_cssCls.collapsed);\n $btn.addClass(_cssCls.active);\n\n self._filter(mode);\n });\n },\n\n /**\n * Filters the items by a criteria\n * @param {String} criteria\n * @private\n */\n _filter: function(criteria) {\n var $items = this.$tree.find(_selectors.items).removeClass(_cssCls.hidden);\n var filter = _filterMap[criteria];\n if (filter) {\n $items.filter(filter).addClass(_cssCls.hidden);\n }\n this._updateSectionCounters(!!filter);\n this.currentFilter = criteria;\n },\n\n /**\n * Selects an item\n * @param {String|jQuery} position The item's position\n * @param {Boolean} [open] Forces the tree to be opened on the selected item\n * @returns {jQuery} Returns the selected item\n * @private\n */\n _select: function(position, open) {\n // find the item to select and extract its hierarchy\n var selected = position && position.jquery ? position : this.$tree.find('[data-position=' + position + ']');\n var hierarchy = selected.parentsUntil(this.$tree);\n\n // collapse the full tree and open only the hierarchy of the selected item\n if (open) {\n this._openOnly(hierarchy);\n }\n\n // select the item\n this.$tree.find(_selectors.actives).removeClass(_cssCls.active);\n hierarchy.add(selected).addClass(_cssCls.active);\n return selected;\n },\n\n /**\n * Opens the tree on the selected item only\n * @returns {jQuery} Returns the selected item\n * @private\n */\n _openSelected: function() {\n // find the selected item and extract its hierarchy\n var selected = this.$tree.find(_selectors.items + _selectors.actives);\n var hierarchy = selected.parentsUntil(this.$tree);\n\n // collapse the full tree and open only the hierarchy of the selected item\n this._openOnly(hierarchy);\n\n return selected;\n },\n\n /**\n * Collapses the full tree and opens only the provided branch\n * @param {jQuery} opened The element to be opened\n * @param {jQuery} [root] The root element from which collapse the panels\n * @private\n */\n _openOnly: function(opened, root) {\n (root || this.$tree).find(_selectors.collapsible).addClass(_cssCls.collapsed);\n opened.removeClass(_cssCls.collapsed);\n },\n\n /**\n * Toggles a panel\n * @param {jQuery} panel The panel to toggle\n * @param {String} [collapseSelector] Selector of panels to collapse\n * @returns {Boolean} Returns `true` if the panel just expanded now\n */\n _togglePanel: function(panel, collapseSelector) {\n var collapsed = panel.hasClass(_cssCls.collapsed);\n\n if (collapseSelector) {\n this.$tree.find(collapseSelector).addClass(_cssCls.collapsed);\n }\n\n if (collapsed) {\n panel.removeClass(_cssCls.collapsed);\n } else {\n panel.addClass(_cssCls.collapsed);\n }\n return collapsed;\n },\n\n /**\n * Sets the icon of a particular item\n * @param {jQuery} $item\n * @param {String} icon\n * @private\n */\n _setItemIcon: function($item, icon) {\n $item.find(_selectors.icons).attr('class', _cssCls.icon + ' icon-' + icon);\n },\n\n /**\n * Sets the icon of a particular item according to its state\n * @param {jQuery} $item\n * @private\n */\n _adjustItemIcon: function($item) {\n var icon = null;\n var defaultIcon = _cssCls.unseen;\n var iconCls = [\n _cssCls.flagged,\n _cssCls.answered,\n _cssCls.viewed\n ];\n\n _.forEach(iconCls, function(cls) {\n if ($item.hasClass(cls)) {\n icon = cls;\n return false;\n }\n });\n\n this._setItemIcon($item, icon || defaultIcon);\n },\n\n /**\n * Toggle the marked state of an item\n * @param {jQuery} $item\n * @param {Boolean} [flag]\n * @private\n */\n _toggleFlag: function($item, flag) {\n $item.toggleClass(_cssCls.flagged, flag);\n this._adjustItemIcon($item);\n },\n\n /**\n * Marks an item for later review\n * @param {jQuery} $item\n * @private\n */\n _mark: function($item) {\n var itemId = $item.data('id');\n var itemPosition = $item.data('position');\n var flag = !$item.hasClass(_cssCls.flagged);\n\n this._toggleFlag($item);\n\n /**\n * A storage of the flag is required\n * @event testReview#mark\n * @param {Boolean} flag - Tells whether the item is marked for review or not\n * @param {Number} position - The item position on which jump\n * @param {String} itemId - The identifier of the target item\n * @param {testReview} testReview - The client testReview component\n */\n this.trigger('mark', [flag, itemPosition, itemId]);\n },\n\n /**\n * Jumps to an item\n * @param {jQuery} $item\n * @private\n */\n _jump: function($item) {\n var itemId = $item.data('id');\n var itemPosition = $item.data('position');\n\n /**\n * A jump to a particular item is required\n * @event testReview#jump\n * @param {Number} position - The item position on which jump\n * @param {String} itemId - The identifier of the target item\n * @param {testReview} testReview - The client testReview component\n */\n this.trigger('jump', [itemPosition, itemId]);\n },\n\n /**\n * Updates the sections related items counters\n * @param {Boolean} filtered\n */\n _updateSectionCounters: function(filtered) {\n var self = this;\n var filter = _filterMap[filtered ? 'filtered' : 'answered'];\n this.$tree.find(_selectors.sections).each(function() {\n var $section = $(this);\n var $items = $section.find(_selectors.items);\n var $filtered = $items.filter(filter);\n var total = $items.length;\n var nb = total - $filtered.length;\n self._writeCount($section.find(_selectors.counters), nb, total);\n });\n },\n\n /**\n * Updates the display according to options\n * @private\n */\n _updateDisplayOptions: function() {\n var reviewScope = _reviewScopes[this.options.reviewScope] || 'test';\n var scopeClass = _cssCls.scope[reviewScope];\n var $root = this.$component;\n _.forEach(_cssCls.scope, function(cls) {\n $root.removeClass(cls);\n });\n if (scopeClass) {\n $root.addClass(scopeClass);\n }\n $root.toggleClass(_cssCls.collapsible, this.options.canCollapse);\n },\n\n /**\n * Updates the local options from the provided context\n * @param {Object} testContext The progression context\n * @private\n */\n _updateOptions: function(testContext) {\n var options = this.options;\n _.forEach(_optionsMap, function(optionKey, contextKey) {\n if (undefined !== testContext[contextKey]) {\n options[optionKey] = testContext[contextKey];\n }\n });\n },\n\n /**\n * Updates the info panel\n */\n _updateInfos: function() {\n var progression = this.progression,\n unanswered = Number(progression.total) - Number(progression.answered);\n\n // update the info panel\n this._writeCount(this.$infoAnswered, progression.answered, progression.total);\n this._writeCount(this.$infoUnanswered, unanswered, progression.total);\n this._writeCount(this.$infoViewed, progression.viewed, progression.total);\n this._writeCount(this.$infoFlagged, progression.flagged, progression.total);\n },\n\n /**\n * Updates a counter\n * @param {jQuery} $place\n * @param {Number} count\n * @param {Number} total\n * @private\n */\n _writeCount: function($place, count, total) {\n $place.text(count + '/' + total);\n },\n\n /**\n * Gets the progression stats for the whole test\n * @param {Object} testContext The progression context\n * @returns {{total: (Number), answered: (Number), viewed: (Number), flagged: (Number)}}\n * @private\n */\n _getProgressionOfTest: function(testContext) {\n return {\n total : testContext.numberItems || 0,\n answered : testContext.numberCompleted || 0,\n viewed : testContext.numberPresented || 0,\n flagged : testContext.numberFlagged || 0\n };\n },\n\n /**\n * Gets the progression stats for the current test part\n * @param {Object} testContext The progression context\n * @returns {{total: (Number), answered: (Number), viewed: (Number), flagged: (Number)}}\n * @private\n */\n _getProgressionOfTestPart: function(testContext) {\n return {\n total : testContext.numberItemsPart || 0,\n answered : testContext.numberCompletedPart || 0,\n viewed : testContext.numberPresentedPart || 0,\n flagged : testContext.numberFlaggedPart || 0\n };\n },\n\n /**\n * Gets the progression stats for the current test section\n * @param {Object} testContext The progression context\n * @returns {{total: (Number), answered: (Number), viewed: (Number), flagged: (Number)}}\n * @private\n */\n _getProgressionOfTestSection: function(testContext) {\n return {\n total : testContext.numberItemsSection || 0,\n answered : testContext.numberCompletedSection || 0,\n viewed : testContext.numberPresentedSection || 0,\n flagged : testContext.numberFlaggedSection || 0\n };\n },\n\n /**\n * Updates the navigation tre\n * @param {Object} testContext The progression context\n */\n _updateTree: function(testContext) {\n var navigatorMap = testContext.navigatorMap;\n var reviewScope = this.options.reviewScope;\n var reviewScopePart = reviewScope === 'testPart';\n var reviewScopeSection = reviewScope === 'testSection';\n var _partsFilter = function(part) {\n if (reviewScopeSection && part.sections) {\n part.sections = _.filter(part.sections, _partsFilter);\n }\n return part.active;\n };\n\n // rebuild the tree\n if (navigatorMap) {\n if (reviewScopePart || reviewScopeSection) {\n // display only the current section\n navigatorMap = _.filter(navigatorMap, _partsFilter);\n }\n\n this.$filterBar.show();\n this.$linearState.hide();\n this.$tree.html(navigatorTreeTpl({\n parts: navigatorMap\n }));\n\n if (this.options.preventsUnseen) {\n // disables all unseen items to prevent the test taker has access to.\n this.$tree.find(_selectors.unseen).addClass(_cssCls.disabled);\n }\n } else {\n this.$filterBar.hide();\n this.$linearState.show();\n this.$tree.empty();\n }\n\n // apply again the current filter\n this._filter(this.$filters.filter(_selectors.actives).data('mode'));\n },\n\n /**\n * Set the marked state of an item\n * @param {Number|String|jQuery} position\n * @param {Boolean} flag\n */\n setItemFlag: function setItemFlag(position, flag) {\n var $item = position && position.jquery ? position : this.$tree.find('[data-position=' + position + ']');\n var progression = this.progression;\n\n // update the item flag\n this._toggleFlag($item, flag);\n\n // update the info panel\n progression.flagged = this.$tree.find(_selectors.flagged).length;\n this._writeCount(this.$infoFlagged, progression.flagged, progression.total);\n this._filter(this.currentFilter);\n },\n\n /**\n * Update the number of flagged items in the test context\n * @param {Object} testContext The test context\n * @param {Number} position The position of the flagged item\n * @param {Boolean} flag The flag state\n */\n updateNumberFlagged: function(testContext, position, flag) {\n var fields = ['numberFlagged'];\n var currentPosition = testContext.itemPosition;\n var currentFound = false, currentSection = null, currentPart = null;\n var itemFound = false, itemSection = null, itemPart = null;\n\n if (testContext.navigatorMap) {\n // find the current item and the marked item inside the navigator map\n // check if the marked item is in the current section\n _.forEach(testContext.navigatorMap, function(part) {\n _.forEach(part && part.sections, function(section) {\n _.forEach(section && section.items, function(item) {\n if (item) {\n if (item.position === position) {\n itemPart = part;\n itemSection = section;\n itemFound = true;\n }\n if (item.position === currentPosition) {\n currentPart = part;\n currentSection = section;\n currentFound = true;\n\n }\n if (itemFound && currentFound) {\n return false;\n }\n }\n });\n\n if (itemFound && currentFound) {\n return false;\n }\n });\n\n if (itemFound && currentFound) {\n return false;\n }\n });\n\n // select the context to update\n if (itemFound && currentPart === itemPart) {\n fields.push('numberFlaggedPart');\n }\n if (itemFound && currentSection === itemSection) {\n fields.push('numberFlaggedSection');\n }\n } else {\n // no navigator map, the current the marked item is in the current section\n fields.push('numberFlaggedPart');\n fields.push('numberFlaggedSection');\n }\n\n _.forEach(fields, function(field) {\n if (field in testContext) {\n testContext[field] += flag ? 1 : -1;\n }\n });\n },\n\n /**\n * Get progression\n * @param {Object} testContext The progression context\n * @returns {object} progression\n */\n getProgression: function getProgression(testContext) {\n var reviewScope = _reviewScopes[this.options.reviewScope] || 'test',\n progressInfoMethod = '_getProgressionOf' + capitalize(reviewScope),\n getProgression = this[progressInfoMethod] || this._getProgressionOfTest,\n progression = getProgression && getProgression(testContext) || {};\n\n return progression;\n },\n\n /**\n * Updates the review screen\n * @param {Object} testContext The progression context\n * @returns {testReview}\n */\n update: function update(testContext) {\n this.progression = this.getProgression(testContext);\n this._updateOptions(testContext);\n this._updateInfos(testContext);\n this._updateTree(testContext);\n this._updateDisplayOptions(testContext);\n return this;\n },\n\n /**\n * Disables the component\n * @returns {testReview}\n */\n disable: function disable() {\n this.disabled = true;\n this.$component.addClass(_cssCls.disabled);\n return this;\n },\n\n /**\n * Enables the component\n * @returns {testReview}\n */\n enable: function enable() {\n this.disabled = false;\n this.$component.removeClass(_cssCls.disabled);\n return this;\n },\n\n /**\n * Hides the component\n * @returns {testReview}\n */\n hide: function hide() {\n this.disabled = true;\n this.hidden = true;\n this.$component.addClass(_cssCls.hidden);\n return this;\n },\n\n /**\n * Shows the component\n * @returns {testReview}\n */\n show: function show() {\n this.disabled = false;\n this.hidden = false;\n this.$component.removeClass(_cssCls.hidden);\n return this;\n },\n\n /**\n * Toggles the display state of the component\n * @param {Boolean} [show] External condition that's tells if the component must be shown or hidden\n * @returns {testReview}\n */\n toggle: function toggle(show) {\n if (undefined === show) {\n show = this.hidden;\n }\n\n if (show) {\n this.show();\n } else {\n this.hide();\n }\n\n return this;\n },\n\n /**\n * Install an event handler on the underlying DOM element\n * @param {String} eventName\n * @returns {testReview}\n */\n on: function on(eventName) {\n var dom = this.$component;\n if (dom) {\n dom.on.apply(dom, arguments);\n }\n\n return this;\n },\n\n /**\n * Uninstall an event handler from the underlying DOM element\n * @param {String} eventName\n * @returns {testReview}\n */\n off: function off(eventName) {\n var dom = this.$component;\n if (dom) {\n dom.off.apply(dom, arguments);\n }\n\n return this;\n },\n\n /**\n * Triggers an event on the underlying DOM element\n * @param {String} eventName\n * @param {Array|Object} extraParameters\n * @returns {testReview}\n */\n trigger : function trigger(eventName, extraParameters) {\n var dom = this.$component;\n\n if (undefined === extraParameters) {\n extraParameters = [];\n }\n if (!_.isArray(extraParameters)) {\n extraParameters = [extraParameters];\n }\n\n extraParameters.push(this);\n\n if (dom) {\n dom.trigger(eventName, extraParameters);\n }\n\n return this;\n }\n };\n\n /**\n * Builds an instance of testReview\n * @param {String|jQuery|HTMLElement} element The element on which install the component\n * @param {Object} [options] A list of extra options\n * @param {String} [options.region] The region on which put the component: left or right\n * @param {String} [options.reviewScope] Limit the review screen to a particular scope:\n * the whole test, the current test part or the current test section)\n * @param {Boolean} [options.preventsUnseen] Prevents the test taker to access unseen items\n * @returns {testReview}\n */\n var testReviewFactory = function(element, options) {\n var component = _.clone(testReview, true);\n return component.init(element, options);\n };\n\n return testReviewFactory;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2015 (original work) Open Assessment Technologies SA ;\n */\n/**\n * @author Jean-Sébastien Conan \n */\ndefine('taoQtiTest/testRunner/progressUpdater',[\n 'jquery',\n 'lodash',\n 'i18n',\n 'ui/progressbar'\n], function ($, _, __) {\n 'use strict';\n\n /**\n * Provides a versatile progress bar updater\n * @type {{init: Function, update: Function}}\n */\n var progressUpdaters = {\n /**\n * Initializes the progress updater\n *\n * @param {String|jQuery|HTMLElement} progressBar The element on which put the progress bar\n * @param {String|jQuery|HTMLElement} progressLabel The element on which put the progress label\n * @returns {progressUpdaters}\n */\n init: function(progressBar, progressLabel) {\n this.progressBar = $(progressBar).progressbar();\n this.progressLabel = $(progressLabel);\n return this;\n },\n\n /**\n * Writes the progress label and update the progress by ratio\n * @param {String} label\n * @param {Number} ratio\n * @returns {progressUpdaters}\n */\n write: function(label, ratio) {\n this.progressLabel.text(label);\n this.progressBar.progressbar('value', ratio);\n return this;\n },\n\n /**\n * Updates the progress bar\n * @param {Object} testContext The progression context\n * @returns {{ratio: number, label: string}}\n */\n update: function(testContext) {\n var progressIndicator = testContext.progressIndicator || 'percentage';\n var progressIndicatorMethod = progressIndicator + 'Progression';\n var getProgression = this[progressIndicatorMethod] || this.percentageProgression;\n var progression = getProgression && getProgression(testContext) || {};\n\n this.write(progression.label, progression.ratio);\n return progression;\n },\n\n /**\n * Updates the progress bar displaying the percentage\n * @param {Object} testContext The progression context\n * @returns {{ratio: number, label: string}}\n */\n percentageProgression: function(testContext) {\n var total = Math.max(1, testContext.numberItems);\n var ratio = Math.floor(testContext.numberCompleted / total * 100);\n return {\n ratio : ratio,\n label : ratio + '%'\n };\n },\n\n /**\n * Updates the progress bar displaying the position\n * @param {Object} testContext The progression context\n * @returns {{ratio: number, label: string}}\n */\n positionProgression: function(testContext) {\n var progressScope = testContext.progressIndicatorScope;\n var progressScopeCounter = {\n test : {\n total : 'numberItems',\n position : 'itemPosition'\n },\n testPart : {\n total : 'numberItemsPart',\n position : 'itemPositionPart'\n },\n testSection : {\n total : 'numberItemsSection',\n position : 'itemPositionSection'\n }\n };\n var counter = progressScopeCounter[progressScope] || progressScopeCounter.test;\n var total = Math.max(1, testContext[counter.total]);\n var position = testContext[counter.position] + 1;\n return {\n ratio : Math.floor(position / total * 100),\n label : __('Item %d of %d', position, total)\n };\n }\n };\n\n /**\n * Builds an instance of progressUpdaters\n * @param {String|jQuery|HTMLElement} progressBar The element on which put the progress bar\n * @param {String|jQuery|HTMLElement} progressLabel The element on which put the progress label\n * @returns {progressUpdaters}\n */\n var progressUpdaterFactory = function(progressBar, progressLabel) {\n var updater = _.clone(progressUpdaters, true);\n return updater.init(progressBar, progressLabel);\n };\n\n return progressUpdaterFactory;\n});\n\n","/**\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2015 (original work) Open Assessment Technologies SA;\n *\n */\n\n/**\n * Metadata to be sent to the server. Will be saved in result storage as a trace variable.\n * Usage example:\n *
          \n * var testMetaData = testMetaDataFactory({\n *   testServiceCallId : this.itemServiceApi.serviceCallId\n * });\n *\n * testMetaData.setData({\n *   'TEST' : {\n *      'TEST_EXIT_CODE' : 'T'\n *   },\n *   'SECTION' : {\n *      'SECTION_EXIT_CODE' : 704\n *   }\n * });\n *\n * testMetaData.addData({'ITEM' : {\n *      'ITEM_START_TIME_CLIENT' : 1443596730143,\n *      'ITEM_END_TIME_CLIENT' : 1443596731301\n *    }\n * });\n * 
          \n *\n * Data for each service call id will be stored in local storage to be able get data\n * after reloading the page or resuming the test session.\n *\n * To clear all data related to current test_call_id used clearData method.\n */\ndefine('taoQtiTest/testRunner/testMetaData',[\n 'lodash'\n], function (_) {\n 'use strict';\n\n /**\n * @param {Object} options\n * @param {string} options.testServiceCallId - test call id.\n */\n var testMetaDataFactory = function testMetaDataFactory(options) {\n var _testServiceCallId,\n _storageKeyPrefix = 'testMetaData_',\n _data = {};\n\n if (!options || options.testServiceCallId === undefined) {\n throw new TypeError(\"testServiceCallId option is required\");\n }\n\n var testMetaData = {\n SECTION_EXIT_CODE : {\n 'COMPLETED_NORMALLY': 700,\n 'QUIT': 701,\n 'COMPLETE_TIMEOUT': 703,\n 'TIMEOUT': 704,\n 'FORCE_QUIT': 705,\n 'IN_PROGRESS': 706,\n 'ERROR': 300\n },\n TEST_EXIT_CODE : {\n 'COMPLETE': 'C',\n 'TERMINATED': 'T',\n 'INCOMPLETE': 'IC',\n 'INCOMPLETE_QUIT': 'IQ',\n 'INACTIVE': 'IA',\n 'CANDIDATE_DISAGREED_WITH_NDA': 'DA'\n },\n /**\n * Return test call id.\n * @returns {string}- Test call id\n */\n getTestServiceCallId : function getTestServiceCallId () {\n return _testServiceCallId;\n },\n\n /**\n * Set test call id.\n * @param {string} value\n */\n setTestServiceCallId : function setTestServiceCallId (value) {\n _testServiceCallId = value;\n },\n\n /**\n * Set meta data. Current data object will be overwritten.\n * @param {Object} data - metadata object\n */\n setData : function setData(data) {\n _data = data;\n setLocalStorageData(JSON.stringify(_data));\n },\n\n /**\n * Add data.\n * @param {Object} data - metadata object\n * @param {Boolean} overwrite - whether the same data should be overwritten. Default - false\n */\n addData : function addData(data, overwrite) {\n data = _.clone(data);\n if (overwrite === undefined) {\n overwrite = false;\n }\n\n if (overwrite) {\n _.merge(_data, data);\n } else {\n _data = _.merge(data, _data);\n }\n setLocalStorageData(JSON.stringify(_data));\n },\n\n /**\n * Get the saved data.\n * The cloned object will be returned to avoid unwanted affecting of the original data.\n * @returns {Object} - metadata object.\n */\n getData : function getData() {\n return _.clone(_data);\n },\n\n /**\n * Clear all data saved in current object and in local storage related to current test call id.\n * @returns {Object} - metadata object.\n */\n clearData : function clearData() {\n _data = {};\n window.localStorage.removeItem(testMetaData.getLocalStorageKey());\n },\n\n getLocalStorageKey : function getLocalStorageKey () {\n return _storageKeyPrefix + _testServiceCallId;\n }\n };\n\n /**\n * Initialize test meta data manager\n */\n function init() {\n _testServiceCallId = options.testServiceCallId;\n testMetaData.setData(getLocalStorageData());\n }\n\n /**\n * Set data to local storage\n * @param {string} val - data to be stored.\n */\n function setLocalStorageData(val) {\n var currentKey = testMetaData.getLocalStorageKey();\n try {\n window.localStorage.setItem(currentKey, val);\n } catch(domException) {\n if (domException.name === 'QuotaExceededError' ||\n domException.name === 'NS_ERROR_DOM_QUOTA_REACHED') {\n var removed = 0,\n i = window.localStorage.length,\n key;\n while (i--) {\n key = localStorage.key(i);\n if (/^testMetaData_.*/.test(key) && key !== currentKey) {\n window.localStorage.removeItem(key);\n removed++;\n }\n }\n if (removed) {\n setLocalStorageData(val);\n } else {\n throw domException;\n }\n } else {\n throw domException;\n }\n }\n }\n\n /**\n * Get data from local storage stored for current call id\n * @returns {*} saved data or empty object\n */\n function getLocalStorageData() {\n var data = window.localStorage.getItem(testMetaData.getLocalStorageKey()),\n result = JSON.parse(data) || {};\n\n return result;\n }\n\n init();\n\n return testMetaData;\n };\n\n return testMetaDataFactory;\n});\n","/*\n * This program is free software; you can redistribute it and/or\n * modify it under the terms of the GNU General Public License\n * as published by the Free Software Foundation; under version 2\n * of the License (non-upgradable).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program; if not, write to the Free Software\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n *\n * Copyright (c) 2015 (original work) Open Assessment Technologies SA;\n *\n */\n\ndefine('taoQtiTest/controller/runtime/testRunner',[\n 'jquery',\n 'lodash',\n 'i18n',\n 'module',\n 'taoQtiTest/testRunner/actionBarTools',\n 'taoQtiTest/testRunner/testReview',\n 'taoQtiTest/testRunner/progressUpdater',\n 'taoQtiTest/testRunner/testMetaData',\n 'serviceApi/ServiceApi',\n 'serviceApi/UserInfoService',\n 'serviceApi/StateStorage',\n 'iframeNotifier',\n 'mathJax',\n 'ui/feedback',\n 'ui/deleter',\n 'moment',\n 'ui/modal',\n 'ui/progressbar'\n],\nfunction (\n $,\n _,\n __,\n module,\n actionBarTools,\n testReview,\n progressUpdater,\n testMetaDataFactory,\n ServiceApi,\n UserInfoService,\n StateStorage,\n iframeNotifier,\n MathJax,\n feedback,\n deleter,\n moment,\n modal\n) {\n\n 'use strict';\n\n var timerIds = [],\n currentTimes = [],\n lastDates = [],\n timeDiffs = [],\n waitingTime = 0,\n $timers,\n $controls,\n timerIndex,\n testMetaData,\n sessionStateService,\n $doc = $(document),\n optionNextSection = 'x-tao-option-nextSection',\n optionNextSectionWarning = 'x-tao-option-nextSectionWarning',\n optionReviewScreen = 'x-tao-option-reviewScreen',\n optionEndTestWarning = 'x-tao-option-endTestWarning',\n optionNoExitTimedSectionWarning = 'x-tao-option-noExitTimedSectionWarning',\n TestRunner = {\n // Constants\n 'TEST_STATE_INITIAL': 0,\n 'TEST_STATE_INTERACTING': 1,\n 'TEST_STATE_MODAL_FEEDBACK': 2,\n 'TEST_STATE_SUSPENDED': 3,\n 'TEST_STATE_CLOSED': 4,\n 'TEST_NAVIGATION_LINEAR': 0,\n 'TEST_NAVIGATION_NONLINEAR': 1,\n 'TEST_ITEM_STATE_INTERACTING': 1,\n\n /**\n * Prepare a transition to another item\n * @param {Function} [callback]\n */\n beforeTransition: function (callback) {\n // Ask the top window to start the loader.\n iframeNotifier.parent('loading');\n\n // Disable buttons.\n this.disableGui();\n\n $controls.$itemFrame.hide();\n $controls.$rubricBlocks.hide();\n $controls.$timerWrapper.hide();\n\n // Wait at least waitingTime ms for a better user experience.\n if (typeof callback === 'function') {\n setTimeout(callback, waitingTime);\n }\n },\n\n /**\n * Complete a transition to another item\n */\n afterTransition: function () {\n this.enableGui();\n\n //ask the top window to stop the loader\n iframeNotifier.parent('unloading');\n testMetaData.addData({\n 'ITEM' : {'ITEM_START_TIME_CLIENT' : Date.now() / 1000}\n });\n },\n\n /**\n * Jumps to a particular item in the test\n * @param {Number} position The position of the item within the test\n */\n jump: function(position) {\n var self = this,\n action = 'jump',\n params = {position: position};\n this.disableGui();\n\n if( this.isJumpOutOfSection(position) && this.isCurrentItemActive() && this.isTimedSection() ){\n this.exitTimedSection(action, params);\n } else {\n this.killItemSession(function() {\n self.actionCall(action, params);\n });\n }\n },\n\n /**\n * Push to server how long user seen that item before to track duration\n * @param {Number} duration\n */\n keepItemTimed: function(duration){\n if (duration) {\n var self = this,\n action = 'keepItemTimed',\n params = {duration: duration};\n self.actionCall(action, params);\n }\n },\n\n /**\n * Marks an item for later review\n * @param {Boolean} flag The state of the flag\n * @param {Number} position The position of the item within the test\n */\n markForReview: function(flag, position) {\n var self = this;\n\n // Ask the top window to start the loader.\n iframeNotifier.parent('loading');\n\n // Disable buttons.\n this.disableGui();\n\n $.ajax({\n url: self.testContext.markForReviewUrl,\n cache: false,\n async: true,\n type: 'POST',\n dataType: 'json',\n data: {\n flag: flag,\n position: position\n },\n success: function(data) {\n // update the item flagged state\n if (self.testReview) {\n self.testReview.setItemFlag(position, flag);\n self.testReview.updateNumberFlagged(self.testContext, position, flag);\n if (self.testContext.itemPosition === position) {\n self.testContext.itemFlagged = flag;\n }\n self.updateTools(self.testContext);\n }\n\n // Enable buttons.\n self.enableGui();\n\n //ask the top window to stop the loader\n iframeNotifier.parent('unloading');\n }\n });\n },\n\n /**\n * Move to the next available item\n */\n moveForward: function () {\n var self = this,\n action = 'moveForward';\n\n function doExitSection() {\n if( self.isTimedSection() && !self.testContext.isTimeout){\n self.exitTimedSection(action);\n } else {\n self.exitSection(action);\n }\n }\n\n this.disableGui();\n\n if( (( this.testContext.numberItemsSection - this.testContext.itemPositionSection - 1) == 0) && this.isCurrentItemActive()){\n if (this.shouldDisplayEndTestWarning()) {\n this.displayEndTestWarning(doExitSection);\n this.enableGui();\n } else {\n doExitSection();\n }\n\n } else {\n this.killItemSession(function () {\n self.actionCall(action);\n });\n }\n },\n\n /**\n * Check if necessary to display an end test warning\n */\n shouldDisplayEndTestWarning: function(){\n return (this.testContext.isLast === true && this.hasOption(optionEndTestWarning));\n },\n\n /**\n * Warns upon exiting test\n */\n displayEndTestWarning: function(nextAction){\n var options = {\n confirmLabel: __('OK'),\n cancelLabel: __('Cancel'),\n showItemCount: false\n };\n\n this.displayExitMessage(\n __('You are about to submit the test. You will not be able to access this test once submitted. Click OK to continue and submit the test.'),\n nextAction,\n options\n );\n },\n\n /**\n * Move to the previous available item\n */\n moveBackward: function () {\n var self = this,\n action = 'moveBackward';\n\n this.disableGui();\n\n if( (this.testContext.itemPositionSection == 0) && this.isCurrentItemActive() && this.isTimedSection() ){\n this.exitTimedSection(action);\n } else {\n this.killItemSession(function () {\n self.actionCall(action);\n });\n }\n },\n\n /**\n * Checks if a position is out of the current section\n * @param {Number} jumpPosition\n * @returns {Boolean}\n */\n isJumpOutOfSection: function(jumpPosition){\n var items = this.getCurrentSectionItems(),\n isJumpToOtherSection = true,\n isValidPosition = (jumpPosition >= 0) && ( jumpPosition < this.testContext.numberItems );\n\n if( isValidPosition){\n for(var i in items ) {\n if (!items.hasOwnProperty(i)) {\n continue;\n }\n if( items[i].position == jumpPosition ){\n isJumpToOtherSection = false;\n break;\n }\n }\n } else {\n isJumpToOtherSection = false;\n }\n\n return isJumpToOtherSection;\n },\n\n /**\n * Exit from the current section. Set the exit code.de\n * @param {String} action\n * @param {Object} params\n * @param {Number} [exitCode]\n */\n exitSection: function(action, params, exitCode){\n var self = this;\n\n testMetaData.addData({\"SECTION\" : {\"SECTION_EXIT_CODE\" : exitCode || testMetaData.SECTION_EXIT_CODE.COMPLETED_NORMALLY}});\n self.killItemSession(function () {\n self.actionCall(action, params);\n });\n },\n\n /**\n * Tries to exit a timed section. Display a confirm message.\n * @param {String} action\n * @param {Object} params\n */\n exitTimedSection: function(action, params){\n var self = this,\n qtiRunner = this.getQtiRunner(),\n doExitTimedSection = function() {\n if (qtiRunner) {\n qtiRunner.updateItemApi();\n }\n self.exitSection(action, params);\n };\n\n if ((action === 'moveForward' && this.shouldDisplayEndTestWarning()) // prevent duplicate warning\n || this.hasOption(optionNoExitTimedSectionWarning) // check if warning is disabled\n || this.testContext.keepTimerUpToTimeout // no need to display the message as we may be able to go back\n ) {\n doExitTimedSection();\n } else {\n this.displayExitMessage(\n __('After you complete the section it would be impossible to return to this section to make changes. Are you sure you want to end the section?'),\n doExitTimedSection,\n { scope: 'testSection' }\n );\n }\n\n this.enableGui();\n },\n\n /**\n * Tries to leave the current section and go to the next\n */\n nextSection: function(){\n var self = this;\n var qtiRunner = this.getQtiRunner();\n var doNextSection = function() {\n self.exitSection('nextSection', null, testMetaData.SECTION_EXIT_CODE.QUIT);\n };\n\n if (qtiRunner) {\n qtiRunner.updateItemApi();\n }\n\n if (this.hasOption(optionNextSectionWarning)) {\n this.displayExitMessage(\n __('After you complete the section it would be impossible to return to this section to make changes. Are you sure you want to end the section?'),\n doNextSection,\n { scope: 'testSection' }\n );\n } else {\n doNextSection();\n }\n\n this.enableGui();\n },\n\n /**\n * Gets the current progression within a particular scope\n * @param {String} [scope]\n * @returns {Object}\n */\n getProgression: function(scope) {\n var scopeSuffixMap = {\n test : '',\n testPart : 'Part',\n testSection : 'Section'\n };\n var scopeSuffix = scope && scopeSuffixMap[scope] || '';\n\n return {\n total : this.testContext['numberItems' + scopeSuffix] || 0,\n answered : this.testContext['numberCompleted' + scopeSuffix] || 0,\n viewed : this.testContext['numberPresented' + scopeSuffix] || 0,\n flagged : this.testContext['numberFlagged' + scopeSuffix] || 0\n };\n },\n\n /**\n * Displays an exit message for a particular scope\n * @param {String} message\n * @param {Function} [action]\n * @param {Object} [options]\n * @param {String} [options.scope]\n * @param {String} [options.confirmLabel] - label of confirm button\n * @param {String} [options.cancelLabel] - label of cancel button\n * @param {Boolean} [options.showItemCount] - display the number of unanswered / flagged items in modal\n * @returns {jQuery} Returns the message box\n */\n displayExitMessage: function(message, action, options) {\n var self = this,\n options = options || {},\n scope = options.scope,\n confirmLabel = options.confirmLabel || __('Yes'),\n cancelLabel = options.cancelLabel || __('No'),\n showItemCount = typeof options.showItemCount !== 'undefined' ? options.showItemCount : true;\n\n var $confirmBox = $('.exit-modal-feedback');\n var progression = this.getProgression(scope);\n var unansweredCount = (progression.total - progression.answered);\n var flaggedCount = progression.flagged;\n\n if (showItemCount) {\n if (unansweredCount && this.isCurrentItemAnswered()) {\n unansweredCount--;\n }\n\n if (flaggedCount && unansweredCount) {\n message = __('You have %s unanswered question(s) and have %s item(s) marked for review.',\n unansweredCount.toString(),\n flaggedCount.toString()\n ) + ' ' + message;\n } else {\n if (flaggedCount) {\n message = __('You have %s item(s) marked for review.', flaggedCount.toString()) + ' ' + message;\n }\n\n if (unansweredCount) {\n message = __('You have %s unanswered question(s).', unansweredCount.toString()) + ' ' + message;\n }\n }\n }\n\n $confirmBox.find('.message').html(message);\n $confirmBox.find('.js-exit-confirm').html(confirmLabel);\n $confirmBox.find('.js-exit-cancel').html(cancelLabel);\n $confirmBox.modal({ width: 500 });\n\n $confirmBox.find('.js-exit-cancel, .modal-close').off('click').on('click', function () {\n $confirmBox.modal('close');\n });\n\n $confirmBox.find('.js-exit-confirm').off('click').on('click', function () {\n $confirmBox.modal('close');\n if (_.isFunction(action)) {\n action.call(self);\n }\n });\n\n return $confirmBox;\n },\n\n /**\n * Kill current item section and execute callback function given as first parameter.\n * Item end execution time will be stored in metadata object to be sent to the server.\n * @param {function} callback\n */\n killItemSession : function (callback) {\n testMetaData.addData({\n 'ITEM' : {\n 'ITEM_END_TIME_CLIENT' : Date.now() / 1000,\n 'ITEM_TIMEZONE' : moment().utcOffset(moment().utcOffset()).format('Z')\n }\n });\n if (typeof callback !== 'function') {\n callback = _.noop;\n }\n this.itemServiceApi.kill(callback);\n },\n\n /**\n * Checks if the current item is active\n * @returns {Boolean}\n */\n isCurrentItemActive: function(){\n return (this.testContext.itemSessionState !=4);\n },\n\n /**\n * Tells is the current item has been answered or not\n * The item is considered answered when at least one response has been set to not empty {base : null}\n *\n * @returns {Boolean}\n */\n isCurrentItemAnswered: function(){\n var answered = false;\n _.forEach(this.getCurrentItemState(), function(state){\n if(state && _.isObject(state.response) && state.response.base !== null){\n answered = true;//at least one response is not null so consider the item answered\n return false;\n }\n });\n return answered;\n },\n\n /**\n * Checks if a particular option is enabled for the current item\n * @param {String} option\n * @returns {Boolean}\n */\n hasOption: function(option) {\n return _.indexOf(this.testContext.categories, option) >= 0;\n },\n\n /**\n * Gets access to the qtiRunner instance\n * @returns {Object}\n */\n getQtiRunner: function(){\n var itemFrame = document.getElementById('qti-item');\n var itemWindow = itemFrame && itemFrame.contentWindow;\n var itemContainerFrame = itemWindow && itemWindow.document.getElementById('item-container');\n var itemContainerWindow = itemContainerFrame && itemContainerFrame.contentWindow;\n return itemContainerWindow && itemContainerWindow.qtiRunner;\n },\n\n /**\n * Checks if the current section is timed\n * @returns {Boolean}\n */\n isTimedSection: function(){\n var timeConstraints = this.testContext.timeConstraints,\n isTimedSection = false;\n for( var index in timeConstraints ){\n if(timeConstraints.hasOwnProperty(index) &&\n timeConstraints[index].qtiClassName === 'assessmentSection' ){\n isTimedSection = true;\n }\n }\n\n return isTimedSection;\n },\n\n /**\n * Gets the list of items owned by the current section\n * @returns {Array}\n */\n getCurrentSectionItems: function(){\n var partId = this.testContext.testPartId,\n navMap = this.testContext.navigatorMap,\n sectionItems;\n\n for( var partIndex in navMap ){\n if( !navMap.hasOwnProperty(partIndex)){\n continue;\n }\n if( navMap[partIndex].id !== partId ){\n continue;\n }\n\n for(var sectionIndex in navMap[partIndex].sections){\n if( !navMap[partIndex].sections.hasOwnProperty(sectionIndex)){\n continue;\n }\n if( navMap[partIndex].sections[sectionIndex].active === true ){\n sectionItems = navMap[partIndex].sections[sectionIndex].items;\n break;\n }\n }\n }\n\n return sectionItems;\n },\n\n /**\n * Skips the current item\n */\n skip: function () {\n var self = this,\n doSkip = function() {\n self.disableGui();\n self.actionCall('skip');\n };\n\n if (this.shouldDisplayEndTestWarning()) {\n this.displayEndTestWarning(doSkip);\n } else {\n doSkip();\n }\n },\n\n /**\n * Handles the timeout state\n */\n timeout: function () {\n var self = this;\n this.disableGui();\n this.testContext.isTimeout = true;\n this.updateTimer();\n\n this.killItemSession(function () {\n var confirmBox = $('.timeout-modal-feedback'),\n testContext = self.testContext,\n confirmBtn = confirmBox.find('.js-timeout-confirm, .modal-close');\n\n if (testContext.numberCompletedSection === testContext.numberItemsSection) {\n testMetaData.addData({\"SECTION\" : {\"SECTION_EXIT_CODE\" : testMetaData.SECTION_EXIT_CODE.COMPLETE_TIMEOUT}});\n } else {\n testMetaData.addData({\"SECTION\" : {\"SECTION_EXIT_CODE\" : testMetaData.SECTION_EXIT_CODE.TIMEOUT}});\n }\n\n self.enableGui();\n confirmBox.modal({width: 500});\n confirmBtn.off('click').on('click', function () {\n confirmBox.modal('close');\n self.actionCall('timeout');\n });\n });\n },\n\n /**\n * Sets the assessment test context object\n * @param {Object} testContext\n */\n setTestContext: function(testContext) {\n this.testContext = testContext;\n this.itemServiceApi = eval(testContext.itemServiceApiCall);\n this.itemServiceApi.setHasBeenPaused(testContext.hasBeenPaused);\n },\n\n\n /**\n * Handles Metadata initialization\n */\n initMetadata: function (){\n testMetaData = testMetaDataFactory({\n testServiceCallId: this.itemServiceApi.serviceCallId\n });\n },\n\n /**\n * Retrieve service responsible for broken session tracking\n * @returns {*}\n */\n getSessionStateService: function () {\n if (!sessionStateService) {\n sessionStateService = this.testContext.sessionStateService({accuracy: 1000});\n }\n return sessionStateService;\n },\n\n /**\n * Updates the GUI\n * @param {Object} testContext\n */\n update: function (testContext) {\n var self = this;\n $controls.$itemFrame.remove();\n\n var $runner = $('#runner');\n $runner.css('height', 'auto');\n\n this.getSessionStateService().restart();\n\n this.setTestContext(testContext);\n this.updateContext();\n this.updateProgress();\n this.updateNavigation();\n this.updateTestReview();\n this.updateInformation();\n this.updateRubrics();\n this.updateTools(testContext);\n this.updateTimer();\n this.updateExitButton();\n this.resetCurrentItemState();\n this.initMetadata();\n\n $controls.$itemFrame = $('