diff --git a/admin/tool/lp/amd/build/frameworkactions.min.js b/admin/tool/lp/amd/build/frameworkactions.min.js new file mode 100644 index 00000000000..5e5ee423ba6 --- /dev/null +++ b/admin/tool/lp/amd/build/frameworkactions.min.js @@ -0,0 +1 @@ +define(["jquery","core/templates","core/ajax","core/notification","core/str"],function(a,b,c,d,e){var f=0,g=0,h=function(c,d){a('[data-region="managecompetencies"]').replaceWith(c),b.runTemplateJS(d)},i=function(a){b.render("tool_lp/manage_competency_frameworks_page",a).done(h).fail(d.exception)},j=function(b){b.preventDefault(),g=a(this).attr("data-frameworkid");var e=c.call([{methodname:"tool_lp_duplicate_competency_framework",args:{id:g}},{methodname:"tool_lp_data_for_competency_frameworks_manage_page",args:{pagecontext:{contextid:f}}}]);e[1].done(i).fail(d.exception)},k=function(){var a=c.call([{methodname:"tool_lp_delete_competency_framework",args:{id:g}},{methodname:"tool_lp_data_for_competency_frameworks_manage_page",args:{pagecontext:{contextid:f}}}]);a[1].done(i).fail(d.exception)},l=function(b){b.preventDefault();var f=a(this).attr("data-frameworkid");g=f;var h=c.call([{methodname:"tool_lp_read_competency_framework",args:{id:g}}]);h[0].done(function(a){e.get_strings([{key:"confirm",component:"moodle"},{key:"deletecompetencyframework",component:"tool_lp",param:a.shortname},{key:"delete",component:"moodle"},{key:"cancel",component:"moodle"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],k)}).fail(d.exception)}).fail(d.exception)};return{deleteHandler:l,duplicateHandler:j,init:function(a){f=a}}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/src/frameworkdelete.js b/admin/tool/lp/amd/src/frameworkactions.js similarity index 79% rename from admin/tool/lp/amd/src/frameworkdelete.js rename to admin/tool/lp/amd/src/frameworkactions.js index 42adff77689..9eb23138bdd 100644 --- a/admin/tool/lp/amd/src/frameworkdelete.js +++ b/admin/tool/lp/amd/src/frameworkactions.js @@ -14,9 +14,9 @@ // along with Moodle. If not, see . /** - * Delete competency frameworks via ajax. + * Competency frameworks actions via ajax. * - * @module tool_lp/frameworkdelete + * @module tool_lp/frameworkactions * @package tool_lp * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -52,6 +52,30 @@ define(['jquery', 'core/templates', 'core/ajax', 'core/notification', 'core/str' .fail(notification.exception); }; + /** + * Duplicate a framework and reload the page. + * @method doDuplicate + * @param {Event} e + */ + var doDuplicate = function(e) { + e.preventDefault(); + + frameworkid = $(this).attr('data-frameworkid'); + + // We are chaining ajax requests here. + var requests = ajax.call([{ + methodname: 'tool_lp_duplicate_competency_framework', + args: { id: frameworkid } + }, { + methodname: 'tool_lp_data_for_competency_frameworks_manage_page', + args: { + pagecontext: { + contextid: pagecontextid + } + } + }]); + requests[1].done(reloadList).fail(notification.exception); + }; /** * Delete a framework and reload the page. */ @@ -107,7 +131,7 @@ define(['jquery', 'core/templates', 'core/ajax', 'core/notification', 'core/str' }; - return /** @alias module:tool_lp/frameworkdelete */ { + return /** @alias module:tool_lp/frameworkactions */ { // Public variables and functions. /** @@ -117,6 +141,13 @@ define(['jquery', 'core/templates', 'core/ajax', 'core/notification', 'core/str' */ deleteHandler: confirmDelete, + /** + * Expose the event handler for duplicate. + * @method duplicateHandler + * @param {Event} e + */ + duplicateHandler: doDuplicate, + /** * Initialise the module. * @method init diff --git a/admin/tool/lp/classes/api.php b/admin/tool/lp/classes/api.php index 691af8a7722..a6d8fb803f3 100644 --- a/admin/tool/lp/classes/api.php +++ b/admin/tool/lp/classes/api.php @@ -353,6 +353,55 @@ class api { return $framework; } + /** + * Duplicate a competency framework by id. + * + * Requires tool/lp:competencymanage capability at the system context. + * + * @param int $id The record to duplicate. All competencies associated and related will be duplicated. + * @return competency_framework the framework duplicated + */ + public static function duplicate_framework($id) { + $framework = new competency_framework($id); + require_capability('tool/lp:competencymanage', $framework->get_context()); + + // Get a uniq idnumber based on the origin framework. + $idnumber = competency_framework::get_unused_idnumber($framework->get_idnumber()); + $framework->set_idnumber($idnumber); + // Adding the suffix copy to the shortname. + $framework->set_shortname(get_string('duplicateditemname', 'tool_lp', $framework->get_shortname())); + $framework->set_id(0); + $framework->create(); + + // Array that match the old competencies ids with the new one to use when copying related competencies. + $matchids = self::duplicate_competency_tree($framework->get_id(), competency::get_framework_tree($id), 0, 0); + + // Copy the related competencies. + $relcomps = related_competency::get_multiple_relations(array_keys($matchids)); + + foreach ($relcomps as $relcomp) { + $compid = $relcomp->get_competencyid(); + $relcompid = $relcomp->get_relatedcompetencyid(); + if (isset($matchids[$compid]) && isset($matchids[$relcompid])) { + $newcompid = $matchids[$compid]; + $newrelcompid = $matchids[$relcompid]; + if ($newcompid < $newrelcompid) { + $relcomp->set_competencyid($newcompid); + $relcomp->set_relatedcompetencyid($newrelcompid); + } else { + $relcomp->set_competencyid($newrelcompid); + $relcomp->set_relatedcompetencyid($newcompid); + } + $relcomp->set_id(0); + $relcomp->create(); + } else { + // Debugging message when there is no match found. + debugging('related competency id not found'); + } + } + return $framework; + } + /** * Delete a competency framework by id. * @@ -1361,4 +1410,42 @@ class api { return false; } + + /** + * Recursively duplicate competencies from a tree, we start duplicating from parents to children to have a correct path. + * This method does not copy the related competencies. + * + * @param int $frameworkid - framework id + * @param competency[] $tree - array of competencies object + * @param int $oldparent - old parent id + * @param int $newparent - new parent id + * @return array $matchids - List of old competencies ids matched with new ids. + */ + protected static function duplicate_competency_tree($frameworkid, $tree, $oldparent = 0, $newparent = 0) { + $matchids = array(); + foreach ($tree as $node) { + if ($node->competency->get_parentid() == $oldparent) { + $parentid = $node->competency->get_id(); + + // Create the competency. + $competency = $node->competency; + $competency->set_competencyframeworkid($frameworkid); + $competency->set_parentid($newparent); + $competency->set_path(''); + $competency->set_id(0); + $competency->create(); + + // Match the old id with the new one. + $matchids[$parentid] = $competency->get_id(); + + if (!empty($node->children)) { + // Duplicate children competency. + $childrenids = self::duplicate_competency_tree($frameworkid, $node->children, $parentid, $competency->get_id()); + // Array_merge does not keep keys when merging so we use the + operator. + $matchids = $matchids + $childrenids; + } + } + } + return $matchids; + } } diff --git a/admin/tool/lp/classes/competency.php b/admin/tool/lp/classes/competency.php index fff17f92ac4..89bc7d826ac 100644 --- a/admin/tool/lp/classes/competency.php +++ b/admin/tool/lp/classes/competency.php @@ -413,4 +413,39 @@ class competency extends persistent { return $DB->count_records_select(self::TABLE, "id $insql", $params, "COUNT(DISTINCT(competencyframeworkid))") == 1; } + /** + * Build a framework tree with competency nodes. + * + * @param int $frameworkid the framework id + * @return node[] tree of framework competency nodes + */ + public static function get_framework_tree($frameworkid) { + + $competencies = self::search('', $frameworkid); + + $parentnode = new stdClass(); + $parentnode->competency = new competency(); + + return self::build_tree($competencies, $parentnode); + } + + /** + * Recursively build up the tree of nodes. + * + * @param array $all - List of all competency classes. + * @param stdClass $parent - the exported parent node + */ + protected static function build_tree($all, $parent) { + $tree = array(); + foreach ($all as $one) { + if ($one->get_parentid() == $parent->competency->get_id()) { + $node = new stdClass(); + $node->competency = $one; + $node->children = self::build_tree($all, $node); + $tree[] = $node; + } + } + return $tree; + } + } diff --git a/admin/tool/lp/classes/competency_framework.php b/admin/tool/lp/classes/competency_framework.php index ae5c39a0205..9edb79846b2 100644 --- a/admin/tool/lp/classes/competency_framework.php +++ b/admin/tool/lp/classes/competency_framework.php @@ -343,4 +343,25 @@ class competency_framework extends persistent { return $list; } + /** + * Get a uniq idnumber. + * + * @param string $idnumber the framework idnumber + * @return string + */ + public static function get_unused_idnumber($idnumber) { + global $DB; + + $currentidnumber = $idnumber; + $counter = 0; + // Iteratere while the idnumber exists. + while ($DB->record_exists_select(static::TABLE, 'idnumber = ?', array($currentidnumber))) { + $suffixidnumber = '_' . ++$counter; + $currentidnumber = substr($idnumber, 0, 100 - strlen($suffixidnumber)).$suffixidnumber; + } + + // Return the uniq idnumber. + return $currentidnumber; + } + } diff --git a/admin/tool/lp/classes/external.php b/admin/tool/lp/classes/external.php index c16dab13f89..0c74171a709 100644 --- a/admin/tool/lp/classes/external.php +++ b/admin/tool/lp/classes/external.php @@ -387,6 +387,56 @@ class external extends external_api { return self::get_competency_framework_external_structure(); } + /** + * Returns description of duplicate_competency_framework() parameters. + * + * @return \external_function_parameters + */ + public static function duplicate_competency_framework_parameters() { + $id = new external_value( + PARAM_INT, + 'Data base record id for the framework', + VALUE_REQUIRED + ); + + $params = array( + 'id' => $id, + ); + return new external_function_parameters($params); + } + + /** + * Expose to AJAX + * @return boolean + */ + public static function duplicate_competency_framework_is_allowed_from_ajax() { + return true; + } + + /** + * Duplicate a competency framework + * + * @param int $id The competency framework id + * @return boolean + */ + public static function duplicate_competency_framework($id) { + $params = self::validate_parameters(self::duplicate_competency_framework_parameters(), + array( + 'id' => $id, + )); + + return api::duplicate_framework($params['id']); + } + + /** + * Returns description of duplicate_competency_framework() result value. + * + * @return \external_description + */ + public static function duplicate_competency_framework_returns() { + return self::get_competency_framework_external_structure(); + } + /** * Returns description of delete_competency_framework() parameters. * diff --git a/admin/tool/lp/classes/related_competency.php b/admin/tool/lp/classes/related_competency.php index ccb27a45511..df9c406886a 100644 --- a/admin/tool/lp/classes/related_competency.php +++ b/admin/tool/lp/classes/related_competency.php @@ -159,4 +159,28 @@ class related_competency extends persistent { return $competencies; } + /** + * Get the related competencies from competency ids. + * + * @param int[] $competencyids Array of competency ids. + * @return related_competency[] + */ + public static function get_multiple_relations($competencyids) { + global $DB; + + list($insql, $params) = $DB->get_in_or_equal($competencyids); + + $records = $DB->get_records_select(self::TABLE, + "competencyid $insql OR relatedcompetencyid $insql", + array_merge($params, $params) + ); + + $relatedcompetencies = array(); + foreach ($records as $record) { + unset($record->id); + $relatedcompetencies[] = new related_competency(null, $record); + } + return $relatedcompetencies; + } + } diff --git a/admin/tool/lp/db/services.php b/admin/tool/lp/db/services.php index 254ef53d3eb..194609ec9b8 100644 --- a/admin/tool/lp/db/services.php +++ b/admin/tool/lp/db/services.php @@ -44,6 +44,14 @@ $functions = array( 'type' => 'read', 'capabilities' => 'tool/lp:competencyview', ), + 'tool_lp_duplicate_competency_framework' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'duplicate_competency_framework', + 'classpath' => '', + 'description' => 'Duplicate a competency framework.', + 'type' => 'write', + 'capabilities' => 'tool/lp:competencymanage', + ), 'tool_lp_delete_competency_framework' => array( 'classname' => 'tool_lp\external', 'methodname' => 'delete_competency_framework', diff --git a/admin/tool/lp/templates/manage_competency_frameworks_page.mustache b/admin/tool/lp/templates/manage_competency_frameworks_page.mustache index 9241343f3fc..e46904930a8 100644 --- a/admin/tool/lp/templates/manage_competency_frameworks_page.mustache +++ b/admin/tool/lp/templates/manage_competency_frameworks_page.mustache @@ -61,6 +61,11 @@ {{#pix}}t/edit{{/pix}} {{#str}}edit{{/str}} +
  • + + {{#pix}}t/copy{{/pix}} {{#str}}duplicate{{/str}} + +
  • {{#pix}}t/delete{{/pix}} {{#str}}delete{{/str}} @@ -89,14 +94,15 @@ {{#js}} // Initialise the JS. -require(['tool_lp/frameworkdelete', +require(['tool_lp/frameworkactions', 'tool_lp/menubar'], - function(deleteMod, menubar) { + function(actionsMod, menubar) { - deleteMod.init({{pagecontextid}}); + actionsMod.init({{pagecontextid}}); menubar.enhance('.competencyframeworkactions', { - "[data-action='deletecompetencyframework']": deleteMod.deleteHandler + "[data-action='deletecompetencyframework']": actionsMod.deleteHandler, + "[data-action='duplicatecompetencyframework']": actionsMod.duplicateHandler, }); }); diff --git a/admin/tool/lp/tests/api_test.php b/admin/tool/lp/tests/api_test.php index 6c1614ce3dd..2d28d60a171 100644 --- a/admin/tool/lp/tests/api_test.php +++ b/admin/tool/lp/tests/api_test.php @@ -241,4 +241,67 @@ class tool_lp_api_testcase extends advanced_testcase { $this->assertEquals($framework3->get_id(), $r2->get_id()); } + /** + * Test duplicate a framework. + */ + public function test_duplicate_framework() { + $lpg = $this->getDataGenerator()->get_plugin_generator('tool_lp'); + $this->resetAfterTest(true); + $this->setAdminUser(); + + $syscontext = context_system::instance(); + $params = array( + 'shortname' => 'shortname_a', + 'idnumber' => 'idnumber_c', + 'description' => 'description', + 'descriptionformat' => FORMAT_HTML, + 'visible' => true, + 'contextid' => $syscontext->id + ); + $framework = $lpg->create_framework($params); + $competency1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $competency2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $competency3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $competency4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $competencyidnumbers = array($competency1->get_idnumber(), + $competency2->get_idnumber(), + $competency3->get_idnumber(), + $competency4->get_idnumber() + ); + + api::add_related_competency($competency1->get_id(), $competency2->get_id()); + api::add_related_competency($competency3->get_id(), $competency4->get_id()); + + $frameworkduplicated1 = api::duplicate_framework($framework->get_id()); + $frameworkduplicated2 = api::duplicate_framework($framework->get_id()); + + $this->assertEquals($framework->get_idnumber().'_1', $frameworkduplicated1->get_idnumber()); + $this->assertEquals($framework->get_idnumber().'_2', $frameworkduplicated2->get_idnumber()); + + $competenciesfr1 = api::list_competencies(array('competencyframeworkid' => $frameworkduplicated1->get_id())); + $competenciesfr2 = api::list_competencies(array('competencyframeworkid' => $frameworkduplicated2->get_id())); + + $competencyidsfr1 = array(); + $competencyidsfr2 = array(); + + foreach ($competenciesfr1 as $cmp) { + $competencyidsfr1[] = $cmp->get_idnumber(); + } + foreach ($competenciesfr2 as $cmp) { + $competencyidsfr2[] = $cmp->get_idnumber(); + } + + $this->assertEmpty(array_diff($competencyidsfr1, $competencyidnumbers)); + $this->assertEmpty(array_diff($competencyidsfr2, $competencyidnumbers)); + $this->assertCount(4, $competenciesfr1); + $this->assertCount(4, $competenciesfr2); + + // Test the related competencies. + reset($competenciesfr1); + $compduplicated1 = current($competenciesfr1); + $relatedcompetencies = $compduplicated1->get_related_competencies(); + $comprelated = current($relatedcompetencies); + $this->assertEquals($comprelated->get_idnumber(), $competency2->get_idnumber()); + } + } diff --git a/admin/tool/lp/version.php b/admin/tool/lp/version.php index c4ed10a33aa..96da15d8ee1 100644 --- a/admin/tool/lp/version.php +++ b/admin/tool/lp/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2015052414; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2015052415; // 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).