From 1896274ff50fdcfe77697ea94a176ab59a2c26e6 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Thu, 5 Nov 2015 14:41:48 +0800 Subject: [PATCH] MDL-51029 tool_lp: Implement competency rules --- .../lp/amd/build/competency_outcomes.min.js | 1 + .../tool/lp/amd/build/competency_rule.min.js | 1 + .../lp/amd/build/competency_rule_all.min.js | 1 + .../amd/build/competency_rule_points.min.js | 1 + .../lp/amd/build/competencyactions.min.js | 2 +- .../lp/amd/build/competencyruleconfig.min.js | 1 + admin/tool/lp/amd/build/competencytree.min.js | 2 +- admin/tool/lp/amd/src/competency_outcomes.js | 83 +++ admin/tool/lp/amd/src/competency_rule.js | 175 ++++++ admin/tool/lp/amd/src/competency_rule_all.js | 60 ++ .../tool/lp/amd/src/competency_rule_points.js | 199 +++++++ admin/tool/lp/amd/src/competencyactions.js | 104 +++- admin/tool/lp/amd/src/competencypicker.js | 2 +- admin/tool/lp/amd/src/competencyruleconfig.js | 548 ++++++++++++++++++ admin/tool/lp/amd/src/competencytree.js | 30 +- admin/tool/lp/classes/api.php | 12 +- admin/tool/lp/classes/competency.php | 120 ++++ admin/tool/lp/classes/competency_rule.php | 101 ++++ admin/tool/lp/classes/competency_rule_all.php | 99 ++++ .../lp/classes/competency_rule_points.php | 199 +++++++ admin/tool/lp/classes/external.php | 38 +- .../output/manage_competencies_page.php | 8 + admin/tool/lp/classes/persistent.php | 4 +- admin/tool/lp/db/install.xml | 6 +- admin/tool/lp/db/upgrade.php | 60 ++ admin/tool/lp/lang/en/tool_lp.php | 14 + admin/tool/lp/styles.css | 41 ++ .../templates/competency_rule_config.mustache | 39 ++ .../templates/competency_rule_points.mustache | 27 + .../lp/templates/competency_summary.mustache | 9 + .../manage_competencies_page.mustache | 7 +- admin/tool/lp/tests/api_test.php | 26 +- admin/tool/lp/tests/competency_rule_test.php | 372 ++++++++++++ admin/tool/lp/tests/persistent_test.php | 34 ++ admin/tool/lp/version.php | 2 +- 35 files changed, 2385 insertions(+), 43 deletions(-) create mode 100644 admin/tool/lp/amd/build/competency_outcomes.min.js create mode 100644 admin/tool/lp/amd/build/competency_rule.min.js create mode 100644 admin/tool/lp/amd/build/competency_rule_all.min.js create mode 100644 admin/tool/lp/amd/build/competency_rule_points.min.js create mode 100644 admin/tool/lp/amd/build/competencyruleconfig.min.js create mode 100644 admin/tool/lp/amd/src/competency_outcomes.js create mode 100644 admin/tool/lp/amd/src/competency_rule.js create mode 100644 admin/tool/lp/amd/src/competency_rule_all.js create mode 100644 admin/tool/lp/amd/src/competency_rule_points.js create mode 100644 admin/tool/lp/amd/src/competencyruleconfig.js create mode 100644 admin/tool/lp/classes/competency_rule.php create mode 100644 admin/tool/lp/classes/competency_rule_all.php create mode 100644 admin/tool/lp/classes/competency_rule_points.php create mode 100644 admin/tool/lp/templates/competency_rule_config.mustache create mode 100644 admin/tool/lp/templates/competency_rule_points.mustache create mode 100644 admin/tool/lp/tests/competency_rule_test.php diff --git a/admin/tool/lp/amd/build/competency_outcomes.min.js b/admin/tool/lp/amd/build/competency_outcomes.min.js new file mode 100644 index 00000000000..2da17456896 --- /dev/null +++ b/admin/tool/lp/amd/build/competency_outcomes.min.js @@ -0,0 +1 @@ +define(["jquery","core/str"],function(a,b){var c=0,d=1,e=2,f=3;return{NONE:c,EVIDENCE:d,COMPLETE:e,RECOMMEND:f,getAll:function(){var a=this;return b.get_strings([{key:"competencyoutcome_none",component:"tool_lp"},{key:"competencyoutcome_evidence",component:"tool_lp"},{key:"competencyoutcome_recommend",component:"tool_lp"},{key:"competencyoutcome_complete",component:"tool_lp"}]).then(function(b){var c={};return c[a.NONE]={code:a.NONE,name:b[0]},c[a.EVIDENCE]={code:a.EVIDENCE,name:b[1]},c[a.RECOMMEND]={code:a.RECOMMEND,name:b[2]},c[a.COMPLETE]={code:a.COMPLETE,name:b[3]},c})},getString:function(b){var c=this,d=c.getAll();return d.then(function(c){return"undefined"==typeof c[b]?a.Deferred().reject().promise():c[b].name})}}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competency_rule.min.js b/admin/tool/lp/amd/build/competency_rule.min.js new file mode 100644 index 00000000000..8939576cf60 --- /dev/null +++ b/admin/tool/lp/amd/build/competency_rule.min.js @@ -0,0 +1 @@ +define(["jquery"],function(a){var b=function(b){this._eventNode=a("
"),this._ready=a.Deferred(),this._tree=b};return b.prototype._competency=null,b.prototype._eventNode=null,b.prototype._ready=null,b.prototype._tree=null,b.prototype.canConfig=function(){return this._tree.hasChildren(this._competency.id)},b.prototype.getConfig=function(){return null},b.prototype.getType=function(){throw new Error("Not implemented")},b.prototype.init=function(){return this._load()},b.prototype.injectTemplate=function(){return a.Deferred().reject().promise()},b.prototype.isValid=function(){return!1},b.prototype._load=function(){return a.when()},b.prototype.on=function(a,b){this._eventNode.on(a,b)},b.prototype.setTargetCompetency=function(a){this._competency=a},b.prototype._trigger=function(a,b){this._eventNode.trigger(a,[b])},b.prototype._triggerChange=function(){this._trigger("change",this)},b}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competency_rule_all.min.js b/admin/tool/lp/amd/build/competency_rule_all.min.js new file mode 100644 index 00000000000..664f90416fa --- /dev/null +++ b/admin/tool/lp/amd/build/competency_rule_all.min.js @@ -0,0 +1 @@ +define(["jquery","core/str","tool_lp/competency_rule"],function(a,b,c){var d=function(){c.apply(this,arguments)};return d.prototype=Object.create(c.prototype),d.prototype.getType=function(){return"tool_lp\\competency_rule_all"},d.prototype.isValid=function(){return!0},d}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competency_rule_points.min.js b/admin/tool/lp/amd/build/competency_rule_points.min.js new file mode 100644 index 00000000000..7a2d9aaa5de --- /dev/null +++ b/admin/tool/lp/amd/build/competency_rule_points.min.js @@ -0,0 +1 @@ +define(["jquery","core/str","core/templates","tool_lp/competency_rule"],function(a,b,c,d){var e=function(){d.apply(this,arguments)};return e.prototype=Object.create(d.prototype),e.prototype._container=null,e.prototype._templateLoaded=!1,e.prototype.getConfig=function(){return JSON.stringify({base:{points:this._getRequiredPoints()},competencies:this._getCompetenciesConfig()})},e.prototype._getCompetenciesConfig=function(){var b=[];return this._container.find("[data-competency]").each(function(){var c=a(this),d=c.data("competency"),e=parseInt(c.find('[name="points"]').val(),10),f=c.find('[name="required"]').prop("checked");b.push({id:d,points:e,required:f?1:0})}),b},e.prototype._getRequiredPoints=function(){return parseInt(this._container.find('[name="requiredpoints"]').val()||1,10)},e.prototype.getType=function(){return"tool_lp\\competency_rule_points"},e.prototype.injectTemplate=function(b){var d,e=this,f=this._tree.getChildren(this._competency.id),g={base:{points:2},competencies:[]};if(this._templateLoaded=!1,e._competency.ruletype==e.getType())try{g=JSON.parse(e._competency.ruleconfig)}catch(h){}return d={requiredpoints:g&&g.base?g.base.points:2,competency:e._competency,children:[]},a.each(f,function(b,c){var e={id:c.id,shortname:c.shortname,required:!1,points:0};g&&a.each(g.competencies,function(a,b){b.id==e.id&&(e.required=b.required?!0:!1,e.points=b.points)}),d.children.push(e)}),c.render("tool_lp/competency_rule_points",d).then(function(a){e._container=b,b.html(a),b.find("input").change(function(){e._triggerChange()}),e._templateLoaded=!0,e._triggerChange()})},e.prototype.isValid=function(){if(!this._templateLoaded)return!1;var b=this._getRequiredPoints(),c=0,d=!0;return a.each(this._getCompetenciesConfig(),function(a,b){b.points<0&&(d=!1),c+=b.points}),d=d&&c>=b},e}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competencyactions.min.js b/admin/tool/lp/amd/build/competencyactions.min.js index 2451f9d8b46..65252539dd4 100644 --- a/admin/tool/lp/amd/build/competencyactions.min.js +++ b/admin/tool/lp/amd/build/competencyactions.min.js @@ -1 +1 @@ -define(["jquery","core/url","core/templates","core/notification","core/str","core/ajax","tool_lp/dragdrop-reorder","tool_lp/tree","tool_lp/dialogue","tool_lp/menubar","tool_lp/competencypicker"],function(a,b,c,d,e,f,g,h,i,j,k){var l,m,n,o,p=null,q=null,r=null,s=function(){var c=a('[data-region="competencyactions"]').data("competency"),d={competencyframeworkid:p.getCompetencyFrameworkId(),pagecontextid:l};null!==c&&(d.parentid=c.id);var e=a.param(d);window.location=b.relativeUrl("/admin/tool/lp/editcompetency.php?"+e)},t=function(){if("undefined"==typeof r&&(r=0),r!=q){var b=a('[data-region="filtercompetencies"]').data("frameworkid"),c=f.call([{methodname:"tool_lp_set_parent_competency",args:{competencyid:q,parentid:r}},{methodname:"tool_lp_data_for_competencies_manage_page",args:{competencyframeworkid:b,search:a('[data-region="filtercompetencies"] input').val()}}]);c[1].done(y).fail(d.exception)}},u=function(b){var c=new h("[data-enhance=movetree]");c.on("selectionchanged",function(b,c){r=a(c).data("id")});var d=a(b.getContent());d.on("click",'[data-action="move"]',function(){b.close(),t()}),d.on("click",'[data-action="cancel"]',function(){b.close()})},v=function(a,b){var c;for(c=0;c
"),this._tree=b,this._rulesModules=c,this._setUp()};return g.prototype._competency=null,g.prototype._eventNode=null,g.prototype._outcomesOption=null,g.prototype._popup=null,g.prototype._ready=null,g.prototype._rules=null,g.prototype._rulesModules=null,g.prototype._tree=null,g.prototype._afterChange=function(){this._isValid()?this._find('[data-action="save"]').prop("disabled",!1):this._find('[data-action="save"]').prop("disabled",!0)},g.prototype._afterRuleConfigChange=function(a,b){b==this._getRule()&&this._afterChange()},g.prototype._afterRender=function(){var a=this;a._find('[name="outcome"]').on("change",function(){a._switchedOutcome()}).trigger("change"),a._find('[name="rule"]').on("change",function(){a._switchedRule()}).trigger("change"),a._find('[data-action="save"]').on("click",function(){a._trigger("save",a._getConfig()),a.close()}),a._find('[data-action="cancel"]').on("click",function(){a.close()})},g.prototype.canBeConfigured=function(){var b=!1;return a.each(this._rules,function(a,c){return c.canConfig()?(b=!0,!1):void 0}),b},g.prototype.close=function(){this._popup.close(),this._popup=null},g.prototype.display=function(){var a=this;if(a._competency)return a._render().then(function(b){return f.get_string("competencyrule","tool_lp").then(function(c){a._popup=new d(c,b,a._afterRender.bind(a))})}).fail(b.exception)},g.prototype._find=function(b){return a(this._popup.getContent()).find(b)},g.prototype._getApplicableOutcomesOptions=function(){var b=this,c=[];return a.each(b._outcomesOption,function(a,d){c.push({code:d.code,name:d.name,selected:d.code==b._competency.ruleoutcome?!0:!1})}),c},g.prototype._getApplicableRulesOptions=function(){var b=this,c=[];return a.each(b._rules,function(a,d){d.canConfig()&&c.push({name:b._getRuleName(d.getType()),type:d.getType(),selected:d.getType()==b._competency.ruletype?!0:!1})}),c},g.prototype._getConfig=function(){var a=this._getRule();return{ruletype:a?a.getType():null,ruleconfig:a?a.getConfig():null,ruleoutcome:this._getOutcome()}},g.prototype._getOutcome=function(){return this._find('[name="outcome"]').val()},g.prototype._getRule=function(){var b,c=this._find('[name="rule"]').val();return a.each(this._rules,function(a,d){return d.getType()==c?(b=d,!1):void 0}),b},g.prototype._getRuleName=function(b){var c,d=this;return a.each(d._rulesModules,function(a,d){return d.type==b?(c=d.name,!1):void 0}),c},g.prototype._initOutcomes=function(){var a=this;return e.getAll().then(function(b){a._outcomesOption=b})},g.prototype._initRules=function(){var b=this,c=[];return a.each(b._rules,function(d,e){var f=e.init().then(function(){e.setTargetCompetency(b._competency),e.on("change",b._afterRuleConfigChange.bind(b))},function(){return b._rules.splice(d,1),a.when()});c.push(f)}),a.when.apply(a.when,c)},g.prototype._isValid=function(){var a=this._getOutcome(),b=this._getRule();return a==e.NONE?!0:b?b.isValid():!1},g.prototype.on=function(a,b){this._eventNode.on(a,b)},g.prototype._preRender=function(){return this.ready()},g.prototype.ready=function(){return this._ready.promise()},g.prototype._render=function(){var a=this;return this._preRender().then(function(){var b;a.canBeConfigured()?(b={},b.outcomes=a._getApplicableOutcomesOptions(),b.rules=a._getApplicableRulesOptions()):b=!1;var d={competencyshortname:a._competency.shortname,config:b};return c.render("tool_lp/competency_rule_config",d)})},g.prototype.setTargetCompetencyId=function(b){var c=this;c._competency=c._tree.getCompetency(b),a.each(c._rules,function(a,b){b.setTargetCompetency(c._competency)})},g.prototype._setUp=function(){var b=this,c=[],d=[];b._ready=a.Deferred(),b._rules=[],a.each(b._rulesModules,function(a,b){d.push(b.amd)}),require(d,function(){a.each(arguments,function(a,c){var d=new c(b._tree);b._rules.push(d)}),c.push(b._initRules()),c.push(b._initOutcomes()),a.when.apply(a.when,c).always(function(){b._ready.resolve()})})},g.prototype._switchedOutcome=function(){var a=this,b=a._getOutcome();return b==e.NONE?(a._find('[data-region="rule-type"]').hide().find('[name="rule"]').val(-1),a._find('[data-region="rule-config"]').empty().hide(),void a._afterChange()):(a._find('[data-region="rule-type"]').show(),a._find('[data-region="rule-config"]').show(),void a._afterChange())},g.prototype._switchedRule=function(){var a=this,b=a._find('[data-region="rule-config"]'),c=a._getRule();return c?void c.injectTemplate(b).then(function(){b.show()},function(){b.empty().hide()}).always(function(){a._afterChange()}):(b.empty().hide(),void a._afterChange())},g.prototype._trigger=function(a,b){this._eventNode.trigger(a,[b])},g}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competencytree.min.js b/admin/tool/lp/amd/build/competencytree.min.js index 6f8f8d05a78..e9cbfba4df1 100644 --- a/admin/tool/lp/amd/build/competencytree.min.js +++ b/admin/tool/lp/amd/build/competencytree.min.js @@ -1 +1 @@ -define(["core/ajax","core/notification","core/templates","tool_lp/tree","jquery"],function(a,b,c,d,e){var f=[],g=0,h="",i="",j="",k=function(a,b){var c=0,d=!1;for(a.haschildren=!1,a.children=[],c=0;c0},reloadCompetencies:function(){return l("").fail(b.exception)},listCompetencies:function(){return f}}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/src/competency_outcomes.js b/admin/tool/lp/amd/src/competency_outcomes.js new file mode 100644 index 00000000000..1ff679e2f38 --- /dev/null +++ b/admin/tool/lp/amd/src/competency_outcomes.js @@ -0,0 +1,83 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle 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, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle 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 Moodle. If not, see . + +/** + * Competency rule config. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', + 'core/str'], + function($, Str) { + + var OUTCOME_NONE = 0, + OUTCOME_EVIDENCE = 1, + OUTCOME_COMPLETE = 2, + OUTCOME_RECOMMEND = 3; + + return /** @alias module:tool_lp/competency_outcomes */ { + + NONE: OUTCOME_NONE, + EVIDENCE: OUTCOME_EVIDENCE, + COMPLETE: OUTCOME_COMPLETE, + RECOMMEND: OUTCOME_RECOMMEND, + + /** + * Get all the outcomes. + * + * @return {Object} Indexed by outcome code, contains code and name. + * @method getAll + */ + getAll: function() { + var self = this; + return Str.get_strings([ + { key: 'competencyoutcome_none', component: 'tool_lp' }, + { key: 'competencyoutcome_evidence', component: 'tool_lp' }, + { key: 'competencyoutcome_recommend', component: 'tool_lp' }, + { key: 'competencyoutcome_complete', component: 'tool_lp' }, + ]).then(function(strings) { + var outcomes = {}; + outcomes[self.NONE] = { code: self.NONE, name: strings[0] }; + outcomes[self.EVIDENCE] = { code: self.EVIDENCE, name: strings[1] }; + outcomes[self.RECOMMEND] = { code: self.RECOMMEND, name: strings[2] }; + outcomes[self.COMPLETE] = { code: self.COMPLETE, name: strings[3] }; + return outcomes; + }); + }, + + /** + * Get the string for an outcome. + * + * @param {Number} id The outcome code. + * @return {Promise Resolved with the string. + * @method getString + */ + getString: function(id) { + var self = this, + all = self.getAll(); + + return all.then(function(outcomes) { + if (typeof outcomes[id] === 'undefined') { + return $.Deferred().reject().promise(); + } + return outcomes[id].name; + }); + } + }; + +}); diff --git a/admin/tool/lp/amd/src/competency_rule.js b/admin/tool/lp/amd/src/competency_rule.js new file mode 100644 index 00000000000..35def4ad7d9 --- /dev/null +++ b/admin/tool/lp/amd/src/competency_rule.js @@ -0,0 +1,175 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle 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, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle 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 Moodle. If not, see . + +/** + * Competency rule base module. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery'], function($) { + + /** + * Competency rule abstract class. + * + * Any competency rule should extend this object. The event 'change' should be + * triggered on the instance when the configuration has changed. This will allow + * the components using the rule to gather the config, or check its validity. + * + * this._triggerChange(); + * + * @param {Tree} tree The competency tree. + */ + var Rule = function(tree) { + this._eventNode = $('
'); + this._ready = $.Deferred(); + this._tree = tree; + }; + + /** @type {Object} The current competency. */ + Rule.prototype._competency = null; + /** @type {Node} The node we attach the events to. */ + Rule.prototype._eventNode = null; + /** @type {Promise} Resolved when the object is ready. */ + Rule.prototype._ready = null; + /** @type {Tree} The competency tree. */ + Rule.prototype._tree = null; + + /** + * Whether or not the current competency can be configured using this rule. + * + * @return {Boolean} + * @method canConfig + */ + Rule.prototype.canConfig = function() { + return this._tree.hasChildren(this._competency.id); + }; + + /** + * The config established by this rule. + * + * To override in subclasses when relevant. + * + * @return {String|null} + * @method getConfig + */ + Rule.prototype.getConfig = function() { + return null; + }; + + /** + * Return the type of the module. + * + * @return {String} + * @method getType + */ + Rule.prototype.getType = function() { + throw new Error('Not implemented'); + }; + + /** + * The init process. + * + * Do not override this, instead override _load. + * + * @return {Promise} Revoled when the plugin is initialised. + * @method init + */ + Rule.prototype.init = function() { + return this._load(); + }; + + /** + * Callback to inject the template. + * + * @param {Node} container Node to inject in. + * @return {Promise} Resolved when done. + * @method injectTemplate + */ + Rule.prototype.injectTemplate = function() { + return $.Deferred().reject().promise(); + }; + + /** + * Whether or not the current config is valid. + * + * Plugins should override this. + * + * @return {Boolean} + * @method _isValid + */ + Rule.prototype.isValid = function() { + return false; + }; + + /** + * Load the class. + * + * @return {Promise} + * @method _load + * @protected + */ + Rule.prototype._load = function() { + return $.when(); + }; + + /** + * Register an event listener. + * + * @param {String} type The event type. + * @param {Function} handler The event listener. + * @method on + */ + Rule.prototype.on = function(type, handler) { + this._eventNode.on(type, handler); + }; + + /** + * Sets the current competency. + * + * @param {Competency} competency + * @method setTargetCompetency + */ + Rule.prototype.setTargetCompetency = function(competency) { + this._competency = competency; + }; + + /** + * Trigger an event. + * + * @param {String} type The type of event. + * @param {Object} The data to pass to the listeners. + * @method _trigger + * @protected + */ + Rule.prototype._trigger = function(type, data) { + this._eventNode.trigger(type, [data]); + }; + + /** + * Trigger the change event. + * + * @method _triggerChange + * @protected + */ + Rule.prototype._triggerChange = function() { + this._trigger('change', this); + }; + + return /** @alias module:tool_lp/competency_rule */ Rule; + +}); diff --git a/admin/tool/lp/amd/src/competency_rule_all.js b/admin/tool/lp/amd/src/competency_rule_all.js new file mode 100644 index 00000000000..dad5a5619ef --- /dev/null +++ b/admin/tool/lp/amd/src/competency_rule_all.js @@ -0,0 +1,60 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle 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, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle 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 Moodle. If not, see . + +/** + * Competency rule all module. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', + 'core/str', + 'tool_lp/competency_rule', + ], + function($, Str, RuleBase) { + + /** + * Competency rule all class. + */ + var Rule = function() { + RuleBase.apply(this, arguments); + }; + Rule.prototype = Object.create(RuleBase.prototype); + + /** + * Return the type of the module. + * + * @return {String} + * @method getType + */ + Rule.prototype.getType = function() { + return 'tool_lp\\competency_rule_all'; + }; + + /** + * Whether or not the current config is valid. + * + * @return {Boolean} + * @method isValid + */ + Rule.prototype.isValid = function() { + return true; + }; + + return /** @alias module:tool_lp/competency_rule_all */ Rule; + +}); diff --git a/admin/tool/lp/amd/src/competency_rule_points.js b/admin/tool/lp/amd/src/competency_rule_points.js new file mode 100644 index 00000000000..700d70ec776 --- /dev/null +++ b/admin/tool/lp/amd/src/competency_rule_points.js @@ -0,0 +1,199 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle 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, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle 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 Moodle. If not, see . + +/** + * Competency rule points module. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', + 'core/str', + 'core/templates', + 'tool_lp/competency_rule', + ], + function($, Str, Templates, RuleBase) { + + /** + * Competency rule points class. + */ + var Rule = function() { + RuleBase.apply(this, arguments); + }; + Rule.prototype = Object.create(RuleBase.prototype); + + /** @type {Node} Reference to the container in which the template was included. */ + Rule.prototype._container = null; + /** @type {Boolean} Whether or not the template was included. */ + Rule.prototype._templateLoaded = false; + + /** + * The config established by this rule. + * + * @return {String} + * @method getConfig + */ + Rule.prototype.getConfig = function() { + return JSON.stringify({ + base: { + points: this._getRequiredPoints(), + }, + competencies: this._getCompetenciesConfig() + }); + }; + + /** + * Gathers the input provided by the user for competencies. + * + * @return {Array} Containing id, points and required. + * @method _getCompetenciesConfig + * @protected + */ + Rule.prototype._getCompetenciesConfig = function() { + var competencies = []; + + this._container.find('[data-competency]').each(function() { + var node = $(this), + id = node.data('competency'), + points = parseInt(node.find('[name="points"]').val(), 10), + required = node.find('[name="required"]').prop('checked'); + + competencies.push({ + id: id, + points: points, + required: required ? 1 : 0 + }); + }); + + return competencies; + }; + + /** + * Fetches the required points set by the user. + * + * @return {Number} + * @method _getRequiredPoints + * @protected + */ + Rule.prototype._getRequiredPoints = function() { + return parseInt(this._container.find('[name="requiredpoints"]').val() || 1, 10); + }; + + /** + * Return the type of the module. + * + * @return {String} + * @method getType + */ + Rule.prototype.getType = function() { + return 'tool_lp\\competency_rule_points'; + }; + + /** + * Callback to inject the template. + * + * @param {Node} container Node to inject in. + * @return {Promise} Resolved when done. + * @method injectTemplate + */ + Rule.prototype.injectTemplate = function(container) { + var self = this, + children = this._tree.getChildren(this._competency.id), + context, + config = { + base: { points: 2 }, + competencies: [] + }; + + this._templateLoaded = false; + + // Only pre-load the configuration when the competency is using this rule. + if (self._competency.ruletype == self.getType()) { + try { + config = JSON.parse(self._competency.ruleconfig); + } catch (e) { + } + } + + context = { + requiredpoints: (config && config.base) ? config.base.points : 2, + competency: self._competency, + children: [] + }; + + $.each(children, function(index, child) { + var competency = { + id: child.id, + shortname: child.shortname, + required: false, + points: 0 + }; + + if (config) { + $.each(config.competencies, function(index, comp) { + if (comp.id == competency.id) { + competency.required = comp.required ? true : false; + competency.points = comp.points; + } + }); + } + + context.children.push(competency); + }); + + return Templates.render('tool_lp/competency_rule_points', context).then(function(html) { + self._container = container; + container.html(html); + container.find('input').change(function() { + self._triggerChange(); + }); + + // We're done, let's trigger a change. + self._templateLoaded = true; + self._triggerChange(); + }); + }; + + /** + * Whether or not the current config is valid. + * + * @return {Boolean} + * @method isValid + */ + Rule.prototype.isValid = function() { + if (!this._templateLoaded) { + return false; + } + + var required = this._getRequiredPoints(), + max = 0, + valid = true; + + $.each(this._getCompetenciesConfig(), function(index, competency) { + if (competency.points < 0) { + valid = false; + } + max += competency.points; + }); + + valid = valid && max >= required; + return valid; + }; + + return /** @alias module:tool_lp/competency_rule_all */ Rule; + +}); diff --git a/admin/tool/lp/amd/src/competencyactions.js b/admin/tool/lp/amd/src/competencyactions.js index 4cbdbe0344f..26e53ee30d7 100644 --- a/admin/tool/lp/amd/src/competencyactions.js +++ b/admin/tool/lp/amd/src/competencyactions.js @@ -31,8 +31,10 @@ define(['jquery', 'tool_lp/tree', 'tool_lp/dialogue', 'tool_lp/menubar', - 'tool_lp/competencypicker'], - function($, url, templates, notification, str, ajax, dragdrop, Ariatree, Dialogue, menubar, Picker) { + 'tool_lp/competencypicker', + 'tool_lp/competency_outcomes', + 'tool_lp/competencyruleconfig'], + function($, url, templates, notification, str, ajax, dragdrop, Ariatree, Dialogue, menubar, Picker, Outcomes, RuleConfig) { // Private variables and functions. /** @var {Object} treeModel - This is an object representing the nodes in the tree. */ @@ -45,10 +47,14 @@ define(['jquery', var pageContextId; /** @type {Object} Picker instance. */ var pickerInstance; + /** @type {Object} Rule config instance. */ + var ruleConfigInstance; /** @type {Object} The competency we're picking a relation to. */ var relatedTarget; /** @type {Object} Taxonomy constants indexed per level. */ var taxonomiesConstants; + /** @type {Array} The rules modules. Values are object containing type, namd and amd. */ + var rulesModules; /** * Respond to choosing the "Add" menu item for the selected node in the tree. @@ -347,6 +353,38 @@ define(['jquery', pickerInstance.display(); }; + var ruleConfigHandler = function(e) { + e.preventDefault(); + relatedTarget = $('[data-region="competencyactions"]').data('competency'); + ruleConfigInstance.setTargetCompetencyId(relatedTarget.id); + ruleConfigInstance.display(); + }; + + var ruleConfigSaveHandler = function(e, config) { + var promise = ajax.call([{ + methodname: 'tool_lp_update_competency', + args: { + id: relatedTarget.id, + shortname: relatedTarget.shortname, + idnumber: relatedTarget.idnumber, + description: relatedTarget.description, + descriptionformat: relatedTarget.descriptionformat, + visible: relatedTarget.visible, + ruletype: config.ruletype, + ruleoutcome: config.ruleoutcome, + ruleconfig: config.ruleconfig, + } + }]); + promise[0].then(function(result) { + if (result) { + relatedTarget.ruletype = config.ruletype; + relatedTarget.ruleoutcome = config.ruleoutcome; + relatedTarget.ruleconfig = config.ruleconfig; + renderCompetencySummary(relatedTarget); + } + }, notification.exception); + }; + /** * Delete a competency. * @method doDelete @@ -375,6 +413,7 @@ define(['jquery', // We don't need to show related actions when showing the competency info. delete competency.showdeleterelatedaction; delete competency.showrelatedcompetencies; + delete competency.showrule; templates.render('tool_lp/competency_summary', competency) .done(function(html) { @@ -511,6 +550,48 @@ define(['jquery', return constant; }; + /** + * Render the competency summary. + * + * @param {Object} competency The competency. + */ + var renderCompetencySummary = function(competency) { + var promise = $.Deferred().resolve().promise(), + context = $.extend({}, competency); + + context.showdeleterelatedaction = true; + context.showrelatedcompetencies = true; + context.showrule = false; + + if (competency.ruleoutcome != Outcomes.NONE) { + // Get the outcome and rule name. + promise = Outcomes.getString(competency.ruleoutcome).then(function(str) { + var name; + $.each(rulesModules, function(index, modInfo) { + if (modInfo.type == competency.ruletype) { + name = modInfo.name; + } + }); + return [str, name]; + }); + } + + promise.then(function(strs) { + if (typeof strs !== 'undefined') { + context.showrule = true; + context.rule = { + outcome: strs[0], + type: strs[1] + }; + } + }).then(function() { + return templates.render('tool_lp/competency_summary', context).then(function(html) { + $('[data-region="competencyinfo"]').html(html); + $('[data-action="deleterelation"]').on('click', deleteRelatedHandler); + }); + }).fail(notification.exception); + }; + /** * Return the string "Add ". * @@ -558,9 +639,6 @@ define(['jquery', } else { var competency = treeModel.getCompetency(id); - competency.showdeleterelatedaction = true; - competency.showrelatedcompetencies = true; - level = treeModel.getCompetencyLevel(id); if (!hasSubLevel(level)) { sublevel = false; @@ -570,10 +648,7 @@ define(['jquery', actionMenu.show(); $('[data-region="competencyactions"]').data('competency', competency); - templates.render('tool_lp/competency_summary', competency).then(function(html) { - $('[data-region="competencyinfo"]').html(html); - $('[data-action="deleterelation"]').on('click', deleteRelatedHandler); - }, notification.exception); + renderCompetencySummary(competency); } strSelectedTaxonomy(level).then(function(str) { @@ -602,11 +677,13 @@ define(['jquery', * @param {Object} model The tree model provides some useful functions for loading and searching competencies. * @param {Number} pagectxid The page context ID. * @param {Object} taxonomies Constants indexed by level. + * @param {Object} rulesMods The modules of the rules. */ - init: function(model, pagectxid, taxonomies) { + init: function(model, pagectxid, taxonomies, rulesMods) { treeModel = model; pageContextId = pagectxid; taxonomiesConstants = taxonomies; + rulesModules = rulesMods; $('[data-region="competencyactions"] [data-action="add"]').on('click', addHandler); @@ -617,7 +694,8 @@ define(['jquery', '[data-action="moveup"]': moveUpHandler, '[data-action="movedown"]': moveDownHandler, '[data-action="linkedcourses"]': seeCoursesHandler, - '[data-action="relatedcompetencies"]': relateCompetenciesHandler.bind(this) + '[data-action="relatedcompetencies"]': relateCompetenciesHandler.bind(this), + '[data-action="competencyrules"]': ruleConfigHandler.bind(this) }); $('[data-region="competencyactionsmenu"]').hide(); $('[data-region="competencyactions"] [data-action="add"]').hide(); @@ -631,6 +709,10 @@ define(['jquery', $('[data-region="managecompetencies"] li').on('drop', dropOver); model.on('selectionchanged', selectionChanged); + + // Prepare the configuration tool. + ruleConfigInstance = new RuleConfig(treeModel, rulesModules); + ruleConfigInstance.on('save', ruleConfigSaveHandler.bind(this)); } }; diff --git a/admin/tool/lp/amd/src/competencypicker.js b/admin/tool/lp/amd/src/competencypicker.js index 243fe8fb04c..4c2ed2569d6 100644 --- a/admin/tool/lp/amd/src/competencypicker.js +++ b/admin/tool/lp/amd/src/competencypicker.js @@ -1,4 +1,4 @@ -// self file is part of Moodle - http://moodle.org/ +// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/admin/tool/lp/amd/src/competencyruleconfig.js b/admin/tool/lp/amd/src/competencyruleconfig.js new file mode 100644 index 00000000000..4894d3689f3 --- /dev/null +++ b/admin/tool/lp/amd/src/competencyruleconfig.js @@ -0,0 +1,548 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle 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, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle 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 Moodle. If not, see . + +/** + * Competency rule config. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', + 'core/notification', + 'core/templates', + 'tool_lp/dialogue', + 'tool_lp/competency_outcomes', + 'core/str'], + function($, Notification, Templates, Dialogue, Outcomes, Str) { + + /** + * Competency rule class. + * + * When implementing this you should attach a listener to the event 'save' + * on the instance. E.g. + * + * var config = new RuleConfig(tree, modules); + * config.on('save', function(e, config) { ... }); + * + * @param {competencytree} tree The competency tree. + * @param {Array} rulesModules The modules containing the rules: [{ typeName: { amd: amdModule, name: ruleName }}]. + */ + var RuleConfig = function(tree, rulesModules) { + this._eventNode = $('
'); + this._tree = tree; + this._rulesModules = rulesModules; + this._setUp(); + }; + + /** @type {Object} The current competency. */ + RuleConfig.prototype._competency = null; + /** @type {Node} The node we attach the events to. */ + RuleConfig.prototype._eventNode = null; + /** @type {Array} Outcomes options. */ + RuleConfig.prototype._outcomesOption = null; + /** @type {Dialogue} The dialogue. */ + RuleConfig.prototype._popup = null; + /** @type {Promise} Resolved when the module is ready. */ + RuleConfig.prototype._ready = null; + /** @type {Array} The rules. */ + RuleConfig.prototype._rules = null; + /** @type {Array} The rules modules. */ + RuleConfig.prototype._rulesModules = null; + /** @type {competencytree} The competency tree. */ + RuleConfig.prototype._tree = null; + + /** + * After change. + * + * Triggered when a change occured. + * + * @return {Void} + * @method _afterChange + * @protected + */ + RuleConfig.prototype._afterChange = function() { + if (!this._isValid()) { + this._find('[data-action="save"]').prop('disabled', true); + } else { + this._find('[data-action="save"]').prop('disabled', false); + } + }; + + /** + * After change in rule's config. + * + * Triggered when a change occured in a specific rule config. + * + * @return {Void} + * @method _afterRuleConfigChange + * @protected + */ + RuleConfig.prototype._afterRuleConfigChange = function(e, rule) { + if (rule != this._getRule()) { + // This rule is not the current one any more, we can ignore. + return; + } + this._afterChange(); + }; + + /** + * After render hook. + * + * @return {Promise} + * @method _afterRender + * @protected + */ + RuleConfig.prototype._afterRender = function() { + var self = this; + + self._find('[name="outcome"]').on('change', function() { + self._switchedOutcome(); + }).trigger('change'); + + self._find('[name="rule"]').on('change', function() { + self._switchedRule(); + }).trigger('change'); + + self._find('[data-action="save"]').on('click', function() { + self._trigger('save', self._getConfig()); + self.close(); + }); + + self._find('[data-action="cancel"]').on('click', function() { + self.close(); + }); + }; + + /** + * Whether the current competency can be configured. + * + * @return {Boolean} + * @method canBeConfigured + */ + RuleConfig.prototype.canBeConfigured = function() { + var can = false; + $.each(this._rules, function(index, rule) { + if (rule.canConfig()) { + can = true; + return false; + } + }); + return can; + }; + + /** + * Close the dialogue. + * + * @method close + */ + RuleConfig.prototype.close = function() { + this._popup.close(); + this._popup = null; + }; + + /** + * Opens the picker. + * + * @param {Number} competencyId The competency ID of the competency to work on. + * @method display + * @return {Promise} + */ + RuleConfig.prototype.display = function() { + var self = this; + if (!self._competency) { + return; + } + return self._render().then(function(html) { + return Str.get_string('competencyrule', 'tool_lp').then(function(title) { + self._popup = new Dialogue( + title, + html, + self._afterRender.bind(self) + ); + }); + }).fail(Notification.exception); + }; + + /** + * Find a node in the dialogue. + * + * @param {String} selector + * @method _find + * @protected + */ + RuleConfig.prototype._find = function(selector) { + return $(this._popup.getContent()).find(selector); + }; + + /** + * Get the applicable outcome options. + * + * @return {Array} + * @method _getApplicableOutcomesOptions + * @protected + */ + RuleConfig.prototype._getApplicableOutcomesOptions = function() { + var self = this, + options = []; + + $.each(self._outcomesOption, function(index, outcome) { + options.push({ + code: outcome.code, + name: outcome.name, + selected: (outcome.code == self._competency.ruleoutcome) ? true : false, + }); + }); + + return options; + }; + + /** + * Get the applicable rules options. + * + * @return {Array} + * @method _getApplicableRulesOptions + * @protected + */ + RuleConfig.prototype._getApplicableRulesOptions = function() { + var self = this, + options = []; + + $.each(self._rules, function(index, rule) { + if (!rule.canConfig()) { + return; + } + options.push({ + name: self._getRuleName(rule.getType()), + type: rule.getType(), + selected: (rule.getType() == self._competency.ruletype) ? true : false, + }); + }); + + return options; + }; + + /** + * Get the full config for the competency. + * + * @return {Object} Contains rule, ruleoutcome and ruleconfig. + * @method _getConfig + * @protected + */ + RuleConfig.prototype._getConfig = function() { + var rule = this._getRule(); + return { + ruletype: rule ? rule.getType() : null, + ruleconfig: rule ? rule.getConfig() : null, + ruleoutcome: this._getOutcome() + }; + }; + + /** + * Get the selected outcome code. + * + * @return {String} + * @method _getOutcome + * @protected + */ + RuleConfig.prototype._getOutcome = function() { + return this._find('[name="outcome"]').val(); + }; + + /** + * Get the selected rule. + * + * @return {null|Rule} + * @method _getRule + * @protected + */ + RuleConfig.prototype._getRule = function() { + var result, + type = this._find('[name="rule"]').val(); + + $.each(this._rules, function(index, rule) { + if (rule.getType() == type) { + result = rule; + return false; + } + }); + + return result; + }; + + /** + * Return the name of a rule. + * + * @param {String} type The type of a rule. + * @return {String} + * @method _getRuleName + * @protected + */ + RuleConfig.prototype._getRuleName = function(type) { + var self = this, + name; + $.each(self._rulesModules, function(index, modInfo) { + if (modInfo.type == type) { + name = modInfo.name; + return false; + } + }); + return name; + }; + + /** + * Initialise the outcomes. + * + * @return {Promise} + * @method _initOutcomes + * @protected + */ + RuleConfig.prototype._initOutcomes = function() { + var self = this; + + return Outcomes.getAll().then(function(outcomes) { + self._outcomesOption = outcomes; + }); + }; + + /** + * Initialise the rules. + * + * @return {Promise} + * @method _initRules + * @protected + */ + RuleConfig.prototype._initRules = function() { + var self = this, + promises = []; + + $.each(self._rules, function(index, rule) { + var promise = rule.init().then(function() { + rule.setTargetCompetency(self._competency); + rule.on('change', self._afterRuleConfigChange.bind(self)); + }, function() { + // Upon failure remove the rule, and resolve the promise. + self._rules.splice(index, 1); + return $.when(); + }); + promises.push(promise); + }); + + return $.when.apply($.when, promises); + }; + + /** + * Whether or not the current config is valid. + * + * @return {Boolean} + * @method _isValid + * @protected + */ + RuleConfig.prototype._isValid = function() { + var outcome = this._getOutcome(), + rule = this._getRule(); + + if (outcome == Outcomes.NONE) { + return true; + } else if (!rule) { + return false; + } + + return rule.isValid(); + }; + + /** + * Register an event listener. + * + * @param {String} type The event type. + * @param {Function} handler The event listener. + * @method on + */ + RuleConfig.prototype.on = function(type, handler) { + this._eventNode.on(type, handler); + }; + + /** + * Hook to executed before render. + * + * @method _preRender + * @protected + * @return {Promise} + */ + RuleConfig.prototype._preRender = function() { + // We need to have all the information about the rule plugins first. + return this.ready(); + }; + + /** + * Returns a promise that is resolved when the module is ready. + * + * @return {Promise} + * @method ready + * @protected + */ + RuleConfig.prototype.ready = function() { + return this._ready.promise(); + }; + + /** + * Render the dialogue. + * + * @method _render + * @protected + * @return {Promise} + */ + RuleConfig.prototype._render = function() { + var self = this; + return this._preRender().then(function() { + var config; + + if (!self.canBeConfigured()) { + config = false; + } else { + config = {}; + config.outcomes = self._getApplicableOutcomesOptions(); + config.rules = self._getApplicableRulesOptions(); + } + + var context = { + competencyshortname: self._competency.shortname, + config: config + }; + + return Templates.render('tool_lp/competency_rule_config', context); + }); + }; + + /** + * Set the target competency. + * + * @param {Number} competencyId The target competency Id. + * @method setTargetCompetencyId + */ + RuleConfig.prototype.setTargetCompetencyId = function(competencyId) { + var self = this; + self._competency = self._tree.getCompetency(competencyId); + $.each(self._rules, function(index, rule) { + rule.setTargetCompetency(self._competency); + }); + }; + + /** + * Set up the instance. + * + * @return {Void} + * @method _setUp + * @protected + */ + RuleConfig.prototype._setUp = function() { + var self = this, + promises = [], + modules = []; + + self._ready = $.Deferred(); + self._rules = []; + + $.each(self._rulesModules, function(index, rule) { + modules.push(rule.amd); + }); + + // Load all the modules. + require(modules, function() { + $.each(arguments, function(index, Module) { + // Instantiate the rule and listen to it. + var rule = new Module(self._tree); + self._rules.push(rule); + }); + + // Load all the option values. + promises.push(self._initRules()); + promises.push(self._initOutcomes()); + + // Ready when everything is done. + $.when.apply($.when, promises).always(function() { + self._ready.resolve(); + }); + }); + }; + + /** + * Called when the user switches outcome. + * + * @return {Void} + * @method _switchedOutcome + * @protected + */ + RuleConfig.prototype._switchedOutcome = function() { + var self = this, + type = self._getOutcome(); + + if (type == Outcomes.NONE) { + // Reset to defaults. + self._find('[data-region="rule-type"]').hide() + .find('[name="rule"]').val(-1); + self._find('[data-region="rule-config"]').empty().hide(); + self._afterChange(); + return; + } + + self._find('[data-region="rule-type"]').show(); + self._find('[data-region="rule-config"]').show(); + self._afterChange(); + }; + + /** + * Called when the user switches rule. + * + * @return {Void} + * @method _switchedRule + * @protected + */ + RuleConfig.prototype._switchedRule = function() { + var self = this, + container = self._find('[data-region="rule-config"]'), + rule = self._getRule(); + + if (!rule) { + container.empty().hide(); + self._afterChange(); + return; + } + + rule.injectTemplate(container).then(function() { + container.show(); + }, function() { + container.empty().hide(); + }).always(function() { + self._afterChange(); + }); + }; + + /** + * Trigger an event. + * + * @param {String} type The type of event. + * @param {Object} The data to pass to the listeners. + * @method _trigger + * @protected + */ + RuleConfig.prototype._trigger = function(type, data) { + this._eventNode.trigger(type, [data]); + }; + + return /** @alias module:tool_lp/competencyruleconfig */ RuleConfig; + +}); diff --git a/admin/tool/lp/amd/src/competencytree.js b/admin/tool/lp/amd/src/competencytree.js index f6730737b8a..a96d28dd718 100644 --- a/admin/tool/lp/amd/src/competencytree.js +++ b/admin/tool/lp/amd/src/competencytree.js @@ -26,7 +26,7 @@ define(['core/ajax', 'core/notification', 'core/templates', 'tool_lp/tree', 'jqu // Private variables and functions. /** @var {Object[]} competencies - Cached list of competencies */ - var competencies = []; + var competencies = {}; /** @var {Number} competencyFrameworkId - The current framework id */ var competencyFrameworkId = 0; @@ -160,6 +160,23 @@ define(['core/ajax', 'core/notification', 'core/templates', 'tool_lp/tree', 'jqu $(treeSelector).on(eventname, handler); }, + /** + * Get the children of a competency. + * + * @param {Number} id The competency ID. + * @return {Array} + * @method getChildren + */ + getChildren: function(id) { + var children = []; + $.each(competencies, function(index, competency) { + if (competency.parentid == id) { + children.push(competency); + } + }); + return children; + }, + /** * Get the competency framework id this model was initiliased with. * @@ -191,6 +208,17 @@ define(['core/ajax', 'core/notification', 'core/templates', 'tool_lp/tree', 'jqu return level; }, + /** + * Whether a competency has children. + * + * @param {Number} id The competency ID. + * @return {Boolean} + * @method hasChildren + */ + hasChildren: function(id) { + return this.getChildren(id).length > 0; + }, + /** * Reload all the page competencies framework competencies. * @method reloadCompetencies diff --git a/admin/tool/lp/classes/api.php b/admin/tool/lp/classes/api.php index 8b2eef356d1..fefe3bf36aa 100644 --- a/admin/tool/lp/classes/api.php +++ b/admin/tool/lp/classes/api.php @@ -209,17 +209,17 @@ class api { * @return boolean */ public static function update_competency($record) { - $current = new competency($record->id); + $competency = new competency($record->id); // First we do a permissions check. - require_capability('tool/lp:competencymanage', $current->get_framework()->get_context()); + require_capability('tool/lp:competencymanage', $competency->get_framework()->get_context()); // Some things should not be changed in an update - they should use a more specific method. - $record->sortorder = $current->get_sortorder(); - $record->parentid = $current->get_parentid(); - $record->competencyframeworkid = $current->get_competencyframeworkid(); + $record->sortorder = $competency->get_sortorder(); + $record->parentid = $competency->get_parentid(); + $record->competencyframeworkid = $competency->get_competencyframeworkid(); - $competency = new competency(0, $record); + $competency->from_record($record); require_capability('tool/lp:competencymanage', $competency->get_framework()->get_context()); // OK - all set. diff --git a/admin/tool/lp/classes/competency.php b/admin/tool/lp/classes/competency.php index aa57612161d..275be7ca467 100644 --- a/admin/tool/lp/classes/competency.php +++ b/admin/tool/lp/classes/competency.php @@ -37,6 +37,15 @@ class competency extends persistent { const TABLE = 'tool_lp_competency'; + /** Outcome none. */ + const OUTCOME_NONE = 0; + /** Outcome evidence. */ + const OUTCOME_EVIDENCE = 1; + /** Outcome complete. */ + const OUTCOME_COMPLETE = 2; + /** Outcome recommend. */ + const OUTCOME_RECOMMEND = 3; + /** @var competency Object before update. */ protected $beforeupdate = null; @@ -78,6 +87,21 @@ class competency extends persistent { 'default' => '/0/', 'type' => PARAM_RAW ), + 'ruleoutcome' => array( + 'choices' => array(self::OUTCOME_NONE, self::OUTCOME_EVIDENCE, self::OUTCOME_COMPLETE, self::OUTCOME_RECOMMEND), + 'default' => self::OUTCOME_NONE, + 'type' => PARAM_INT + ), + 'ruletype' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED + ), + 'ruleconfig' => array( + 'default' => null, + 'type' => PARAM_RAW, + 'null' => NULL_ALLOWED + ), 'competencyframeworkid' => array( 'default' => 0, 'type' => PARAM_INT @@ -215,6 +239,37 @@ class competency extends persistent { return related_competency::get_related_competencies($this->get_id()); } + /** + * Get the rule object. + * + * @return null|competency_rule + */ + public function get_rule_object() { + $rule = $this->get_ruletype(); + + if (!$rule || !is_subclass_of($rule, '\tool_lp\competency_rule')) { + // Double check that the rule is extending the right class to avoid bad surprises. + return null; + } + + return new $rule($this); + } + + /** + * Check if the competency is the parent of passed competencies. + * + * @param array $ids IDs of supposedly direct children. + * @return boolean + */ + public function is_parent_of(array $ids) { + global $DB; + + list($insql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); + $params['parentid'] = $this->get_id(); + + return $DB->count_records_select(self::TABLE, "id $insql AND parentid = :parentid", $params) == count($ids); + } + /** * Helper method to set the path. * @@ -404,6 +459,53 @@ class competency extends persistent { return true; } + /** + * Validate the rule. + * + * @param string $value The ID. + * @return true|lang_string + */ + protected function validate_ruletype($value) { + if ($value === null) { + return true; + } + + if (!class_exists($value) || !is_subclass_of($value, '\tool_lp\competency_rule')) { + return new lang_string('invaliddata', 'error'); + } + + return true; + } + + /** + * Validate the rule config. + * + * @param string $value The ID. + * @return true|lang_string + */ + protected function validate_ruleconfig($value) { + $rule = $this->get_rule_object(); + + // We don't have a rule. + if (empty($rule)) { + if ($value === null) { + // No config, perfect. + return true; + } else if (empty($rule) && !$value !== null) { + // Config but no rules, whoops! + return new lang_string('invaliddata', 'error'); + } + } + + $valid = $rule->validate_config($value); + if ($valid !== true) { + // Whoops! + return new lang_string('invaliddata', 'error'); + } + + return true; + } + /** * Return whether or not the competency IDs share the same framework. * @@ -416,6 +518,24 @@ class competency extends persistent { return $DB->count_records_select(self::TABLE, "id $insql", $params, "COUNT(DISTINCT(competencyframeworkid))") == 1; } + /** + * Get the available rules. + * + * @return array Keys are the class names, values is an object containing name and amd. + */ + public static function get_available_rules() { + // Fully qualified class names withough leading slashes because get_class() does not add them either. + $rules = array( + 'tool_lp\competency_rule_all' => (object) array(), + 'tool_lp\competency_rule_points' => (object) array(), + ); + foreach ($rules as $class => $rule) { + $rule->name = $class::get_name(); + $rule->amd = $class::get_amd_module(); + } + return $rules; + } + /** * Build a framework tree with competency nodes. * diff --git a/admin/tool/lp/classes/competency_rule.php b/admin/tool/lp/classes/competency_rule.php new file mode 100644 index 00000000000..23d7e6029ef --- /dev/null +++ b/admin/tool/lp/classes/competency_rule.php @@ -0,0 +1,101 @@ +. + +/** + * Competency rule base. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; + +/** + * Competency rule base abstract class. + * + * Rules are attached to a competency and then tested against a user competency + * to determine whether or not it matches. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class competency_rule { + + /** @var competency The competency. */ + protected $competency; + + /** + * Constructor. + * + * @param competency $competency The competency. + */ + public function __construct(competency $competency) { + $class = $competency->get_ruletype(); + if (!$class || !($this instanceof $class)) { + throw new coding_exception('This competency does not use this rule.'); + } + + $this->competency = $competency; + } + + /** + * Get the rule config. + * + * @return mixed + */ + protected function get_config() { + return $this->competency->get_ruleconfig(); + } + + /** + * Whether or not the rule is matched. + * + * @param user_competency $usercompetency The user competency to test against. + * @return bool + */ + abstract public function matches(user_competency $usercompetency); + + /** + * Validate the rule config. + * + * @return bool + */ + abstract public function validate_config($value); + + /** + * Get the fully qualified name of the AMD module to use. + * + * @return string + */ + public static function get_amd_module() { + throw new coding_exception('Method not implemented.'); + } + + /** + * The name of the rule. + * + * @return lang_string + */ + public static function get_name() { + throw new coding_exception('Method not implemented.'); + } + +} diff --git a/admin/tool/lp/classes/competency_rule_all.php b/admin/tool/lp/classes/competency_rule_all.php new file mode 100644 index 00000000000..5c6cc2adc30 --- /dev/null +++ b/admin/tool/lp/classes/competency_rule_all.php @@ -0,0 +1,99 @@ +. + +/** + * Competency rule all. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp; +defined('MOODLE_INTERNAL') || die(); + +use lang_string; + +/** + * Competency rule all class. + * + * This rule is considered matched when all the children of a competency are completed. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_rule_all extends competency_rule { + + /** + * Whether or not the rule is matched. + * + * @return bool + */ + public function matches(user_competency $usercompetency) { + global $DB; + + // TODO Improve performance here, perhaps the caller could already provide records. + $children = competency::get_records(array('parentid' => $usercompetency->get_competencyid())); + + if (empty($children)) { + // Leaves are not compatible with this rule. + return false; + } + + $ids = array(); + foreach ($children as $child) { + $ids[] = $child->get_id(); + } + + list($insql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); + $sql = "userid = :userid + AND proficiency = :proficiency + AND competencyid $insql"; + $params['userid'] = $usercompetency->get_userid(); + $params['proficiency'] = 1; + + // Is the user is marked as proficient in all children? + return user_competency::count_records_select($sql, $params) === count($ids); + } + + /** + * Validate the rule config. + * + * @return bool + */ + public function validate_config($value) { + return $value === null; + } + + /** + * Validate the rule config. + * + * @return bool + */ + public static function get_amd_module() { + return 'tool_lp/competency_rule_all'; + } + + /** + * The name of the rule. + * + * @return lang_string + */ + public static function get_name() { + return new lang_string('allchildrenarecomplete', 'tool_lp'); + } +} diff --git a/admin/tool/lp/classes/competency_rule_points.php b/admin/tool/lp/classes/competency_rule_points.php new file mode 100644 index 00000000000..a1d9a21bce6 --- /dev/null +++ b/admin/tool/lp/classes/competency_rule_points.php @@ -0,0 +1,199 @@ +. + +/** + * Competency rule points based. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp; +defined('MOODLE_INTERNAL') || die(); + +use lang_string; + + +/** + * Competency rule points based class. + * + * This rule matches when related competencies contribute for a required number of points. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_rule_points extends competency_rule { + + /** + * Get the rule config. + * + * @return mixed + */ + protected function get_config() { + $config = parent::get_config(); + return json_decode($config); + } + + /** + * Whether or not the rule is matched. + * + * @return bool + */ + public function matches(user_competency $usercompetency) { + global $DB; + + $config = $this->get_config(); + $pointsrequired = $config->base->points; + + // Index by competency ID and extract required. + $compsrules = array(); + $requiredids = array(); + foreach ($config->competencies as $comp) { + $compsrules[$comp->id] = $comp; + if ($comp->required) { + $requiredids[$comp->id] = $comp->id; + } + } + + // Find all the user competency records. + list($insql, $params) = $DB->get_in_or_equal(array_keys($compsrules), SQL_PARAMS_NAMED); + $sql = "userid = :userid + AND proficiency = :proficiency + AND competencyid $insql"; + $params['userid'] = $usercompetency->get_userid(); + $params['proficiency'] = 1; + $ucs = user_competency::get_records_select($sql, $params, '', 'competencyid'); + + // Check that all the required are found. + if (!empty($requiredids)) { + $unmetrequired = array_diff_key($requiredids, $ucs); + if (!empty($unmetrequired)) { + return false; + } + } + + // Check that we have enough points. + $points = 0; + foreach ($compsrules as $compid => $comp) { + if (array_key_exists($compid, $ucs)) { + $points += $comp->points; + } + } + + return $points >= $pointsrequired; + } + + /** + * Validate the rule config. + * + * @return bool + */ + public function validate_config($value) { + $compids = array(); + $config = json_decode($value); + if ($config === null || !isset($config->base) || !isset($config->competencies)) { + return false; + } + + if (!isset($config->base->points)) { + return false; + } + + try { + $requiredpoints = validate_param($config->base->points, PARAM_INT); + } catch (\invalid_parameter_exception $e) { + return false; + } + + if ($requiredpoints < 1) { + return false; + } + + $totalpoints = 0; + + // Validate the competency info. + foreach ($config->competencies as $competency) { + + // Cannot include self. + if ($competency->id == $this->competency->get_id()) { + return false; + } + + // Check for duplicates. + if (in_array($competency->id, $compids)) { + return false; + } + + // Check for required fields. + if (!isset($competency->id) + || !isset($competency->points) + || !isset($competency->required)) { + return false; + } + + // Validate the parameters. + try { + validate_param($competency->id, PARAM_INT); + $points = validate_param($competency->points, PARAM_INT); + validate_param($competency->required, PARAM_BOOL); + } catch (\invalid_parameter_exception $e) { + return false; + } + + $totalpoints += $points; + if ($points < 0) { + return false; + } + + $compids[] = $competency->id; + } + + // No competencies, that's strange. + if (empty($compids)) { + return false; + } + + // Impossible to reach the points required. + if ($requiredpoints > $totalpoints) { + return false; + } + + // Check that all the competencies are children of the competency. + // We may want to relax this check at a later stage if we want to allow competencies + // to be linked throughout the whole framework. + return $this->competency->is_parent_of($compids); + } + + /** + * Validate the rule config. + * + * @return bool + */ + public static function get_amd_module() { + return 'tool_lp/competency_rule_points'; + } + + /** + * The name of the rule. + * + * @return lang_string + */ + public static function get_name() { + return new lang_string('pointsrequiredaremet', 'tool_lp'); + } +} diff --git a/admin/tool/lp/classes/external.php b/admin/tool/lp/classes/external.php index 28f759fa96c..aeb2e7d21ef 100644 --- a/admin/tool/lp/classes/external.php +++ b/admin/tool/lp/classes/external.php @@ -1247,6 +1247,24 @@ class external extends external_api { 'Is this framework visible?', VALUE_REQUIRED ); + $ruletype = new external_value( + PARAM_RAW, + 'The type of rule', + VALUE_DEFAULT, + null + ); + $ruleoutcome = new external_value( + PARAM_INT, + 'The outcome when the rule matches. Constant value competency::OUTCOME_*', + VALUE_DEFAULT, + competency::OUTCOME_NONE + ); + $ruleconfig = new external_value( + PARAM_RAW, + 'The extra config of the rule.', + VALUE_DEFAULT, + null + ); $params = array( 'id' => $id, @@ -1255,6 +1273,9 @@ class external extends external_api { 'description' => $description, 'descriptionformat' => $descriptionformat, 'visible' => $visible, + 'ruletype' => $ruletype, + 'ruleoutcome' => $ruleoutcome, + 'ruleconfig' => $ruleconfig, ); return new external_function_parameters($params); } @@ -1275,7 +1296,10 @@ class external extends external_api { $idnumber, $description, $descriptionformat, - $visible) { + $visible, + $ruletype = null, + $ruleoutcome = competency::OUTCOME_NONE, + $ruleconfig = null) { $params = self::validate_parameters(self::update_competency_parameters(), array( @@ -1284,7 +1308,10 @@ class external extends external_api { 'idnumber' => $idnumber, 'description' => $description, 'descriptionformat' => $descriptionformat, - 'visible' => $visible + 'visible' => $visible, + 'ruletype' => $ruletype, + 'ruleoutcome' => $ruleoutcome, + 'ruleconfig' => $ruleconfig, )); $params = (object) $params; @@ -1555,7 +1582,12 @@ class external extends external_api { 'framework' => self::get_competency_framework_external_structure(), 'canmanage' => new external_value(PARAM_BOOL, 'True if this user has permission to manage competency frameworks'), 'pagecontextid' => new external_value(PARAM_INT, 'Context id for the framework'), - 'search' => new external_value(PARAM_RAW, 'Current search string') + 'search' => new external_value(PARAM_RAW, 'Current search string'), + 'rulesmodules' => new external_multiple_structure(new external_single_structure(array( + 'type' => new external_value(PARAM_RAW, 'The rule type'), + 'amd' => new external_value(PARAM_RAW, 'The AMD module of the rule'), + 'name' => new external_value(PARAM_TEXT, 'The name of the rule'), + ))) )); } diff --git a/admin/tool/lp/classes/output/manage_competencies_page.php b/admin/tool/lp/classes/output/manage_competencies_page.php index 09c5a18c46f..72d05b8cb0a 100644 --- a/admin/tool/lp/classes/output/manage_competencies_page.php +++ b/admin/tool/lp/classes/output/manage_competencies_page.php @@ -31,6 +31,7 @@ use stdClass; use moodle_url; use context_system; use tool_lp\api; +use tool_lp\competency; use tool_lp\competency_framework; /** @@ -119,6 +120,13 @@ class manage_competencies_page implements renderable, templatable { $data->search = $this->search; $data->pagecontextid = $this->pagecontext->id; + $rules = competency::get_available_rules(); + foreach ($rules as $type => $rule) { + $rule->name = (string) $rule->name; + $rule->type = $type; + } + $data->rulesmodules = json_encode(array_values($rules)); + return $data; } } diff --git a/admin/tool/lp/classes/persistent.php b/admin/tool/lp/classes/persistent.php index 8f6bbe84907..84b2c55afe9 100644 --- a/admin/tool/lp/classes/persistent.php +++ b/admin/tool/lp/classes/persistent.php @@ -680,8 +680,8 @@ abstract class persistent { // We return class instances. $instances = array(); - foreach ($records as $record) { - array_push($instances, new static(0, $record)); + foreach ($records as $key => $record) { + $instances[$key] = new static(0, $record); } return $instances; diff --git a/admin/tool/lp/db/install.xml b/admin/tool/lp/db/install.xml index 4c1deee50b3..b300ea5d17e 100644 --- a/admin/tool/lp/db/install.xml +++ b/admin/tool/lp/db/install.xml @@ -16,6 +16,9 @@ + + + @@ -25,6 +28,7 @@ + @@ -134,7 +138,7 @@
- +
diff --git a/admin/tool/lp/db/upgrade.php b/admin/tool/lp/db/upgrade.php index c60e05a4f33..f42d4646452 100644 --- a/admin/tool/lp/db/upgrade.php +++ b/admin/tool/lp/db/upgrade.php @@ -310,5 +310,65 @@ function xmldb_tool_lp_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2015052427, 'tool', 'lp'); } + if ($oldversion < 2015111001) { + + // Define field ruletype to be added to tool_lp_competency. + $table = new xmldb_table('tool_lp_competency'); + $field = new xmldb_field('ruletype', XMLDB_TYPE_CHAR, '100', null, null, null, null, 'sortorder'); + + // Conditionally launch add field ruletype. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Lp savepoint reached. + upgrade_plugin_savepoint(true, 2015111001, 'tool', 'lp'); + } + + if ($oldversion < 2015111002) { + + // Define field ruleoutcome to be added to tool_lp_competency. + $table = new xmldb_table('tool_lp_competency'); + $field = new xmldb_field('ruleoutcome', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'ruletype'); + + // Conditionally launch add field ruleoutcome. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Lp savepoint reached. + upgrade_plugin_savepoint(true, 2015111002, 'tool', 'lp'); + } + + if ($oldversion < 2015111003) { + + // Define field ruleconfig to be added to tool_lp_competency. + $table = new xmldb_table('tool_lp_competency'); + $field = new xmldb_field('ruleconfig', XMLDB_TYPE_TEXT, null, null, null, null, null, 'ruleoutcome'); + + // Conditionally launch add field ruleconfig. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Lp savepoint reached. + upgrade_plugin_savepoint(true, 2015111003, 'tool', 'lp'); + } + + if ($oldversion < 2015111004) { + + // Define index ruleoutcome (not unique) to be added to tool_lp_competency. + $table = new xmldb_table('tool_lp_competency'); + $index = new xmldb_index('ruleoutcome', XMLDB_INDEX_NOTUNIQUE, array('ruleoutcome')); + + // Conditionally launch add index ruleoutcome. + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Lp savepoint reached. + upgrade_plugin_savepoint(true, 2015111004, 'tool', 'lp'); + } + return true; } diff --git a/admin/tool/lp/lang/en/tool_lp.php b/admin/tool/lp/lang/en/tool_lp.php index d041a3f1cad..9326fd80ca1 100644 --- a/admin/tool/lp/lang/en/tool_lp.php +++ b/admin/tool/lp/lang/en/tool_lp.php @@ -29,6 +29,8 @@ $string['addnewcompetencyframework'] = 'Add new competency framework'; $string['addnewplan'] = 'Add new learning plan'; $string['addnewtemplate'] = 'Add new learning plan template'; $string['addrelatedcompetency'] = 'Add related competency'; +$string['allchildrenarecomplete'] = 'All children are complete'; +$string['cannotaddrules'] = 'This competency cannot be configured.'; $string['competencies'] = 'Competencies'; $string['competenciesforframework'] = 'Competencies for {$a}'; $string['competencycreated'] = 'Competency created'; @@ -37,8 +39,13 @@ $string['competencyframeworkcreated'] = 'Competency framework created.'; $string['competencyframeworkname'] = 'Name'; $string['competencyframeworks'] = 'Competency Frameworks'; $string['competencyframeworkupdated'] = 'Competency framework updated.'; +$string['competencyoutcome_complete'] = 'Mark as complete'; +$string['competencyoutcome_evidence'] = 'Attach an evidence'; +$string['competencyoutcome_none'] = 'None'; +$string['competencyoutcome_recommend'] = 'Recommend the competency'; $string['competencypicker'] = 'Competency picker'; $string['competencyrelatedcompetencies'] = '{$a} related competencies'; +$string['competencyrule'] = 'Competency rule'; $string['competencyupdated'] = 'Competency updated'; $string['configurescale'] = 'Configure scales'; $string['coursecompetencies'] = 'Course competencies'; @@ -72,6 +79,7 @@ $string['invalidpersistent'] = 'Invalid persistent'; $string['invalidplan'] = 'Invalid learning plan'; $string['invalidtaxonomy'] = 'Invalid taxonomy: {$a}'; $string['itemstoadd'] = 'Items to add'; +$string['aisrequired'] = '\'{$a}\' is required'; $string['learningplans'] = 'Learning plans'; $string['levela'] = 'Level {$a}'; $string['linkcoursecompetencies'] = 'Link course competencies'; @@ -112,6 +120,7 @@ $string['nolinkedcourses'] = 'No courses are using this competency'; $string['norelatedcompetencies'] = 'No other competencies have been related to this competency.'; $string['notemplates'] = 'No learning plan templates have been created yet.'; $string['nouserplans'] = 'No learning plans have been created yet.'; +$string['outcome'] = 'Outcome'; $string['parentcompetency'] = 'Parent competency'; $string['plancreated'] = 'Learning plan created'; $string['plandescription'] = 'Description'; @@ -123,6 +132,9 @@ $string['plantemplate'] = 'Select template'; $string['plantemplate_help'] = 'A learning plan created from a template will contain a list of competencies that match the template. Updates to the template will be reflected in any plan created from that template.'; $string['planupdated'] = 'Learning plan updated'; $string['pluginname'] = 'Learning Plans'; +$string['points'] = 'Points'; +$string['pointsgivenfor'] = 'Points given for \'{$a}\''; +$string['pointsrequiredaremet'] = 'Points required are met'; $string['proficient'] = 'Proficient'; $string['relatedcompetencies'] = 'Related competencies'; $string['savechanges'] = 'Save changes'; @@ -196,8 +208,10 @@ $string['templatecreated'] = 'Learning plan template created'; $string['templatename'] = 'Name'; $string['templates'] = 'Learning plan templates'; $string['templateupdated'] = 'Learning plan template updated'; +$string['totalrequiredtocomplete'] = 'Total required to complete'; $string['usercompetencystatus_idle'] = 'Idle'; $string['usercompetencystatus_inreview'] = 'In review'; $string['usercompetencystatus_waitingforreview'] = 'Waiting for review'; $string['visible'] = 'Visible'; $string['visible_help'] = 'A competency framework can be hidden from teachers. This could be useful if a framework is still in the process of being developed.'; +$string['when'] = 'When'; diff --git a/admin/tool/lp/styles.css b/admin/tool/lp/styles.css index d80b8a7e456..f6bbc73a083 100644 --- a/admin/tool/lp/styles.css +++ b/admin/tool/lp/styles.css @@ -52,6 +52,47 @@ vertical-align: text-top; } +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-base"] { + display: table; + width: 100%; +} +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"], +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] { + display: table-row; +} +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"] label, +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] label { + padding-right: 10px; +} +.dir-rtl.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"] label, +.dir-rtl.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] label { + padding-left: 10px; + padding-right: 0; +} +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"] label, +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"] select, +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] label, +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] select { + display: table-cell; +} +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"] select, +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] select { + width: 100%; +} + +.path-admin-tool-lp .competency-rule-points { + margin-top: 10px; +} +.path-admin-tool-lp .competency-rule-points table input { + margin-bottom: 0; +} +.path-admin-tool-lp .competency-rule-points tr[data-competency] th { + font-weight: normal; +} +.path-admin-tool-lp .competency-rule-points input[type="number"] { + width: 50px; +} + /** Styles required for menubar js */ .tool-lp-menu li { diff --git a/admin/tool/lp/templates/competency_rule_config.mustache b/admin/tool/lp/templates/competency_rule_config.mustache new file mode 100644 index 00000000000..7c64ab6504e --- /dev/null +++ b/admin/tool/lp/templates/competency_rule_config.mustache @@ -0,0 +1,39 @@ +
+
+ + {{^config}} +

{{#str}}cannotaddrules, tool_lp{{/str}}

+ {{/config}} + + {{#config}} +
+
+ + +
+
+ + +
+
+
+
+ {{/config}} +
+ +
+ {{#config}} + + {{/config}} + +
+
diff --git a/admin/tool/lp/templates/competency_rule_points.mustache b/admin/tool/lp/templates/competency_rule_points.mustache new file mode 100644 index 00000000000..ec8c60f628d --- /dev/null +++ b/admin/tool/lp/templates/competency_rule_points.mustache @@ -0,0 +1,27 @@ +
+
+ + + + + + + + + {{#children}} + + + + + + {{/children}} + + + + + + + + +
{{#str}}points, tool_lp{{/str}}{{#str}}required{{/str}}
{{shortname}}
{{#str}}totalrequiredtocomplete, tool_lp{{/str}}
+
diff --git a/admin/tool/lp/templates/competency_summary.mustache b/admin/tool/lp/templates/competency_summary.mustache index 2a22ef14566..53989bc6ed6 100644 --- a/admin/tool/lp/templates/competency_summary.mustache +++ b/admin/tool/lp/templates/competency_summary.mustache @@ -6,3 +6,12 @@ {{#showrelatedcompetencies}} {{> tool_lp/related_competencies }} {{/showrelatedcompetencies}} +{{#showrule}} +
{{#str}}competencyrule, tool_lp{{/str}}
+
+
{{#str}}outcome, tool_lp{{/str}}
+
{{rule.outcome}}
+
{{#str}}when, tool_lp{{/str}}
+
{{rule.type}}
+
+{{/showrule}} diff --git a/admin/tool/lp/templates/manage_competencies_page.mustache b/admin/tool/lp/templates/manage_competencies_page.mustache index 87bf02d7795..88e5dc1215a 100644 --- a/admin/tool/lp/templates/manage_competencies_page.mustache +++ b/admin/tool/lp/templates/manage_competencies_page.mustache @@ -94,6 +94,11 @@ {{#pix}}t/add{{/pix}} {{#str}}addrelatedcompetency, tool_lp{{/str}} +
  • + + {{#pix}}t/edit{{/pix}} {{#str}}competencyrule, tool_lp{{/str}} + +
  • @@ -113,7 +118,7 @@ require(['tool_lp/tree', 'tool_lp/competencytree', 'tool_lp/competencyactions', '{{search}}', '[data-enhance=tree]'); - actions.init(treeModel, {{pagecontextid}}, {{{framework.taxonomies}}}); + actions.init(treeModel, {{pagecontextid}}, {{{framework.taxonomies}}}, {{{rulesmodules}}}); }); {{/js}} diff --git a/admin/tool/lp/tests/api_test.php b/admin/tool/lp/tests/api_test.php index 3503dc3be5d..dbec9cb9e65 100644 --- a/admin/tool/lp/tests/api_test.php +++ b/admin/tool/lp/tests/api_test.php @@ -221,24 +221,22 @@ class tool_lp_api_testcase extends advanced_testcase { // Get frameworks list order by shortname desc. $result = api::list_frameworks('shortname', 'DESC', null, 3, context_system::instance()); - $r1 = (object) $result[0]; - $r2 = (object) $result[1]; - $r3 = (object) $result[2]; - - $this->assertEquals($framework1->get_id(), $r3->get_id()); - $this->assertEquals($framework2->get_id(), $r2->get_id()); - $this->assertEquals($framework3->get_id(), $r1->get_id()); + $f = (object) array_shift($result); + $this->assertEquals($framework3->get_id(), $f->get_id()); + $f = (object) array_shift($result); + $this->assertEquals($framework2->get_id(), $f->get_id()); + $f = (object) array_shift($result); + $this->assertEquals($framework1->get_id(), $f->get_id()); // Get frameworks list order by idnumber asc. $result = api::list_frameworks('idnumber', 'ASC', null, 3, context_system::instance()); - $r1 = (object) $result[0]; - $r2 = (object) $result[1]; - $r3 = (object) $result[2]; - - $this->assertEquals($framework1->get_id(), $r3->get_id()); - $this->assertEquals($framework2->get_id(), $r1->get_id()); - $this->assertEquals($framework3->get_id(), $r2->get_id()); + $f = (object) array_shift($result); + $this->assertEquals($framework2->get_id(), $f->get_id()); + $f = (object) array_shift($result); + $this->assertEquals($framework3->get_id(), $f->get_id()); + $f = (object) array_shift($result); + $this->assertEquals($framework1->get_id(), $f->get_id()); } /** diff --git a/admin/tool/lp/tests/competency_rule_test.php b/admin/tool/lp/tests/competency_rule_test.php new file mode 100644 index 00000000000..3e864ab5ed4 --- /dev/null +++ b/admin/tool/lp/tests/competency_rule_test.php @@ -0,0 +1,372 @@ +. + +/** + * Competency rule tests. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +use tool_lp\user_competency; +use tool_lp\competency; +use tool_lp\competency_rule_all; +use tool_lp\competency_rule_points; + +/** + * Competency rule testcase. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_lp_competency_rule_testcase extends externallib_advanced_testcase { + + public function test_rule_all_matching() { + $this->resetAfterTest(true); + + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('tool_lp'); + $u1 = $this->getDataGenerator()->create_user(); + + // Set up the framework and competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), + 'ruletype' => 'tool_lp\competency_rule_all')); + $c11 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id(), + 'ruletype' => 'tool_lp\competency_rule_all')); + $c111 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c11->get_id(), + 'ruletype' => 'tool_lp\competency_rule_all')); + $c112 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c11->get_id(), + 'ruletype' => 'tool_lp\competency_rule_all')); + $c12 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id(), + 'ruletype' => 'tool_lp\competency_rule_all')); + $c13 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id(), + 'ruletype' => 'tool_lp\competency_rule_all')); + $c131 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c13->get_id(), + 'ruletype' => 'tool_lp\competency_rule_all')); + + // Create some user competency records. + $uc1 = $lpg->create_user_competency(array('competencyid' => $c1->get_id(), 'userid' => $u1->id)); + $uc11 = $lpg->create_user_competency(array('competencyid' => $c11->get_id(), 'userid' => $u1->id, 'proficiency' => 1)); + $uc111 = $lpg->create_user_competency(array('competencyid' => $c111->get_id(), 'userid' => $u1->id, 'proficiency' => 1)); + $uc112 = $lpg->create_user_competency(array('competencyid' => $c112->get_id(), 'userid' => $u1->id, 'proficiency' => 1)); + $uc12 = $lpg->create_user_competency(array('competencyid' => $c12->get_id(), 'userid' => $u1->id)); + $uc13 = new user_competency(0, (object) array('userid' => $u1->id, 'competencyid' => $c13->get_id())); + + // Not all children are met. + $cr = new competency_rule_all($c1); + $this->assertFalse($cr->matches($uc1)); + + // All children are met. + $cr = new competency_rule_all($c11); + $this->assertTrue($cr->matches($uc11)); + + // The competency doesn't have any children. + $cr = new competency_rule_all($c12); + $this->assertFalse($cr->matches($uc12)); + + // The competency doesn't have saved user competency records. + $cr = new competency_rule_all($c13); + $this->assertFalse($cr->matches($uc13)); + } + + public function test_rule_points_validation() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $lpg = $this->getDataGenerator()->get_plugin_generator('tool_lp'); + $framework = $lpg->create_framework(); + $framework2 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + $c4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $cx = $lpg->create_competency(array('competencyframeworkid' => $framework2->get_id())); + + $c1->set_ruletype('tool_lp\competency_rule_points'); + $rule = new competency_rule_points($c1); + + // Invalid config. + $config = json_encode(array()); + $this->assertFalse($rule->validate_config($config)); + + // Missing required points. + $config = json_encode(array( + 'base' => array(), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Invalid required points. + $config = json_encode(array( + 'base' => array('points' => 'abc'), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Less than 1 required points. + $config = json_encode(array( + 'base' => array('points' => 0), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Not enough required points. + $config = json_encode(array( + 'base' => array('points' => 3), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Duplicate competency. + $config = json_encode(array( + 'base' => array('points' => 1), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Competency includes itself. + $config = json_encode(array( + 'base' => array('points' => 1), + 'competencies' => array( + array('id' => $c1->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Cannot use negative points. + $config = json_encode(array( + 'base' => array('points' => 1), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => -1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Not competencies set. + $config = json_encode(array( + 'base' => array('points' => 1), + 'competencies' => array( + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // There is a competency that is not a child. + $config = json_encode(array( + 'base' => array('points' => 1), + 'competencies' => array( + array('id' => $c1->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // There is a competency from another framework in there. + $config = json_encode(array( + 'base' => array('points' => 1), + 'competencies' => array( + array('id' => $cx->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // A normal config. + $config = json_encode(array( + 'base' => array('points' => 4), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => 3, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 2, 'required' => 1), + ) + )); + $this->assertTrue($rule->validate_config($config)); + } + + public function test_rule_points_matching() { + $this->resetAfterTest(true); + + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('tool_lp'); + $u1 = $this->getDataGenerator()->create_user(); + + // Set up the framework and competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c1->set_ruletype('tool_lp\competency_rule_points'); + $comprule = new competency_rule_points($c1); + $c11 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + $c12 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + $c13 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + $c14 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + + // Create some user competency records. + $uc1 = $lpg->create_user_competency(array('competencyid' => $c1->get_id(), 'userid' => $u1->id)); + $uc11 = $lpg->create_user_competency(array('competencyid' => $c11->get_id(), 'userid' => $u1->id, 'proficiency' => 1)); + $uc12 = $lpg->create_user_competency(array('competencyid' => $c12->get_id(), 'userid' => $u1->id, 'proficiency' => 1)); + $uc13 = $lpg->create_user_competency(array('competencyid' => $c13->get_id(), 'userid' => $u1->id)); + + // Enough points. + $rule = array( + 'base' => array('points' => 8), + 'competencies' => array( + array( + 'id' => $c11->get_id(), + 'points' => 4, + 'required' => 0 + ), + array( + 'id' => $c12->get_id(), + 'points' => 4, + 'required' => 0 + ), + ) + ); + $c1->set_ruleconfig(json_encode($rule)); + $c1->update(); + $this->assertTrue($comprule->matches($uc1)); + + // Not enough points. + $rule = array( + 'base' => array('points' => 8), + 'competencies' => array( + array( + 'id' => $c11->get_id(), + 'points' => 4, + 'required' => 0 + ), + array( + 'id' => $c13->get_id(), + 'points' => 4, + 'required' => 0 + ), + ) + ); + $c1->set_ruleconfig(json_encode($rule)); + $c1->update(); + $this->assertFalse($comprule->matches($uc1)); + + // One required that is not met but points were OK. + $rule = array( + 'base' => array('points' => 8), + 'competencies' => array( + array( + 'id' => $c11->get_id(), + 'points' => 4, + 'required' => 0 + ), + array( + 'id' => $c12->get_id(), + 'points' => 4, + 'required' => 0 + ), + array( + 'id' => $c13->get_id(), + 'points' => 4, + 'required' => 1 + ), + ) + ); + $c1->set_ruleconfig(json_encode($rule)); + $c1->update(); + $this->assertFalse($comprule->matches($uc1)); + + // One required, one not, should match. + $rule = array( + 'base' => array('points' => 8), + 'competencies' => array( + array( + 'id' => $c11->get_id(), + 'points' => 4, + 'required' => 0 + ), + array( + 'id' => $c12->get_id(), + 'points' => 4, + 'required' => 1 + ), + ) + ); + $c1->set_ruleconfig(json_encode($rule)); + $c1->update(); + $this->assertTrue($comprule->matches($uc1)); + + // All required and should match. + $rule = array( + 'base' => array('points' => 8), + 'competencies' => array( + array( + 'id' => $c11->get_id(), + 'points' => 4, + 'required' => 1 + ), + array( + 'id' => $c12->get_id(), + 'points' => 4, + 'required' => 1 + ), + ) + ); + $c1->set_ruleconfig(json_encode($rule)); + $c1->update(); + $this->assertTrue($comprule->matches($uc1)); + + // All required, but one doesn't have a user record. + $rule = array( + 'base' => array('points' => 4), + 'competencies' => array( + array( + 'id' => $c12->get_id(), + 'points' => 4, + 'required' => 1 + ), + array( + 'id' => $c14->get_id(), + 'points' => 4, + 'required' => 1 + ), + ) + ); + $c1->set_ruleconfig(json_encode($rule)); + $c1->update(); + $this->assertFalse($comprule->matches($uc1)); + } + +} diff --git a/admin/tool/lp/tests/persistent_test.php b/admin/tool/lp/tests/persistent_test.php index 8f4bbed829a..8c9f28d867c 100644 --- a/admin/tool/lp/tests/persistent_test.php +++ b/admin/tool/lp/tests/persistent_test.php @@ -92,6 +92,20 @@ class tool_lp_persistent_testcase extends advanced_testcase { 'usermodified' => array( 'default' => 0, 'type' => PARAM_INT + ), + 'ruletype' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'ruleconfig' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'ruleoutcome' => array( + 'type' => PARAM_RAW, + 'default' => 0 ) ); $this->assertEquals($expected, tool_lp_testable_persistent::properties_definition()); @@ -113,6 +127,9 @@ class tool_lp_persistent_testcase extends advanced_testcase { 'timecreated' => 0, 'timemodified' => 0, 'usermodified' => 0, + 'ruletype' => null, + 'ruleconfig' => null, + 'ruleoutcome' => 0, ); $this->assertEquals($expected, $p->to_record()); } @@ -133,6 +150,9 @@ class tool_lp_persistent_testcase extends advanced_testcase { 'timecreated' => 2, 'timemodified' => 3, 'usermodified' => 4, + 'ruletype' => null, + 'ruleconfig' => null, + 'ruleoutcome' => 0, ); $p->from_record($data); $this->assertEquals($data, $p->to_record()); @@ -408,6 +428,20 @@ class tool_lp_testable_persistent extends \tool_lp\persistent { 'default' => 0, 'null' => NULL_ALLOWED ), + 'ruletype' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'ruleconfig' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'ruleoutcome' => array( + 'type' => PARAM_RAW, + 'default' => 0 + ) ); } diff --git a/admin/tool/lp/version.php b/admin/tool/lp/version.php index 052ea8edb9b..e148eb1fcc6 100644 --- a/admin/tool/lp/version.php +++ b/admin/tool/lp/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015052427; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2015111004; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2014110400; // Requires this Moodle version. $plugin->component = 'tool_lp'; // Full name of the plugin (used for diagnostics).