MDL-51029 tool_lp: Implement competency rules

This commit is contained in:
Frederic Massart
2016-04-18 10:58:41 +08:00
parent 192569ed17
commit 1896274ff5
35 changed files with 2385 additions and 43 deletions
+1
View File
@@ -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})}}});
+1
View File
@@ -0,0 +1 @@
define(["jquery"],function(a){var b=function(b){this._eventNode=a("<div>"),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});
+1
View File
@@ -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});
+1
View File
@@ -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});
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
define(["jquery","core/notification","core/templates","tool_lp/dialogue","tool_lp/competency_outcomes","core/str"],function(a,b,c,d,e,f){var g=function(b,c){this._eventNode=a("<div></div>"),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});
+1 -1
View File
@@ -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;c<b.length;c++)d=b[c],d.parentid==a.id&&(a.haschildren=!0,a.children.push(d),k(d,b))},l=function(b){var l=e.Deferred();return c.render("tool_lp/loading",{}).done(function(m,n){c.replaceNodeContents(e(i),m,n);var o=a.call([{methodname:"tool_lp_search_competencies",args:{searchtext:b,competencyframeworkid:g,includerelated:!0}}]);o[0].done(function(a){f=[];var b=0;for(b=0;b<a.length;b++)f[a[b].id]=a[b];var g=[],m=!1;for(b=0;b<a.length;b++)m=a[b],0===parseInt(m.parentid,10)&&(g.push(m),k(m,a));var n={shortname:h,competencies:g};c.render("tool_lp/competencies_tree_root",n).done(function(a,b){c.replaceNodeContents(e(i),e(a).html(),b);var g=new d(i);if(j){var h=e(i).find("[data-id="+j+"]");h.length&&g.updateFocus(h)}l.resolve(f)}).fail(l.reject)}).fail(l.reject)}),l.promise()},m=function(a,b){j=b.attr("data-id")};return{init:function(a,c,d,e){g=a,h=c,i=e,l(d).fail(b.exception),this.on("selectionchanged",m)},on:function(a,b){e(i).on(a,b)},getCompetencyFrameworkId:function(){return g},getCompetency:function(a){return f[a]},getCompetencyLevel:function(a){var b=this.getCompetency(a),c=b.path.replace(/^\/|\/$/g,"").split("/").length;return c},reloadCompetencies:function(){return l("").fail(b.exception)},listCompetencies:function(){return f}}});
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;c<b.length;c++)d=b[c],d.parentid==a.id&&(a.haschildren=!0,a.children.push(d),k(d,b))},l=function(b){var l=e.Deferred();return c.render("tool_lp/loading",{}).done(function(m,n){c.replaceNodeContents(e(i),m,n);var o=a.call([{methodname:"tool_lp_search_competencies",args:{searchtext:b,competencyframeworkid:g,includerelated:!0}}]);o[0].done(function(a){f=[];var b=0;for(b=0;b<a.length;b++)f[a[b].id]=a[b];var g=[],m=!1;for(b=0;b<a.length;b++)m=a[b],0===parseInt(m.parentid,10)&&(g.push(m),k(m,a));var n={shortname:h,competencies:g};c.render("tool_lp/competencies_tree_root",n).done(function(a,b){c.replaceNodeContents(e(i),e(a).html(),b);var g=new d(i);if(j){var h=e(i).find("[data-id="+j+"]");h.length&&g.updateFocus(h)}l.resolve(f)}).fail(l.reject)}).fail(l.reject)}),l.promise()},m=function(a,b){j=b.attr("data-id")};return{init:function(a,c,d,e){g=a,h=c,i=e,l(d).fail(b.exception),this.on("selectionchanged",m)},on:function(a,b){e(i).on(a,b)},getChildren:function(a){var b=[];return e.each(f,function(c,d){d.parentid==a&&b.push(d)}),b},getCompetencyFrameworkId:function(){return g},getCompetency:function(a){return f[a]},getCompetencyLevel:function(a){var b=this.getCompetency(a),c=b.path.replace(/^\/|\/$/g,"").split("/").length;return c},hasChildren:function(a){return this.getChildren(a).length>0},reloadCompetencies:function(){return l("").fail(b.exception)},listCompetencies:function(){return f}}});
@@ -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 <http://www.gnu.org/licenses/>.
/**
* 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;
});
}
};
});
+175
View File
@@ -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 <http://www.gnu.org/licenses/>.
/**
* 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 = $('<div>');
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;
});
@@ -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 <http://www.gnu.org/licenses/>.
/**
* 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;
});
@@ -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 <http://www.gnu.org/licenses/>.
/**
* 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;
});
+93 -11
View File
@@ -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 <taxonomy>".
*
@@ -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));
}
};
+1 -1
View File
@@ -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
@@ -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 <http://www.gnu.org/licenses/>.
/**
* 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 = $('<div></div>');
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;
});
+29 -1
View File
@@ -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
+6 -6
View File
@@ -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.
+120
View File
@@ -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.
*
+101
View File
@@ -0,0 +1,101 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* 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.');
}
}
@@ -0,0 +1,99 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* 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');
}
}
@@ -0,0 +1,199 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* 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');
}
}
+35 -3
View File
@@ -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'),
)))
));
}
@@ -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;
}
}
+2 -2
View File
@@ -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;
+5 -1
View File
@@ -16,6 +16,9 @@
<FIELD NAME="visible" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Used to hide the competency."/>
<FIELD NAME="path" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Used to speed up queries that use an entire branch of the tree. Looks like /5/34/54."/>
<FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Relative sort order within the branch"/>
<FIELD NAME="ruletype" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="ruleoutcome" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="ruleconfig" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The time this competency was created."/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The time this competency was last modified."/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The user who last modified this competency"/>
@@ -25,6 +28,7 @@
</KEYS>
<INDEXES>
<INDEX NAME="idnumberframework" UNIQUE="true" FIELDS="competencyframeworkid, idnumber"/>
<INDEX NAME="ruleoutcome" UNIQUE="false" FIELDS="ruleoutcome"/>
</INDEXES>
</TABLE>
<TABLE NAME="tool_lp_competency_framework" COMMENT="List of competency frameworks.">
@@ -134,7 +138,7 @@
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="tool_lp_user_competency" COMMENT="User competencies">
<TABLE NAME="tool_lp_user_competency" COMMENT="User competencies">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="User associated to the competency."/>
+60
View File
@@ -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;
}
+14
View File
@@ -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';
+41
View File
@@ -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 {
@@ -0,0 +1,39 @@
<div data-region="competencyruleconfig">
<div data-region="content">
{{^config}}
<p>{{#str}}cannotaddrules, tool_lp{{/str}}</p>
{{/config}}
{{#config}}
<div data-region="rule-base">
<div data-region="rule-outcome">
<label>{{#str}}outcome, tool_lp{{/str}}</label>
<select name="outcome" ng-label="{{#str}}outcome, tool_lp{{/str}}">
{{#outcomes}}
<option value="{{code}}" {{#selected}}selected{{/selected}}>{{name}}</option>
{{/outcomes}}
</select>
</div>
<div data-region="rule-type">
<label>{{#str}}when, tool_lp{{/str}}</label>
<select name="rule" ng-label="{{#str}}when, tool_lp{{/str}}">
<option value="-1">{{#str}}choosedots{{/str}}</option>
{{#rules}}
<option value="{{type}}" {{#selected}}selected{{/selected}}>{{name}}</option>
{{/rules}}
</select>
</div>
</div>
<div data-region="rule-config">
</div>
{{/config}}
</div>
<div data-region="footer">
{{#config}}
<input type="button" data-action="save" value="{{#str}}savechanges{{/str}}"/>
{{/config}}
<input type="button" data-action="cancel" value="{{#str}}cancel{{/str}}"/>
</div>
</div>
@@ -0,0 +1,27 @@
<div class="competency-rule-points">
<table class="table table-condensed">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{{#str}}points, tool_lp{{/str}}</th>
<th scope="col">{{#str}}required{{/str}}</th>
</tr>
</thead>
<tbody>
{{#children}}
<tr data-competency="{{id}}">
<th scope="row">{{shortname}}</th>
<td><input type="number" min="0" value="{{points}}" name="points" aria-label="{{#str}}pointsgivenfor, tool_lp, {{competency.shortname}}{{/str}}"></td>
<td><input type="checkbox" value="1" name="required" {{#required}}checked{{/required}} aria-label="{{#str}}aisrequired, tool_lp, {{competency.shortname}}{{/str}}"></td>
</tr>
{{/children}}
</tbody>
<tfoot>
<tr>
<th scope="row">{{#str}}totalrequiredtocomplete, tool_lp{{/str}}</th>
<td><input type="number" min="1" value="{{requiredpoints}}" name="requiredpoints" aria-label="{{#str}}totalrequiredtocomplete, tool_lp{{/str}}"></td>
<td> </td>
</tr>
</tfoot>
</table>
</div>
@@ -6,3 +6,12 @@
{{#showrelatedcompetencies}}
{{> tool_lp/related_competencies }}
{{/showrelatedcompetencies}}
{{#showrule}}
<h5>{{#str}}competencyrule, tool_lp{{/str}}</h5>
<dl>
<dt>{{#str}}outcome, tool_lp{{/str}}</dt>
<dd>{{rule.outcome}}</dd>
<dt>{{#str}}when, tool_lp{{/str}}</dt>
<dd>{{rule.type}}</dd>
</dl>
{{/showrule}}
@@ -94,6 +94,11 @@
{{#pix}}t/add{{/pix}} {{#str}}addrelatedcompetency, tool_lp{{/str}}
</a>
</li>
<li>
<a href="#" data-action="competencyrules">
{{#pix}}t/edit{{/pix}} {{#str}}competencyrule, tool_lp{{/str}}
</a>
</li>
</ul>
</li>
</ul>
@@ -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}}
+12 -14
View File
@@ -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());
}
/**
@@ -0,0 +1,372 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* 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));
}
}
+34
View File
@@ -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
)
);
}
+1 -1
View File
@@ -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).