From a8902ee2d73e48a40a5b9f246a20c5fe2f0e29f1 Mon Sep 17 00:00:00 2001 From: Serge Gauthier Date: Wed, 23 Dec 2015 10:22:17 -0500 Subject: [PATCH] MDL-52432 tool_lp: Add ability to unlink a plan from its template --- admin/tool/lp/amd/build/planactions.min.js | 2 +- admin/tool/lp/amd/src/planactions.js | 59 +++++++++++++++++++ admin/tool/lp/classes/api.php | 5 ++ admin/tool/lp/classes/external.php | 45 ++++++++++++++ .../lp/classes/external/plan_exporter.php | 6 ++ admin/tool/lp/db/services.php | 10 +++- admin/tool/lp/lang/en/tool_lp.php | 2 + admin/tool/lp/templates/plan_page.mustache | 8 +-- admin/tool/lp/templates/plans_page.mustache | 7 +++ admin/tool/lp/tests/api_test.php | 18 +++++- admin/tool/lp/version.php | 2 +- 11 files changed, 156 insertions(+), 8 deletions(-) diff --git a/admin/tool/lp/amd/build/planactions.min.js b/admin/tool/lp/amd/build/planactions.min.js index 1b4f8b5a2a7..c6bc873a38b 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._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)})},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))},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._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/src/planactions.js b/admin/tool/lp/amd/src/planactions.js index d0332c25437..edc3880285a 100644 --- a/admin/tool/lp/amd/src/planactions.js +++ b/admin/tool/lp/amd/src/planactions.js @@ -305,6 +305,63 @@ define(['jquery', var data = this._findPlanData($(e.target)); this.completePlan(data); }; + + /** + * Unlink plan and reload the region. + * + * @param {Object} planData Plan data from plan node. + */ + PlanActions.prototype._doUnlinkPlan = function(planData) { + var self = this, + calls = [{ + methodname: 'tool_lp_unlink_plan_from_template', + args: { planid: planData.id} + }]; + self._callAndRefresh(calls, planData); + }; + + /** + * Unlink a plan process. + * + * @param {Object} planData Plan data from plan node. + */ + PlanActions.prototype.unlinkPlan = function(planData) { + var self = this, + requests = ajax.call([{ + methodname: 'tool_lp_read_plan', + args: { id: planData.id } + }]); + + requests[0].done(function(plan) { + str.get_strings([ + { key: 'confirm', component: 'moodle' }, + { key: 'unlinkplantemplateconfirm', component: 'tool_lp', param: plan.name }, + { key: 'unlinkplantemplate', component: 'tool_lp' }, + { key: 'cancel', component: 'moodle' } + ]).done(function (strings) { + notification.confirm( + strings[0], // Confirm. + strings[1], // Unlink plan X? + strings[2], // Unlink. + strings[3], // Cancel. + function() { + self._doUnlinkPlan(planData); + }.bind(self) + ); + }).fail(notification.exception); + }).fail(notification.exception); + }; + + /** + * Unlink plan handler. + * + * @param {Event} e The event. + */ + PlanActions.prototype._unlinkPlanHandler = function(e) { + e.preventDefault(); + var data = this._findPlanData($(e.target)); + this.unlinkPlan(data); + }; /** * Find the plan data from the plan node. @@ -339,6 +396,7 @@ define(['jquery', '[data-action="plan-delete"]': self._deletePlanHandler.bind(self), '[data-action="plan-complete"]': self._completePlanHandler.bind(self), '[data-action="plan-reopen"]': self._reopenPlanHandler.bind(self), + '[data-action="plan-unlink"]': self._unlinkPlanHandler.bind(self), }); }; @@ -355,6 +413,7 @@ define(['jquery', wrapper.find('[data-action="plan-delete"]').click(self._deletePlanHandler.bind(self)); wrapper.find('[data-action="plan-complete"]').click(self._completePlanHandler.bind(self)); wrapper.find('[data-action="plan-reopen"]').click(self._reopenPlanHandler.bind(self)); + wrapper.find('[data-action="plan-unlink"]').click(self._unlinkPlanHandler.bind(self)); }; return PlanActions; diff --git a/admin/tool/lp/classes/api.php b/admin/tool/lp/classes/api.php index e316307e82c..69432e90bf0 100644 --- a/admin/tool/lp/classes/api.php +++ b/admin/tool/lp/classes/api.php @@ -1741,6 +1741,11 @@ class api { throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', ''); } + // Only plan with status DRAFT or ACTIVE can be unliked.. + if ($plan->get_status() == plan::STATUS_COMPLETE) { + throw new coding_exception('Only draft or active plan can be unliked from a template'); + } + // Early exit, it's already done... if (!$plan->is_based_on_template()) { return true; diff --git a/admin/tool/lp/classes/external.php b/admin/tool/lp/classes/external.php index 3091bdf4710..a74dbebd445 100644 --- a/admin/tool/lp/classes/external.php +++ b/admin/tool/lp/classes/external.php @@ -4422,4 +4422,49 @@ class external extends external_api { return evidence_exporter::get_read_structure(); } + /** + * Returns description of unlink_plan_from_template_() parameters. + * + * @return \external_function_parameters + */ + public static function unlink_plan_from_template_parameters() { + $planid = new external_value( + PARAM_INT, + 'Data base record id for the plan', + VALUE_REQUIRED + ); + + $params = array( + 'planid' => $planid, + ); + return new external_function_parameters($params); + } + + /** + * Unlink the plan from the template. + * + * @param int $planid The plan id + * @return bool + */ + public static function unlink_plan_from_template($planid) { + $params = self::validate_parameters(self::unlink_plan_from_template_parameters(), + array( + 'planid' => $planid, + )); + + $plan = new plan($params['planid']); + self::validate_context($plan->get_context()); + + return api::unlink_plan_from_template($plan); + } + + /** + * Returns description of unlink_plan_from_template_() result value. + * + * @return \external_value + */ + public static function unlink_plan_from_template_returns() { + return new external_value(PARAM_BOOL, 'True if the unlink was successful'); + } + } diff --git a/admin/tool/lp/classes/external/plan_exporter.php b/admin/tool/lp/classes/external/plan_exporter.php index 0074a75a1d7..523087a2d09 100644 --- a/admin/tool/lp/classes/external/plan_exporter.php +++ b/admin/tool/lp/classes/external/plan_exporter.php @@ -53,6 +53,9 @@ class plan_exporter extends persistent_exporter { $this->persistent->get_status() == $classname::STATUS_COMPLETE, 'usercancomplete' => $this->persistent->can_manage() && $this->persistent->get_status() == $classname::STATUS_ACTIVE, + 'usercanunlink' => $this->persistent->can_manage() && + $this->persistent->get_status() != $classname::STATUS_COMPLETE && + $this->persistent->is_based_on_template(), 'iscompleted' => $this->persistent->get_status() == $classname::STATUS_COMPLETE, 'duedateformatted' => userdate($this->persistent->get_duedate()) ); @@ -85,6 +88,9 @@ class plan_exporter extends persistent_exporter { 'usercancomplete' => array( 'type' => PARAM_BOOL, ), + 'usercanunlink' => array( + 'type' => PARAM_BOOL, + ), 'iscompleted' => array( 'type' => PARAM_BOOL ), diff --git a/admin/tool/lp/db/services.php b/admin/tool/lp/db/services.php index 43453268df7..8649e409b7d 100644 --- a/admin/tool/lp/db/services.php +++ b/admin/tool/lp/db/services.php @@ -662,7 +662,6 @@ $functions = array( 'capabilities' => 'tool/lp:userevidencemanageown', 'ajax' => true, ), - 'tool_lp_grade_competency_in_plan' => array( 'classname' => 'tool_lp\external', 'methodname' => 'grade_competency_in_plan', @@ -699,5 +698,14 @@ $functions = array( 'capabilities' => 'tool/lp:coursecompetencyread', 'ajax' => true, ), + 'tool_lp_unlink_plan_from_template' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'unlink_plan_from_template', + 'classpath' => '', + 'description' => 'Unlink a plan form it template.', + 'type' => 'write', + 'capabilities' => 'tool/lp:planmanage', + 'ajax' => true, + ), ); diff --git a/admin/tool/lp/lang/en/tool_lp.php b/admin/tool/lp/lang/en/tool_lp.php index a0df904242c..cb3caebbc03 100644 --- a/admin/tool/lp/lang/en/tool_lp.php +++ b/admin/tool/lp/lang/en/tool_lp.php @@ -279,6 +279,8 @@ $string['templatename'] = 'Name'; $string['templates'] = 'Learning plan templates'; $string['templateupdated'] = 'Learning plan template updated'; $string['totalrequiredtocomplete'] = 'Total required to complete'; +$string['unlinkplantemplate'] = 'Unlink from template'; +$string['unlinkplantemplateconfirm'] = 'Unlink the plan \'{$a}\' from its template? Any change made to the template will no longer be applied to the plan. This action can not be undone.'; $string['uponcoursecompletion'] = 'Upon course completion:'; $string['usercompetencystatus_idle'] = 'Idle'; $string['usercompetencystatus_inreview'] = 'In review'; diff --git a/admin/tool/lp/templates/plan_page.mustache b/admin/tool/lp/templates/plan_page.mustache index 934c8398f28..d59310cab44 100644 --- a/admin/tool/lp/templates/plan_page.mustache +++ b/admin/tool/lp/templates/plan_page.mustache @@ -44,10 +44,10 @@ {{#canread}} {{/canread}} - {{plan.template.shortname}} - {{#canread}} - - {{/canread}} + {{plan.template.shortname}}{{#canread}}{{/canread}} + {{#plan.usercanunlink}} + ({{#str}}unlinkplantemplate, tool_lp{{/str}}) + {{/plan.usercanunlink}} {{/plan.template}} {{#description}} diff --git a/admin/tool/lp/templates/plans_page.mustache b/admin/tool/lp/templates/plans_page.mustache index a2adeb4aa16..686b081247a 100644 --- a/admin/tool/lp/templates/plans_page.mustache +++ b/admin/tool/lp/templates/plans_page.mustache @@ -93,6 +93,13 @@ {{/usercancomplete}} + {{#usercanunlink}} +
  • + + {{#pix}}t/edit{{/pix}} {{#str}}unlinkplantemplate, tool_lp{{/str}} + +
  • + {{/usercanunlink}}
  • {{#pix}}t/delete{{/pix}} {{#str}}deletethisplan, tool_lp{{/str}} diff --git a/admin/tool/lp/tests/api_test.php b/admin/tool/lp/tests/api_test.php index 8eba5602a73..cae943e53c2 100644 --- a/admin/tool/lp/tests/api_test.php +++ b/admin/tool/lp/tests/api_test.php @@ -29,6 +29,7 @@ use tool_lp\api; use tool_lp\competency; use tool_lp\evidence; use tool_lp\user_competency; +use tool_lp\plan; /** * API tests. @@ -519,8 +520,9 @@ class tool_lp_api_testcase extends advanced_testcase { 'sortorder' => 8)); $tplc2a = $lpg->create_template_competency(array('templateid' => $tpl2->get_id(), 'competencyid' => $c2a->get_id())); - $plan1 = $lpg->create_plan(array('userid' => $u1->id, 'templateid' => $tpl1->get_id())); + $plan1 = $lpg->create_plan(array('userid' => $u1->id, 'templateid' => $tpl1->get_id(), 'status' => plan::STATUS_ACTIVE)); $plan2 = $lpg->create_plan(array('userid' => $u2->id, 'templateid' => $tpl2->get_id())); + $plan3 = $lpg->create_plan(array('userid' => $u1->id, 'templateid' => $tpl1->get_id(), 'status' => plan::STATUS_COMPLETE)); // Check that we have what we expect at this stage. $this->assertEquals(2, \tool_lp\template_competency::count_records(array('templateid' => $tpl1->get_id()))); @@ -549,6 +551,20 @@ class tool_lp_api_testcase extends advanced_testcase { $this->assertTrue($plan2->is_based_on_template()); $this->assertEquals(null, $plan2->get_origtemplateid()); + // Check we can unlink draft plan. + try { + api::unlink_plan_from_template($plan2); + } catch (coding_exception $e) { + $this->fail('Fail to unlink draft plan.'); + } + + // Check we can not unlink completed plan. + try { + api::unlink_plan_from_template($plan3); + $this->fail('We can not unlink completed plan.'); + } catch (coding_exception $e) { + } + // Even the order remains. $plan1comps = \tool_lp\plan_competency::list_competencies($plan1->get_id()); $before = reset($tpl1comps); diff --git a/admin/tool/lp/version.php b/admin/tool/lp/version.php index 0e351d4147b..28a69e93014 100644 --- a/admin/tool/lp/version.php +++ b/admin/tool/lp/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015111034; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2015111035; // 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).