\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 += \"\\n \\n
\"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Translation\", options) : helperMissing.call(depth0, \"__\", \"Translation\", options)))\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, \"Original\", options) : helperMissing.call(depth0, \"__\", \"Original\", options)))\n + \"
\\n
\\n
\\n\";\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 += \"\\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
\\n\";\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, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, functionType=\"function\", self=this;\n\nfunction program1(depth0,data) {\n \n \n return \"
\";\n }\n\nfunction program3(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 }\nfunction program4(depth0,data) {\n \n \n return \" readonly\";\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 + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The translated test title.\", options) : helperMissing.call(depth0, \"__\", \"The translated test title.\", options)))\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 original title of the test.\", options) : helperMissing.call(depth0, \"__\", \"The original title of the test.\", options)))\n + \"\\n
\\n
\\n
\\n\";\n return buffer;\n }\n\nfunction program8(depth0,data) {\n \n var buffer = \"\", stack1, 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, \"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(9, program9, 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(12, program12, 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(18, program18, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\";\n return buffer;\n }\nfunction program9(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(10, program10, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
\\n \";\n return buffer;\n }\nfunction program10(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 program12(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(16, program16, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
\\n
\\n
\\n \";\n return buffer;\n }\nfunction program13(depth0,data) {\n \n var buffer = \"\", stack1, helper;\n buffer += \"\\n \\n \";\n return buffer;\n }\nfunction program14(depth0,data) {\n \n \n return \"selected=\\\"selected\\\"\";\n }\n\nfunction program16(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 program18(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 stack1 = helpers['if'].call(depth0, (depth0 && depth0.translation), {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 stack1 = helpers['if'].call(depth0, (depth0 && depth0.showIdentifier), {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 stack1 = helpers['if'].call(depth0, (depth0 && depth0.translation), {hash:{},inverse:self.program(8, program8, data),fn:self.program(6, program6, 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, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, functionType=\"function\", self=this;\n\nfunction program1(depth0,data) {\n \n \n return \"
\";\n }\n\nfunction program3(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 }\nfunction program4(depth0,data) {\n \n \n return \" readonly\";\n }\n\nfunction program6(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, \"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(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, \"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(11, program11, 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(18, program18, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n \";\n return buffer;\n }\nfunction program7(depth0,data) {\n \n \n return \"checked\";\n }\n\nfunction program9(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 program11(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(12, program12, 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(14, program14, 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(16, program16, 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, \"Checking this box prevents the test-taker from navigating to other items until the current item constraints, if any, are met. Unchecking this box will allow free navigation even if the responses don't comply with the item constraints set.\", options) : helperMissing.call(depth0, \"__\", \"Checking this box prevents the test-taker from navigating to other items until the current item constraints, if any, are met. Unchecking this box will allow free navigation even if the responses don't comply with the item constraints set.\", options)))\n + \"\\n
\\n
\\n
\\n\\n
\\n \";\n return buffer;\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, \"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 program14(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 program16(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 program18(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(19, program19, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
\\n \";\n return buffer;\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, \"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 stack1 = helpers['if'].call(depth0, (depth0 && depth0.translation), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\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 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, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, functionType=\"function\", self=this;\n\nfunction program1(depth0,data) {\n \n \n return \"
\";\n }\n\nfunction program3(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 }\nfunction program4(depth0,data) {\n \n \n return \" readonly\";\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 + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"The translated section title.\", options) : helperMissing.call(depth0, \"__\", \"The translated section title.\", options)))\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 original title of the section.\", options) : helperMissing.call(depth0, \"__\", \"The original title of the section.\", options)))\n + \"\\n
\\n
\\n
\\n\";\n return buffer;\n }\n\nfunction program8(depth0,data) {\n \n var buffer = \"\", stack1, 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, \"The section title.\", options) : helperMissing.call(depth0, \"__\", \"The section title.\", options)))\n + \"\\n
\\n
\\n
\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.isSubsection), {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 stack1 = helpers['if'].call(depth0, (depth0 && depth0.showVisible), {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.showKeepTogether), {hash:{},inverse:self.noop,fn:self.program(13, program13, 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(15, program15, 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(17, program17, 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(19, program19, 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(21, program21, 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(23, program23, 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(25, program25, 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(27, program27, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\";\n return buffer;\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, \"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 program11(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 program13(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 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, \"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 program17(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 program19(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 program21(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 program23(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 program25(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, \"Checking this box prevents the test-taker from navigating to other items until the current item constraints, if any, are met. Unchecking this box will allow free navigation even if the responses don't comply with the item constraints set.\", options) : helperMissing.call(depth0, \"__\", \"Checking this box prevents the test-taker from navigating to other items until the current item constraints, if any, are met. Unchecking this box will allow free navigation even if the responses don't comply with the item constraints set.\", options)))\n + \"\\n
\\n
\\n
\\n \";\n return buffer;\n }\n\nfunction program27(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(28, program28, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
\\n \";\n return buffer;\n }\nfunction program28(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 stack1 = helpers['if'].call(depth0, (depth0 && depth0.translation), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\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 stack1 = helpers['if'].call(depth0, (depth0 && depth0.showIdentifier), {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 stack1 = helpers['if'].call(depth0, (depth0 && depth0.translation), {hash:{},inverse:self.program(8, program8, data),fn:self.program(6, program6, 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 \n return \"
\";\n }\n\nfunction program3(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 }\nfunction program4(depth0,data) {\n \n \n return \" readonly\";\n }\n\nfunction program6(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 program8(depth0,data) {\n \n var buffer = \"\", stack1, 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\\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(9, program9, 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(11, program11, 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(13, program13, 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(15, program15, 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(17, program17, 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(19, program19, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\";\n return buffer;\n }\nfunction program9(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 program11(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 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 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 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, \"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 program17(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, \"Checking this box prevents the test-taker from navigating to other items until the current item constraints, if any, are met. Unchecking this box will allow free navigation even if the responses don't comply with the item constraints set.\", options) : helperMissing.call(depth0, \"__\", \"Checking this box prevents the test-taker from navigating to other items until the current item constraints, if any, are met. Unchecking this box will allow free navigation even if the responses don't comply with the item constraints set.\", options)))\n + \"\\n
\\n
\\n
\\n \";\n return buffer;\n }\n\nfunction program19(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(22, program22, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n
\\n \";\n return buffer;\n }\nfunction program20(depth0,data) {\n \n \n return \"hidden\";\n }\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 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 stack1 = helpers['if'].call(depth0, (depth0 && depth0.translation), {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 = (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(3, program3, 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(6, program6, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\";\n stack1 = helpers.unless.call(depth0, (depth0 && depth0.translation), {hash:{},inverse:self.noop,fn:self.program(8, program8, 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 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, self=this, functionType=\"function\";\n\nfunction program1(depth0,data) {\n \n \n return \"
\";\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 + 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 program5(depth0,data) {\n \n var buffer = \"\", 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, \"Set the rubric block as a feedback block\", options) : helperMissing.call(depth0, \"__\", \"Set the rubric block as a feedback block\", 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 outcome identifier the feedback block is related to\", options) : helperMissing.call(depth0, \"__\", \"Set outcome identifier the feedback block is related to\", 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 value of the outcome that will activate the feedback block\", options) : helperMissing.call(depth0, \"__\", \"Set the value of the outcome that will activate the feedback block\", options)))\n + \"\\n
\\n
\\n
\\n
\\n \";\n return buffer;\n }\n\n buffer += \"\\n \";\n stack1 = helpers['if'].call(depth0, (depth0 && depth0.translation), {hash:{},inverse:self.noop,fn:self.program(1, program1, 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, \"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(3, program3, 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(5, program5, data),data:data});\n if(stack1 || stack1 === 0) { buffer += stack1; }\n buffer += \"\\n\";\n return buffer;\n }); });\n","\ndefine('tpl!taoQtiTest/controller/creator/templates/translation-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, self=this;\n\nfunction program1(depth0,data) {\n \n \n return \"checked\";\n }\n\n buffer += \"\\n\\n \\n
\"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Test translation status\", options) : helperMissing.call(depth0, \"__\", \"Test translation status\", 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, \"Translation completed\", options) : helperMissing.call(depth0, \"__\", \"Translation completed\", options)))\n + \" \\n \\n
\\n
\\n
\\n
\\n \"\n + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, \"Define the status of the translation.\", options) : helperMissing.call(depth0, \"__\", \"Define the status of the translation.\", options)))\n + \"\\n
\\n
\\n
\\n
\\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 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 \";\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 += \"\\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\";\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) 2014-2024 (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/translation-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], function (\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 translationProps,\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 translation: applyTemplateConfiguration(translationProps),\n categorypresets: applyTemplateConfiguration(categoryPresets),\n subsection: applyTemplateConfiguration(sectionProps)\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-2024 (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 components 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 // jQuery 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.configuration-issue';\n const $test = $('.tlb-button-on').parents('.test-creator-test');\n\n // finds error current element if any\n const errors = $(e.currentTarget).find('span.validate-error');\n const currentTargetId = $(e.currentTarget).find('span[data-bind=\"identifier\"]').attr('id');\n const currentTargetIdSelector = `[id=\"${currentTargetId && currentTargetId.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 if (currentTargetId) {\n $(currentTargetIdSelector).find(warningIconSelector).first().css('display', 'none');\n }\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 if (currentTargetId) {\n $(currentTargetIdSelector).find(warningIconSelector).first().css('display', 'inline');\n }\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 createSearchChoice: (category) => category.match(/^[a-zA-Z_][a-zA-Z0-9_-]*$/)\n ? { id: category, text: category }\n : null,\n formatNoMatches: () => __('Category name not allowed'),\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) + '' + modelValue['qti-type'] + '>';\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-2024 (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 'taoQtiTest/controller/creator/encoders/dom2qti',\n 'taoQtiTest/controller/creator/helpers/qtiElement',\n 'taoQtiTest/controller/creator/qtiContentCreator',\n 'ckeditor'\n], function (\n $,\n _,\n __,\n hider,\n dialogAlert,\n namespaceHelper,\n actions,\n Dom2QtiEncoder,\n qtiElementHelper,\n qtiContentCreator\n) {\n 'use strict';\n\n /**\n * The rubricblockView 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(creatorContext, rubricModel, $rubricBlock) {\n const modelOverseer = creatorContext.getModelOverseer();\n const areaBroker = creatorContext.getAreaBroker();\n const $rubricBlockContent = $('.rubricblock-content', $rubricBlock);\n const $rubricBlockOrigin = $('.rubricblock-origin-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 const rubric = qtiElementHelper.lookupElement(rubricModel, 'rubricBlock', 'content');\n const wrapper = qtiElementHelper.lookupElement(rubricModel, 'rubricBlock.div.feedbackBlock', 'content');\n const 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 const rubric = qtiElementHelper.lookupElement(rubricModel, 'rubricBlock', 'content') || {};\n const wrapper = qtiElementHelper.lookupElement(rubricModel, 'rubricBlock.div.feedbackBlock', 'content');\n const content = wrapper ? wrapper.content : rubric.content;\n const html = ensureWrap(Dom2QtiEncoder.encode(content));\n\n // Destroy any existing CKEditor instance\n qtiContentCreator.destroy(creatorContext, $rubricBlockContent).then(() => {\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 * Show the original content of the rubric block.\n */\n function showOriginContent() {\n const rubric = qtiElementHelper.lookupElement(rubricModel, 'rubricBlock', 'originContent') || {};\n const html = ensureWrap(Dom2QtiEncoder.encode(rubric.originContent));\n $rubricBlockOrigin.html(html);\n $rubricBlock.addClass('translation');\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 const activated = feedback && feedback.activated;\n const 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 = [\n qtiElementHelper.create('div', {\n content: [\n qtiElementHelper.create('feedbackBlock', {\n outcomeIdentifier: feedback.outcome,\n identifier: feedback.matchValue,\n content: rubricModel.content\n })\n ]\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 const $view = propView.getView();\n const $feedbackOutcomeLine = $('.rubric-feedback-outcome', $view);\n const $feedbackMatchLine = $('.rubric-feedback-match-value', $view);\n const $feedbackOutcome = $('[name=feedback-outcome]', $view);\n const $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 const activated = rubricModel.feedback && rubricModel.feedback.activated;\n // build the list of outcomes in a way select2 can understand\n const outcomes = _.map(modelOverseer.getOutcomesNames(), 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', () => {\n $feedbackOutcome.val('');\n updateOutcomes();\n });\n bindEvent($rubricBlock, 'outcome-updated', () => {\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 const $select = $('[name=view]', $propContainer);\n\n bindEvent($select.select2({ width: '100%' }), 'select2-removed', () => {\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(\n rubricModel,\n 'rubricBlock.div.feedbackBlock.outcomeIdentifier',\n 'content'\n ),\n matchValue: qtiElementHelper.lookupProperty(\n rubricModel,\n 'rubricBlock.div.feedbackBlock.identifier',\n 'content'\n )\n };\n\n modelOverseer\n .before('scoring-write.' + rubricModel.uid, () => {\n const 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, () => {\n // will notify the user of any removed feedbacks\n if (modelOverseer.changedRubricBlock) {\n /** @todo: provide a way to cancel changes */\n dialogAlert(\n __('Some rubric blocks have been updated to reflect the changes in the list of outcomes.')\n );\n modelOverseer.changedRubricBlock = 0;\n }\n });\n\n actions.properties($rubricBlock, 'rubricblock', rubricModel, propHandler);\n\n modelToEditor();\n if (rubricModel.translation) {\n showOriginContent();\n }\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', () => {\n qtiContentCreator.destroy(creatorContext, $rubricBlockContent);\n });\n\n $rubricBlockContent.on('editorfocus', () => {\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\n .getContentCreatorPanelArea()\n .find('.test-content')\n .on('scroll', () => {\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) 2024 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);\n */\ndefine('taoQtiTest/controller/creator/helpers/translation',['jquery', 'i18n', 'services/translation', 'taoQtiTest/controller/creator/helpers/testModel'], function (\n $,\n __,\n translationService,\n testModelHelper\n) {\n const translationStatusLabels = {\n translating: __('In progress'),\n translated: __('Translation completed'),\n pending: __('Pending'),\n none: __('No translation')\n };\n const translationStatusIcons = {\n translating: 'remove',\n translated: 'success',\n pending: 'info',\n none: 'warning'\n };\n\n /**\n * Process all sections and subsections in the model.\n * @param {object} section\n * @param {function} cb\n */\n function recurseSections(section, cb) {\n cb(section);\n if (section.sectionParts) {\n section.sectionParts.forEach(sectionPart => recurseSections(sectionPart, cb));\n }\n }\n\n /**\n * Get JSON data from a URL.\n * @param {string} url - The URL to get the JSON data from.\n * @returns {Promise}\n */\n const getJSON = url => new Promise((resolve, reject) => $.getJSON(url).done(resolve).fail(reject));\n\n return {\n /**\n * Get the translation status badge info.\n * @param {string} translationStatus - The translation status.\n * @returns {object}\n * @returns {string} object.status - The translation status.\n * @returns {string} object.label - A translated label for the translation status.\n * @returns {string} object.icon - The icon for the translation status.\n */\n getTranslationStatusBadgeInfo(translationStatus) {\n if (!translationStatus) {\n translationStatus = 'none';\n }\n return {\n status: translationStatus,\n label: translationStatusLabels[translationStatus],\n icon: translationStatusIcons[translationStatus]\n };\n },\n\n /**\n * Get the status of the translation.\n * @param {object} data\n * @returns {string}\n */\n getTranslationStatus(data) {\n const translation = data && translationService.getTranslationsProgress(data.resources)[0];\n if (!translation || translation == 'pending') {\n return 'translating';\n }\n return translation;\n },\n\n /**\n * Get the language of the translation.\n * @param {object} data\n * @returns {object}\n * @returns {string} object.value\n * @returns {string} object.literal\n */\n getTranslationLanguage(data) {\n return data && translationService.getTranslationsLanguage(data.resources)[0];\n },\n\n /**\n * Get the translation configuration.\n * @param {string} testUri - The test URI.\n * @param {string} originTestUri - The origin test URI.\n * @returns {Promise}\n */\n getTranslationConfig(testUri, originTestUri) {\n return translationService\n .getTranslations(originTestUri, translation => translation.resourceUri === testUri)\n .then(data => {\n const translation = this.getTranslationStatus(data);\n const language = this.getTranslationLanguage(data);\n\n const config = { translationStatus: translation };\n if (language) {\n config.translationLanguageUri = language.value;\n config.translationLanguageCode = language.literal;\n }\n return config;\n });\n },\n\n /**\n * Update the model from the origin.\n * @param {object} model - The model to update.\n * @param {string} originUrl - The origin URL.\n * @returns {Promise