diff --git a/admin/tool/lp/amd/build/base.min.js b/admin/tool/lp/amd/build/base.min.js
new file mode 100644
index 00000000000..c9e4818dbfb
--- /dev/null
+++ b/admin/tool/lp/amd/build/base.min.js
@@ -0,0 +1 @@
+define(["jquery"],function(a){var b=function(){this._eventNode=a("
")};return b.prototype._eventNode=null,b.prototype.on=function(a,b){this._eventNode.on(a,b)},b.prototype._trigger=function(a,b){this._eventNode.trigger(a,[b])},b});
\ No newline at end of file
diff --git a/admin/tool/lp/amd/build/planactions.min.js b/admin/tool/lp/amd/build/planactions.min.js
index c6bc873a38b..530665ca73b 100644
--- a/admin/tool/lp/amd/build/planactions.min.js
+++ b/admin/tool/lp/amd/build/planactions.min.js
@@ -1 +1 @@
-define(["jquery","core/templates","core/ajax","core/notification","core/str","tool_lp/menubar"],function(a,b,c,d,e,f){var g=function(a){if(this._type=a,"plan"===a)this._region='[data-region="plan-page"]',this._planNode='[data-region="plan-page"]',this._template="tool_lp/plan_page",this._contextMethod="tool_lp_data_for_plan_page";else{if("plans"!==a)throw new TypeError("Unexpected type.");this._region='[data-region="plans"]',this._planNode='[data-region="plan-node"]',this._template="tool_lp/plans_page",this._contextMethod="tool_lp_data_for_plans_page"}};return g.prototype._contextMethod=null,g.prototype._planNode=null,g.prototype._region=null,g.prototype._template=null,g.prototype._type=null,g.prototype._getContextArgs=function(a){var b=this,c={};return"plan"===b._type?c={planid:a.id}:"plans"===b._type&&(c={userid:a.userid}),c},g.prototype._renderView=function(c){var e=this;b.render(e._template,c).done(function(c,d){a(e._region).replaceWith(c),b.runTemplateJS(d)}.bind(e)).fail(d.exception)},g.prototype._callAndRefresh=function(b,e){var f=this;return b.push({methodname:f._contextMethod,args:f._getContextArgs(e)}),a.when.apply(a.when,c.call(b)).then(function(){f._renderView.call(f,arguments[arguments.length-1])}).fail(d.exception)},g.prototype._doDelete=function(a){var b=this,c=[{methodname:"tool_lp_delete_plan",args:{id:a.id}}];b._callAndRefresh(c,a)},g.prototype.deletePlan=function(a){var b,f=this;b=c.call([{methodname:"tool_lp_read_plan",args:{id:a.id}}]),b[0].done(function(b){e.get_strings([{key:"confirm",component:"moodle"},{key:"deleteplan",component:"tool_lp",param:b.name},{key:"delete",component:"moodle"},{key:"cancel",component:"moodle"}]).done(function(b){d.confirm(b[0],b[1],b[2],b[3],function(){f._doDelete(a)}.bind(f))}).fail(d.exception)}).fail(d.exception)},g.prototype._deletePlanHandler=function(b){b.preventDefault();var c=this._findPlanData(a(b.target));this.deletePlan(c)},g.prototype._doReopenPlan=function(a){var b=this,c=[{methodname:"tool_lp_reopen_plan",args:{planid:a.id}}];b._callAndRefresh(c,a)},g.prototype.reopenPlan=function(a){var b=this,f=c.call([{methodname:"tool_lp_read_plan",args:{id:a.id}}]);f[0].done(function(c){e.get_strings([{key:"confirm",component:"moodle"},{key:"reopenplanconfirm",component:"tool_lp",param:c.name},{key:"reopenplan",component:"tool_lp"},{key:"cancel",component:"moodle"}]).done(function(c){d.confirm(c[0],c[1],c[2],c[3],function(){b._doReopenPlan(a)}.bind(b))}).fail(d.exception)}).fail(d.exception)},g.prototype._reopenPlanHandler=function(b){b.preventDefault();var c=this._findPlanData(a(b.target));this.reopenPlan(c)},g.prototype._doCompletePlan=function(a){var b=this,c=[{methodname:"tool_lp_complete_plan",args:{planid:a.id}}];b._callAndRefresh(c,a)},g.prototype.completePlan=function(a){var b=this,f=c.call([{methodname:"tool_lp_read_plan",args:{id:a.id}}]);f[0].done(function(c){e.get_strings([{key:"confirm",component:"moodle"},{key:"completeplanconfirm",component:"tool_lp",param:c.name},{key:"completeplan",component:"tool_lp"},{key:"cancel",component:"moodle"}]).done(function(c){d.confirm(c[0],c[1],c[2],c[3],function(){b._doCompletePlan(a)}.bind(b))}).fail(d.exception)}).fail(d.exception)},g.prototype._completePlanHandler=function(b){b.preventDefault();var c=this._findPlanData(a(b.target));this.completePlan(c)},g.prototype._doUnlinkPlan=function(a){var b=this,c=[{methodname:"tool_lp_unlink_plan_from_template",args:{planid:a.id}}];b._callAndRefresh(c,a)},g.prototype.unlinkPlan=function(a){var b=this,f=c.call([{methodname:"tool_lp_read_plan",args:{id:a.id}}]);f[0].done(function(c){e.get_strings([{key:"confirm",component:"moodle"},{key:"unlinkplantemplateconfirm",component:"tool_lp",param:c.name},{key:"unlinkplantemplate",component:"tool_lp"},{key:"cancel",component:"moodle"}]).done(function(c){d.confirm(c[0],c[1],c[2],c[3],function(){b._doUnlinkPlan(a)}.bind(b))}).fail(d.exception)}).fail(d.exception)},g.prototype._unlinkPlanHandler=function(b){b.preventDefault();var c=this._findPlanData(a(b.target));this.unlinkPlan(c)},g.prototype._findPlanData=function(b){var c,d=b.parentsUntil(a(this._region).parent(),this._planNode);if(1!=d.length)throw new Error("The plan node was not located.");if(c=d.data(),"undefined"==typeof c||"undefined"==typeof c.id)throw new Error("Plan data could not be found.");return c},g.prototype.enhanceMenubar=function(a){var b=this;f.enhance(a,{'[data-action="plan-delete"]':b._deletePlanHandler.bind(b),'[data-action="plan-complete"]':b._completePlanHandler.bind(b),'[data-action="plan-reopen"]':b._reopenPlanHandler.bind(b),'[data-action="plan-unlink"]':b._unlinkPlanHandler.bind(b)})},g.prototype.registerEvents=function(){var b=a(this._region),c=this;b.find('[data-action="plan-delete"]').click(c._deletePlanHandler.bind(c)),b.find('[data-action="plan-complete"]').click(c._completePlanHandler.bind(c)),b.find('[data-action="plan-reopen"]').click(c._reopenPlanHandler.bind(c)),b.find('[data-action="plan-unlink"]').click(c._unlinkPlanHandler.bind(c))},g});
\ No newline at end of file
+define(["jquery","core/templates","core/ajax","core/notification","core/str","tool_lp/menubar"],function(a,b,c,d,e,f){var g=function(a){if(this._type=a,"plan"===a)this._region='[data-region="plan-page"]',this._planNode='[data-region="plan-page"]',this._template="tool_lp/plan_page",this._contextMethod="tool_lp_data_for_plan_page";else{if("plans"!==a)throw new TypeError("Unexpected type.");this._region='[data-region="plans"]',this._planNode='[data-region="plan-node"]',this._template="tool_lp/plans_page",this._contextMethod="tool_lp_data_for_plans_page"}};return g.prototype._contextMethod=null,g.prototype._planNode=null,g.prototype._region=null,g.prototype._template=null,g.prototype._type=null,g.prototype._getContextArgs=function(a){var b=this,c={};return"plan"===b._type?c={planid:a.id}:"plans"===b._type&&(c={userid:a.userid}),c},g.prototype.refresh=function(b){var c=this._findPlanData(a(b));this._callAndRefresh([],c)},g.prototype._renderView=function(c){var e=this;b.render(e._template,c).done(function(c,d){a(e._region).replaceWith(c),b.runTemplateJS(d)}.bind(e)).fail(d.exception)},g.prototype._callAndRefresh=function(b,e){var f=this;return b.push({methodname:f._contextMethod,args:f._getContextArgs(e)}),a.when.apply(a.when,c.call(b)).then(function(){f._renderView.call(f,arguments[arguments.length-1])}).fail(d.exception)},g.prototype._doDelete=function(a){var b=this,c=[{methodname:"tool_lp_delete_plan",args:{id:a.id}}];b._callAndRefresh(c,a)},g.prototype.deletePlan=function(a){var b,f=this;b=c.call([{methodname:"tool_lp_read_plan",args:{id:a.id}}]),b[0].done(function(b){e.get_strings([{key:"confirm",component:"moodle"},{key:"deleteplan",component:"tool_lp",param:b.name},{key:"delete",component:"moodle"},{key:"cancel",component:"moodle"}]).done(function(b){d.confirm(b[0],b[1],b[2],b[3],function(){f._doDelete(a)}.bind(f))}).fail(d.exception)}).fail(d.exception)},g.prototype._deletePlanHandler=function(b){b.preventDefault();var c=this._findPlanData(a(b.target));this.deletePlan(c)},g.prototype._doReopenPlan=function(a){var b=this,c=[{methodname:"tool_lp_reopen_plan",args:{planid:a.id}}];b._callAndRefresh(c,a)},g.prototype.reopenPlan=function(a){var b=this,f=c.call([{methodname:"tool_lp_read_plan",args:{id:a.id}}]);f[0].done(function(c){e.get_strings([{key:"confirm",component:"moodle"},{key:"reopenplanconfirm",component:"tool_lp",param:c.name},{key:"reopenplan",component:"tool_lp"},{key:"cancel",component:"moodle"}]).done(function(c){d.confirm(c[0],c[1],c[2],c[3],function(){b._doReopenPlan(a)}.bind(b))}).fail(d.exception)}).fail(d.exception)},g.prototype._reopenPlanHandler=function(b){b.preventDefault();var c=this._findPlanData(a(b.target));this.reopenPlan(c)},g.prototype._doCompletePlan=function(a){var b=this,c=[{methodname:"tool_lp_complete_plan",args:{planid:a.id}}];b._callAndRefresh(c,a)},g.prototype.completePlan=function(a){var b=this,f=c.call([{methodname:"tool_lp_read_plan",args:{id:a.id}}]);f[0].done(function(c){e.get_strings([{key:"confirm",component:"moodle"},{key:"completeplanconfirm",component:"tool_lp",param:c.name},{key:"completeplan",component:"tool_lp"},{key:"cancel",component:"moodle"}]).done(function(c){d.confirm(c[0],c[1],c[2],c[3],function(){b._doCompletePlan(a)}.bind(b))}).fail(d.exception)}).fail(d.exception)},g.prototype._completePlanHandler=function(b){b.preventDefault();var c=this._findPlanData(a(b.target));this.completePlan(c)},g.prototype._doUnlinkPlan=function(a){var b=this,c=[{methodname:"tool_lp_unlink_plan_from_template",args:{planid:a.id}}];b._callAndRefresh(c,a)},g.prototype.unlinkPlan=function(a){var b=this,f=c.call([{methodname:"tool_lp_read_plan",args:{id:a.id}}]);f[0].done(function(c){e.get_strings([{key:"confirm",component:"moodle"},{key:"unlinkplantemplateconfirm",component:"tool_lp",param:c.name},{key:"unlinkplantemplate",component:"tool_lp"},{key:"cancel",component:"moodle"}]).done(function(c){d.confirm(c[0],c[1],c[2],c[3],function(){b._doUnlinkPlan(a)}.bind(b))}).fail(d.exception)}).fail(d.exception)},g.prototype._unlinkPlanHandler=function(b){b.preventDefault();var c=this._findPlanData(a(b.target));this.unlinkPlan(c)},g.prototype._findPlanData=function(b){var c,d=b.parentsUntil(a(this._region).parent(),this._planNode);if(1!=d.length)throw new Error("The plan node was not located.");if(c=d.data(),"undefined"==typeof c||"undefined"==typeof c.id)throw new Error("Plan data could not be found.");return c},g.prototype.enhanceMenubar=function(a){var b=this;f.enhance(a,{'[data-action="plan-delete"]':b._deletePlanHandler.bind(b),'[data-action="plan-complete"]':b._completePlanHandler.bind(b),'[data-action="plan-reopen"]':b._reopenPlanHandler.bind(b),'[data-action="plan-unlink"]':b._unlinkPlanHandler.bind(b)})},g.prototype.registerEvents=function(){var b=a(this._region),c=this;b.find('[data-action="plan-delete"]').click(c._deletePlanHandler.bind(c)),b.find('[data-action="plan-complete"]').click(c._completePlanHandler.bind(c)),b.find('[data-action="plan-reopen"]').click(c._reopenPlanHandler.bind(c)),b.find('[data-action="plan-unlink"]').click(c._unlinkPlanHandler.bind(c))},g});
\ No newline at end of file
diff --git a/admin/tool/lp/amd/build/user_competency_workflow.min.js b/admin/tool/lp/amd/build/user_competency_workflow.min.js
new file mode 100644
index 00000000000..91f979eaf81
--- /dev/null
+++ b/admin/tool/lp/amd/build/user_competency_workflow.min.js
@@ -0,0 +1 @@
+define(["jquery","core/templates","core/ajax","core/notification","core/str","tool_lp/menubar","tool_lp/base"],function(a,b,c,d,e,f,g){var h=function(){g.prototype.constructor.apply(this,[])};return h.prototype=Object.create(g.prototype),h.prototype._nodeSelector='[data-node="user-competency"]',h.prototype._cancelReviewRequest=function(a){var b={methodname:"tool_lp_user_competency_cancel_review_request",args:{userid:a.userid,competencyid:a.competencyid}};c.call([b])[0].then(function(){this._trigger("review-request-cancelled",a),this._trigger("status-changed",a)}.bind(this),function(){this._trigger("error-occured",a)}.bind(this))},h.prototype.cancelReviewRequest=function(a){this._cancelReviewRequest(a)},h.prototype._cancelReviewRequestHandler=function(b){b.preventDefault();var c=this._findUserCompetencyData(a(b.target));this.cancelReviewRequest(c)},h.prototype._requestReview=function(a){var b={methodname:"tool_lp_user_competency_request_review",args:{userid:a.userid,competencyid:a.competencyid}};c.call([b])[0].then(function(){this._trigger("review-requested",a),this._trigger("status-changed",a)}.bind(this),function(){this._trigger("error-occured",a)}.bind(this))},h.prototype.requestReview=function(a){this._requestReview(a)},h.prototype._requestReviewHandler=function(b){b.preventDefault();var c=this._findUserCompetencyData(a(b.target));this.requestReview(c)},h.prototype._startReview=function(a){var b={methodname:"tool_lp_user_competency_start_review",args:{userid:a.userid,competencyid:a.competencyid}};c.call([b])[0].then(function(){this._trigger("review-started",a),this._trigger("status-changed",a)}.bind(this),function(){this._trigger("error-occured",a)}.bind(this))},h.prototype.startReview=function(a){this._startReview(a)},h.prototype._startReviewHandler=function(b){b.preventDefault();var c=this._findUserCompetencyData(a(b.target));this.startReview(c)},h.prototype._stopReview=function(a){var b={methodname:"tool_lp_user_competency_stop_review",args:{userid:a.userid,competencyid:a.competencyid}};c.call([b])[0].then(function(){this._trigger("review-stopped",a),this._trigger("status-changed",a)}.bind(this),function(){this._trigger("error-occured",a)}.bind(this))},h.prototype.stopReview=function(a){this._stopReview(a)},h.prototype._stopReviewHandler=function(b){b.preventDefault();var c=this._findUserCompetencyData(a(b.target));this.stopReview(c)},h.prototype.enhanceMenubar=function(a){f.enhance(a,{'[data-action="request-review"]':this._requestReviewHandler.bind(this),'[data-action="cancel-review-request"]':this._cancelReviewRequestHandler.bind(this)})},h.prototype._findUserCompetencyData=function(a){var b,c=a.parents(this._nodeSelector);if(1!=c.length)throw new Error("The evidence node was not located.");if(b=c.data(),"undefined"==typeof b||"undefined"==typeof b.userid||"undefined"==typeof b.competencyid)throw new Error("User competency data could not be found.");return b},h.prototype.enhanceMenubar=function(a){f.enhance(a,{'[data-action="request-review"]':this._requestReviewHandler.bind(this),'[data-action="cancel-review-request"]':this._cancelReviewRequestHandler.bind(this),'[data-action="start-review"]':this._startReviewHandler.bind(this),'[data-action="stop-review"]':this._stopReviewHandler.bind(this)})},h.prototype.registerEvents=function(b){var c=a(b);c.find('[data-action="request-review"]').click(this._requestReviewHandler.bind(this)),c.find('[data-action="cancel-review-request"]').click(this._cancelReviewRequestHandler.bind(this)),c.find('[data-action="start-review"]').click(this._startReviewHandler.bind(this)),c.find('[data-action="stop-review"]').click(this._stopReviewHandler.bind(this))},h});
\ No newline at end of file
diff --git a/admin/tool/lp/amd/src/base.js b/admin/tool/lp/amd/src/base.js
new file mode 100644
index 00000000000..2168e760be3
--- /dev/null
+++ b/admin/tool/lp/amd/src/base.js
@@ -0,0 +1,59 @@
+// 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 .
+
+/**
+ * Base javascript module.
+ *
+ * @module tool_lp/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
+ */
+define(['jquery'], function($) {
+
+ /**
+ * Base class.
+ */
+ var Base = function() {
+ this._eventNode = $('');
+ };
+
+ /** @type {Node} The node we attach the events to. */
+ Base.prototype._eventNode = null;
+
+ /**
+ * Register an event listener.
+ *
+ * @param {String} type The event type.
+ * @param {Function} handler The event listener.
+ * @method on
+ */
+ Base.prototype.on = function(type, handler) {
+ this._eventNode.on(type, handler);
+ };
+
+ /**
+ * Trigger an event.
+ *
+ * @param {String} type The type of event.
+ * @param {Object} The data to pass to the listeners.
+ * @method _trigger
+ */
+ Base.prototype._trigger = function(type, data) {
+ this._eventNode.trigger(type, [data]);
+ };
+
+ return /** @alias module:tool_lp/base */ Base;
+});
diff --git a/admin/tool/lp/amd/src/planactions.js b/admin/tool/lp/amd/src/planactions.js
index edc3880285a..dba37c80034 100644
--- a/admin/tool/lp/amd/src/planactions.js
+++ b/admin/tool/lp/amd/src/planactions.js
@@ -93,6 +93,18 @@ define(['jquery',
return args;
};
+ /**
+ * Refresh the plan view.
+ *
+ * This is useful when you only want to refresh the view.
+ *
+ * @param {String} selector The node to search the plan data from.
+ */
+ PlanActions.prototype.refresh = function(selector) {
+ var planData = this._findPlanData($(selector));
+ this._callAndRefresh([], planData);
+ };
+
/**
* Callback to render the region template.
*
diff --git a/admin/tool/lp/amd/src/user_competency_workflow.js b/admin/tool/lp/amd/src/user_competency_workflow.js
new file mode 100644
index 00000000000..4534dac67ac
--- /dev/null
+++ b/admin/tool/lp/amd/src/user_competency_workflow.js
@@ -0,0 +1,301 @@
+// 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 .
+
+/**
+ * User competency workflow.
+ *
+ * @module tool_lp/user_competency_workflow
+ * @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/templates',
+ 'core/ajax',
+ 'core/notification',
+ 'core/str',
+ 'tool_lp/menubar',
+ 'tool_lp/base'],
+ function($, Templates, Ajax, Notification, Str, Menubar, Base) {
+
+ /**
+ * UserCompetencyWorkflow class.
+ *
+ * @param {String} selector The node containing the buttons to switch mode.
+ */
+ var UserCompetencyWorkflow = function() {
+ Base.prototype.constructor.apply(this, []);
+ };
+ UserCompetencyWorkflow.prototype = Object.create(Base.prototype);
+
+ /** @type {String} The selector to find the user competency data. */
+ UserCompetencyWorkflow.prototype._nodeSelector = '[data-node="user-competency"]';
+
+ /**
+ * Cancel a review request and refresh the view.
+ *
+ * @param {Object} data The user competency data.
+ * @return {Void}
+ * @method _cancelReviewRequest
+ */
+ UserCompetencyWorkflow.prototype._cancelReviewRequest = function(data) {
+ var call = {
+ methodname: 'tool_lp_user_competency_cancel_review_request',
+ args: {
+ userid: data.userid,
+ competencyid: data.competencyid
+ }
+ };
+
+ Ajax.call([call])[0].then(function() {
+ this._trigger('review-request-cancelled', data);
+ this._trigger('status-changed', data);
+ }.bind(this), function() {
+ this._trigger('error-occured', data);
+ }.bind(this));
+ };
+
+ /**
+ * Cancel a review request an refresh the view.
+ *
+ * @param {Object} data The user competency data.
+ * @return {Void}
+ * @method cancelReviewRequest
+ */
+ UserCompetencyWorkflow.prototype.cancelReviewRequest = function(data) {
+ this._cancelReviewRequest(data);
+ };
+
+ /**
+ * Cancel a review request handler.
+ *
+ * @param {Event} e The event.
+ * @return {Void}
+ * @method _cancelReviewRequestHandler
+ */
+ UserCompetencyWorkflow.prototype._cancelReviewRequestHandler = function(e) {
+ e.preventDefault();
+ var data = this._findUserCompetencyData($(e.target));
+ this.cancelReviewRequest(data);
+ };
+
+ /**
+ * Request a review and refresh the view.
+ *
+ * @param {Object} data The user competency data.
+ * @return {Void}
+ * @method _requestReview
+ */
+ UserCompetencyWorkflow.prototype._requestReview = function(data) {
+ var call = {
+ methodname: 'tool_lp_user_competency_request_review',
+ args: {
+ userid: data.userid,
+ competencyid: data.competencyid
+ }
+ };
+
+ Ajax.call([call])[0].then(function() {
+ this._trigger('review-requested', data);
+ this._trigger('status-changed', data);
+ }.bind(this), function() {
+ this._trigger('error-occured', data);
+ }.bind(this));
+ };
+
+ /**
+ * Request a review.
+ *
+ * @param {Object} data The user competency data.
+ * @return {Void}
+ * @method requestReview
+ */
+ UserCompetencyWorkflow.prototype.requestReview = function(data) {
+ this._requestReview(data);
+ };
+
+ /**
+ * Request a review handler.
+ *
+ * @param {Event} e The event.
+ * @return {Void}
+ * @method _requestReviewHandler
+ */
+ UserCompetencyWorkflow.prototype._requestReviewHandler = function(e) {
+ e.preventDefault();
+ var data = this._findUserCompetencyData($(e.target));
+ this.requestReview(data);
+ };
+
+ /**
+ * Start a review and refresh the view.
+ *
+ * @param {Object} data The user competency data.
+ * @return {Void}
+ * @method _startReview
+ */
+ UserCompetencyWorkflow.prototype._startReview = function(data) {
+ var call = {
+ methodname: 'tool_lp_user_competency_start_review',
+ args: {
+ userid: data.userid,
+ competencyid: data.competencyid
+ }
+ };
+
+ Ajax.call([call])[0].then(function() {
+ this._trigger('review-started', data);
+ this._trigger('status-changed', data);
+ }.bind(this), function() {
+ this._trigger('error-occured', data);
+ }.bind(this));
+ };
+
+ /**
+ * Start a review.
+ *
+ * @param {Object} data The user competency data.
+ * @return {Void}
+ * @method startReview
+ */
+ UserCompetencyWorkflow.prototype.startReview = function(data) {
+ this._startReview(data);
+ };
+
+ /**
+ * Start a review handler.
+ *
+ * @param {Event} e The event.
+ * @return {Void}
+ * @method _startReviewHandler
+ */
+ UserCompetencyWorkflow.prototype._startReviewHandler = function(e) {
+ e.preventDefault();
+ var data = this._findUserCompetencyData($(e.target));
+ this.startReview(data);
+ };
+
+ /**
+ * Stop a review and refresh the view.
+ *
+ * @param {Object} data The user competency data.
+ * @return {Void}
+ * @method _stopReview
+ */
+ UserCompetencyWorkflow.prototype._stopReview = function(data) {
+ var call = {
+ methodname: 'tool_lp_user_competency_stop_review',
+ args: {
+ userid: data.userid,
+ competencyid: data.competencyid
+ }
+ };
+
+ Ajax.call([call])[0].then(function() {
+ this._trigger('review-stopped', data);
+ this._trigger('status-changed', data);
+ }.bind(this), function() {
+ this._trigger('error-occured', data);
+ }.bind(this));
+ };
+
+ /**
+ * Stop a review.
+ *
+ * @param {Object} data The user competency data.
+ * @return {Void}
+ * @method stopReview
+ */
+ UserCompetencyWorkflow.prototype.stopReview = function(data) {
+ this._stopReview(data);
+ };
+
+ /**
+ * Stop a review handler.
+ *
+ * @param {Event} e The event.
+ * @return {Void}
+ * @method _stopReviewHandler
+ */
+ UserCompetencyWorkflow.prototype._stopReviewHandler = function(e) {
+ e.preventDefault();
+ var data = this._findUserCompetencyData($(e.target));
+ this.stopReview(data);
+ };
+
+ /**
+ * Enhance a menu bar.
+ *
+ * @param {String} selector Menubar selector.
+ */
+ UserCompetencyWorkflow.prototype.enhanceMenubar = function(selector) {
+ Menubar.enhance(selector, {
+ '[data-action="request-review"]': this._requestReviewHandler.bind(this),
+ '[data-action="cancel-review-request"]': this._cancelReviewRequestHandler.bind(this),
+ });
+ };
+
+ /**
+ * Find the user competency data from a node.
+ *
+ * @param {Node} node The node to search from.
+ * @return {Object} User competency data.
+ */
+ UserCompetencyWorkflow.prototype._findUserCompetencyData = function(node) {
+ var parent = node.parents(this._nodeSelector),
+ data;
+
+ if (parent.length != 1) {
+ throw new Error('The evidence node was not located.');
+ }
+
+ data = parent.data();
+ if (typeof data === 'undefined' || typeof data.userid === 'undefined' || typeof data.competencyid === 'undefined') {
+ throw new Error('User competency data could not be found.');
+ }
+
+ return data;
+ };
+
+ /**
+ * Enhance a menu bar.
+ *
+ * @param {String} selector Menubar selector.
+ */
+ UserCompetencyWorkflow.prototype.enhanceMenubar = function(selector) {
+ Menubar.enhance(selector, {
+ '[data-action="request-review"]': this._requestReviewHandler.bind(this),
+ '[data-action="cancel-review-request"]': this._cancelReviewRequestHandler.bind(this),
+ '[data-action="start-review"]': this._startReviewHandler.bind(this),
+ '[data-action="stop-review"]': this._stopReviewHandler.bind(this),
+ });
+ };
+
+ /**
+ * Register the events in the region.
+ *
+ * @param {String} selector The base selector to search nodes in and attach events.
+ */
+ UserCompetencyWorkflow.prototype.registerEvents = function(selector) {
+ var wrapper = $(selector);
+
+ wrapper.find('[data-action="request-review"]').click(this._requestReviewHandler.bind(this));
+ wrapper.find('[data-action="cancel-review-request"]').click(this._cancelReviewRequestHandler.bind(this));
+ wrapper.find('[data-action="start-review"]').click(this._startReviewHandler.bind(this));
+ wrapper.find('[data-action="stop-review"]').click(this._stopReviewHandler.bind(this));
+ };
+
+ return /** @alias module:tool_lp/user_competency_actions */ UserCompetencyWorkflow;
+});
diff --git a/admin/tool/lp/classes/api.php b/admin/tool/lp/classes/api.php
index 6e168aab5c3..2bd11b4bdd0 100644
--- a/admin/tool/lp/classes/api.php
+++ b/admin/tool/lp/classes/api.php
@@ -2044,6 +2044,7 @@ class api {
throw new coding_exception('A user competency plan is missing');
} else {
$uc = user_competency::create_relation($plan->get_userid(), $competency->get_id());
+ $uc->create();
}
}
@@ -2256,6 +2257,110 @@ class api {
return $competencyfrom->update();
}
+ /**
+ * Cancel a user competency review request.
+ *
+ * @param int $userid The user ID.
+ * @param int $competencyid The competency ID.
+ * @return bool
+ */
+ public static function user_competency_cancel_review_request($userid, $competencyid) {
+ if (!plan::can_read_user($userid)) {
+ $context = context_user::instance($userid);
+ throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
+ }
+
+ $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
+ if (!$uc || $uc->get_status() != user_competency::STATUS_WAITING_FOR_REVIEW) {
+ throw new coding_exception('The competency can not be cancel review request at this stage.');
+ } else if (!$uc->can_request_review()) {
+ throw new required_capability_exception($context, 'tool/lp:userevidencemanage', 'nopermissions', '');
+ }
+
+ $uc->set_status(user_competency::STATUS_IDLE);
+ return $uc->update();
+ }
+
+ /**
+ * Request a user competency review.
+ *
+ * @param int $userid The user ID.
+ * @param int $competencyid The competency ID.
+ * @return bool
+ */
+ public static function user_competency_request_review($userid, $competencyid) {
+ if (!plan::can_read_user($userid)) {
+ $context = context_user::instance($userid);
+ throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
+ }
+
+ $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
+ if (!$uc) {
+ $uc = user_competency::create_relation($userid, $competencyid);
+ $uc->create();
+ }
+
+ if ($uc->get_status() != user_competency::STATUS_IDLE) {
+ throw new coding_exception('The competency can not be sent for review at this stage.');
+ } else if (!$uc->can_request_review()) {
+ throw new required_capability_exception($context, 'tool/lp:userevidencemanage', 'nopermissions', '');
+ }
+
+ $uc->set_status(user_competency::STATUS_WAITING_FOR_REVIEW);
+ return $uc->update();
+ }
+
+ /**
+ * Start a user competency review.
+ *
+ * @param int $userid The user ID.
+ * @param int $competencyid The competency ID.
+ * @return bool
+ */
+ public static function user_competency_start_review($userid, $competencyid) {
+ global $USER;
+
+ if (!plan::can_read_user($userid)) {
+ $context = context_user::instance($userid);
+ throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
+ }
+
+ $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
+ if (!$uc || $uc->get_status() != user_competency::STATUS_WAITING_FOR_REVIEW) {
+ throw new coding_exception('The competency review can not be started at this stage.');
+ } else if (!$uc->can_review()) {
+ throw new required_capability_exception($context, 'tool/lp:competencygrade', 'nopermissions', '');
+ }
+
+ $uc->set_status(user_competency::STATUS_IN_REVIEW);
+ $uc->set_reviewerid($USER->id);
+ return $uc->update();
+ }
+
+ /**
+ * Stop a user competency review.
+ *
+ * @param int $userid The user ID.
+ * @param int $competencyid The competency ID.
+ * @return bool
+ */
+ public static function user_competency_stop_review($userid, $competencyid) {
+ if (!plan::can_read_user($userid)) {
+ $context = context_user::instance($userid);
+ throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
+ }
+
+ $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
+ if (!$uc || $uc->get_status() != user_competency::STATUS_IN_REVIEW) {
+ throw new coding_exception('The competency review can not be stopped at this stage.');
+ } else if (!$uc->can_review()) {
+ throw new required_capability_exception($context, 'tool/lp:competencygrade', 'nopermissions', '');
+ }
+
+ $uc->set_status(user_competency::STATUS_IDLE);
+ return $uc->update();
+ }
+
/**
* List all the related competencies.
*
diff --git a/admin/tool/lp/classes/external.php b/admin/tool/lp/classes/external.php
index f9d834acfa5..f03a53d4820 100644
--- a/admin/tool/lp/classes/external.php
+++ b/admin/tool/lp/classes/external.php
@@ -2678,6 +2678,163 @@ class external extends external_api {
return new external_value(PARAM_BOOL, 'True if successful.');
}
+ /**
+ * Returns description of external function parameters.
+ *
+ * @return \external_function_parameters
+ */
+ public static function user_competency_cancel_review_request_parameters() {
+ return new external_function_parameters(array(
+ 'userid' => new external_value(PARAM_INT, 'The user ID'),
+ 'competencyid' => new external_value(PARAM_INT, 'The competency ID'),
+ ));
+ }
+
+ /**
+ * External function user_competency_cancel_review_request.
+ *
+ * @param int $id The ID description.
+ * @return boolean
+ */
+ public static function user_competency_cancel_review_request($userid, $competencyid) {
+ $params = self::validate_parameters(self::user_competency_request_review_parameters(), array(
+ 'userid' => $userid,
+ 'competencyid' => $competencyid
+ ));
+
+ $context = context_user::instance($params['userid']);
+ self::validate_context($context);
+
+ return api::user_competency_cancel_review_request($userid, $competencyid);
+ }
+
+ /**
+ * Returns description of external function result value.
+ *
+ * @return \external_function_parameters
+ */
+ public static function user_competency_cancel_review_request_returns() {
+ new external_value(PARAM_BOOL, 'The success');
+ }
+
+ /**
+ * Returns description of external function parameters.
+ *
+ * @return \external_function_parameters
+ */
+ public static function user_competency_request_review_parameters() {
+ return new external_function_parameters(array(
+ 'userid' => new external_value(PARAM_INT, 'The user ID'),
+ 'competencyid' => new external_value(PARAM_INT, 'The competency ID'),
+ ));
+ }
+
+ /**
+ * External function user_competency_request_review.
+ *
+ * @param int $userid The user ID.
+ * @param int $competencyid The competency ID.
+ * @return boolean
+ */
+ public static function user_competency_request_review($userid, $competencyid) {
+ $params = self::validate_parameters(self::user_competency_request_review_parameters(), array(
+ 'userid' => $userid,
+ 'competencyid' => $competencyid
+ ));
+
+ $context = context_user::instance($params['userid']);
+ self::validate_context($context);
+
+ return api::user_competency_request_review($userid, $competencyid);
+ }
+
+ /**
+ * Returns description of external function result value.
+ *
+ * @return \external_function_parameters
+ */
+ public static function user_competency_request_review_returns() {
+ new external_value(PARAM_BOOL, 'The success');
+ }
+
+ /**
+ * Returns description of external function parameters.
+ *
+ * @return \external_function_parameters
+ */
+ public static function user_competency_start_review_parameters() {
+ return new external_function_parameters(array(
+ 'userid' => new external_value(PARAM_INT, 'The user ID'),
+ 'competencyid' => new external_value(PARAM_INT, 'The competency ID'),
+ ));
+ }
+
+ /**
+ * External function user_competency_start_review.
+ *
+ * @param int $id The ID description.
+ * @return boolean
+ */
+ public static function user_competency_start_review($userid, $competencyid) {
+ $params = self::validate_parameters(self::user_competency_request_review_parameters(), array(
+ 'userid' => $userid,
+ 'competencyid' => $competencyid
+ ));
+
+ $context = context_user::instance($params['userid']);
+ self::validate_context($context);
+
+ return api::user_competency_start_review($userid, $competencyid);
+ }
+
+ /**
+ * Returns description of external function result value.
+ *
+ * @return \external_function_parameters
+ */
+ public static function user_competency_start_review_returns() {
+ new external_value(PARAM_BOOL, 'The success');
+ }
+
+ /**
+ * Returns description of external function parameters.
+ *
+ * @return \external_function_parameters
+ */
+ public static function user_competency_stop_review_parameters() {
+ return new external_function_parameters(array(
+ 'userid' => new external_value(PARAM_INT, 'The user ID'),
+ 'competencyid' => new external_value(PARAM_INT, 'The competency ID'),
+ ));
+ }
+
+ /**
+ * External function user_competency_stop_review.
+ *
+ * @param int $id The ID description.
+ * @return boolean
+ */
+ public static function user_competency_stop_review($userid, $competencyid) {
+ $params = self::validate_parameters(self::user_competency_request_review_parameters(), array(
+ 'userid' => $userid,
+ 'competencyid' => $competencyid
+ ));
+
+ $context = context_user::instance($params['userid']);
+ self::validate_context($context);
+
+ return api::user_competency_stop_review($userid, $competencyid);
+ }
+
+ /**
+ * Returns description of external function result value.
+ *
+ * @return \external_function_parameters
+ */
+ public static function user_competency_stop_review_returns() {
+ new external_value(PARAM_BOOL, 'The success');
+ }
+
/**
* Count the competencies (visible to this user) in this learning plan template.
*
diff --git a/admin/tool/lp/classes/external/comment_area_exporter.php b/admin/tool/lp/classes/external/comment_area_exporter.php
index 7b49e97d93f..43f5f23abb5 100644
--- a/admin/tool/lp/classes/external/comment_area_exporter.php
+++ b/admin/tool/lp/classes/external/comment_area_exporter.php
@@ -24,6 +24,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_lp\external;
+require_once($CFG->dirroot . '/comment/lib.php');
+
defined('MOODLE_INTERNAL') || die();
use comment;
@@ -51,6 +53,7 @@ class comment_area_exporter extends exporter {
$data->courseid = $comment->get_courseid();
$data->contextid = $comment->get_context()->id;
$data->cid = $comment->get_cid();
+
parent::__construct($data, $related);
}
diff --git a/admin/tool/lp/classes/external/user_competency_exporter.php b/admin/tool/lp/classes/external/user_competency_exporter.php
index 64836124d14..a65014479d6 100644
--- a/admin/tool/lp/classes/external/user_competency_exporter.php
+++ b/admin/tool/lp/classes/external/user_competency_exporter.php
@@ -24,6 +24,7 @@
namespace tool_lp\external;
defined('MOODLE_INTERNAL') || die();
+use core_user;
use renderer_base;
use stdClass;
use tool_lp\user_competency;
@@ -68,17 +69,66 @@ class user_competency_exporter extends persistent_exporter {
}
$result->statusname = $statusname;
+ $result->canrequestreview = $this->persistent->can_request_review();
+ $result->canreview = $this->persistent->can_review();
+
+ $result->isstatusidle = $this->persistent->get_status() == user_competency::STATUS_IDLE;
+ $result->isstatusinreview = $this->persistent->get_status() == user_competency::STATUS_IN_REVIEW;
+ $result->isstatuswaitingforreview = $this->persistent->get_status() == user_competency::STATUS_WAITING_FOR_REVIEW;
+
+ $result->isrequestreviewallowed = $result->canrequestreview && $result->isstatusidle;
+ $result->iscancelreviewrequestallowed = $result->canrequestreview && $result->isstatuswaitingforreview;
+ $result->isstartreviewallowed = $result->canreview && $result->isstatuswaitingforreview;
+ $result->isstopreviewallowed = $result->canreview && $result->isstatusinreview;
+
+ if (!empty($result->isstatusinreview)) {
+ // TODO Make this more efficient.
+ $userexporter = new user_summary_exporter(core_user::get_user($this->persistent->get_reviewerid(), '*', MUST_EXIST));
+ $result->reviewer = $userexporter->export($output);
+ }
+
return (array) $result;
}
protected static function define_other_properties() {
return array(
+ 'canrequestreview' => array(
+ 'type' => PARAM_BOOL,
+ ),
+ 'canreview' => array(
+ 'type' => PARAM_BOOL,
+ ),
'gradename' => array(
'type' => PARAM_TEXT
),
+ 'isrequestreviewallowed' => array(
+ 'type' => PARAM_BOOL,
+ ),
+ 'iscancelreviewrequestallowed' => array(
+ 'type' => PARAM_BOOL,
+ ),
+ 'isstartreviewallowed' => array(
+ 'type' => PARAM_BOOL,
+ ),
+ 'isstopreviewallowed' => array(
+ 'type' => PARAM_BOOL,
+ ),
+ 'isstatusidle' => array(
+ 'type' => PARAM_BOOL,
+ ),
+ 'isstatusinreview' => array(
+ 'type' => PARAM_BOOL,
+ ),
+ 'isstatuswaitingforreview' => array(
+ 'type' => PARAM_BOOL,
+ ),
'proficiencyname' => array(
'type' => PARAM_RAW
),
+ 'reviewer' => array(
+ 'type' => user_summary_exporter::read_properties_definition(),
+ 'optional' => true
+ ),
'statusname' => array(
'type' => PARAM_RAW
),
diff --git a/admin/tool/lp/classes/external/user_competency_summary_exporter.php b/admin/tool/lp/classes/external/user_competency_summary_exporter.php
index 179171949c7..53f21cdeda5 100644
--- a/admin/tool/lp/classes/external/user_competency_summary_exporter.php
+++ b/admin/tool/lp/classes/external/user_competency_summary_exporter.php
@@ -26,6 +26,7 @@ namespace tool_lp\external;
use context_user;
use renderer_base;
use stdClass;
+use tool_lp\user_competency;
/**
* Class for exporting user competency data with additional related data.
@@ -76,7 +77,11 @@ class user_competency_summary_exporter extends exporter {
'evidence' => array(
'type' => evidence_exporter::read_properties_definition(),
'multiple' => true
- )
+ ),
+ 'commentarea' => array(
+ 'type' => comment_area_exporter::read_properties_definition(),
+ 'optional' => true
+ ),
);
}
@@ -143,6 +148,13 @@ class user_competency_summary_exporter extends exporter {
$result->evidence = $allevidence;
}
+ $usercompetency = !empty($this->related['usercompetency']) ? $this->related['usercompetency'] : null;
+
+ if (!empty($usercompetency)) {
+ $commentareaexporter = new comment_area_exporter($usercompetency->get_comment_object());
+ $result->commentarea = $commentareaexporter->export($output);
+ }
+
return (array) $result;
}
}
diff --git a/admin/tool/lp/classes/output/user_competency_summary_in_plan.php b/admin/tool/lp/classes/output/user_competency_summary_in_plan.php
index e0a8b2ba3bc..6d3581d1d08 100644
--- a/admin/tool/lp/classes/output/user_competency_summary_in_plan.php
+++ b/admin/tool/lp/classes/output/user_competency_summary_in_plan.php
@@ -22,6 +22,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_lp\output;
+defined('MOODLE_INTERNAL') || die();
use renderable;
use templatable;
@@ -64,7 +65,7 @@ class user_competency_summary_in_plan implements renderable, templatable {
global $DB;
$plan = api::read_plan($this->planid);
- $pc = api::get_plan_competency($this->planid, $this->competencyid);
+ $pc = api::get_plan_competency($plan, $this->competencyid);
$competency = $pc->competency;
$usercompetency = $pc->usercompetency;
$usercompetencyplan = $pc->usercompetencyplan;
diff --git a/admin/tool/lp/classes/user_competency.php b/admin/tool/lp/classes/user_competency.php
index 6977d34ada9..da51f170332 100644
--- a/admin/tool/lp/classes/user_competency.php
+++ b/admin/tool/lp/classes/user_competency.php
@@ -24,6 +24,9 @@
namespace tool_lp;
defined('MOODLE_INTERNAL') || die();
+use coding_exception;
+use context_user;
+use comment;
use lang_string;
/**
@@ -86,6 +89,24 @@ class user_competency extends persistent {
);
}
+ /**
+ * Can the current user send the user competency for review?
+ *
+ * @return bool
+ */
+ public function can_request_review() {
+ return static::can_request_review_user($this->get_userid());
+ }
+
+ /**
+ * Can the current user review the user competency?
+ *
+ * @return bool
+ */
+ public function can_review() {
+ return static::can_review_user($this->get_userid());
+ }
+
/**
* Human readable status name.
*
@@ -131,6 +152,30 @@ class user_competency extends persistent {
return $list;
}
+ /**
+ * Get the comment object.
+ *
+ * @return comment
+ */
+ public function get_comment_object() {
+ global $CFG;
+ require_once($CFG->dirroot . '/comment/lib.php');
+
+ if (!$this->get_id()) {
+ throw new coding_exception('The user competency record must exist.');
+ }
+
+ $comment = new comment((object) array(
+ 'context' => $this->get_context(),
+ 'component' => 'tool_lp',
+ 'itemid' => $this->get_id(),
+ 'area' => 'user_competency',
+ 'showcount' => true,
+ ));
+ $comment->set_fullwidth(true);
+ return $comment;
+ }
+
/**
* Return the competency Object.
*
@@ -140,6 +185,15 @@ class user_competency extends persistent {
return new competency($this->get_competencyid());
}
+ /**
+ * Get the context.
+ *
+ * @return context The context.
+ */
+ public function get_context() {
+ return context_user::instance($this->get_userid());
+ }
+
/**
* Validate the user ID.
*
@@ -230,6 +284,28 @@ class user_competency extends persistent {
return true;
}
+ /**
+ * Can the current user send a user's competency for review?
+ *
+ * @param int $userid The user ID the competency belongs to.
+ * @return bool
+ */
+ public static function can_request_review_user($userid) {
+ // We can request a review when we can attach evidence of prior learning.
+ return user_evidence::can_manage_user($userid);
+ }
+
+ /**
+ * Can the current user send review the user competency?
+ *
+ * @param int $userid The user ID the competency belongs to.
+ * @return bool
+ */
+ public static function can_review_user($userid) {
+ // We can review a competency when we can grade them.
+ return has_capability('tool/lp:competencygrade', context_user::instance($userid));
+ }
+
/**
* Create a new user_competency object.
*
diff --git a/admin/tool/lp/db/access.php b/admin/tool/lp/db/access.php
index 8eb87441871..7142c2f5cf8 100644
--- a/admin/tool/lp/db/access.php
+++ b/admin/tool/lp/db/access.php
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
$capabilities = array(
+ // Competencies.
'tool/lp:competencymanage' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSECAT,
@@ -43,6 +44,22 @@ $capabilities = array(
),
'clonepermissionsfrom' => 'moodle/block:view'
),
+ 'tool/lp:competencysuggestgrade' => array(
+ 'captype' => 'write',
+ 'contextlevel' => CONTEXT_COURSE, // And CONTEXT_USER.
+ 'archetypes' => array(
+ 'teacher' => CAP_ALLOW
+ ),
+ ),
+ 'tool/lp:competencygrade' => array(
+ 'captype' => 'write',
+ 'contextlevel' => CONTEXT_COURSE, // And CONTEXT_USER.
+ 'archetypes' => array(
+ 'editingteacher' => CAP_ALLOW,
+ 'manager' => CAP_ALLOW
+ ),
+ ),
+ // Course competencies.
'tool/lp:coursecompetencymanage' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
@@ -68,6 +85,7 @@ $capabilities = array(
),
'clonepermissionsfrom' => 'moodle/block:view'
),
+ // User plans.
'tool/lp:planmanage' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_USER,
@@ -125,6 +143,7 @@ $capabilities = array(
),
'clonepermissionsfrom' => 'moodle/site:config'
),
+ // Template.
'tool/lp:templatemanage' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSECAT,
@@ -163,19 +182,4 @@ $capabilities = array(
),
'clonepermissionsfrom' => 'moodle/site:config'
),
- 'tool/lp:competencysuggestgrade' => array(
- 'captype' => 'write',
- 'contextlevel' => CONTEXT_COURSE, // And CONTEXT_USER.
- 'archetypes' => array(
- 'teacher' => CAP_ALLOW
- ),
- ),
- 'tool/lp:competencygrade' => array(
- 'captype' => 'write',
- 'contextlevel' => CONTEXT_COURSE, // And CONTEXT_USER.
- 'archetypes' => array(
- 'editingteacher' => CAP_ALLOW,
- 'manager' => CAP_ALLOW
- ),
- ),
);
diff --git a/admin/tool/lp/db/messages.php b/admin/tool/lp/db/messages.php
new file mode 100644
index 00000000000..0140afe7162
--- /dev/null
+++ b/admin/tool/lp/db/messages.php
@@ -0,0 +1,29 @@
+.
+
+/**
+ * Message producers.
+ *
+ * @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();
+
+$messageproviders = array (
+ 'user_competency_comment' => array(),
+);
\ No newline at end of file
diff --git a/admin/tool/lp/db/services.php b/admin/tool/lp/db/services.php
index 8649e409b7d..396924c350f 100644
--- a/admin/tool/lp/db/services.php
+++ b/admin/tool/lp/db/services.php
@@ -662,6 +662,45 @@ $functions = array(
'capabilities' => 'tool/lp:userevidencemanageown',
'ajax' => true,
),
+
+ // User competency.
+ 'tool_lp_user_competency_cancel_review_request' => array(
+ 'classname' => 'tool_lp\external',
+ 'methodname' => 'user_competency_cancel_review_request',
+ 'classpath' => '',
+ 'description' => 'Cancel a review request.',
+ 'type' => 'write',
+ 'capabilities' => 'tool/lp:userevidencemanageown',
+ 'ajax' => true,
+ ),
+ 'tool_lp_user_competency_request_review' => array(
+ 'classname' => 'tool_lp\external',
+ 'methodname' => 'user_competency_request_review',
+ 'classpath' => '',
+ 'description' => 'Request a review.',
+ 'type' => 'write',
+ 'capabilities' => 'tool/lp:userevidencemanageown',
+ 'ajax' => true,
+ ),
+ 'tool_lp_user_competency_start_review' => array(
+ 'classname' => 'tool_lp\external',
+ 'methodname' => 'user_competency_start_review',
+ 'classpath' => '',
+ 'description' => 'Start a review.',
+ 'type' => 'write',
+ 'capabilities' => 'tool/lp:competencygrade',
+ 'ajax' => true,
+ ),
+ 'tool_lp_user_competency_stop_review' => array(
+ 'classname' => 'tool_lp\external',
+ 'methodname' => 'user_competency_stop_review',
+ 'classpath' => '',
+ 'description' => 'Stop a review.',
+ 'type' => 'write',
+ 'capabilities' => 'tool/lp:competencygrade',
+ 'ajax' => true,
+ ),
+
'tool_lp_grade_competency_in_plan' => array(
'classname' => 'tool_lp\external',
'methodname' => 'grade_competency_in_plan',
diff --git a/admin/tool/lp/lang/en/tool_lp.php b/admin/tool/lp/lang/en/tool_lp.php
index d6c1bda9f55..3770c10f41d 100644
--- a/admin/tool/lp/lang/en/tool_lp.php
+++ b/admin/tool/lp/lang/en/tool_lp.php
@@ -23,6 +23,7 @@
*/
$string['actions'] = 'Actions';
+$string['acommentwaspostedonacompetency'] = 'A comment was posted on a competency.';
$string['addcohorts'] = 'Add cohorts';
$string['addcompetency'] = 'Add competency';
$string['addingcompetencywillresetparentrule'] = 'Adding a new competency will remove the rule set on \'{$a}\'. Do you want to continue?';
@@ -37,6 +38,7 @@ $string['allchildrenarecomplete'] = 'All children are complete';
$string['aplanswerecreated'] = '{$a} plans were created.';
$string['aplanswerecreatedmoremayrequiresync'] = '{$a} plans were created, more will be created during the next synchronization.';
$string['assigncohorts'] = 'Assign cohorts';
+$string['cancelreviewrequest'] = 'Cancel review request';
$string['cannotaddrules'] = 'This competency cannot be configured.';
$string['cannotcreateuserplanswhentemplateduedateispassed'] = 'New user plans can not be created: this template\'s due date has, or is about to, expire.';
$string['cannotcreateuserplanswhentemplatehidden'] = 'New user plans can not be created while this template is hidden.';
@@ -156,6 +158,7 @@ $string['lp:userevidencemanage'] = 'Manage evidence of prior learning';
$string['lp:userevidencemanageown'] = 'Manage own evidence of prior learning';
$string['lp:userevidenceread'] = 'View evidence of prior learning of a user';
$string['managecompetenciesandframeworks'] = 'Manage competencies and frameworks';
+$string['messageprovider:user_competency_comment'] = 'Comment posted on a competency.';
$string['move'] = 'Move';
$string['movecompetency'] = 'Move competency';
$string['movecompetencyafter'] = 'Move competency after \'{$a}\'';
@@ -201,6 +204,7 @@ $string['proficient'] = 'Proficient';
$string['rate'] = 'Rate';
$string['rating'] = 'Rating';
$string['relatedcompetencies'] = 'Related competencies';
+$string['requestreview'] = 'Request review';
$string['reopenplan'] = 'Reopen this learning plan';
$string['reopenplanconfirm'] = 'Reopen the plan \'{$a}\'? The status of the users competencies that was recorded at the time the plan was previously completed will be deleted, and the plan will become active again.';
$string['savechanges'] = 'Save changes';
@@ -213,8 +217,10 @@ $string['selectedcompetency'] = 'Selected competency';
$string['selectcohortstosync'] = 'Select cohorts to sync';
$string['selectuserstocreateplansfor'] = 'Select users to create plans for';
$string['shortname'] = 'Name';
+$string['startreview'] = 'Start review';
$string['state'] = 'State';
$string['status'] = 'Status';
+$string['stopreview'] = 'Stop review';
$string['stopsyncingcohort'] = 'Stop syncing cohort';
$string['suggest'] = 'Suggest';
$string['syncplanscohorts'] = 'Sync plans from template cohorts';
diff --git a/admin/tool/lp/lib.php b/admin/tool/lp/lib.php
index cfb9e8283c4..f765920a9c9 100644
--- a/admin/tool/lp/lib.php
+++ b/admin/tool/lp/lib.php
@@ -169,3 +169,64 @@ function tool_lp_pluginfile($course, $cm, $context, $filearea, $args, $forcedown
send_stored_file($file, null, 0, $forcedownload);
}
+
+/**
+ * Hook when a comment is added.
+ *
+ * @param stdClass $comment The comment.
+ * @param stdClass $params The parameters.
+ * @return array
+ */
+function tool_lp_comment_add($comment, $params) {
+ if ($params->commentarea == 'user_competency') {
+ $uc = new \tool_lp\user_competency($params->itemid);
+
+ // When the owner comments we message the reviewer, otherwise we message the current user.
+ $recipient = $comment->userid == $uc->get_userid() ? $uc->get_reviewerid() : $uc->get_userid();
+ if (!$recipient) {
+ return;
+ }
+
+ $message = new stdClass();
+ $message->component = 'tool_lp';
+ $message->name = 'user_competency_comment';
+ $message->notification = 1;
+ $message->userto = $recipient;
+ $message->userfrom = $comment->userid;
+ // TODO Make message useful.
+ $message->fullmessage = get_string('acommentwaspostedonacompetency', 'tool_lp');
+ $message->fullmessageformat = FORMAT_PLAIN;
+ $message->fullmessagehtml = '';
+ $message->smallmessage = get_string('acommentwaspostedonacompetency', 'tool_lp');
+ // TODO Make URL useful.
+ $message->contexturl = new moodle_url('/admin/tool/lp/plans.php', array('userid' => $uc->get_userid()));
+ $message->contexturlname = get_string('userplans', 'tool_lp');
+
+ message_send($message);
+ }
+}
+
+/**
+ * Return the permissions of for the comments.
+ *
+ * @param stdClass $params The parameters.
+ * @return array
+ */
+function tool_lp_comment_permissions($params) {
+ if ($params->commentarea == 'user_competency') {
+ $uc = new \tool_lp\user_competency($params->itemid);
+ $can = \tool_lp\plan::can_read_user($uc->get_userid());
+ return array('post' => $can, 'view' => $can);
+ }
+ return array('post' => false, 'view' => false);
+}
+
+/**
+ * Validates comments.
+ *
+ * @param stdClass $params The parameters.
+ * @return array
+ */
+function tool_lp_comment_validate($params) {
+ return true;
+}
diff --git a/admin/tool/lp/templates/plan_page.mustache b/admin/tool/lp/templates/plan_page.mustache
index 96caba22fd4..3ffa4d5521e 100644
--- a/admin/tool/lp/templates/plan_page.mustache
+++ b/admin/tool/lp/templates/plan_page.mustache
@@ -66,15 +66,13 @@
{{#str}}proficient, tool_lp{{/str}} |
{{^plan.iscompleted}}
{{#str}}status, tool_lp{{/str}} |
- {{/plan.iscompleted}}
- {{#plan.canbeedited}}
{{#str}}actions, tool_lp{{/str}} |
- {{/plan.canbeedited}}
+ {{/plan.iscompleted}}
-
+
{{#competencies}}
-
+
|
{{#plan.canbeedited}}
@@ -91,13 +89,32 @@
| {{usercompetency.proficiencyname}} |
{{usercompetency.statusname}} |
{{/plan.iscompleted}}
- {{#plan.canbeedited}}
+ {{^plan.iscompleted}}
-
+
|
- {{/plan.canbeedited}}
+ {{/plan.iscompleted}}
{{/competencies}}
@@ -114,19 +131,25 @@
-{{#plan.canmanage}}
{{#js}}
-require(['tool_lp/competencies', 'tool_lp/planactions'], function(mod, actionsMod) {
- (new mod({{plan.id}}, 'plan', {{contextid}}));
-
+require(['tool_lp/competencies', 'tool_lp/planactions', 'tool_lp/user_competency_workflow'], function(mod, actionsMod, UserCompWorkflow) {
var planActions = new actionsMod('plan');
- planActions.registerEvents();
+
+ {{#plan.canmanage}}
+ (new mod({{plan.id}}, 'plan', {{contextid}}));
+ planActions.registerEvents();
+ {{/plan.canmanage}}
+
+ var ucw = new UserCompWorkflow();
+ ucw.enhanceMenubar('.user-competency-actions');
+ ucw.on('status-changed', planActions.refresh.bind(planActions, '[data-region="plan-summary"]'));
+ ucw.on('error-occured', planActions.refresh.bind(planActions, '[data-region="plan-summary"]'));
});
{{/js}}
-{{/plan.canmanage}}
{{#js}}
require(['tool_lp/competencydialogue'], function(Compdialogue) {
var competencydialogue = new Compdialogue({includerelated : true});
competencydialogue.watch('[data-region="plan-page"]');
+
});
{{/js}}
diff --git a/admin/tool/lp/templates/user_competency_summary_in_plan.mustache b/admin/tool/lp/templates/user_competency_summary_in_plan.mustache
index 2efa2005332..63583d79be5 100644
--- a/admin/tool/lp/templates/user_competency_summary_in_plan.mustache
+++ b/admin/tool/lp/templates/user_competency_summary_in_plan.mustache
@@ -40,14 +40,25 @@
This template does not have an example context because it includes ajax functionality.
}}
{{#usercompetencysummary}}
-
+
{{#competency}}
{{> tool_lp/competency_summary }}
{{/competency}}
- - {{#str}}rating, tool_lp{{/str}}
{{#usercompetency}}
+ - {{#str}}status, tool_lp{{/str}}
+ - {{statusname}}
+
+ {{#isstatusinreview}}
+ - {{reviewer.fullname}}
+ {{/isstatusinreview}}
+
+ {{#isrequestreviewallowed}}{{/isrequestreviewallowed}}
+ {{#iscancelreviewrequestallowed}}{{/iscancelreviewrequestallowed}}
+ {{#isstartreviewallowed}}{{/isstartreviewallowed}}
+ {{#isstopreviewallowed}}{{/isstopreviewallowed}}
+
- {{#str}}proficient, tool_lp{{/str}}
-
@@ -56,9 +67,9 @@
- {{#str}}rating, tool_lp{{/str}}
- {{gradename}}
- {{#cangradeorsuggest}}
-
{{#js}}
- require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info'], function($, mod, info) {
+ require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info', 'tool_lp/user_competency_workflow'], function($, mod, info, UserCompWorkflow) {
var scaleConfig = JSON.parse('{{{competency.scaleconfiguration}}}');
- var inlineGrader = new mod('grade-competency-form-{{uniqid}}', scaleConfig, '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
var competencyElement = $(document.getElementById('grade-competency-form-{{uniqid}}')).closest('[data-region=user-competency-full-info]');
var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}');
+ var ucw = new UserCompWorkflow();
+ ucw.registerEvents('[data-region="user-competency-status"]');
+ ucw.on('status-changed', infoReloader.reload.bind(infoReloader));
+ ucw.on('error-occured', infoReloader.reload.bind(infoReloader));
+
+ var inlineGrader = new mod('grade-competency-form-{{uniqid}}', scaleConfig, '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
});
{{/js}}
- {{/cangradeorsuggest}}
{{/usercompetency}}
{{#usercompetencyplan}}
+ - {{#str}}rating, tool_lp{{/str}}
- {{gradename}} - {{#str}}plancompleted, tool_lp{{/str}}
{{#cangradeorsuggest}}
{{$gradeform}}
@@ -101,6 +117,9 @@
{{/usercompetencyplan}}
+ {{#commentarea}}
+ {{>tool_lp/comment_area}}
+ {{/commentarea}}
- {{#str}}evidence, tool_lp{{/str}}
-
diff --git a/admin/tool/lp/tests/externallib_test.php b/admin/tool/lp/tests/externallib_test.php
index fee697f955d..3630e82c208 100644
--- a/admin/tool/lp/tests/externallib_test.php
+++ b/admin/tool/lp/tests/externallib_test.php
@@ -1405,7 +1405,7 @@ class tool_lp_external_testcase extends externallib_advanced_testcase {
$plan = $lpg->create_plan(array('userid' => $this->user->id, 'templateid' => $tpl->get_id()));
$uc1a = $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c1a->get_id(),
- 'status' => user_competency::STATUS_IN_REVIEW));
+ 'status' => user_competency::STATUS_IN_REVIEW, 'reviewerid' => $this->creator->id));
$uc1b = $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c1b->get_id()));
$uc2b = $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c2b->get_id(),
'grade' => 2, 'proficiency' => 1));