From d64bb90b657b5f811452fedea7dd023aea6e151f Mon Sep 17 00:00:00 2001 From: Anton Tsymuk Date: Tue, 1 Sep 2020 19:55:16 +0300 Subject: [PATCH 01/12] refactor qti item loader to use es6 --- src/qtiItem/core/Loader.js | 340 +++++++++++++++++++------------------ 1 file changed, 177 insertions(+), 163 deletions(-) diff --git a/src/qtiItem/core/Loader.js b/src/qtiItem/core/Loader.js index 5a2b18a4..3c369a5a 100644 --- a/src/qtiItem/core/Loader.js +++ b/src/qtiItem/core/Loader.js @@ -23,23 +23,51 @@ import qtiClasses from 'taoQtiItem/qtiItem/core/qtiClasses'; import Element from 'taoQtiItem/qtiItem/core/Element'; import xmlNsHandler from 'taoQtiItem/qtiItem/helper/xmlNsHandler'; import moduleLoader from 'core/moduleLoader'; +import responseHelper from 'taoQtiItem/qtiItem/helper/response'; + +/** + * If a property is given as a serialized JSON object, parse it directly to a JS object + */ +const loadPortableCustomElementProperties = (portableElement, rawProperties) => { + var properties = {}; + + _.forOwn(rawProperties, function (value, key) { + try { + properties[key] = JSON.parse(value); + } catch (e) { + properties[key] = value; + } + }); + portableElement.properties = properties; +}; + +const loadPortableCustomElementData = (portableElement, data) => { + portableElement.typeIdentifier = data.typeIdentifier; + portableElement.markup = data.markup; + portableElement.entryPoint = data.entryPoint; + portableElement.libraries = data.libraries; + portableElement.setNamespace('', data.xmlns); + + loadPortableCustomElementProperties(portableElement, data.properties); +}; var Loader = Class.extend({ - init: function(item, classesLocation) { + init(item, classesLocation) { this.qti = {}; //loaded qti classes are store here this.classesLocation = {}; - this.item = item || null; //starts either from scratch or with an existing item object + this.setClassesLocation(classesLocation || qtiClasses); //load default location for qti classes model }, - setClassesLocation: function(qtiClasses) { - _.extend(this.classesLocation, qtiClasses); + setClassesLocation(qtiClassesList) { + _.extend(this.classesLocation, qtiClassesList); + return this; }, - getRequiredClasses: function(data) { - var ret = [], - i; - for (i in data) { + getRequiredClasses(data) { + let ret = []; + + for (let i in data) { if (i === 'qtiClass' && data[i] !== '_container' && i !== 'rootElement') { //although a _container is a concrete class in TAO, it is not defined in QTI standard ret.push(data[i]); @@ -48,113 +76,116 @@ var Loader = Class.extend({ ret = _.union(ret, this.getRequiredClasses(data[i])); } } + return ret; }, - loadRequiredClasses: function(data, callback, reload) { - var i; - var requiredClass, - requiredClasses = this.getRequiredClasses(data, reload), - required = []; + loadRequiredClasses(data, callback, reload) { + let requiredClass; + const requiredClasses = this.getRequiredClasses(data, reload); + const required = []; - for (i in requiredClasses) { + for (let i in requiredClasses) { requiredClass = requiredClasses[i]; if (this.classesLocation[requiredClass]) { required.push({ - module : this.classesLocation[requiredClass], + module: this.classesLocation[requiredClass], category: 'qti' }); } else { - throw new Error('missing qti class location declaration : ' + requiredClass); + throw new Error(`missing qti class location declaration : ${requiredClass}`); } } moduleLoader([], () => true).addList(required).load().then(loadeded => { - loadeded.forEach( QtiClass => { + loadeded.forEach(QtiClass => { this.qti[QtiClass.prototype.qtiClass] = QtiClass; }); callback.call(this, this.qti); }); }, - getLoadedClasses: function() { + getLoadedClasses() { return _.keys(this.qti); }, - loadItemData: function(data, callback) { - var _this = this; - _this.loadRequiredClasses(data, function(Qti) { - var i; + loadItemData(data, callback) { + this.loadRequiredClasses(data, (Qti) => { if (typeof data === 'object' && data.qtiClass === 'assessmentItem') { //unload an item from it's serial (in case of a reload) if (data.serial) { Element.unsetElement(data.serial); } - _this.item = new Qti.assessmentItem(data.serial, data.attributes || {}); - _this.loadContainer(_this.item.getBody(), data.body); + this.item = new Qti.assessmentItem(data.serial, data.attributes || {}); + this.loadContainer(this.item.getBody(), data.body); + + for (let i in data.outcomes) { + const outcome = this.buildOutcome(data.outcomes[i]); - for (i in data.outcomes) { - var outcome = _this.buildOutcome(data.outcomes[i]); if (outcome) { - _this.item.addOutcomeDeclaration(outcome); + this.item.addOutcomeDeclaration(outcome); } } - for (i in data.feedbacks) { - var feedback = _this.buildElement(data.feedbacks[i]); + + for (let i in data.feedbacks) { + const feedback = this.buildElement(data.feedbacks[i]); + if (feedback) { - _this.item.addModalFeedback(feedback); + this.item.addModalFeedback(feedback); } } - for (i in data.stylesheets) { - var stylesheet = _this.buildElement(data.stylesheets[i]); + + for (let i in data.stylesheets) { + const stylesheet = this.buildElement(data.stylesheets[i]); + if (stylesheet) { - _this.item.addStylesheet(stylesheet); + this.item.addStylesheet(stylesheet); } } //important : build responses after all modal feedbacks and outcomes has been loaded, because the simple feedback rules need to reference them - for (i in data.responses) { - var response = _this.buildResponse(data.responses[i]); + for (let i in data.responses) { + const response = this.buildResponse(data.responses[i]); + if (response) { - _this.item.addResponseDeclaration(response); + this.item.addResponseDeclaration(response); + + const feedbackRules = data.responses[i].feedbackRules; - var feedbackRules = data.responses[i].feedbackRules; if (feedbackRules) { - _.forIn(feedbackRules, function(fbData, serial) { - response.feedbackRules[serial] = _this.buildSimpleFeedbackRule(fbData, response); + _.forIn(feedbackRules, (fbData, serial) => { + response.feedbackRules[serial] = this.buildSimpleFeedbackRule(fbData, response); }); } } } if (data.responseProcessing) { - _this.item.setResponseProcessing(_this.buildResponseProcessing(data.responseProcessing)); + this.item.setResponseProcessing(this.buildResponseProcessing(data.responseProcessing)); } - _this.item.setNamespaces(data.namespaces); - _this.item.setSchemaLocations(data.schemaLocations); - _this.item.setApipAccessibility(data.apipAccessibility); + this.item.setNamespaces(data.namespaces); + this.item.setSchemaLocations(data.schemaLocations); + this.item.setApipAccessibility(data.apipAccessibility); } if (typeof callback === 'function') { - callback.call(_this, _this.item); + callback.call(this, this.item); } }); }, - loadAndBuildElement: function(data, callback) { - var _this = this; - - _this.loadRequiredClasses(data, function(Qti) { - var element = _this.buildElement(data); + loadAndBuildElement(data, callback) { + this.loadRequiredClasses(data, () => { + const element = this.buildElement(data); if (typeof callback === 'function') { - callback.call(_this, element); + callback.call(this, element); } }); }, - loadElement: function(element, data, callback) { - var _this = this; - this.loadRequiredClasses(data, function() { - _this.loadElementData(element, data); + loadElement(element, data, callback) { + this.loadRequiredClasses(data, () => { + this.loadElementData(element, data); + if (typeof callback === 'function') { - callback.call(_this, element); + callback.call(this, element); } }); }, @@ -166,33 +197,32 @@ var Loader = Class.extend({ * @param {function} callback * @returns {undefined} */ - loadElements: function(data, callback) { - var _this = this; - - if (_this.item) { - this.loadRequiredClasses(data, function() { - var allElements = _this.item.getComposingElements(); - - for (var i in data) { - var elementData = data[i]; - if (elementData && elementData.qtiClass && elementData.serial) { - //find and update element - if (allElements[elementData.serial]) { - _this.loadElementData(allElements[elementData.serial], elementData); - } + loadElements(data, callback) { + if (!this.item) { + throw new Error('QtiLoader : cannot load elements in empty item'); + } + + this.loadRequiredClasses(data, () => { + const allElements = this.item.getComposingElements(); + + for (let i in data) { + const elementData = data[i]; + + if (elementData && elementData.qtiClass && elementData.serial) { + //find and update element + if (allElements[elementData.serial]) { + this.loadElementData(allElements[elementData.serial], elementData); } } + } - if (typeof callback === 'function') { - callback.call(_this, _this.item); - } - }); - } else { - throw 'QtiLoader : cannot load elements in empty item'; - } + if (typeof callback === 'function') { + callback.call(this, this.item); + } + }); }, - buildResponse: function(data) { - var response = this.buildElement(data); + buildResponse(data) { + const response = this.buildElement(data); response.template = data.howMatch || null; response.defaultValue = data.defaultValue || null; @@ -210,8 +240,8 @@ var Loader = Class.extend({ return response; }, - buildSimpleFeedbackRule: function(data, response) { - var feedbackRule = this.buildElement(data); + buildSimpleFeedbackRule(data, response) { + const feedbackRule = this.buildElement(data); feedbackRule.setCondition(response, data.condition, data.comparedValue || null); @@ -221,23 +251,27 @@ var Loader = Class.extend({ feedbackRule.feedbackElse = this.item.modalFeedbacks[data.feedbackElse] || null; //associate the compared outcome to the feedbacks if applicable - var response = feedbackRule.comparedOutcome; + const comparedOutcome = feedbackRule.comparedOutcome; + if (feedbackRule.feedbackThen) { - feedbackRule.feedbackThen.data('relatedResponse', response); + feedbackRule.feedbackThen.data('relatedResponse', comparedOutcome); } + if (feedbackRule.feedbackElse) { - feedbackRule.feedbackElse.data('relatedResponse', response); + feedbackRule.feedbackElse.data('relatedResponse', comparedOutcome); } return feedbackRule; }, - buildOutcome: function(data) { - var outcome = this.buildElement(data); + buildOutcome(data) { + const outcome = this.buildElement(data); outcome.defaultValue = data.defaultValue || null; + return outcome; }, - buildResponseProcessing: function(data) { - var rp = this.buildElement(data); + buildResponseProcessing(data) { + const rp = this.buildElement(data); + if (data && data.processingType) { if (data.processingType === 'custom') { rp.xml = data.data; @@ -246,49 +280,49 @@ var Loader = Class.extend({ rp.processingType = 'templateDriven'; } } + return rp; }, - loadContainer: function(bodyObject, bodyData) { + loadContainer(bodyObject, bodyData) { if (!Element.isA(bodyObject, '_container')) { - throw 'bodyObject must be a QTI Container'; + throw new Error('bodyObject must be a QTI Container'); } - if ( - bodyData && - typeof bodyData.body === 'string' && - (typeof bodyData.elements === 'array' || typeof bodyData.elements === 'object') - ) { - for (var serial in bodyData.elements) { - var eltData = bodyData.elements[serial]; - //check if class is loaded: - var element = this.buildElement(eltData); - if (element) { - bodyObject.setElement(element, bodyData.body); - } + if (!(bodyData && typeof bodyData.body === 'string' && typeof bodyData.elements === 'object')) { + throw new Error('wrong bodydata format'); + } + + for (let serial in bodyData.elements) { + const eltData = bodyData.elements[serial]; + const element = this.buildElement(eltData); + + //check if class is loaded: + if (element) { + bodyObject.setElement(element, bodyData.body); } - bodyObject.body(xmlNsHandler.stripNs(bodyData.body)); - } else { - throw 'wrong bodydata format'; } + + bodyObject.body(xmlNsHandler.stripNs(bodyData.body)); }, - buildElement: function(elementData) { - var elt = null; - if (elementData && elementData.qtiClass && elementData.serial) { - var className = elementData.qtiClass; - if (this.qti[className]) { - elt = new this.qti[className](elementData.serial); - this.loadElementData(elt, elementData); - } else { - throw 'the qti element class does not exist: ' + className; - } - } else { - throw 'wrong elementData format'; + buildElement(elementData) { + if (!(elementData && elementData.qtiClass && elementData.serial)) { + throw new Error('wrong elementData format'); + } + + const className = elementData.qtiClass; + + if (!this.qti[className]) { + throw new Error(`the qti element class does not exist: ${className}`); } + + const elt = new this.qti[className](elementData.serial); + this.loadElementData(elt, elementData); + return elt; }, - loadElementData: function(element, data) { + loadElementData(element, data) { //merge attributes when loading element data - var attributes = _.defaults(data.attributes || {}, element.attributes || {}); + const attributes = _.defaults(data.attributes || {}, element.attributes || {}); element.setAttributes(attributes); if (element.body && data.body) { @@ -317,7 +351,7 @@ var Loader = Class.extend({ return element; }, - loadInteractionData: function(interaction, data) { + loadInteractionData(interaction, data) { if (Element.isA(interaction, 'blockInteraction')) { if (data.prompt) { this.loadContainer(interaction.prompt.getBody(), data.prompt); @@ -330,25 +364,29 @@ var Loader = Class.extend({ this.loadPciData(interaction, data); } }, - buildInteractionChoices: function(interaction, data) { - //note: Qti.ContainerInteraction (Qti.GapMatchInteraction and Qti.HottextInteraction) has already been parsed by builtElement(interacionData); + buildInteractionChoices(interaction, data) { + // note: Qti.ContainerInteraction (Qti.GapMatchInteraction and Qti.HottextInteraction) has already been parsed by builtElement(interacionData); if (data.choices) { if (Element.isA(interaction, 'matchInteraction')) { - for (var set = 0; set < 2; set++) { + for (let set = 0; set < 2; set++) { if (!data.choices[set]) { - throw 'missing match set #' + set; + throw new Error(`missing match set #${set}`); } - var matchSet = data.choices[set]; - for (var serial in matchSet) { - var choice = this.buildElement(matchSet[serial]); + + const matchSet = data.choices[set]; + + for (let serial in matchSet) { + const choice = this.buildElement(matchSet[serial]); + if (choice) { interaction.addChoice(choice, set); } } } } else { - for (var serial in data.choices) { - var choice = this.buildElement(data.choices[serial]); + for (let serial in data.choices) { + const choice = this.buildElement(data.choices[serial]); + if (choice) { interaction.addChoice(choice); } @@ -357,8 +395,9 @@ var Loader = Class.extend({ if (Element.isA(interaction, 'graphicGapMatchInteraction')) { if (data.gapImgs) { - for (var serial in data.gapImgs) { - var gapImg = this.buildElement(data.gapImgs[serial]); + for (let serial in data.gapImgs) { + const gapImg = this.buildElement(data.gapImgs[serial]); + if (gapImg) { interaction.addGapImg(gapImg); } @@ -367,7 +406,7 @@ var Loader = Class.extend({ } } }, - loadChoiceData: function(choice, data) { + loadChoiceData(choice, data) { if (Element.isA(choice, 'textVariableChoice')) { choice.val(data.text); } else if (Element.isA(choice, 'gapImg')) { @@ -381,8 +420,9 @@ var Loader = Class.extend({ //has already been taken care of in buildElement() } }, - loadObjectData: function(object, data) { + loadObjectData(object, data) { object.setAttributes(data.attributes); + //@todo: manage object like a container if (data._alt) { if (data._alt.qtiClass === 'object') { @@ -392,49 +432,23 @@ var Loader = Class.extend({ } } }, - loadMathData: function(math, data) { + loadMathData(math, data) { math.ns = data.ns || {}; math.setMathML(data.mathML || ''); - _.forIn(data.annotations || {}, function(value, encoding) { + _.forIn(data.annotations || {}, (value, encoding) => { math.setAnnotation(encoding, value); }); }, - loadTooltipData: function(tooltip, data) { + loadTooltipData(tooltip, data) { tooltip.content(data.content); }, - loadPciData: function(pci, data) { + loadPciData(pci, data) { loadPortableCustomElementData(pci, data); }, - loadPicData: function(pic, data) { + loadPicData(pic, data) { loadPortableCustomElementData(pic, data); } }); -function loadPortableCustomElementData(portableElement, data) { - portableElement.typeIdentifier = data.typeIdentifier; - portableElement.markup = data.markup; - portableElement.entryPoint = data.entryPoint; - portableElement.libraries = data.libraries; - portableElement.setNamespace('', data.xmlns); - - loadPortableCustomElementProperties(portableElement, data.properties); -} - -/** - * If a property is given as a serialized JSON object, parse it directly to a JS object - */ -function loadPortableCustomElementProperties(portableElement, rawProperties) { - var properties = {}; - - _.forOwn(rawProperties, function(value, key) { - try { - properties[key] = JSON.parse(value); - } catch (e) { - properties[key] = value; - } - }); - portableElement.properties = properties; -} - export default Loader; From 48bd449d5d936e993aedfe98749e85785f0369b9 Mon Sep 17 00:00:00 2001 From: Anton Tsymuk Date: Sat, 5 Sep 2020 16:35:52 +0300 Subject: [PATCH 02/12] recognize response template and response processing type from response rules --- src/qtiItem/core/Loader.js | 57 ++++++++--- src/qtiItem/helper/response.js | 48 +++++++--- src/qtiItem/helper/responseRules.js | 141 ++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 23 deletions(-) create mode 100644 src/qtiItem/helper/responseRules.js diff --git a/src/qtiItem/core/Loader.js b/src/qtiItem/core/Loader.js index 3c369a5a..c8380fba 100644 --- a/src/qtiItem/core/Loader.js +++ b/src/qtiItem/core/Loader.js @@ -142,8 +142,32 @@ var Loader = Class.extend({ } //important : build responses after all modal feedbacks and outcomes has been loaded, because the simple feedback rules need to reference them + let responseRules = data.responseProcessing && data.responseProcessing.responseRules + ? [...data.responseProcessing.responseRules] + : []; for (let i in data.responses) { - const response = this.buildResponse(data.responses[i]); + const responseIdentifier = data.responses[i].identifier; + const responseRuleItemIndex = responseRules.findIndex(({ responseIf: { + expression: { + expressions: [expression = {}] = [], + } = {} + } = {} }) => expression.attributes + && expression.attributes.identifier === responseIdentifier + || ( + expression.expressions + && expression.expressions[0] + && expression.expressions[0].attributes + && expression.expressions[0].attributes.identifier === responseIdentifier + ) + ); + const [responseRule] = responseRuleItemIndex !== -1 + ? responseRules.splice(responseRuleItemIndex, 1) + : []; + + const response = this.buildResponse( + data.responses[i], + responseRule + ); if (response) { this.item.addResponseDeclaration(response); @@ -159,7 +183,14 @@ var Loader = Class.extend({ } if (data.responseProcessing) { - this.item.setResponseProcessing(this.buildResponseProcessing(data.responseProcessing)); + const customResponseProcessing = responseRules.length > 0 + || ( + this.item.responses + && Object.keys(this.item.responses) + .some((responseKey) => !this.item.responses[responseKey].template) + ); + + this.item.setResponseProcessing(this.buildResponseProcessing(data.responseProcessing, customResponseProcessing)); } this.item.setNamespaces(data.namespaces); this.item.setSchemaLocations(data.schemaLocations); @@ -221,10 +252,14 @@ var Loader = Class.extend({ } }); }, - buildResponse(data) { + buildResponse(data, responseRule) { const response = this.buildElement(data); - response.template = data.howMatch || null; + response.template = responseHelper.getTemplateUriFromName( + responseHelper.getTemplateNameFromResponseRules(data.identifier, responseRule) + ) + || null; + response.defaultValue = data.defaultValue || null; response.correctResponse = data.correctResponses || null; @@ -269,16 +304,14 @@ var Loader = Class.extend({ return outcome; }, - buildResponseProcessing(data) { + buildResponseProcessing(data, customResponseProcessing) { const rp = this.buildElement(data); - if (data && data.processingType) { - if (data.processingType === 'custom') { - rp.xml = data.data; - rp.processingType = 'custom'; - } else { - rp.processingType = 'templateDriven'; - } + if (customResponseProcessing) { + rp.xml = data.data; + rp.processingType = 'custom'; + } else { + rp.processingType = 'templateDriven'; } return rp; diff --git a/src/qtiItem/helper/response.js b/src/qtiItem/helper/response.js index c6e69f4f..57e38269 100644 --- a/src/qtiItem/helper/response.js +++ b/src/qtiItem/helper/response.js @@ -17,40 +17,66 @@ * */ import _ from 'lodash'; +import { responseRules as responseRulesHelper } from 'taoQtiItem/qtiItem/helper/responseRules'; -var _templateNames = { +const _templateNames = { MATCH_CORRECT: 'http://www.imsglobal.org/question/qti_v2p1/rptemplates/match_correct', MAP_RESPONSE: 'http://www.imsglobal.org/question/qti_v2p1/rptemplates/map_response', MAP_RESPONSE_POINT: 'http://www.imsglobal.org/question/qti_v2p1/rptemplates/map_response_point', NONE: 'no_response_processing' }; + export default { - isUsingTemplate: function isUsingTemplate(response, tpl) { + isUsingTemplate(response, tpl) { if (_.isString(tpl)) { if (tpl === response.template || _templateNames[tpl] === response.template) { return true; } } + return false; }, - isValidTemplateName: function isValidTemplateName(tplName) { + isValidTemplateName(tplName) { return !!this.getTemplateUriFromName(tplName); }, - getTemplateUriFromName: function getTemplateUriFromName(tplName) { - if (_templateNames[tplName]) { - return _templateNames[tplName]; - } - return ''; + getTemplateUriFromName(tplName) { + return _templateNames[tplName] || ''; }, - getTemplateNameFromUri: function getTemplateNameFromUri(tplUri) { - var tplName = ''; - _.forIn(_templateNames, function(uri, name) { + getTemplateNameFromUri(tplUri) { + let tplName = ''; + + _.forIn(_templateNames, (uri, name) => { if (uri === tplUri) { tplName = name; return false; } }); + return tplName; + }, + getTemplateNameFromResponseRules(responseIdentifier, responseRules) { + if (!responseRules) { + return 'NONE'; + } + + const { + responseIf: { + responseRules: [outcomeRules = {}] = [], + } = {} + } = responseRules; + const { + attributes: { + identifier: outcomeIdentifier, + } = {}, + } = outcomeRules; + + if (!outcomeRules) { + return ''; + } + + return Object.keys(responseRulesHelper).find( + (key) => _.isEqual(responseRules, responseRulesHelper[key](responseIdentifier, outcomeIdentifier)) + ); } }; diff --git a/src/qtiItem/helper/responseRules.js b/src/qtiItem/helper/responseRules.js new file mode 100644 index 00000000..ad2ae040 --- /dev/null +++ b/src/qtiItem/helper/responseRules.js @@ -0,0 +1,141 @@ +export const responseRules = { + MATCH_CORRECT: (responseIdentifier, outcomeIdentifier) => ({ + qtiClass: 'responseCondition', + responseIf: { + qtiClass: 'responseIf', + expression: { + qtiClass: 'match', + expressions: [ + { + qtiClass: 'variable', + attributes: { + identifier: responseIdentifier, + }, + }, + { + qtiClass: 'correct', + attributes: { + identifier: responseIdentifier, + }, + }, + ], + }, + responseRules: [ + { + qtiClass: 'setOutcomeValue', + attributes: { + identifier: outcomeIdentifier, + }, + expression: { + qtiClass: 'sum', + expressions: [ + { + qtiClass: 'variable', + attributes: { + identifier: outcomeIdentifier, + }, + }, + { + qtiClass: 'baseValue', + attributes: { + baseType: 'integer' + }, + value: '1', + }, + ], + }, + }, + ], + }, + }), + MAP_RESPONSE: (responseIdentifier, outcomeIdentifier) => ({ + qtiClass: 'responseCondition', + responseIf: { + qtiClass: 'responseIf', + expression: { + qtiClass: 'not', + expressions: [ + { + qtiClass: 'isNull', + expressions: [{ + qtiClass: 'variable', + attributes: { + identifier: responseIdentifier, + }, + }], + }, + ], + }, + responseRules: [ + { + qtiClass: 'setOutcomeValue', + attributes: { + identifier: outcomeIdentifier, + }, + expression: { + qtiClass: 'sum', + expressions: [ + { + qtiClass: 'variable', + attributes: { + identifier: outcomeIdentifier, + } + }, + { + qtiClass: 'mapResponse', + attributes: { + identifier: responseIdentifier, + }, + }, + ], + }, + }, + ], + }, + }), + MAP_RESPONSE_POINT: (responseIdentifier, outcomeIdentifier) => ({ + qtiClass: 'responseCondition', + responseIf: { + qtiClass: 'responseIf', + expression: { + qtiClass: 'not', + expressions: [ + { + qtiClass: 'isNull', + expressions: [{ + qtiClass: 'variable', + attributes: { + identifier: responseIdentifier, + }, + }], + }, + ], + }, + responseRules: [ + { + qtiClass: 'setOutcomeValue', + attributes: { + identifier: outcomeIdentifier, + }, + expression: { + qtiClass: 'sum', + expressions: [ + { + qtiClass: 'variable', + attributes: { + identifier: outcomeIdentifier, + } + }, + { + qtiClass: 'mapResponsePoint', + attributes: { + identifier: responseIdentifier, + }, + }, + ], + }, + }, + ], + }, + }), +}; From 529a06b81865a2d6a7cc08bcd6c6d2106649081e Mon Sep 17 00:00:00 2001 From: Anton Tsymuk Date: Sat, 5 Sep 2020 16:36:16 +0300 Subject: [PATCH 03/12] add unit tests --- test/qtiItem/core/loader/test.html | 22 + test/qtiItem/core/loader/test.js | 1118 +++++++++++++++++++ test/qtiItem/core/loader/testQtiClass.js | 11 + test/qtiItem/helper/response/test.html | 21 + test/qtiItem/helper/response/test.js | 159 +++ test/qtiItem/helper/responseRules/test.html | 21 + test/qtiItem/helper/responseRules/test.js | 197 ++++ 7 files changed, 1549 insertions(+) create mode 100644 test/qtiItem/core/loader/test.html create mode 100644 test/qtiItem/core/loader/test.js create mode 100644 test/qtiItem/core/loader/testQtiClass.js create mode 100644 test/qtiItem/helper/response/test.html create mode 100644 test/qtiItem/helper/response/test.js create mode 100644 test/qtiItem/helper/responseRules/test.html create mode 100644 test/qtiItem/helper/responseRules/test.js diff --git a/test/qtiItem/core/loader/test.html b/test/qtiItem/core/loader/test.html new file mode 100644 index 00000000..f5d1bf05 --- /dev/null +++ b/test/qtiItem/core/loader/test.html @@ -0,0 +1,22 @@ + + + + + QTI Item loader + + + + +
+
+ + diff --git a/test/qtiItem/core/loader/test.js b/test/qtiItem/core/loader/test.js new file mode 100644 index 00000000..1af05596 --- /dev/null +++ b/test/qtiItem/core/loader/test.js @@ -0,0 +1,1118 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; under version 2 + * of the License (non-upgradable). + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (c) 2016 (original work) Open Assessment Technologies SA + **/ +define([ + 'taoQtiItem/qtiItem/core/Loader', + 'taoQtiItem/test/qtiItem/core/loader/testQtiClass', + 'taoQtiItem/qtiItem/core/Container', + 'taoQtiItem/qtiItem/core/Tooltip', + 'taoQtiItem/qtiItem/core/PortableInfoControl', + 'taoQtiItem/qtiItem/core/interactions/CustomInteraction', + 'taoQtiItem/qtiItem/core/Math', + 'taoQtiItem/qtiItem/core/choices/TextVariableChoice', + 'taoQtiItem/qtiItem/core/choices/GapText', + 'taoQtiItem/qtiItem/core/interactions/ChoiceInteraction', + 'taoQtiItem/qtiItem/core/interactions/MatchInteraction', + 'taoQtiItem/qtiItem/core/interactions/GraphicGapMatchInteraction', + 'taoQtiItem/qtiItem/core/variables/ResponseDeclaration', + 'taoQtiItem/qtiItem/helper/responseTemplate', +], function (...args) { + const [ + QtiItemLoader, + TestQtiClass, + ContainerQtiClass, + TooltipQtiClass, + InfoControlQtiClass, + CustomInteractionQtiClass, + MathQtiClass, + TextVariableChoiceQtiClass, + GapTextQtiClass, + ChoiceInteractionQtiClass, + MatchInteractionQtiClass, + GraphicGapMatchInteractionQtiClass, + ResponseDeclarationQtiClass, + responseTemplateHelper, + ] = args; + + QUnit.module('QTI item loader'); + + QUnit.test('loader module', function (assert) { + assert.equal(typeof QtiItemLoader, 'function', 'The pluginFactory module exposes a function'); + assert.equal(typeof new QtiItemLoader(), 'object', 'The plugin factory produces an instance'); + }); + + const pluginApi = [ + { + name: 'init', + title: 'init', + }, + { + name: 'setClassesLocation', + title: 'setClassesLocation', + }, + { + name: 'getRequiredClasses', + title: 'getRequiredClasses', + }, + { + name: 'loadRequiredClasses', + title: 'loadRequiredClasses', + }, + { + name: 'getLoadedClasses', + title: 'getLoadedClasses', + }, + { + name: 'loadItemData', + title: 'loadItemData', + }, + { + name: 'loadAndBuildElement', + title: 'loadAndBuildElement', + }, + { + name: 'loadElement', + title: 'loadElement', + }, + { + name: 'loadElements', + title: 'loadElements', + }, + { + name: 'buildResponse', + title: 'buildResponse', + }, + { + name: 'buildSimpleFeedbackRule', + title: 'buildSimpleFeedbackRule', + }, + { + name: 'buildOutcome', + title: 'buildOutcome', + }, + { + name: 'buildResponseProcessing', + title: 'buildResponseProcessing', + }, + { + name: 'loadContainer', + title: 'loadContainer', + }, + { + name: 'buildElement', + title: 'buildElement', + }, + { + name: 'loadElementData', + title: 'loadElementData', + }, + { + name: 'loadInteractionData', + title: 'loadInteractionData', + }, + { + name: 'buildInteractionChoices', + title: 'buildInteractionChoices', + }, + { + name: 'loadChoiceData', + title: 'loadChoiceData', + }, + { + name: 'loadObjectData', + title: 'loadObjectData', + }, + { + name: 'loadMathData', + title: 'loadMathData', + }, + { + name: 'loadTooltipData', + title: 'loadTooltipData', + }, + { + name: 'loadPciData', + title: 'loadPciData', + }, + { + name: 'loadPicData', + title: 'loadPicData', + }, + ]; + QUnit.cases.init(pluginApi).test('loader API ', function (data, assert) { + const loader = new QtiItemLoader(); + + assert.equal( + typeof loader[data.name], + 'function', + `The pluginFactory instances expose a "${data.name}" function` + ); + }); + + QUnit.test('init', function (assert) { + const item = { foo: 'bar' }; + const loader = new QtiItemLoader(item); + const { classesLocation, item: loaderItem, qti } = loader; + + assert.equal( + typeof classesLocation, + 'object', + 'The loader init classesLocation' + ); + assert.equal( + typeof qti, + 'object', + 'The loader init qti' + ); + assert.deepEqual( + loaderItem, + item, + 'The loader init item' + ); + }); + + QUnit.test('setClassesLocation', function (assert) { + const initialClassesLocation = { foo: 'bar' }; + const loader = new QtiItemLoader(null, initialClassesLocation); + const additionalClassesLocation = { test: 'test' }; + + loader.setClassesLocation(additionalClassesLocation); + + const { classesLocation } = loader; + assert.deepEqual( + classesLocation, + { + foo: 'bar', + test: 'test', + }, + 'setClassesLocation extends classesLocation' + ); + }); + + QUnit.test('getRequiredClasses', function (assert) { + const loader = new QtiItemLoader(); + const data = { + qtiClass: 'test', + foo: { + qtiClass: 'test1', + bar: { + qtiClass: 'test2', + } + } + }; + + const actual = loader.getRequiredClasses(data); + + assert.deepEqual( + actual, + ['test', 'test1', 'test2'], + 'getRequiredClasses returns list of qti classes' + ); + }); + + QUnit.test('loadRequiredClasses', function (assert) { + const ready = assert.async(); + const initialClassesLocation = { testQtiClass: 'taoQtiItem/test/qtiItem/core/loader/testQtiClass' }; + const loader = new QtiItemLoader(null, initialClassesLocation); + const data = { + qtiClass: 'testQtiClass', + }; + + loader.loadRequiredClasses(data, ({ testQtiClass }) => { + assert.equal( + typeof testQtiClass, + 'function', + 'loadRequiredClasses load required classes' + ); + + ready(); + }); + }); + + QUnit.test('getLoadedClasses', function (assert) { + const ready = assert.async(); + const initialClassesLocation = { testQtiClass: 'taoQtiItem/test/qtiItem/core/loader/testQtiClass' }; + const loader = new QtiItemLoader(null, initialClassesLocation); + const data = { + qtiClass: 'testQtiClass', + }; + + loader.loadRequiredClasses(data, () => { + const [testQtiClassName] = loader.getLoadedClasses(); + + assert.equal( + testQtiClassName, + 'testQtiClass', + 'getLoadedClasses loaded qti classes' + ); + + ready(); + }); + }); + + QUnit.test('loadObjectData', function (assert) { + const loader = new QtiItemLoader(); + const additionalClassesLocation = { testQtiClass: 'taoQtiItem/test/qtiItem/core/loader/testQtiClass' }; + const element = new TestQtiClass(); + const data = { + attributes: { + foo: 'bar', + }, + }; + const altQtiClass = { + foo: 'bar', + }; + + loader.setClassesLocation(additionalClassesLocation); + loader.loadObjectData(element, data); + + assert.deepEqual( + element.attributes, + data.attributes, + 'loadObjectData assign attributes to element' + ); + + loader.loadObjectData(element, Object.assign({}, data, { _alt: altQtiClass })); + + assert.deepEqual( + element._alt, + altQtiClass, + 'loadObjectData assign _alt to element' + ); + }); + + QUnit.test('loadContainer', function (assert) { + const ready = assert.async(); + const loader = new QtiItemLoader(); + const additionalClassesLocation = { testQtiClass: 'taoQtiItem/test/qtiItem/core/loader/testQtiClass' }; + const element = new ContainerQtiClass(); + const testElemnt = new TestQtiClass(); + const data = { + body: 'test', + elements: { + testQtiElement: { + qtiClass: 'testQtiClass', + serial: 'testQtiClass', + }, + }, + }; + + loader.setClassesLocation(additionalClassesLocation); + + assert.throws( + () => loader.loadContainer(testElemnt), + new Error('bodyObject must be a QTI Container'), + 'throws error in case if element is not container' + ); + + assert.throws( + () => loader.loadContainer(element), + new Error('wrong bodydata format'), + 'throws error in case if there is no body data' + ); + + loader.loadRequiredClasses({ qtiClass: 'testQtiClass' }, () => { + loader.loadContainer(element, data); + + assert.equal( + element.elements.testQtiClass.serial, + 'testQtiClass', + 'loadContainer load container elements' + ); + + assert.equal( + element.elements.testQtiClass.serial, + 'testQtiClass', + 'loadContainer load container body' + ); + + ready(); + }); + }); + + QUnit.test('buildElement', function (assert) { + const ready = assert.async(); + const loader = new QtiItemLoader(); + const additionalClassesLocation = { testQtiClass: 'taoQtiItem/test/qtiItem/core/loader/testQtiClass' }; + const data = { + qtiClass: 'testQtiClass', + serial: 'buildElement', + }; + + loader.setClassesLocation(additionalClassesLocation); + + assert.throws( + () => loader.buildElement(), + new Error('wrong elementData format'), + 'throws error in case if element data in wron format' + ); + + assert.throws( + () => loader.buildElement(data), + new Error('the qti element class does not exist: testQtiClass'), + 'throws error in case if qti class does not exist' + ); + + loader.loadRequiredClasses({ qtiClass: 'testQtiClass' }, () => { + const element = loader.buildElement(data); + + assert.equal( + element.serial, + 'buildElement', + 'loadContainer build qti element' + ); + + ready(); + }); + }); + + QUnit.test('loadAndBuildElement', function (assert) { + const ready = assert.async(); + const loader = new QtiItemLoader(); + const additionalClassesLocation = { testQtiClass: 'taoQtiItem/test/qtiItem/core/loader/testQtiClass' }; + const data = { + qtiClass: 'testQtiClass', + serial: 'loadAndBuildElement', + }; + + loader.setClassesLocation(additionalClassesLocation); + + loader.loadAndBuildElement(data, (element) => { + assert.equal( + element.serial, + 'loadAndBuildElement', + 'loadAndBuildElement load qti class and build qti element' + ); + + ready(); + }); + }); + + QUnit.test('loadTooltipData', function (assert) { + const loader = new QtiItemLoader(); + const element = new TooltipQtiClass(); + const data = { + content: 'test', + }; + + loader.loadTooltipData(element, data); + + assert.equal( + element.content(), + 'test', + 'loadAndBuildElement assign content to element' + ); + }); + + QUnit.test('loadPicData', function (assert) { + const loader = new QtiItemLoader(); + const element = new InfoControlQtiClass(); + const data = { + entryPoint: 'testEntryPoint', + libraries: 'testLibraries', + markup: 'testMarkup', + properties: { + prop: 'test', + propJson: '{ "foo": "bar" }', + }, + typeIdentifier: 'testTypeIdentifier', + xmlns: 'testXmlns', + }; + + loader.loadPicData(element, data); + + assert.deepEqual( + { + entryPoint: element.entryPoint, + libraries: element.libraries, + markup: element.markup, + xmlns: element.ns.uri, + typeIdentifier: element.typeIdentifier, + }, + { + entryPoint: 'testEntryPoint', + libraries: 'testLibraries', + markup: 'testMarkup', + xmlns: 'testXmlns', + typeIdentifier: 'testTypeIdentifier', + }, + 'loadPicData assign data to element' + ); + + assert.deepEqual( + element.properties, + { + prop: 'test', + propJson: { + foo: 'bar', + } + }, + 'loadPicData map properties' + ); + }); + + QUnit.test('loadPciData', function (assert) { + const loader = new QtiItemLoader(); + const element = new CustomInteractionQtiClass(); + const data = { + entryPoint: 'testEntryPoint', + libraries: 'testLibraries', + markup: 'testMarkup', + properties: { + prop: 'test', + propJson: '{ "foo": "bar" }', + }, + typeIdentifier: 'testTypeIdentifier', + xmlns: 'testXmlns', + }; + + loader.loadPciData(element, data); + + assert.deepEqual( + { + entryPoint: element.entryPoint, + libraries: element.libraries, + markup: element.markup, + xmlns: element.ns.uri, + typeIdentifier: element.typeIdentifier, + }, + { + entryPoint: 'testEntryPoint', + libraries: 'testLibraries', + markup: 'testMarkup', + xmlns: 'testXmlns', + typeIdentifier: 'testTypeIdentifier', + }, + 'loadPciData assign data to element' + ); + + assert.deepEqual( + element.properties, + { + prop: 'test', + propJson: { + foo: 'bar', + } + }, + 'loadPciData map properties' + ); + }); + + QUnit.test('loadMathData', function (assert) { + const loader = new QtiItemLoader(); + const element = new MathQtiClass(); + const data = { + mathML: 'testMathML', + annotations: { + foo: 'bar', + }, + }; + + loader.loadMathData(element, data); + + assert.equal( + element.mathML, + 'testMathML', + 'loadMathData assing loadMathData' + ); + + assert.deepEqual( + element.annotations, + { + foo: 'bar', + }, + 'loadMathData assing annotations' + ); + }); + + QUnit.test('loadChoiceData', function (assert) { + const loader = new QtiItemLoader(); + const textChoiceElement = new TextVariableChoiceQtiClass(); + const gapTextElement = new GapTextQtiClass(); + const data = { + text: 'testText', + }; + + loader.loadChoiceData(textChoiceElement, data); + loader.loadChoiceData(gapTextElement, data); + + assert.equal( + textChoiceElement.text, + 'testText', + 'loadChoiceData assign value to TextVariableChoice element' + ); + + assert.equal( + gapTextElement.bdy.bdy, + 'testText', + 'loadChoiceData assign bdy to GapText element' + ); + }); + + QUnit.test('buildInteractionChoices', function (assert) { + const ready = assert.async(); + const loader = new QtiItemLoader(); + const choiceInteractionElement = new ChoiceInteractionQtiClass(); + const matchInteractionElement = new MatchInteractionQtiClass(); + const graphicGapMatchInteractionElement = new GraphicGapMatchInteractionQtiClass(); + const qtiData = { + qtiClass: 'simpleChoice', + gapImg: { + qtiClass: 'gapImg', + }, + }; + const choiceInteractionData = { + choices: { + choiceInteractionChoice: { + qtiClass: 'simpleChoice', + serial: 'choiceInteractionChoice', + }, + }, + }; + const matchInteractionData = { + choices: [ + { + matchInteractionChoice0: { + qtiClass: 'simpleChoice', + serial: 'matchInteractionChoice0', + }, + }, + { + matchInteractionChoice1: { + qtiClass: 'simpleChoice', + serial: 'matchInteractionChoice1', + }, + } + ], + }; + const graphicGapMatchInteractionData = { + gapImgs: { + gapImg: { + qtiClass: 'gapImg', + serial: 'gapImg', + } + }, + choices: { + graphicGapMatchInteractionChoice: { + qtiClass: 'simpleChoice', + serial: 'graphicGapMatchInteractionChoice', + }, + }, + }; + + loader.loadRequiredClasses(qtiData, () => { + loader.buildInteractionChoices(choiceInteractionElement, choiceInteractionData); + loader.buildInteractionChoices(matchInteractionElement, matchInteractionData); + loader.buildInteractionChoices(graphicGapMatchInteractionElement, graphicGapMatchInteractionData); + + assert.equal( + choiceInteractionElement.choices.choiceInteractionChoice.serial, + 'choiceInteractionChoice', + 'buildInteractionChoices assign choices to choiceInteraction' + ); + + assert.equal( + matchInteractionElement.choices[0].matchInteractionChoice0.serial, + 'matchInteractionChoice0', + 'buildInteractionChoices assign choices to matchInteraction' + ); + + assert.equal( + matchInteractionElement.choices[1].matchInteractionChoice1.serial, + 'matchInteractionChoice1', + 'buildInteractionChoices assign choices to matchInteraction' + ); + + assert.equal( + graphicGapMatchInteractionElement.choices.graphicGapMatchInteractionChoice.serial, + 'graphicGapMatchInteractionChoice', + 'buildInteractionChoices assign choices to graphicGapMatchInteraction' + ); + + assert.equal( + graphicGapMatchInteractionElement.gapImgs.gapImg.serial, + 'gapImg', + 'buildInteractionChoices assign choices to graphicGapMatchInteraction' + ); + + ready(); + }); + }); + + QUnit.test('loadInteractionData', function (assert) { + const loader = new QtiItemLoader(); + const element = new CustomInteractionQtiClass(); + const data = { + entryPoint: 'testEntryPoint', + libraries: 'testLibraries', + markup: 'testMarkup', + properties: {}, + typeIdentifier: 'testTypeIdentifier', + xmlns: 'testXmlns', + }; + + loader.loadInteractionData(element, data); + + assert.deepEqual( + { + entryPoint: element.entryPoint, + libraries: element.libraries, + markup: element.markup, + xmlns: element.ns.uri, + typeIdentifier: element.typeIdentifier, + }, + { + entryPoint: 'testEntryPoint', + libraries: 'testLibraries', + markup: 'testMarkup', + xmlns: 'testXmlns', + typeIdentifier: 'testTypeIdentifier', + }, + 'loadInteractionData load data for pci interaction' + ); + }); + + QUnit.test('loadElementData', function (assert) { + const loader = new QtiItemLoader(); + const element = new CustomInteractionQtiClass(); + const data = { + attributes: { + foo: 'bar', + }, + }; + + loader.loadElementData(element, data); + + assert.deepEqual( + element.attributes, + { + foo: 'bar', + }, + 'loadElementData assign attributes' + ); + }); + + QUnit.test('loadElement', function (assert) { + const ready = assert.async(); + const loader = new QtiItemLoader(); + const choiceInteractionElement = new ChoiceInteractionQtiClass(); + const choiceInteractionData = { + choices: { + loadElementChoice: { + qtiClass: 'simpleChoice', + serial: 'loadElementChoice', + }, + }, + }; + + loader.loadElement(choiceInteractionElement, choiceInteractionData, (element) => { + assert.equal( + element.choices.loadElementChoice.serial, + 'loadElementChoice', + 'loadElement load choice interaction data' + ); + + ready(); + }); + }); + + QUnit.test('buildOutcome', function (assert) { + const ready = assert.async(); + const initialClassesLocation = { testQtiClass: 'taoQtiItem/test/qtiItem/core/loader/testQtiClass' }; + const loader = new QtiItemLoader(null, initialClassesLocation); + const qtiData = { + qtiClass: 'testQtiClass', + }; + const data = { + defaultValue: 'testDefaultValue', + qtiClass: 'testQtiClass', + serial: 'buildOutcomeElement', + }; + + loader.loadRequiredClasses(qtiData, () => { + const outcomeElement = loader.buildOutcome(data); + + assert.equal( + outcomeElement.defaultValue, + 'testDefaultValue', + 'buildOutcome assign default value' + ); + + ready(); + }); + }); + + QUnit.test('buildResponseProcessing', function (assert) { + const ready = assert.async(); + const initialClassesLocation = { testQtiClass: 'taoQtiItem/test/qtiItem/core/loader/testQtiClass' }; + const loader = new QtiItemLoader(null, initialClassesLocation); + const qtiData = { + qtiClass: 'testQtiClass', + }; + const data = { + qtiClass: 'testQtiClass', + serial: 'buildResponseProcessingElement', + }; + const customData = { + data: 'testData', + qtiClass: 'testQtiClass', + serial: 'customBuildResponseProcessingElement', + }; + + loader.loadRequiredClasses(qtiData, () => { + const responseProcessing = loader.buildResponseProcessing(data); + const customResponseProcessing = loader.buildResponseProcessing(customData, true); + + assert.equal( + responseProcessing.processingType, + 'templateDriven', + 'buildResponseProcessing buld response prcessing' + ); + + assert.equal( + customResponseProcessing.processingType, + 'custom', + 'buildResponseProcessing buld custom response prcessing' + ); + + assert.equal( + customResponseProcessing.xml, + 'testData', + 'buildResponseProcessing assign xml to custom response prcessing' + ); + + ready(); + }); + }); + + QUnit.test('buildSimpleFeedbackRule', function (assert) { + const ready = assert.async(); + const initialClassesLocation = { + _simpleFeedbackRule: 'taoQtiItem/qtiItem/core/response/SimpleFeedbackRule' + }; + const loader = new QtiItemLoader(null, initialClassesLocation); + const responseDeclaration = new ResponseDeclarationQtiClass(); + const qtiData = { + qtiClass: '_simpleFeedbackRule', + }; + const data = { + qtiClass: '_simpleFeedbackRule', + serial: 'buildSimpleFeedbackRule', + feedbackOutcome: 'testFeedbackOutcome', + feedbackThen: 'testFeedbackThen', + feedbackElse: 'testFeedbackElse', + condition: 'correct', + }; + loader.item = { + outcomes: { + testFeedbackOutcome: 'testFeedbackOutcome', + }, + modalFeedbacks: { + testFeedbackThen: { + data: () => assert.ok( + true, + 'buildSimpleFeedbackRule set data to feedback then' + ), + }, + testFeedbackElse: { + data: () => assert.ok( + true, + 'buildSimpleFeedbackRule set data to feedback else' + ), + }, + }, + }; + + loader.loadRequiredClasses(qtiData, () => { + const element = loader.buildSimpleFeedbackRule(data, responseDeclaration); + + assert.equal( + element.condition, + 'correct', + 'buildSimpleFeedbackRule assign condition to feedback rule' + ); + assert.equal( + element.feedbackOutcome, + 'testFeedbackOutcome', + 'buildSimpleFeedbackRule assign feedback outcome to feedback rule' + ); + + ready(); + }); + }); + + QUnit.test('buildResponse', function (assert) { + const ready = assert.async(); + const initialClassesLocation = { testQtiClass: 'taoQtiItem/test/qtiItem/core/loader/testQtiClass' }; + const loader = new QtiItemLoader(null, initialClassesLocation); + const qtiData = { + qtiClass: 'testQtiClass', + }; + const data = { + correctResponses: 'testCorrectResponse', + defaultValue: 'testDefaultValue', + identifier: 'testIdentifier', + mapping: { foo: 'bar' }, + mappingAttributes: 'testMappingAttributes', + qtiClass: 'testQtiClass', + serial: 'buildResponse', + }; + + loader.loadRequiredClasses(qtiData, () => { + const element = loader.buildResponse(data); + + assert.equal( + element.template, + 'no_response_processing', + 'buildResponse assign template to response' + ); + + assert.equal( + element.defaultValue, + 'testDefaultValue', + 'buildResponse assign default value to response' + ); + + assert.equal( + element.correctResponse, + 'testCorrectResponse', + 'buildResponse assign correct response to response' + ); + + assert.deepEqual( + element.mapEntries, + { foo: 'bar' }, + 'buildResponse assign map entries to response' + ); + + assert.equal( + element.mappingAttributes, + 'testMappingAttributes', + 'buildResponse assign mapping attributes to response' + ); + + ready(); + }); + }); + + QUnit.test('loadElements', function (assert) { + const ready = assert.async(); + const initialClassesLocation = { testQtiClass: 'taoQtiItem/test/qtiItem/core/loader/testQtiClass' }; + const loader = new QtiItemLoader(null, initialClassesLocation); + const testElement = new TestQtiClass('loadElements'); + const data = { + testElement: { + attributes: 'test attributes', + qtiClass: 'testQtiClass', + serial: 'loadElements', + } + }; + const composingElements = { + loadElements: testElement, + }; + loader.item = { + getComposingElements: () => composingElements, + }; + + loader.loadElements(data, () => { + assert.equal( + testElement.attributes, + 'test attributes', + 'loadElements load elements data' + ); + + loader.item = null; + + assert.throws( + () => loader.loadElements(), + new Error('QtiLoader : cannot load elements in empty item'), + 'throws error in case if item is not initialized' + ); + + ready(); + }); + }); + + QUnit.test('loadItemData', function (assert) { + const ready = assert.async(); + const loader = new QtiItemLoader(); + const customData = { + qtiClass: 'customInteraction', + serial: 'customLoadItemData', + }; + const data = { + apipAccessibility: 'testApipAccessibility', + body: { + body: 'testBody', + elements: {}, + }, + feedbacks: { + testFeedback: { + qtiClass: 'modalFeedback', + serial: 'loadItemDataTestFeedback', + }, + }, + namespaces: 'testNamespace', + outcomes: { + testOutcome: { + identifier: 'testoutcome', + qtiClass: 'outcomeDeclaration', + serial: 'loadItemDataTestOutcome', + }, + }, + qtiClass: 'assessmentItem', + schemaLocations: 'testSchemaLocations', + serial: 'loadItemData', + stylesheets: { + testStyleSheets: { + qtiClass: 'stylesheet', + serial: 'loadItemDataTestStyleSheet', + }, + }, + responseProcessing: { + qtiClass: 'responseProcessing', + responseRules: [ + responseTemplateHelper.responseTemplates.MATCH_CORRECT('testresponse', 'testoutcome'), + ], + serial: 'loadItemDataResponseProcessing', + }, + responses: { + loadItemDataResponse: { + identifier: 'testresponse', + qtiClass: 'responseDeclaration', + serial: 'loadItemDataResponse', + } + } + }; + + loader.loadItemData(customData, (emptyItem) => { + assert.equal( + emptyItem, + null, + 'loadItemData does not initialize item in case of wrong qti class' + ); + + loader.loadItemData(data, (item) => { + assert.equal( + item.serial, + 'loadItemData', + 'loadItemData load item' + ); + + assert.equal( + item.bdy.bdy, + 'testBody', + 'loadItemData load container' + ); + + assert.equal( + item.outcomes.loadItemDataTestOutcome.serial, + 'loadItemDataTestOutcome', + 'loadItemData load outcomes' + ); + + assert.equal( + item.modalFeedbacks.loadItemDataTestFeedback.serial, + 'loadItemDataTestFeedback', + 'loadItemData load feedbacks' + ); + + assert.equal( + item.stylesheets.loadItemDataTestStyleSheet.serial, + 'loadItemDataTestStyleSheet', + 'loadItemData load style sheets' + ); + + assert.equal( + item.responses.loadItemDataResponse.serial, + 'loadItemDataResponse', + 'loadItemData load responses' + ); + + assert.equal( + item.responseProcessing.serial, + 'loadItemDataResponseProcessing', + 'loadItemData load response processing' + ); + + assert.equal( + item.responseProcessing.processingType, + 'templateDriven', + 'loadItemData recognize response processing type' + ); + + assert.equal( + item.namespaces, + 'testNamespace', + 'loadItemData assign namespace' + ); + + assert.equal( + item.schemaLocations, + 'testSchemaLocations', + 'loadItemData assign schemaLocations' + ); + + assert.equal( + item.apipAccessibility, + 'testApipAccessibility', + 'loadItemData assign apipAccessibility' + ); + + ready(); + }); + }); + }); + + QUnit.test('loadItemData::customResponseProcessing', function (assert) { + const ready = assert.async(); + const loader = new QtiItemLoader(); + const data = { + body: { + body: 'testBody', + elements: {}, + }, + qtiClass: 'assessmentItem', + serial: 'loadItemDataCustomResponseProcessing', + responseProcessing: { + qtiClass: 'responseProcessing', + responseRules: [ + responseTemplateHelper.responseTemplates.MATCH_CORRECT('testresponse', 'testoutcome'), + responseTemplateHelper.responseTemplates.MATCH_CORRECT('testresponse1', 'testoutcome1'), + ], + serial: 'loadItemDataCustomResponseProcessingResponseProcessing', + }, + responses: { + loadItemDataResponse: { + identifier: 'testresponse', + qtiClass: 'responseDeclaration', + serial: 'lloadItemDataCustomResponseProcessingResponse', + } + } + }; + + loader.loadItemData(data, (item) => { + assert.equal( + item.responseProcessing.processingType, + 'custom', + 'loadItemData recognize response processing type' + ); + + ready(); + }); + }); +}); diff --git a/test/qtiItem/core/loader/testQtiClass.js b/test/qtiItem/core/loader/testQtiClass.js new file mode 100644 index 00000000..96a59c40 --- /dev/null +++ b/test/qtiItem/core/loader/testQtiClass.js @@ -0,0 +1,11 @@ +define(['taoQtiItem/qtiItem/core/Element'], function ( + Element +) { + 'use strict'; + + const TestQtiClass = Element.extend({ + qtiClass: 'testQtiClass', + }); + + return TestQtiClass; +}); diff --git a/test/qtiItem/helper/response/test.html b/test/qtiItem/helper/response/test.html new file mode 100644 index 00000000..fc9ebba5 --- /dev/null +++ b/test/qtiItem/helper/response/test.html @@ -0,0 +1,21 @@ + + + + + XML Namespace Handler Test + + + + +
+
+ + diff --git a/test/qtiItem/helper/response/test.js b/test/qtiItem/helper/response/test.js new file mode 100644 index 00000000..197fc8b4 --- /dev/null +++ b/test/qtiItem/helper/response/test.js @@ -0,0 +1,159 @@ +/** + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; under version 2 + * of the License (non-upgradable). + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (c) 2017 (original work) Open Assessment Technologies SA; + */ +define([ + 'taoQtiItem/qtiItem/helper/response', +], function (responseHelper) { + 'use strict'; + + const responseIdentifier = 'testResponseIdentifier'; + const outcomeIdentifier = 'testOutcomeIdentifier'; + + const responseRule = { + qtiClass: 'responseCondition', + responseIf: { + qtiClass: 'responseIf', + expression: { + qtiClass: 'match', + expressions: [ + { + qtiClass: 'variable', + attributes: { + identifier: responseIdentifier, + }, + }, + { + qtiClass: 'correct', + attributes: { + identifier: responseIdentifier, + }, + }, + ], + }, + responseRules: [ + { + qtiClass: 'setOutcomeValue', + attributes: { + identifier: outcomeIdentifier, + }, + expression: { + qtiClass: 'sum', + expressions: [ + { + qtiClass: 'variable', + attributes: { + identifier: outcomeIdentifier, + }, + }, + { + qtiClass: 'baseValue', + attributes: { + baseType: 'integer' + }, + value: '1', + }, + ], + }, + }, + ], + }, + }; + + QUnit.test('isUsingTemplate', function (assert) { + assert.equal( + responseHelper.isUsingTemplate(), + false, + 'return false if not template passed' + ); + + assert.equal( + responseHelper.isUsingTemplate( + { + template: 'MATCH_CORRECT', + }, + 'MATCH_CORRECT' + ), + true, + 'check template name' + ); + + assert.equal( + responseHelper.isUsingTemplate( + { + template: 'http://www.imsglobal.org/question/qti_v2p1/rptemplates/match_correct', + }, + 'MATCH_CORRECT' + ), + true, + 'check template url' + ); + }); + + QUnit.test('isValidTemplateName', function (assert) { + assert.equal( + responseHelper.isValidTemplateName('wrong'), + false, + 'return false if can not recognize template' + ); + + assert.equal( + responseHelper.isValidTemplateName('MATCH_CORRECT'), + true, + 'return true for known template' + ); + }); + + QUnit.test('getTemplateUriFromName', function (assert) { + assert.equal( + responseHelper.getTemplateUriFromName('wrong'), + '', + 'return empty string if can not recognize template' + ); + + assert.equal( + responseHelper.getTemplateUriFromName('MATCH_CORRECT'), + 'http://www.imsglobal.org/question/qti_v2p1/rptemplates/match_correct', + 'return template url for known template' + ); + }); + + QUnit.test('getTemplateNameFromUri', function (assert) { + assert.equal( + responseHelper.getTemplateNameFromUri('wrong'), + '', + 'return empty string if can not recognize template' + ); + + assert.equal( + responseHelper.getTemplateNameFromUri('http://www.imsglobal.org/question/qti_v2p1/rptemplates/match_correct'), + 'MATCH_CORRECT', + 'return template url for known template' + ); + }); + + QUnit.test('getTemplateNameFromResponseRules', function (assert) { + assert.ok( + typeof responseHelper.getTemplateNameFromResponseRules({}, {}) === 'undefined', + 'return undefined if can not recognize template' + ); + + assert.ok( + responseHelper.getTemplateNameFromResponseRules(responseIdentifier, responseRule), + 'return template name for known template' + ); + }); +}); diff --git a/test/qtiItem/helper/responseRules/test.html b/test/qtiItem/helper/responseRules/test.html new file mode 100644 index 00000000..50decf7a --- /dev/null +++ b/test/qtiItem/helper/responseRules/test.html @@ -0,0 +1,21 @@ + + + + + XML Namespace Handler Test + + + + +
+
+ + diff --git a/test/qtiItem/helper/responseRules/test.js b/test/qtiItem/helper/responseRules/test.js new file mode 100644 index 00000000..bf5f6818 --- /dev/null +++ b/test/qtiItem/helper/responseRules/test.js @@ -0,0 +1,197 @@ +/** + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; under version 2 + * of the License (non-upgradable). + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (c) 2017 (original work) Open Assessment Technologies SA; + */ +define([ + 'taoQtiItem/qtiItem/helper/responseRules', +], function (responseRulesHelper) { + 'use strict'; + + const responseIdentifier = 'testResponseIdentifier'; + const outcomeIdentifier = 'testOutcomeIdentifier'; + + const responseRules = { + MATCH_CORRECT: { + qtiClass: 'responseCondition', + responseIf: { + qtiClass: 'responseIf', + expression: { + qtiClass: 'match', + expressions: [ + { + qtiClass: 'variable', + attributes: { + identifier: responseIdentifier, + }, + }, + { + qtiClass: 'correct', + attributes: { + identifier: responseIdentifier, + }, + }, + ], + }, + responseRules: [ + { + qtiClass: 'setOutcomeValue', + attributes: { + identifier: outcomeIdentifier, + }, + expression: { + qtiClass: 'sum', + expressions: [ + { + qtiClass: 'variable', + attributes: { + identifier: outcomeIdentifier, + }, + }, + { + qtiClass: 'baseValue', + attributes: { + baseType: 'integer' + }, + value: '1', + }, + ], + }, + }, + ], + }, + }, + MAP_RESPONSE: { + qtiClass: 'responseCondition', + responseIf: { + qtiClass: 'responseIf', + expression: { + qtiClass: 'not', + expressions: [ + { + qtiClass: 'isNull', + expressions: [{ + qtiClass: 'variable', + attributes: { + identifier: responseIdentifier, + }, + }], + }, + ], + }, + responseRules: [ + { + qtiClass: 'setOutcomeValue', + attributes: { + identifier: outcomeIdentifier, + }, + expression: { + qtiClass: 'sum', + expressions: [ + { + qtiClass: 'variable', + attributes: { + identifier: outcomeIdentifier, + } + }, + { + qtiClass: 'mapResponse', + attributes: { + identifier: responseIdentifier, + }, + }, + ], + }, + }, + ], + }, + }, + MAP_RESPONSE_POINT: { + qtiClass: 'responseCondition', + responseIf: { + qtiClass: 'responseIf', + expression: { + qtiClass: 'not', + expressions: [ + { + qtiClass: 'isNull', + expressions: [{ + qtiClass: 'variable', + attributes: { + identifier: responseIdentifier, + }, + }], + }, + ], + }, + responseRules: [ + { + qtiClass: 'setOutcomeValue', + attributes: { + identifier: outcomeIdentifier, + }, + expression: { + qtiClass: 'sum', + expressions: [ + { + qtiClass: 'variable', + attributes: { + identifier: outcomeIdentifier, + } + }, + { + qtiClass: 'mapResponsePoint', + attributes: { + identifier: responseIdentifier, + }, + }, + ], + }, + }, + ], + }, + }, + }; + + QUnit.test('MATCH_CORRECT', function (assert) { + const actual = responseRulesHelper.responseRules.MATCH_CORRECT(responseIdentifier, outcomeIdentifier); + + assert.deepEqual( + actual, + responseRules.MATCH_CORRECT, + 'build MATCH_CORRECT response rule' + ); + }); + + QUnit.test('MAP_RESPONSE', function (assert) { + const actual = responseRulesHelper.responseRules.MAP_RESPONSE(responseIdentifier, outcomeIdentifier); + + assert.deepEqual( + actual, + responseRules.MAP_RESPONSE, + 'build MAP_RESPONSE response rule' + ); + }); + + QUnit.test('MAP_RESPONSE_POINT', function (assert) { + const actual = responseRulesHelper.responseRules.MAP_RESPONSE_POINT(responseIdentifier, outcomeIdentifier); + + assert.deepEqual( + actual, + responseRules.MAP_RESPONSE_POINT, + 'build MAP_RESPONSE_POINT response rule' + ); + }); +}); From 3f386d90bbd39ad76043c8c144cae1f39d1ecc23 Mon Sep 17 00:00:00 2001 From: Anton Tsymuk Date: Sat, 5 Sep 2020 20:53:56 +0300 Subject: [PATCH 04/12] fix unit tests --- test/qtiItem/core/loader/test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/qtiItem/core/loader/test.js b/test/qtiItem/core/loader/test.js index 1af05596..25a08fac 100644 --- a/test/qtiItem/core/loader/test.js +++ b/test/qtiItem/core/loader/test.js @@ -29,7 +29,7 @@ define([ 'taoQtiItem/qtiItem/core/interactions/MatchInteraction', 'taoQtiItem/qtiItem/core/interactions/GraphicGapMatchInteraction', 'taoQtiItem/qtiItem/core/variables/ResponseDeclaration', - 'taoQtiItem/qtiItem/helper/responseTemplate', + 'taoQtiItem/qtiItem/helper/responseRules', ], function (...args) { const [ QtiItemLoader, @@ -45,7 +45,7 @@ define([ MatchInteractionQtiClass, GraphicGapMatchInteractionQtiClass, ResponseDeclarationQtiClass, - responseTemplateHelper, + responseRulesHelper, ] = args; QUnit.module('QTI item loader'); @@ -986,7 +986,7 @@ define([ responseProcessing: { qtiClass: 'responseProcessing', responseRules: [ - responseTemplateHelper.responseTemplates.MATCH_CORRECT('testresponse', 'testoutcome'), + responseRulesHelper.responseRules.MATCH_CORRECT('testresponse', 'testoutcome'), ], serial: 'loadItemDataResponseProcessing', }, @@ -1091,8 +1091,8 @@ define([ responseProcessing: { qtiClass: 'responseProcessing', responseRules: [ - responseTemplateHelper.responseTemplates.MATCH_CORRECT('testresponse', 'testoutcome'), - responseTemplateHelper.responseTemplates.MATCH_CORRECT('testresponse1', 'testoutcome1'), + responseRulesHelper.responseRules.MATCH_CORRECT('testresponse', 'testoutcome'), + responseRulesHelper.responseRules.MATCH_CORRECT('testresponse1', 'testoutcome1'), ], serial: 'loadItemDataCustomResponseProcessingResponseProcessing', }, @@ -1100,7 +1100,7 @@ define([ loadItemDataResponse: { identifier: 'testresponse', qtiClass: 'responseDeclaration', - serial: 'lloadItemDataCustomResponseProcessingResponse', + serial: 'loadItemDataCustomResponseProcessingResponse', } } }; From 2a500428ece252c7e52ecc7623002fa8dcfcab0d Mon Sep 17 00:00:00 2001 From: Anton Tsymuk Date: Mon, 7 Sep 2020 09:50:20 +0300 Subject: [PATCH 05/12] backward compatibility for response tempate --- src/qtiItem/core/Loader.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qtiItem/core/Loader.js b/src/qtiItem/core/Loader.js index c8380fba..225e9f4e 100644 --- a/src/qtiItem/core/Loader.js +++ b/src/qtiItem/core/Loader.js @@ -258,6 +258,7 @@ var Loader = Class.extend({ response.template = responseHelper.getTemplateUriFromName( responseHelper.getTemplateNameFromResponseRules(data.identifier, responseRule) ) + || data.howMatch || null; response.defaultValue = data.defaultValue || null; From 8885240ae5ff2598a8d78478a2270b05b797d773 Mon Sep 17 00:00:00 2001 From: Anton Tsymuk Date: Mon, 7 Sep 2020 10:05:55 +0300 Subject: [PATCH 06/12] update version --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 001946a3..bde3e475 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@oat-sa/tao-item-runner-qti", - "version": "0.11.1", + "version": "0.12.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8aaf39d0..7d180062 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@oat-sa/tao-item-runner-qti", - "version": "0.11.1", + "version": "0.12.0", "displayName": "TAO Item Runner QTI", "description": "TAO QTI Item Runner modules", "files": [ From 6e2f46aa576e49b54b114ecb70715d0ea9031b5d Mon Sep 17 00:00:00 2001 From: Anton Tsymuk Date: Tue, 8 Sep 2020 16:35:37 +0300 Subject: [PATCH 07/12] check for outcomeIdentifier instead of outcomeRules --- src/qtiItem/helper/response.js | 2 +- test/qtiItem/helper/response/test.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/qtiItem/helper/response.js b/src/qtiItem/helper/response.js index 57e38269..449be5d7 100644 --- a/src/qtiItem/helper/response.js +++ b/src/qtiItem/helper/response.js @@ -71,7 +71,7 @@ export default { } = {}, } = outcomeRules; - if (!outcomeRules) { + if (!outcomeIdentifier) { return ''; } diff --git a/test/qtiItem/helper/response/test.js b/test/qtiItem/helper/response/test.js index 197fc8b4..5073343a 100644 --- a/test/qtiItem/helper/response/test.js +++ b/test/qtiItem/helper/response/test.js @@ -146,8 +146,9 @@ define([ }); QUnit.test('getTemplateNameFromResponseRules', function (assert) { - assert.ok( - typeof responseHelper.getTemplateNameFromResponseRules({}, {}) === 'undefined', + assert.equal( + responseHelper.getTemplateNameFromResponseRules({}, {}), + '', 'return undefined if can not recognize template' ); From 89e4894c0b1f81dd1039932f31f377f2533166b2 Mon Sep 17 00:00:00 2001 From: Anton Tsymuk Date: Wed, 9 Sep 2020 09:51:32 +0300 Subject: [PATCH 08/12] update license in changed and created files --- src/qtiItem/core/Loader.js | 2 +- src/qtiItem/helper/response.js | 2 +- src/qtiItem/helper/responseRules.js | 18 ++++++++++++++++++ test/qtiItem/core/loader/test.js | 2 +- test/qtiItem/core/loader/testQtiClass.js | 17 +++++++++++++++++ test/qtiItem/helper/response/test.js | 2 +- test/qtiItem/helper/responseRules/test.js | 2 +- 7 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/qtiItem/core/Loader.js b/src/qtiItem/core/Loader.js index 225e9f4e..c0f352bf 100644 --- a/src/qtiItem/core/Loader.js +++ b/src/qtiItem/core/Loader.js @@ -13,7 +13,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2015 (original work) Open Assessment Technologies SA ; + * Copyright (c) 2015-2020 (original work) Open Assessment Technologies SA ; * */ //@todo : move this to the ../helper directory diff --git a/src/qtiItem/helper/response.js b/src/qtiItem/helper/response.js index 449be5d7..5a87597d 100644 --- a/src/qtiItem/helper/response.js +++ b/src/qtiItem/helper/response.js @@ -13,7 +13,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2014-2017 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); + * Copyright (c) 2014-2020 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); * */ import _ from 'lodash'; diff --git a/src/qtiItem/helper/responseRules.js b/src/qtiItem/helper/responseRules.js index ad2ae040..b1a34a69 100644 --- a/src/qtiItem/helper/responseRules.js +++ b/src/qtiItem/helper/responseRules.js @@ -1,3 +1,21 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; under version 2 + * of the License (non-upgradable). + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (c) 2020 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); + * + */ export const responseRules = { MATCH_CORRECT: (responseIdentifier, outcomeIdentifier) => ({ qtiClass: 'responseCondition', diff --git a/test/qtiItem/core/loader/test.js b/test/qtiItem/core/loader/test.js index 25a08fac..bc19214c 100644 --- a/test/qtiItem/core/loader/test.js +++ b/test/qtiItem/core/loader/test.js @@ -13,7 +13,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2016 (original work) Open Assessment Technologies SA + * Copyright (c) 2020 (original work) Open Assessment Technologies SA **/ define([ 'taoQtiItem/qtiItem/core/Loader', diff --git a/test/qtiItem/core/loader/testQtiClass.js b/test/qtiItem/core/loader/testQtiClass.js index 96a59c40..7ee8ac3b 100644 --- a/test/qtiItem/core/loader/testQtiClass.js +++ b/test/qtiItem/core/loader/testQtiClass.js @@ -1,3 +1,20 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; under version 2 + * of the License (non-upgradable). + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (c) 2020 (original work) Open Assessment Technologies SA + **/ define(['taoQtiItem/qtiItem/core/Element'], function ( Element ) { diff --git a/test/qtiItem/helper/response/test.js b/test/qtiItem/helper/response/test.js index 5073343a..3fce8ea4 100644 --- a/test/qtiItem/helper/response/test.js +++ b/test/qtiItem/helper/response/test.js @@ -13,7 +13,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2017 (original work) Open Assessment Technologies SA; + * Copyright (c) 2020 (original work) Open Assessment Technologies SA; */ define([ 'taoQtiItem/qtiItem/helper/response', diff --git a/test/qtiItem/helper/responseRules/test.js b/test/qtiItem/helper/responseRules/test.js index bf5f6818..62a93b63 100644 --- a/test/qtiItem/helper/responseRules/test.js +++ b/test/qtiItem/helper/responseRules/test.js @@ -13,7 +13,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2017 (original work) Open Assessment Technologies SA; + * Copyright (c) 2020 (original work) Open Assessment Technologies SA; */ define([ 'taoQtiItem/qtiItem/helper/responseRules', From 62367845d06c358f7065c76e95d63db617b2a6af Mon Sep 17 00:00:00 2001 From: Anton Tsymuk Date: Mon, 14 Sep 2020 18:26:57 +0300 Subject: [PATCH 09/12] fix comments --- src/qtiItem/core/Loader.js | 4 ++-- test/qtiItem/core/loader/test.js | 33 ++++---------------------------- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/src/qtiItem/core/Loader.js b/src/qtiItem/core/Loader.js index c0f352bf..2ba62bd3 100644 --- a/src/qtiItem/core/Loader.js +++ b/src/qtiItem/core/Loader.js @@ -31,7 +31,7 @@ import responseHelper from 'taoQtiItem/qtiItem/helper/response'; const loadPortableCustomElementProperties = (portableElement, rawProperties) => { var properties = {}; - _.forOwn(rawProperties, function (value, key) { + _.forOwn(rawProperties, (value, key) => { try { properties[key] = JSON.parse(value); } catch (e) { @@ -107,7 +107,7 @@ var Loader = Class.extend({ return _.keys(this.qti); }, loadItemData(data, callback) { - this.loadRequiredClasses(data, (Qti) => { + this.loadRequiredClasses(data, Qti => { if (typeof data === 'object' && data.qtiClass === 'assessmentItem') { //unload an item from it's serial (in case of a reload) if (data.serial) { diff --git a/test/qtiItem/core/loader/test.js b/test/qtiItem/core/loader/test.js index bc19214c..0b6749cc 100644 --- a/test/qtiItem/core/loader/test.js +++ b/test/qtiItem/core/loader/test.js @@ -55,111 +55,86 @@ define([ assert.equal(typeof new QtiItemLoader(), 'object', 'The plugin factory produces an instance'); }); - const pluginApi = [ + QUnit.cases.init([ { - name: 'init', title: 'init', }, { - name: 'setClassesLocation', title: 'setClassesLocation', }, { - name: 'getRequiredClasses', title: 'getRequiredClasses', }, { - name: 'loadRequiredClasses', title: 'loadRequiredClasses', }, { - name: 'getLoadedClasses', title: 'getLoadedClasses', }, { - name: 'loadItemData', title: 'loadItemData', }, { - name: 'loadAndBuildElement', title: 'loadAndBuildElement', }, { - name: 'loadElement', title: 'loadElement', }, { - name: 'loadElements', title: 'loadElements', }, { - name: 'buildResponse', title: 'buildResponse', }, { - name: 'buildSimpleFeedbackRule', title: 'buildSimpleFeedbackRule', }, { - name: 'buildOutcome', title: 'buildOutcome', }, { - name: 'buildResponseProcessing', title: 'buildResponseProcessing', }, { - name: 'loadContainer', title: 'loadContainer', }, { - name: 'buildElement', title: 'buildElement', }, { - name: 'loadElementData', title: 'loadElementData', }, { - name: 'loadInteractionData', title: 'loadInteractionData', }, { - name: 'buildInteractionChoices', title: 'buildInteractionChoices', }, { - name: 'loadChoiceData', title: 'loadChoiceData', }, { - name: 'loadObjectData', title: 'loadObjectData', }, { - name: 'loadMathData', title: 'loadMathData', }, { - name: 'loadTooltipData', title: 'loadTooltipData', }, { - name: 'loadPciData', title: 'loadPciData', }, { - name: 'loadPicData', title: 'loadPicData', }, - ]; - QUnit.cases.init(pluginApi).test('loader API ', function (data, assert) { + ]).test('loader API ', function (data, assert) { const loader = new QtiItemLoader(); assert.equal( - typeof loader[data.name], + typeof loader[data.title], 'function', - `The pluginFactory instances expose a "${data.name}" function` + `The pluginFactory instances expose a "${data.title}" function` ); }); From 3d2e72b5a61a2558c4acf25c335f3f81797609ce Mon Sep 17 00:00:00 2001 From: Anton Tsymuk Date: Wed, 16 Sep 2020 09:12:41 +0300 Subject: [PATCH 10/12] additionaly check for feedback response rules --- src/qtiItem/core/Loader.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/qtiItem/core/Loader.js b/src/qtiItem/core/Loader.js index 2ba62bd3..0f290ea1 100644 --- a/src/qtiItem/core/Loader.js +++ b/src/qtiItem/core/Loader.js @@ -152,7 +152,7 @@ var Loader = Class.extend({ expressions: [expression = {}] = [], } = {} } = {} }) => expression.attributes - && expression.attributes.identifier === responseIdentifier + && expression.attributes.identifier === responseIdentifier || ( expression.expressions && expression.expressions[0] @@ -176,7 +176,29 @@ var Loader = Class.extend({ if (feedbackRules) { _.forIn(feedbackRules, (fbData, serial) => { + const { + attributes: { + identifier: feedbackOutcomeIdentifier, + } = {} + } = data.outcomes[fbData.feedbackOutcome] || {}; response.feedbackRules[serial] = this.buildSimpleFeedbackRule(fbData, response); + + // feedback response rule from response rules array + const feedbackResponseRuleIndex = responseRules.findIndex(({ + responseIf: { + responseRules: [setOutcomeResponseRule = {}] = [], + } = {} + }) => { + const { attributes = {}, qtiClass } = setOutcomeResponseRule; + const outcomeIdentifier = attributes.identifier; + + return feedbackOutcomeIdentifier === outcomeIdentifier + && qtiClass === 'setOutcomeValue'; + }); + + if (feedbackResponseRuleIndex !== -1) { + responseRules.splice(feedbackResponseRuleIndex, 1); + } }); } } From 17931ea708a51261a948c219334228a317c5b86a Mon Sep 17 00:00:00 2001 From: Anton Tsymuk Date: Wed, 16 Sep 2020 09:12:51 +0300 Subject: [PATCH 11/12] update unit tests --- test/qtiItem/core/loader/test.js | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/qtiItem/core/loader/test.js b/test/qtiItem/core/loader/test.js index 0b6749cc..65966b37 100644 --- a/test/qtiItem/core/loader/test.js +++ b/test/qtiItem/core/loader/test.js @@ -1090,4 +1090,73 @@ define([ ready(); }); }); + + QUnit.test('loadItemData::feedbackRules', function (assert) { + const ready = assert.async(); + const loader = new QtiItemLoader(); + const data = { + body: { + body: 'testBody', + elements: {}, + }, + qtiClass: 'assessmentItem', + serial: 'loadItemDataFeedbackRuleItem', + outcomes: { + testOutcome: { + attributes: { + identifier: 'testoutcome', + }, + qtiClass: 'outcomeDeclaration', + serial: 'feedbackRuleOutcome', + }, + }, + responseProcessing: { + qtiClass: 'responseProcessing', + responseRules: [ + { + responseIf: { + responseRules: [ + { + attributes: { + identifier: 'testoutcome', + }, + qtiClass: 'setOutcomeValue' + } + ], + }, + }, + ], + serial: 'loadItemDataFeedbackRuleResponseProcessing', + }, + responses: { + loadItemDataResponse: { + identifier: 'testresponse', + qtiClass: 'responseDeclaration', + serial: 'loadItemDataFeedbackRuleResponse', + feedbackRules: { + simpleFeedbackRule: { + comparedOutcome: 'comparedoutcome', + comparedValue: 0, + condition: 'correct', + feedbackElse: '', + feedbackOutcome: 'testOutcome', + feedbackThen: 'feedbackthen', + qtiClass: '_simpleFeedbackRule', + serial: 'feedbackrule', + } + }, + } + } + }; + + loader.loadItemData(data, (item) => { + assert.equal( + item.responseProcessing.processingType, + 'templateDriven', + 'loadItemData recognize response processing type of item with feedbackrules' + ); + + ready(); + }); + }); }); From 4ba20d8cbd85e46a81684d820b802e8518560186 Mon Sep 17 00:00:00 2001 From: Anton Tsymuk Date: Wed, 16 Sep 2020 09:28:38 +0300 Subject: [PATCH 12/12] update comment --- src/qtiItem/core/Loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qtiItem/core/Loader.js b/src/qtiItem/core/Loader.js index 0f290ea1..d76d9ebe 100644 --- a/src/qtiItem/core/Loader.js +++ b/src/qtiItem/core/Loader.js @@ -183,7 +183,7 @@ var Loader = Class.extend({ } = data.outcomes[fbData.feedbackOutcome] || {}; response.feedbackRules[serial] = this.buildSimpleFeedbackRule(fbData, response); - // feedback response rule from response rules array + // remove feedback response rule from response rules array const feedbackResponseRuleIndex = responseRules.findIndex(({ responseIf: { responseRules: [setOutcomeResponseRule = {}] = [],