--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * File.
+ *
+ * @package core_competency
+ * @copyright 2016 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Save processing when the user will not be able to access anything.
+if (has_capability('moodle/site:config', $systemcontext)) {
+
+ $parentname = 'competencies';
+
+ // Settings page.
+ $settings = new admin_settingpage('competencysettings', new lang_string('competenciessettings', 'core_competency'),
+ 'moodle/site:config', false);
+ $ADMIN->add($parentname, $settings);
+
+ // Load the full tree of settings.
+ if ($ADMIN->fulltree) {
+ $setting = new admin_setting_configcheckbox('core_competency/enabled',
+ new lang_string('enablecompetencies', 'core_competency'),
+ new lang_string('enablecompetencies_desc', 'core_competency'), 1);
+ $settings->add($setting);
+
+ $setting = new admin_setting_configcheckbox('core_competency/pushcourseratingstouserplans',
+ new lang_string('pushcourseratingstouserplans', 'core_competency'),
+ new lang_string('pushcourseratingstouserplans_desc', 'core_competency'), 1);
+ $settings->add($setting);
+ }
+
+}
$temp->add(new admin_setting_configexecutable('aspellpath', new lang_string('aspellpath', 'admin'), new lang_string('edhelpaspellpath'), ''));
$temp->add(new admin_setting_configexecutable('pathtodot', new lang_string('pathtodot', 'admin'), new lang_string('pathtodot_help', 'admin'), ''));
$temp->add(new admin_setting_configexecutable('pathtogs', new lang_string('pathtogs', 'admin'), new lang_string('pathtogs_help', 'admin'), '/usr/bin/gs'));
+$temp->add(new admin_setting_configexecutable('pathtounoconv', new lang_string('pathtounoconv', 'admin'), new lang_string('pathtounoconv_help', 'admin'), '/usr/bin/unoconv'));
$ADMIN->add('server', $temp);
$ADMIN->add('root', new admin_category('users', new lang_string('users','admin')));
$ADMIN->add('root', new admin_category('courses', new lang_string('courses','admin')));
$ADMIN->add('root', new admin_category('grades', new lang_string('grades')));
+$ADMIN->add('root', new admin_category('competencies', new lang_string('competencies', 'core_competency')));
$ADMIN->add('root', new admin_category('badges', new lang_string('badges'), empty($CFG->enablebadges)));
$ADMIN->add('root', new admin_category('location', new lang_string('location','admin')));
$ADMIN->add('root', new admin_category('language', new lang_string('language')));
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class exposing the api for the cohortroles tool.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_cohortroles;
+
+use stdClass;
+use context_system;
+use core_competency\invalid_persistent_exception;
+
+/**
+ * Class for doing things with cohort roles.
+ *
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class api {
+
+ /**
+ * Create a cohort role assignment from a record containing all the data for the class.
+ *
+ * Requires moodle/role:manage capability at the system context.
+ *
+ * @param stdClass $record Record containing all the data for an instance of the class.
+ * @return competency
+ */
+ public static function create_cohort_role_assignment(stdClass $record) {
+ $cohortroleassignment = new cohort_role_assignment(0, $record);
+ $context = context_system::instance();
+
+ // First we do a permissions check.
+ require_capability('moodle/role:manage', $context);
+
+ // Validate before we check for existing records.
+ if (!$cohortroleassignment->is_valid()) {
+ throw new invalid_persistent_exception($cohortroleassignment->get_errors());
+ }
+
+ $existing = cohort_role_assignment::get_record((array) $record);
+ if (!empty($existing)) {
+ return false;
+ } else {
+ // OK - all set.
+ $cohortroleassignment->create();
+ }
+ return $cohortroleassignment;
+ }
+
+ /**
+ * Delete a cohort role assignment by id.
+ *
+ * Requires moodle/role:manage capability at the system context.
+ *
+ * @param int $id The record to delete. This will also remove this role from the user for all users in the system.
+ * @return boolean
+ */
+ public static function delete_cohort_role_assignment($id) {
+ $cohortroleassignment = new cohort_role_assignment($id);
+ $context = context_system::instance();
+
+ // First we do a permissions check.
+ require_capability('moodle/role:manage', $context);
+
+ // OK - all set.
+ return $cohortroleassignment->delete();
+ }
+
+ /**
+ * Perform a search based on the provided filters and return a paginated list of records.
+ *
+ * Requires moodle/role:manage capability at the system context.
+ *
+ * @param string $sort The column to sort on
+ * @param string $order ('ASC' or 'DESC')
+ * @param int $skip Number of records to skip (pagination)
+ * @param int $limit Max of records to return (pagination)
+ * @return array of cohort_role_assignment
+ */
+ public static function list_cohort_role_assignments($sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
+ $context = context_system::instance();
+
+ // First we do a permissions check.
+ require_capability('moodle/role:manage', $context);
+
+ // OK - all set.
+ return cohort_role_assignment::get_records(array(), $sort, $order, $skip, $limit);
+ }
+
+ /**
+ * Perform a search based on the provided filters and return a paginated list of records.
+ *
+ * Requires moodle/role:manage capability at system context.
+ *
+ * @return int
+ */
+ public static function count_cohort_role_assignments() {
+ $context = context_system::instance();
+
+ // First we do a permissions check.
+ require_capability('moodle/role:manage', $context);
+
+ // OK - all set.
+ return cohort_role_assignment::count_records();
+ }
+
+ /**
+ * Sync all roles - adding and deleting role assignments as required.
+ *
+ * Slow. Should only be called from a background task.
+ *
+ * Requires moodle/role:manage capability at the system context.
+ *
+ * @return array('rolesadded' => array of (useridassignedto, useridassignedover, roleid),
+ * 'rolesremoved' => array of (useridassignedto, useridassignedover, roleid))
+ */
+ public static function sync_all_cohort_roles() {
+ global $DB;
+
+ $context = context_system::instance();
+
+ // First we do a permissions check.
+ require_capability('moodle/role:manage', $context);
+
+ // Ok ready to go.
+ $rolesadded = array();
+ $rolesremoved = array();
+
+ // Get all cohort role assignments and group them by user and role.
+ $all = cohort_role_assignment::get_records(array(), 'userid, roleid');
+ // We build an better structure to loop on.
+ $info = array();
+ foreach ($all as $cra) {
+ if (!isset($info[$cra->get_userid()])) {
+ $info[$cra->get_userid()] = array();
+ }
+ if (!isset($info[$cra->get_userid()][$cra->get_roleid()])) {
+ $info[$cra->get_userid()][$cra->get_roleid()] = array();
+ }
+ array_push($info[$cra->get_userid()][$cra->get_roleid()], $cra->get_cohortid());
+ }
+ // Then for each user+role combo - find user context in the cohort without a role assigned.
+
+ foreach ($info as $userid => $roles) {
+ foreach ($roles as $roleid => $cohorts) {
+ list($cohortsql, $params) = $DB->get_in_or_equal($cohorts, SQL_PARAMS_NAMED);
+
+ $params['usercontext'] = CONTEXT_USER;
+ $params['roleid'] = $roleid;
+ $params['userid'] = $userid;
+
+ $sql = 'SELECT u.id AS userid, ra.id, ctx.id AS contextid
+ FROM {user} u
+ JOIN {cohort_members} cm ON u.id = cm.userid
+ JOIN {context} ctx ON u.id = ctx.instanceid AND ctx.contextlevel = :usercontext
+ LEFT JOIN {role_assignments} ra ON ra.contextid = ctx.id
+ AND ra.roleid = :roleid
+ AND ra.userid = :userid
+ WHERE cm.cohortid ' . $cohortsql . '
+ AND ra.id IS NULL';
+
+ $toadd = $DB->get_records_sql($sql, $params);
+
+ foreach ($toadd as $add) {
+ role_assign($roleid, $userid, $add->contextid, 'tool_cohortroles');
+ $rolesadded[] = array(
+ 'useridassignedto' => $userid,
+ 'useridassignedover' => $add->userid,
+ 'roleid' => $roleid
+ );
+ }
+ }
+ }
+
+ // And for each user+role combo - find user context not in the cohort with a role assigned.
+ // If the role was assigned by this component, unassign the role.
+ foreach ($info as $userid => $roles) {
+ foreach ($roles as $roleid => $cohorts) {
+ // Now we are looking for entries NOT in the cohort.
+ list($cohortsql, $params) = $DB->get_in_or_equal($cohorts, SQL_PARAMS_NAMED);
+
+ $params['usercontext'] = CONTEXT_USER;
+ $params['roleid'] = $roleid;
+ $params['userid'] = $userid;
+ $params['component'] = 'tool_cohortroles';
+
+ $sql = 'SELECT u.id as userid, ra.id, ctx.id AS contextid
+ FROM {user} u
+ JOIN {context} ctx ON u.id = ctx.instanceid AND ctx.contextlevel = :usercontext
+ JOIN {role_assignments} ra ON ra.contextid = ctx.id AND ra.roleid = :roleid AND ra.userid = :userid
+ LEFT JOIN {cohort_members} cm ON u.id = cm.userid
+ AND cm.cohortid ' . $cohortsql . '
+ WHERE ra.component = :component AND cm.cohortid IS NULL';
+
+ $toremove = $DB->get_records_sql($sql, $params);
+ foreach ($toremove as $remove) {
+ role_unassign($roleid, $userid, $remove->contextid, 'tool_cohortroles');
+ $rolesremoved[] = array(
+ 'useridassignedto' => $userid,
+ 'useridassignedover' => $remove->userid,
+ 'roleid' => $roleid
+ );
+ }
+ }
+ }
+
+ return array('rolesadded' => $rolesadded, 'rolesremoved' => $rolesremoved);
+ }
+
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for cohort_role_assignment persistence.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_cohortroles;
+
+use lang_string;
+use core_competency\persistent;
+
+/**
+ * Class for loading/storing cohort_role_assignments from the DB.
+ *
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cohort_role_assignment extends persistent {
+
+ /** Table name for user_competency persistency */
+ const TABLE = 'tool_cohortroles';
+
+ /**
+ * Return the definition of the properties of this model.
+ *
+ * @return array
+ */
+ protected static function define_properties() {
+ return array(
+ 'userid' => array(
+ 'type' => PARAM_INT,
+ ),
+ 'roleid' => array(
+ 'type' => PARAM_INT,
+ ),
+ 'cohortid' => array(
+ 'type' => PARAM_INT,
+ )
+ );
+ }
+
+ /**
+ * Validate the user ID.
+ *
+ * @param int $value The value.
+ * @return true|lang_string
+ */
+ protected function validate_userid($value) {
+ global $DB;
+
+ if (!$DB->record_exists('user', array('id' => $value))) {
+ return new lang_string('invaliduserid', 'error');
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate the role ID.
+ *
+ * @param int $value The value.
+ * @return true|lang_string
+ */
+ protected function validate_roleid($value) {
+ global $DB;
+
+ if (!$DB->record_exists('role', array('id' => $value))) {
+ return new lang_string('invalidroleid', 'error');
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate the cohort ID.
+ *
+ * @param int $value The value.
+ * @return true|lang_string
+ */
+ protected function validate_cohortid($value) {
+ global $DB;
+
+ if (!$DB->record_exists('cohort', array('id' => $value))) {
+ return new lang_string('invalidcohortid', 'error');
+ }
+
+ return true;
+ }
+
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Assign role to cohort form.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_cohortroles\form;
+defined('MOODLE_INTERNAL') || die();
+
+use moodleform;
+use context_system;
+
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Assign role to cohort form.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assign_role_cohort extends moodleform {
+
+ /**
+ * Form definition.
+ */
+ public function definition() {
+ global $PAGE;
+
+ $mform = $this->_form;
+ $roles = get_roles_for_contextlevels(CONTEXT_USER);
+
+ if (empty($roles)) {
+ $output = $PAGE->get_renderer('tool_cohortroles');
+ $warning = $output->notify_problem(get_string('noassignableroles', 'tool_cohortroles'));
+ $mform->addElement('html', $warning);
+ return;
+ }
+
+ $options = array(
+ 'ajax' => 'tool_lp/form-user-selector',
+ 'multiple' => true
+ );
+ $mform->addElement('autocomplete', 'userids', get_string('selectusers', 'tool_cohortroles'), array(), $options);
+ $mform->addRule('userids', null, 'required');
+
+ $names = role_get_names();
+ $options = array();
+ foreach ($roles as $idx => $roleid) {
+ $options[$roleid] = $names[$roleid]->localname;
+ }
+
+ $mform->addElement('select', 'roleid', get_string('selectrole', 'tool_cohortroles'), $options);
+ $mform->addRule('roleid', null, 'required');
+
+ $context = context_system::instance();
+ $options = array(
+ 'ajax' => 'tool_lp/form-cohort-selector',
+ 'multiple' => true,
+ 'data-contextid' => $context->id,
+ 'data-includes' => 'all'
+ );
+ $mform->addElement('autocomplete', 'cohortids', get_string('selectcohorts', 'tool_cohortroles'), array(), $options);
+ $mform->addRule('cohortids', null, 'required');
+ $mform->addElement('submit', 'submit', get_string('assign', 'tool_cohortroles'));
+ }
+
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cohort role assignments table
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_cohortroles\output;
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/tablelib.php');
+
+use context_helper;
+use context_system;
+use html_writer;
+use moodle_url;
+use table_sql;
+
+/**
+ * Cohort role assignments table.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cohort_role_assignments_table extends table_sql {
+
+ /**
+ * Sets up the table.
+ *
+ * @param string $uniqueid Unique id of table.
+ * @param moodle_url $url The base URL.
+ */
+ public function __construct($uniqueid, $url) {
+ global $CFG;
+ parent::__construct($uniqueid);
+ $context = context_system::instance();
+
+ $this->context = $context;
+
+ $this->rolenames = role_get_names();
+
+ // This object should not be used without the right permissions.
+ require_capability('moodle/role:manage', $context);
+
+ $this->useridfield = 'userid';
+
+ // Define columns in the table.
+ $this->define_table_columns();
+
+ $this->define_baseurl($url);
+ // Define configs.
+ $this->define_table_configs();
+ }
+
+ /**
+ * Role name column.
+ *
+ * @param array $data Row data.
+ * @return string
+ */
+ protected function col_rolename($data) {
+ return $this->rolenames[$data->roleid]->localname;
+ }
+
+ /**
+ * Cohort name column.
+ *
+ * @param array $data Row data.
+ * @return string
+ */
+ protected function col_cohortname($data) {
+ global $OUTPUT;
+
+ $record = (object) array(
+ 'id' => $data->cohortid,
+ 'idnumber' => $data->cohortidnumber,
+ 'description' => $data->cohortdescription,
+ 'visible' => $data->cohortvisible,
+ 'name' => $data->cohortname
+ );
+ $context = context_helper::instance_by_id($data->cohortcontextid);
+
+ $exporter = new \tool_lp\external\cohort_summary_exporter($record, array('context' => $context));
+ $cohort = $exporter->export($OUTPUT);
+
+ $html = $OUTPUT->render_from_template('tool_cohortroles/cohort-in-list', $cohort);
+ return $html;
+ }
+
+ /**
+ * Actions column.
+ *
+ * @param array $data Row data.
+ * @return string
+ */
+ protected function col_actions($data) {
+ global $OUTPUT;
+
+ $action = new \confirm_action(get_string('removecohortroleassignmentconfirm', 'tool_cohortroles'));
+ $url = new moodle_url($this->baseurl);
+ $url->params(array('removecohortroleassignment' => $data->id, 'sesskey' => sesskey()));
+ $pix = new \pix_icon('t/delete', get_string('removecohortroleassignment', 'tool_cohortroles'));
+ return $OUTPUT->action_link($url, '', $action, null, $pix);
+ }
+
+ /**
+ * Setup the headers for the table.
+ */
+ protected function define_table_columns() {
+ $extrafields = get_extra_user_fields($this->context);
+
+ // Define headers and columns.
+ $cols = array(
+ 'cohortname' => get_string('cohort', 'cohort'),
+ 'rolename' => get_string('role'),
+ 'fullname' => get_string('name'),
+ );
+
+ // Add headers for extra user fields.
+ foreach ($extrafields as $field) {
+ if (get_string_manager()->string_exists($field, 'moodle')) {
+ $cols[$field] = get_string($field);
+ } else {
+ $cols[$field] = $field;
+ }
+ }
+
+ // Add remaining headers.
+ $cols = array_merge($cols, array('actions' => get_string('actions')));
+
+ $this->define_columns(array_keys($cols));
+ $this->define_headers(array_values($cols));
+ }
+
+ /**
+ * Define table configs.
+ */
+ protected function define_table_configs() {
+ $this->collapsible(false);
+ $this->sortable(true, 'lastname', SORT_ASC);
+ $this->pageable(true);
+ $this->no_sorting('actions');
+ }
+
+ /**
+ * Builds the SQL query.
+ *
+ * @param bool $count When true, return the count SQL.
+ * @return array containing sql to use and an array of params.
+ */
+ protected function get_sql_and_params($count = false) {
+ $fields = 'uca.id, uca.cohortid, uca.userid, uca.roleid, ';
+ $fields .= 'c.name as cohortname, c.idnumber as cohortidnumber, c.contextid as cohortcontextid, ';
+ $fields .= 'c.visible as cohortvisible, c.description as cohortdescription, ';
+
+ // Add extra user fields that we need for the graded user.
+ $extrafields = get_extra_user_fields($this->context);
+ foreach ($extrafields as $field) {
+ $fields .= 'u.' . $field . ', ';
+ }
+ $fields .= get_all_user_name_fields(true, 'u');
+
+ if ($count) {
+ $select = "COUNT(1)";
+ } else {
+ $select = "$fields";
+ }
+
+ $sql = "SELECT $select
+ FROM {tool_cohortroles} uca
+ JOIN {user} u ON u.id = uca.userid
+ JOIN {cohort} c ON c.id = uca.cohortid";
+ $params = array();
+
+ // Add order by if needed.
+ if (!$count && $sqlsort = $this->get_sql_sort()) {
+ $sql .= " ORDER BY " . $sqlsort;
+ }
+
+ return array($sql, $params);
+ }
+
+ /**
+ * Override the default implementation to set a decent heading level.
+ */
+ public function print_nothing_to_display() {
+ global $OUTPUT;
+ echo $this->render_reset_button();
+ $this->print_initials_bar();
+ echo $OUTPUT->heading(get_string('nothingtodisplay'), 4);
+ }
+
+ /**
+ * Query the DB.
+ *
+ * @param int $pagesize size of page for paginated displayed table.
+ * @param bool $useinitialsbar do you want to use the initials bar.
+ */
+ public function query_db($pagesize, $useinitialsbar = true) {
+ global $DB;
+
+ list($countsql, $countparams) = $this->get_sql_and_params(true);
+ list($sql, $params) = $this->get_sql_and_params();
+ $total = $DB->count_records_sql($countsql, $countparams);
+ $this->pagesize($pagesize, $total);
+ $this->rawdata = $DB->get_records_sql($sql, $params, $this->get_page_start(), $this->get_page_size());
+
+ // Set initial bars.
+ if ($useinitialsbar) {
+ $this->initialbars($total > $pagesize);
+ }
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderer class for cohort roles
+ *
+ * @package tool_cohortroles
+ * @copyright 2016 Issam Taboubi <issam.taboubi@umontreal.ca>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_cohortroles\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use plugin_renderer_base;
+use renderable;
+
+/**
+ * Renderer class for cohort roles
+ *
+ * @package tool_cohortroles
+ * @copyright 2016 Issam Taboubi <issam.taboubi@umontreal.ca>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+
+ /**
+ * Output a nofication.
+ *
+ * @param string $message the message to print out
+ * @return string HTML fragment.
+ * @see \core\output\notification
+ */
+ public function notify_message($message) {
+ $n = new \core\output\notification($message, \core\output\notification::NOTIFY_INFO);
+ return $this->render($n);
+ }
+
+ /**
+ * Output an error notification.
+ *
+ * @param string $message the message to print out
+ * @return string HTML fragment.
+ * @see \core\output\notification
+ */
+ public function notify_problem($message) {
+ $n = new \core\output\notification($message, \core\output\notification::NOTIFY_ERROR);
+ return $this->render($n);
+ }
+
+ /**
+ * Output a success notification.
+ *
+ * @param string $message the message to print out
+ * @return string HTML fragment.
+ * @see \core\output\notification
+ */
+ public function notify_success($message) {
+ $n = new \core\output\notification($message, \core\output\notification::NOTIFY_SUCCESS);
+ return $this->render($n);
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Scheduled task for syncing cohort roles.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_cohortroles\task;
+
+use core\task\scheduled_task;
+use tool_cohortroles\api;
+
+/**
+ * Scheduled task for syncing cohort roles.
+ *
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cohort_role_sync extends scheduled_task {
+
+ /**
+ * Get name.
+ * @return string
+ */
+ public function get_name() {
+ // Shown in admin screens.
+ return get_string('taskname', 'tool_cohortroles');
+ }
+
+ /**
+ * Execute.
+ */
+ public function execute() {
+ mtrace('Sync cohort roles...');
+ $result = api::sync_all_cohort_roles();
+
+ mtrace('Added ' . count($result['rolesadded']));
+ mtrace('Removed ' . count($result['rolesremoved']));
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<XMLDB PATH="admin/tool/cohortroles/db" VERSION="20151201" COMMENT="XMLDB file for Moodle admin/tool/cohortroles"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
+>
+ <TABLES>
+ <TABLE NAME="tool_cohortroles" COMMENT="Mapping of users to cohort role assignments.">
+ <FIELDS>
+ <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+ <FIELD NAME="cohortid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The cohort to sync"/>
+ <FIELD NAME="roleid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The role to assign"/>
+ <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The user to sync"/>
+ <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The time this record was created"/>
+ <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The time this record was modified."/>
+ <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Who last modified this record?"/>
+ </FIELDS>
+ <KEYS>
+ <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+ </KEYS>
+ <INDEXES>
+ <INDEX NAME="cohortuserrole" UNIQUE="true" FIELDS="cohortid, roleid, userid"/>
+ </INDEXES>
+ </TABLE>
+ </TABLES>
+</XMLDB>
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tasks definitions.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = array(
+ array(
+ 'classname' => 'tool_cohortroles\task\cohort_role_sync',
+ 'blocking' => 0,
+ 'minute' => 'R',
+ 'hour' => '*',
+ 'day' => '*',
+ 'dayofweek' => '*',
+ 'month' => '*'
+ ),
+);
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Assign roles for a user.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/tablelib.php');
+
+$removeid = optional_param('removecohortroleassignment', 0, PARAM_INT);
+
+admin_externalpage_setup('toolcohortroles');
+$context = context_system::instance();
+
+$pageurl = new moodle_url('/admin/tool/cohortroles/index.php');
+
+$output = $PAGE->get_renderer('tool_cohortroles');
+
+echo $output->header();
+$title = get_string('assignroletocohort', 'tool_cohortroles');
+echo $output->heading($title);
+
+$form = new tool_cohortroles\form\assign_role_cohort();
+
+if ($removeid) {
+ require_sesskey();
+
+ $result = \tool_cohortroles\api::delete_cohort_role_assignment($removeid);
+ if ($result) {
+ $notification = get_string('cohortroleassignmentremoved', 'tool_cohortroles');
+ echo $output->notify_success($notification);
+ } else {
+ $notification = get_string('cohortroleassignmentnotremoved', 'tool_cohortroles');
+ echo $output->notify_problem($notification);
+ }
+ echo $output->continue_button(new moodle_url($pageurl));
+} else if ($data = $form->get_data()) {
+ require_sesskey();
+ // We must create them all or none.
+ $saved = 0;
+ foreach ($data->userids as $userid) {
+ if (empty($data->cohortids)) {
+ $data->cohortids = array();
+ }
+ foreach ($data->cohortids as $cohortid) {
+ $params = (object) array('userid' => $userid, 'cohortid' => $cohortid, 'roleid' => $data->roleid);
+ $result = \tool_cohortroles\api::create_cohort_role_assignment($params);
+ if ($result) {
+ $saved++;
+ }
+ }
+ }
+ if ($saved == 0) {
+ $notification = get_string('nocohortroleassignmentssaved', 'tool_cohortroles');
+ echo $output->notify_problem($notification);
+ } else if ($saved == 1) {
+ $notification = get_string('onecohortroleassignmentsaved', 'tool_cohortroles');
+ echo $output->notify_success($notification);
+ } else {
+ $notification = get_string('acohortroleassignmentssaved', 'tool_cohortroles', $saved);
+ echo $output->notify_success($notification);
+ }
+
+ echo $output->continue_button(new moodle_url($pageurl));
+} else {
+ $form->display();
+
+ $title = get_string('existingcohortroles', 'tool_cohortroles');
+ echo $output->heading($title);
+ $url = new moodle_url('/admin/tool/cohortroles/index.php');
+ $table = new tool_cohortroles\output\cohort_role_assignments_table(uniqid(), $url);
+ echo $table->out(50, true);
+
+ echo $output->spacer();
+ echo $output->notify_message(get_string('backgroundsync', 'tool_cohortroles'));
+}
+
+echo $output->footer();
+
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'tool_userroles', language 'en'
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['acohortroleassignmentssaved'] = '{$a} cohort role assignments were saved.';
+$string['assign'] = 'Assign';
+$string['assignroletocohort'] = 'Assign role to users in the context of all members of these cohorts';
+$string['backgroundsync'] = 'Note: Roles assigned on this page will not take affect immediately, the changes to role assignments will be made by a background task.';
+$string['cohortroleassignmentremoved'] = 'The cohort role assignment was removed.';
+$string['cohortroleassignmentnotremoved'] = 'The cohort role assignment was not removed.';
+$string['cohortroles'] = 'Cohort roles';
+$string['existingcohortroles'] = 'Existing cohort role assignments';
+$string['managecohortroles'] = 'Assign user roles for entire cohort';
+$string['noassignableroles'] = 'No roles can be assigned at a user context level. <a href="../../roles/manage.php">Manage roles</a>';
+$string['nocohortroleassignmentssaved'] = 'No cohort role assignments were saved.';
+$string['onecohortroleassignmentsaved'] = 'One cohort role assignment was saved.';
+$string['pluginname'] = 'Cohort roles management';
+$string['removecohortroleassignment'] = 'Remove cohort role assignment';
+$string['removecohortroleassignmentconfirm'] = 'Are you sure you want to remove this cohort role assignment? This role will be removed for this user in all other user contexts.';
+$string['selectcohorts'] = 'Select cohorts';
+$string['selectrole'] = 'Select role';
+$string['selectusers'] = 'Select users to assign role';
+$string['taskname'] = 'Sync cohort role assignments';
+$string['thisuserroles'] = 'Roles assigned relative to this user';
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Link to user roles management.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+$str = get_string('managecohortroles', 'tool_cohortroles');
+$ADMIN->add('roles', new admin_externalpage('toolcohortroles', $str, '/admin/tool/cohortroles/index.php', 'moodle/role:manage'));
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template tool_cohortroles/cohort-in-list
+
+ Moodle template for displaying a cohort in a list.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * id cohort id field
+ * name cohort name field
+ * idnumber cohort idnumber field
+ * description cohort description field
+ * visible cohort visible field
+
+ Example context (json):
+ { "id": "1",
+ "name": "Cohort 1",
+ "visible": true,
+ "idnumber": "014",
+ "description": "Some users"
+ }
+}}
+{{> tool_lp/form-cohort-selector-suggestion }}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * API tests.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use tool_cohortroles\api;
+
+/**
+ * API tests.
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_cohortroles_api_testcase extends advanced_testcase {
+ /** @var stdClass $cohort */
+ protected $cohort = null;
+
+ /** @var stdClass $userassignto */
+ protected $userassignto = null;
+
+ /** @var stdClass $userassignover */
+ protected $userassignover = null;
+
+ /** @var stdClass $role */
+ protected $role = null;
+
+ /**
+ * Setup function- we will create a course and add an assign instance to it.
+ */
+ protected function setUp() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+
+ // Create some users.
+ $this->cohort = $this->getDataGenerator()->create_cohort();
+ $this->userassignto = $this->getDataGenerator()->create_user();
+ $this->userassignover = $this->getDataGenerator()->create_user();
+ $this->roleid = create_role('Sausage Roll', 'sausageroll', 'mmmm');
+ cohort_add_member($this->cohort->id, $this->userassignover->id);
+ }
+
+
+ public function test_create_cohort_role_assignment_without_permission() {
+ $this->setExpectedException('required_capability_exception');
+ $this->setUser($this->userassignto);
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ api::create_cohort_role_assignment($params);
+ }
+
+ public function test_create_cohort_role_assignment_with_invalid_data() {
+ $this->setExpectedException('core_competency\invalid_persistent_exception');
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => -8,
+ 'cohortid' => $this->cohort->id
+ );
+ api::create_cohort_role_assignment($params);
+ }
+
+ public function test_create_cohort_role_assignment() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+ $this->assertNotEmpty($result->get_id());
+ $this->assertEquals($result->get_userid(), $this->userassignto->id);
+ $this->assertEquals($result->get_roleid(), $this->roleid);
+ $this->assertEquals($result->get_cohortid(), $this->cohort->id);
+ }
+
+ public function test_delete_cohort_role_assignment_without_permission() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+ $this->setExpectedException('required_capability_exception');
+ $this->setUser($this->userassignto);
+ api::delete_cohort_role_assignment($result->get_id());
+ }
+
+ public function test_delete_cohort_role_assignment_with_invalid_data() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+ $this->setExpectedException('dml_missing_record_exception');
+ api::delete_cohort_role_assignment($result->get_id() + 1);
+ }
+
+ public function test_delete_cohort_role_assignment() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+ $worked = api::delete_cohort_role_assignment($result->get_id());
+ $this->assertTrue($worked);
+ }
+
+ public function test_list_cohort_role_assignments() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+
+ $list = api::list_cohort_role_assignments();
+ $list[0]->is_valid();
+ $this->assertEquals($list[0], $result);
+ }
+
+ public function test_count_cohort_role_assignments() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+
+ $count = api::count_cohort_role_assignments();
+ $this->assertEquals($count, 1);
+ }
+
+ public function test_sync_all_cohort_roles() {
+ $this->setAdminUser();
+ $params = (object) array(
+ 'userid' => $this->userassignto->id,
+ 'roleid' => $this->roleid,
+ 'cohortid' => $this->cohort->id
+ );
+ $result = api::create_cohort_role_assignment($params);
+
+ // Verify roles are assigned when users enter the cohort.
+ $sync = api::sync_all_cohort_roles();
+
+ $rolesadded = array(array(
+ 'useridassignedto' => $this->userassignto->id,
+ 'useridassignedover' => $this->userassignover->id,
+ 'roleid' => $this->roleid
+ ));
+ $rolesremoved = array();
+ $expected = array('rolesadded' => $rolesadded,
+ 'rolesremoved' => $rolesremoved);
+ $this->assertEquals($sync, $expected);
+
+ // Verify roles are removed when users leave the cohort.
+ cohort_remove_member($this->cohort->id, $this->userassignover->id);
+ $sync = api::sync_all_cohort_roles();
+
+ $rolesadded = array();
+ $rolesremoved = array(array(
+ 'useridassignedto' => $this->userassignto->id,
+ 'useridassignedover' => $this->userassignover->id,
+ 'roleid' => $this->roleid
+ ));
+ $expected = array('rolesadded' => $rolesadded,
+ 'rolesremoved' => $rolesremoved);
+ $this->assertEquals($sync, $expected);
+
+ // Verify roles assigned by any other component are not removed.
+ $usercontext = context_user::instance($this->userassignover->id);
+ role_assign($this->roleid, $this->userassignto->id, $usercontext->id);
+ $sync = api::sync_all_cohort_roles();
+
+ $rolesadded = array();
+ $rolesremoved = array();
+ $expected = array('rolesadded' => $rolesadded,
+ 'rolesremoved' => $rolesremoved);
+ $this->assertEquals($sync, $expected);
+
+ // Remove manual role assignment.
+ role_unassign($this->roleid, $this->userassignto->id, $usercontext->id);
+ // Add someone to the cohort again...
+ cohort_add_member($this->cohort->id, $this->userassignover->id);
+ $sync = api::sync_all_cohort_roles();
+ $rolesadded = array(array(
+ 'useridassignedto' => $this->userassignto->id,
+ 'useridassignedover' => $this->userassignover->id,
+ 'roleid' => $this->roleid
+ ));
+ $rolesremoved = array();
+ $expected = array('rolesadded' => $rolesadded,
+ 'rolesremoved' => $rolesremoved);
+ $this->assertEquals($sync, $expected);
+
+ // Verify no fatal errors when a cohort is deleted.
+ cohort_delete_cohort($this->cohort);
+ $sync = api::sync_all_cohort_roles();
+
+ $rolesadded = array();
+ $rolesremoved = array(array(
+ 'useridassignedto' => $this->userassignto->id,
+ 'useridassignedover' => $this->userassignover->id,
+ 'roleid' => $this->roleid
+ ));
+ $expected = array('rolesadded' => $rolesadded,
+ 'rolesremoved' => $rolesremoved);
+ $this->assertEquals($sync, $expected);
+ }
+
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Plugin version info
+ *
+ * @package tool_cohortroles
+ * @copyright 2015 Damyon Wiese
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+$plugin->version = 2015111018; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires = 2014110400; // Requires this Moodle version.
+$plugin->component = 'tool_cohortroles'; // Full name of the plugin (used for diagnostics).
+
+$plugin->dependencies = array(
+ 'tool_lp' => ANY_VERSION
+);
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+ <KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="timecreated" UNIQUE="false" FIELDS="timecreated"/>
defined('MOODLE_INTERNAL') || die();
function xmldb_logstore_standard_upgrade($oldversion) {
- global $CFG;
+ global $CFG, $DB;
+
+ $dbman = $DB->get_manager();
// Moodle v2.8.0 release upgrade line.
// Put any upgrade step following this.
// Moodle v3.0.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2016041200) {
+ // This could take a long time. Unfortunately, no way to know how long, and no way to do progress, so setting for 1 hour.
+ upgrade_set_timeout(3600);
+
+ // Define key contextid (foreign) to be added to logstore_standard_log.
+ $table = new xmldb_table('logstore_standard_log');
+ $key = new xmldb_key('contextid', XMLDB_KEY_FOREIGN, array('contextid'), 'context', array('id'));
+
+ // Launch add key contextid.
+ $dbman->add_key($table, $key);
+
+ // Standard savepoint reached.
+ upgrade_plugin_savepoint(true, 2016041200, 'logstore', 'standard');
+ }
+
return true;
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2015111600; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2016041200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2015111000; // Requires this Moodle version.
$plugin->component = 'logstore_standard'; // Full name of the plugin (used for diagnostics).
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Action selector.
+ *
+ * To handle 'save' events use: actionselector.on('save')
+ * This will receive the information to display in popup.
+ * The actions have the format [{'text': sometext, 'value' : somevalue}].
+ *
+ * @package tool_lp
+ * @copyright 2016 Serge Gauthier - <serge.gauthier.2@umontreal.ca>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery',
+ 'core/notification',
+ 'core/ajax',
+ 'core/templates',
+ 'tool_lp/dialogue',
+ 'tool_lp/event_base'],
+ function($, Notification, Ajax, Templates, Dialogue, EventBase) {
+
+ /**
+ * Action selector class.
+ * @param {String} title The title of popup.
+ * @param {String} message The message to display.
+ * @param {object} actions The actions that can be selected.
+ * @param {String} confirm Text for confirm button.
+ * @param {String} cancel Text for cancel button.
+ */
+ var ActionSelector = function(title, message, actions, confirm, cancel) {
+ var self = this;
+
+ EventBase.prototype.constructor.apply(this, []);
+ self._title = title;
+ self._message = message;
+ self._actions = actions;
+ self._confirm = confirm;
+ self._cancel = cancel;
+ self._selectedValue = null;
+ self._reset();
+ };
+
+ ActionSelector.prototype = Object.create(EventBase.prototype);
+
+ /** @type {String} The value that was selected. */
+ ActionSelector.prototype._selectedValue = null;
+ /** @type {Dialogue} The reference to the dialogue. */
+ ActionSelector.prototype._popup = null;
+ /** @type {String} The title of popup. */
+ ActionSelector.prototype._title = null;
+ /** @type {String} The message in popup. */
+ ActionSelector.prototype._message = null;
+ /** @type {object} The information for radion buttons. */
+ ActionSelector.prototype._actions = null;
+ /** @type {String} The text for confirm button. */
+ ActionSelector.prototype._confirm = null;
+ /** @type {String} The text for cancel button. */
+ ActionSelector.prototype._cancel = null;
+
+ /**
+ * Hook to executed after the view is rendered.
+ *
+ * @method _afterRender
+ */
+ ActionSelector.prototype._afterRender = function() {
+ var self = this;
+
+ // Confirm button is disabled until a choice is done.
+ self._find('[data-action="action-selector-confirm"]').attr('disabled', 'disabled');
+
+ // Add listener for radio buttons change.
+ self._find('[data-region="action-selector-radio-buttons"]').change(function() {
+ self._selectedValue = $("input[type='radio']:checked").val();
+ self._find('[data-action="action-selector-confirm"]').removeAttr('disabled');
+ self._refresh.bind(self);
+ }.bind(self));
+
+ // Add listener for cancel.
+ self._find('[data-action="action-selector-cancel"]').click(function(e) {
+ e.preventDefault();
+ self.close();
+ }.bind(self));
+
+ // Add listener for confirm.
+ self._find('[data-action="action-selector-confirm"]').click(function(e) {
+ e.preventDefault();
+ if (!self._selectedValue.length) {
+ return;
+ }
+ self._trigger('save', { action: self._selectedValue });
+ self.close();
+ }.bind(self));
+ };
+
+ /**
+ * Close the dialogue.
+ *
+ * @method close
+ */
+ ActionSelector.prototype.close = function() {
+ var self = this;
+ self._popup.close();
+ self._reset();
+ };
+
+ /**
+ * Opens the action selector.
+ *
+ * @method display
+ * @return {Promise}
+ */
+ ActionSelector.prototype.display = function() {
+ var self = this;
+ return self._render().then(function(html) {
+ self._popup = new Dialogue(
+ self._title,
+ html,
+ self._afterRender.bind(self)
+ );
+ }.bind(self)).fail(Notification.exception);
+ };
+
+ /**
+ * Find a node in the dialogue.
+ *
+ * @param {String} selector
+ * @method _find
+ */
+ ActionSelector.prototype._find = function(selector) {
+ return $(this._popup.getContent()).find(selector);
+ };
+
+ /**
+ * Refresh the view.
+ *
+ * @method _refresh
+ * @return {Promise}
+ */
+ ActionSelector.prototype._refresh = function() {
+ var self = this;
+ return self._render().then(function(html) {
+ self._find('[data-region="action-selector"]').replaceWith(html);
+ self._afterRender();
+ }.bind(self));
+ };
+
+ /**
+ * Render the dialogue.
+ *
+ * @method _render
+ * @return {Promise}
+ */
+ ActionSelector.prototype._render = function() {
+ var self = this;
+ var choices = [];
+ for (var i in self._actions) {
+ choices.push(self._actions[i]);
+ }
+ var content = {'message': self._message, 'choices' : choices,
+ 'confirm' : self._confirm, 'cancel' : self._cancel};
+
+ return Templates.render('tool_lp/action_selector', content);
+ };
+
+ /**
+ * Reset the dialogue properties.
+ *
+ * This does not reset everything, just enough to reset the UI.
+ *
+ * @method _reset
+ */
+ ActionSelector.prototype._reset = function() {
+ this._popup = null;
+ this._selectedValue = '';
+ };
+
+ return /** @alias module:tool_lp/actionselector */ ActionSelector;
+
+});
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Handle add/remove competency links.
+ *
+ * @module tool_lp/competencies
+ * @package tool_lp
+ * @copyright 2015 Damyon Wiese <damyon@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery',
+ 'core/notification',
+ 'core/ajax',
+ 'core/templates',
+ 'core/str',
+ 'tool_lp/competencypicker',
+ 'tool_lp/dragdrop-reorder'],
+ function($, notification, ajax, templates, str, Picker, dragdrop) {
+
+ /**
+ * Constructor
+ *
+ * @param {Number} itemid
+ * @param {String} itemtype
+ */
+ var competencies = function(itemid, itemtype, pagectxid) {
+ this.itemid = itemid;
+ this.itemtype = itemtype;
+ this.pageContextId = pagectxid;
+ this.pickerInstance = null;
+
+ $('[data-region="actions"] button').prop('disabled', false);
+ this.registerEvents();
+ this.registerDragDrop();
+ };
+
+ /**
+ * Initialise the drag/drop code.
+ * @method registerDragDrop
+ */
+ competencies.prototype.registerDragDrop = function() {
+ var localthis = this;
+ // Init this module.
+ str.get_string('movecompetency', 'tool_lp').done(
+ function(movestring) {
+ dragdrop.dragdrop('movecompetency',
+ movestring,
+ { identifier: 'movecompetency', component: 'tool_lp'},
+ { identifier: 'movecompetencyafter', component: 'tool_lp'},
+ 'drag-samenode',
+ 'drag-parentnode',
+ 'drag-handlecontainer',
+ function(drag, drop) {
+ localthis.handleDrop.call(localthis, drag, drop);
+ });
+ }
+ ).fail(notification.exception);
+
+ };
+
+ /**
+ * Handle a drop from a drag/drop operation.
+ *
+ * @method handleDrop
+ * @param {DOMNode} drag The dragged node.
+ * @param {DOMNode} drop The dropped on node.
+ */
+ competencies.prototype.handleDrop = function(drag, drop) {
+ var fromid = $(drag).data('id');
+ var toid = $(drop).data('id');
+ var localthis = this;
+ var requests = [];
+
+ if (localthis.itemtype == 'course') {
+ requests = ajax.call([
+ {
+ methodname: 'core_competency_reorder_course_competency',
+ args: { courseid: localthis.itemid, competencyidfrom: fromid, competencyidto: toid }
+ }
+ ]);
+ } else if (localthis.itemtype == 'template') {
+ requests = ajax.call([
+ {
+ methodname: 'core_competency_reorder_template_competency',
+ args: { templateid: localthis.itemid, competencyidfrom: fromid, competencyidto: toid }
+ }
+ ]);
+ } else if (localthis.itemtype == 'plan') {
+ requests = ajax.call([
+ {
+ methodname: 'core_competency_reorder_plan_competency',
+ args: { planid: localthis.itemid, competencyidfrom: fromid, competencyidto: toid }
+ }
+ ]);
+ } else {
+ return null;
+ }
+
+ requests[0].fail(notification.exception);
+ };
+
+ /**
+ * Pick a competency
+ *
+ * @method pickCompetency
+ */
+ competencies.prototype.pickCompetency = function() {
+ var self = this;
+ var requests;
+ var pagerender;
+ var pageregion;
+ var pageContextIncludes;
+
+ if (!self.pickerInstance) {
+ if (self.itemtype === 'template' || self.itemtype === 'course') {
+ pageContextIncludes = 'parents';
+ }
+ self.pickerInstance = new Picker(self.pageContextId, false, pageContextIncludes);
+ self.pickerInstance.on('save', function(e, data) {
+ var compIds = data.competencyIds;
+
+ if (self.itemtype === "course") {
+ requests = [];
+
+ $.each(compIds, function(index, compId) {
+ requests.push({
+ methodname: 'core_competency_add_competency_to_course',
+ args: { courseid: self.itemid, competencyid: compId }
+ });
+ });
+ requests.push({
+ methodname: 'tool_lp_data_for_course_competencies_page',
+ args: { courseid: self.itemid }
+ });
+
+ pagerender = 'tool_lp/course_competencies_page';
+ pageregion = 'coursecompetenciespage';
+
+ } else if (self.itemtype === "template") {
+ requests = [];
+
+ $.each(compIds, function(index, compId) {
+ requests.push({
+ methodname: 'core_competency_add_competency_to_template',
+ args: { templateid: self.itemid, competencyid: compId }
+ });
+ });
+ requests.push({
+ methodname: 'tool_lp_data_for_template_competencies_page',
+ args: { templateid: self.itemid, pagecontext: { contextid: self.pageContextId }}
+ });
+ pagerender = 'tool_lp/template_competencies_page';
+ pageregion = 'templatecompetenciespage';
+ } else if (self.itemtype === "plan") {
+ requests = [];
+
+ $.each(compIds, function(index, compId) {
+ requests.push({
+ methodname: 'core_competency_add_competency_to_plan',
+ args: { planid: self.itemid, competencyid: compId }
+ });
+ });
+ requests.push({
+ methodname: 'tool_lp_data_for_plan_page',
+ args: { planid: self.itemid}
+ });
+ pagerender = 'tool_lp/plan_page';
+ pageregion = 'plan-page';
+ }
+
+ ajax.call(requests)[requests.length - 1].then(function(context) {
+ return templates.render(pagerender, context).done(function(html, js) {
+ $('[data-region="' + pageregion + '"]').replaceWith(html);
+ templates.runTemplateJS(js);
+ });
+ }, notification.exception);
+ });
+ }
+
+ self.pickerInstance.display();
+ };
+
+ /**
+ * Delete the link between competency and course, template or plan. Reload the page.
+ *
+ * @method doDelete
+ * @param {int} deleteid The id of record to delete.
+ */
+ competencies.prototype.doDelete = function(deleteid) {
+ var localthis = this;
+ var requests = [],
+ pagerender = '',
+ pageregion = '';
+
+ // Delete the link and reload the page template.
+ if (localthis.itemtype == 'course') {
+ requests = ajax.call([
+ { methodname: 'core_competency_remove_competency_from_course',
+ args: { courseid: localthis.itemid, competencyid: deleteid } },
+ { methodname: 'tool_lp_data_for_course_competencies_page',
+ args: { courseid: localthis.itemid } }
+ ]);
+ pagerender = 'tool_lp/course_competencies_page';
+ pageregion = 'coursecompetenciespage';
+ } else if (localthis.itemtype == 'template') {
+ requests = ajax.call([
+ { methodname: 'core_competency_remove_competency_from_template',
+ args: { templateid: localthis.itemid, competencyid: deleteid } },
+ { methodname: 'tool_lp_data_for_template_competencies_page',
+ args: { templateid: localthis.itemid, pagecontext: { contextid: localthis.pageContextId } } }
+ ]);
+ pagerender = 'tool_lp/template_competencies_page';
+ pageregion = 'templatecompetenciespage';
+ } else if (localthis.itemtype == 'plan') {
+ requests = ajax.call([
+ { methodname: 'core_competency_remove_competency_from_plan',
+ args: { planid: localthis.itemid, competencyid: deleteid } },
+ { methodname: 'tool_lp_data_for_plan_page',
+ args: { planid: localthis.itemid } }
+ ]);
+ pagerender = 'tool_lp/plan_page';
+ pageregion = 'plan-page';
+ }
+
+ requests[1].done(function(context) {
+ templates.render(pagerender, context).done(function(html, js) {
+ $('[data-region="' + pageregion + '"]').replaceWith(html);
+ templates.runTemplateJS(js);
+ }).fail(notification.exception);
+ }).fail(notification.exception);
+
+ };
+
+ /**
+ * Show a confirm dialogue before deleting a competency.
+ *
+ * @method deleteHandler
+ * @param {int} deleteid The id of record to delete.
+ */
+ competencies.prototype.deleteHandler = function(deleteid) {
+ var localthis = this;
+ var requests = [];
+ var message;
+
+ if (localthis.itemtype == 'course') {
+ message = 'unlinkcompetencycourse';
+ } else if (localthis.itemtype == 'template') {
+ message = 'unlinkcompetencytemplate';
+ } else if (localthis.itemtype == 'plan') {
+ message = 'unlinkcompetencyplan';
+ } else {
+ return;
+ }
+
+ requests = ajax.call([{
+ methodname: 'core_competency_read_competency',
+ args: { id: deleteid }
+ }]);
+
+ requests[0].done(function(competency) {
+ str.get_strings([
+ { key: 'confirm', component: 'moodle' },
+ { key: message, component: 'tool_lp', param: competency.shortname },
+ { key: 'confirm', component: 'moodle' },
+ { key: 'cancel', component: 'moodle' }
+ ]).done(function (strings) {
+ notification.confirm(
+ strings[0], // Confirm.
+ strings[1], // Unlink the competency X from the course?
+ strings[2], // Confirm.
+ strings[3], // Cancel.
+ function() {
+ localthis.doDelete(deleteid);
+ }
+ );
+ }).fail(notification.exception);
+ }).fail(notification.exception);
+ };
+
+ /**
+ * Register the javascript event handlers for this page.
+ *
+ * @method registerEvents
+ */
+ competencies.prototype.registerEvents = function() {
+ var localthis = this;
+
+ if (localthis.itemtype == 'course') {
+ // Course completion rule handling.
+ $('[data-region="coursecompetenciespage"]').on('change', 'select[data-field="ruleoutcome"]', function(e){
+ var requests = [];
+ var pagerender = 'tool_lp/course_competencies_page';
+ var pageregion = 'coursecompetenciespage';
+ var coursecompetencyid = $(e.target).data('id');
+ var ruleoutcome = $(e.target).val();
+ requests = ajax.call([
+ { methodname: 'core_competency_set_course_competency_ruleoutcome',
+ args: { coursecompetencyid: coursecompetencyid, ruleoutcome: ruleoutcome } },
+ { methodname: 'tool_lp_data_for_course_competencies_page',
+ args: { courseid: localthis.itemid } }
+ ]);
+
+ requests[1].done(function(context) {
+ templates.render(pagerender, context).done(function(html, js) {
+ $('[data-region="' + pageregion + '"]').replaceWith(html);
+ templates.runTemplateJS(js);
+ }).fail(notification.exception);
+ }).fail(notification.exception);
+ });
+ }
+
+ $('[data-region="actions"] button').click(function(e) {
+ e.preventDefault();
+ localthis.pickCompetency();
+ });
+ $('[data-action="delete-competency-link"]').click(function(e) {
+ e.preventDefault();
+
+ var deleteid = $(e.target).closest('[data-id]').data('id');
+ localthis.deleteHandler(deleteid);
+ });
+ };
+
+ return /** @alias module:tool_lp/competencies */ competencies;
+});
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Competency rule config.
+ *
+ * @package tool_lp
+ * @copyright 2015 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery',
+ 'core/str'],
+ function($, Str) {
+
+ var OUTCOME_NONE = 0,
+ OUTCOME_EVIDENCE = 1,
+ OUTCOME_COMPLETE = 2,
+ OUTCOME_RECOMMEND = 3;
+
+ return /** @alias module:tool_lp/competency_outcomes */ {
+
+ NONE: OUTCOME_NONE,
+ EVIDENCE: OUTCOME_EVIDENCE,
+ COMPLETE: OUTCOME_COMPLETE,
+ RECOMMEND: OUTCOME_RECOMMEND,
+
+ /**
+ * Get all the outcomes.
+ *
+ * @return {Object} Indexed by outcome code, contains code and name.
+ * @method getAll
+ */
+ getAll: function() {
+ var self = this;
+ return Str.get_strings([
+ { key: 'competencyoutcome_none', component: 'tool_lp' },
+ { key: 'competencyoutcome_evidence', component: 'tool_lp' },
+ { key: 'competencyoutcome_recommend', component: 'tool_lp' },
+ { key: 'competencyoutcome_complete', component: 'tool_lp' },
+ ]).then(function(strings) {
+ var outcomes = {};
+ outcomes[self.NONE] = { code: self.NONE, name: strings[0] };
+ outcomes[self.EVIDENCE] = { code: self.EVIDENCE, name: strings[1] };
+ outcomes[self.RECOMMEND] = { code: self.RECOMMEND, name: strings[2] };
+ outcomes[self.COMPLETE] = { code: self.COMPLETE, name: strings[3] };
+ return outcomes;
+ });
+ },
+
+ /**
+ * Get the string for an outcome.
+ *
+ * @param {Number} id The outcome code.
+ * @return {Promise Resolved with the string.
+ * @method getString
+ */
+ getString: function(id) {
+ var self = this,
+ all = self.getAll();
+
+ return all.then(function(outcomes) {
+ if (typeof outcomes[id] === 'undefined') {
+ return $.Deferred().reject().promise();
+ }
+ return outcomes[id].name;
+ });
+ }
+ };
+
+});
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Competency rule base module.
+ *
+ * @package tool_lp
+ * @copyright 2015 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery'], function($) {
+
+ /**
+ * Competency rule abstract class.
+ *
+ * Any competency rule should extend this object. The event 'change' should be
+ * triggered on the instance when the configuration has changed. This will allow
+ * the components using the rule to gather the config, or check its validity.
+ *
+ * this._triggerChange();
+ *
+ * @param {Tree} tree The competency tree.
+ */
+ var Rule = function(tree) {
+ this._eventNode = $('<div>');
+ this._ready = $.Deferred();
+ this._tree = tree;
+ };
+
+ /** @type {Object} The current competency. */
+ Rule.prototype._competency = null;
+ /** @type {Node} The node we attach the events to. */
+ Rule.prototype._eventNode = null;
+ /** @type {Promise} Resolved when the object is ready. */
+ Rule.prototype._ready = null;
+ /** @type {Tree} The competency tree. */
+ Rule.prototype._tree = null;
+
+ /**
+ * Whether or not the current competency can be configured using this rule.
+ *
+ * @return {Boolean}
+ * @method canConfig
+ */
+ Rule.prototype.canConfig = function() {
+ return this._tree.hasChildren(this._competency.id);
+ };
+
+ /**
+ * The config established by this rule.
+ *
+ * To override in subclasses when relevant.
+ *
+ * @return {String|null}
+ * @method getConfig
+ */
+ Rule.prototype.getConfig = function() {
+ return null;
+ };
+
+ /**
+ * Return the type of the module.
+ *
+ * @return {String}
+ * @method getType
+ */
+ Rule.prototype.getType = function() {
+ throw new Error('Not implemented');
+ };
+
+ /**
+ * The init process.
+ *
+ * Do not override this, instead override _load.
+ *
+ * @return {Promise} Revoled when the plugin is initialised.
+ * @method init
+ */
+ Rule.prototype.init = function() {
+ return this._load();
+ };
+
+ /**
+ * Callback to inject the template.
+ *
+ * @param {Node} container Node to inject in.
+ * @return {Promise} Resolved when done.
+ * @method injectTemplate
+ */
+ Rule.prototype.injectTemplate = function() {
+ return $.Deferred().reject().promise();
+ };
+
+ /**
+ * Whether or not the current config is valid.
+ *
+ * Plugins should override this.
+ *
+ * @return {Boolean}
+ * @method _isValid
+ */
+ Rule.prototype.isValid = function() {
+ return false;
+ };
+
+ /**
+ * Load the class.
+ *
+ * @return {Promise}
+ * @method _load
+ * @protected
+ */
+ Rule.prototype._load = function() {
+ return $.when();
+ };
+
+ /**
+ * Register an event listener.
+ *
+ * @param {String} type The event type.
+ * @param {Function} handler The event listener.
+ * @method on
+ */
+ Rule.prototype.on = function(type, handler) {
+ this._eventNode.on(type, handler);
+ };
+
+ /**
+ * Sets the current competency.
+ *
+ * @param {Competency} competency
+ * @method setTargetCompetency
+ */
+ Rule.prototype.setTargetCompetency = function(competency) {
+ this._competency = competency;
+ };
+
+ /**
+ * Trigger an event.
+ *
+ * @param {String} type The type of event.
+ * @param {Object} The data to pass to the listeners.
+ * @method _trigger
+ * @protected
+ */
+ Rule.prototype._trigger = function(type, data) {
+ this._eventNode.trigger(type, [data]);
+ };
+
+ /**
+ * Trigger the change event.
+ *
+ * @method _triggerChange
+ * @protected
+ */
+ Rule.prototype._triggerChange = function() {
+ this._trigger('change', this);
+ };
+
+ return /** @alias module:tool_lp/competency_rule */ Rule;
+
+});
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Competency rule all module.
+ *
+ * @package tool_lp
+ * @copyright 2015 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery',
+ 'core/str',
+ 'tool_lp/competency_rule',
+ ],
+ function($, Str, RuleBase) {
+
+ /**
+ * Competency rule all class.
+ */
+ var Rule = function() {
+ RuleBase.apply(this, arguments);
+ };
+ Rule.prototype = Object.create(RuleBase.prototype);
+
+ /**
+ * Return the type of the module.
+ *
+ * @return {String}
+ * @method getType
+ */
+ Rule.prototype.getType = function() {
+ return 'core_competency\\competency_rule_all';
+ };
+
+ /**
+ * Whether or not the current config is valid.
+ *
+ * @return {Boolean}
+ * @method isValid
+ */
+ Rule.prototype.isValid = function() {
+ return true;
+ };
+
+ return /** @alias module:tool_lp/competency_rule_all */ Rule;
+
+});
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Competency rule points module.
+ *
+ * @package tool_lp
+ * @copyright 2015 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery',
+ 'core/str',
+ 'core/templates',
+ 'tool_lp/competency_rule',
+ ],
+ function($, Str, Templates, RuleBase) {
+
+ /**
+ * Competency rule points class.
+ */
+ var Rule = function() {
+ RuleBase.apply(this, arguments);
+ };
+ Rule.prototype = Object.create(RuleBase.prototype);
+
+ /** @type {Node} Reference to the container in which the template was included. */
+ Rule.prototype._container = null;
+ /** @type {Boolean} Whether or not the template was included. */
+ Rule.prototype._templateLoaded = false;
+
+ /**
+ * The config established by this rule.
+ *
+ * @return {String}
+ * @method getConfig
+ */
+ Rule.prototype.getConfig = function() {
+ return JSON.stringify({
+ base: {
+ points: this._getRequiredPoints(),
+ },
+ competencies: this._getCompetenciesConfig()
+ });
+ };
+
+ /**
+ * Gathers the input provided by the user for competencies.
+ *
+ * @return {Array} Containing id, points and required.
+ * @method _getCompetenciesConfig
+ * @protected
+ */
+ Rule.prototype._getCompetenciesConfig = function() {
+ var competencies = [];
+
+ this._container.find('[data-competency]').each(function() {
+ var node = $(this),
+ id = node.data('competency'),
+ points = parseInt(node.find('[name="points"]').val(), 10),
+ required = node.find('[name="required"]').prop('checked');
+
+ competencies.push({
+ id: id,
+ points: points,
+ required: required ? 1 : 0
+ });
+ });
+
+ return competencies;
+ };
+
+ /**
+ * Fetches the required points set by the user.
+ *
+ * @return {Number}
+ * @method _getRequiredPoints
+ * @protected
+ */
+ Rule.prototype._getRequiredPoints = function() {
+ return parseInt(this._container.find('[name="requiredpoints"]').val() || 1, 10);
+ };
+
+ /**
+ * Return the type of the module.
+ *
+ * @return {String}
+ * @method getType
+ */
+ Rule.prototype.getType = function() {
+ return 'core_competency\\competency_rule_points';
+ };
+
+ /**
+ * Callback to inject the template.
+ *
+ * @param {Node} container Node to inject in.
+ * @return {Promise} Resolved when done.
+ * @method injectTemplate
+ */
+ Rule.prototype.injectTemplate = function(container) {
+ var self = this,
+ children = this._tree.getChildren(this._competency.id),
+ context,
+ config = {
+ base: { points: 2 },
+ competencies: []
+ };
+
+ this._templateLoaded = false;
+
+ // Only pre-load the configuration when the competency is using this rule.
+ if (self._competency.ruletype == self.getType()) {
+ try {
+ config = JSON.parse(self._competency.ruleconfig);
+ } catch (e) {
+ }
+ }
+
+ context = {
+ requiredpoints: (config && config.base) ? config.base.points : 2,
+ competency: self._competency,
+ children: []
+ };
+
+ $.each(children, function(index, child) {
+ var competency = {
+ id: child.id,
+ shortname: child.shortname,
+ required: false,
+ points: 0
+ };
+
+ if (config) {
+ $.each(config.competencies, function(index, comp) {
+ if (comp.id == competency.id) {
+ competency.required = comp.required ? true : false;
+ competency.points = comp.points;
+ }
+ });
+ }
+
+ context.children.push(competency);
+ });
+
+ return Templates.render('tool_lp/competency_rule_points', context).then(function(html) {
+ self._container = container;
+ container.html(html);
+ container.find('input').change(function() {
+ self._triggerChange();
+ });
+
+ // We're done, let's trigger a change.
+ self._templateLoaded = true;
+ self._triggerChange();
+ });
+ };
+
+ /**
+ * Whether or not the current config is valid.
+ *
+ * @return {Boolean}
+ * @method isValid
+ */
+ Rule.prototype.isValid = function() {
+ if (!this._templateLoaded) {
+ return false;
+ }
+
+ var required = this._getRequiredPoints(),
+ max = 0,
+ valid = true;
+
+ $.each(this._getCompetenciesConfig(), function(index, competency) {
+ if (competency.points < 0) {
+ valid = false;
+ }
+ max += competency.points;
+ });
+
+ valid = valid && max >= required;
+ return valid;
+ };
+
+ return /** @alias module:tool_lp/competency_rule_all */ Rule;
+
+});
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Handle selection changes and actions on the competency tree.
+ *
+ * @module tool_lp/competencyactions
+ * @package tool_lp
+ * @copyright 2015 Damyon Wiese <damyon@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery',
+ 'core/url',
+ 'core/templates',
+ 'core/notification',
+ 'core/str',
+ 'core/ajax',
+ 'tool_lp/dragdrop-reorder',
+ 'tool_lp/tree',
+ 'tool_lp/dialogue',
+ 'tool_lp/menubar',
+ 'tool_lp/competencypicker',
+ 'tool_lp/competency_outcomes',
+ 'tool_lp/competencyruleconfig'],
+ function($, url, templates, notification, str, ajax, dragdrop, Ariatree, Dialogue, menubar, Picker, Outcomes, RuleConfig) {
+
+ // Private variables and functions.
+ /** @var {Object} treeModel - This is an object representing the nodes in the tree. */
+ var treeModel = null;
+ /** @var {Node} moveSource - The start of a drag operation */
+ var moveSource = null;
+ /** @var {Node} moveTarget - The end of a drag operation */
+ var moveTarget = null;
+ /** @var {Number} pageContextId The page context ID. */
+ var pageContextId;
+ /** @type {Object} Picker instance. */
+ var pickerInstance;
+ /** @type {Object} Rule config instance. */
+ var ruleConfigInstance;
+ /** @type {Object} The competency we're picking a relation to. */
+ var relatedTarget;
+ /** @type {Object} Taxonomy constants indexed per level. */
+ var taxonomiesConstants;
+ /** @type {Array} The rules modules. Values are object containing type, namd and amd. */
+ var rulesModules;
+ /** @type {Number} the selected competency ID. */
+ var selectedCompetencyId = null;
+
+ /**
+ * Respond to choosing the "Add" menu item for the selected node in the tree.
+ * @method addHandler
+ */
+ var addHandler = function() {
+ var parent = $('[data-region="competencyactions"]').data('competency');
+
+ var params = {
+ competencyframeworkid : treeModel.getCompetencyFrameworkId(),
+ pagecontextid: pageContextId
+ };
+
+ if (parent !== null) {
+ // We are adding at a sub node.
+ params.parentid = parent.id;
+ }
+
+ var relocate = function() {
+ var queryparams = $.param(params);
+ window.location = url.relativeUrl('/admin/tool/lp/editcompetency.php?' + queryparams);
+ };
+
+ if (parent !== null && treeModel.hasRule(parent.id)) {
+ str.get_strings([
+ { key: 'confirm', component: 'moodle' },
+ { key: 'addingcompetencywillresetparentrule', component: 'tool_lp', param: parent.shortname },
+ { key: 'yes', component: 'core' },
+ { key: 'no', component: 'core' }
+ ]).done(function (strings) {
+ notification.confirm(
+ strings[0],
+ strings[1],
+ strings[2],
+ strings[3],
+ relocate
+ );
+ }).fail(notification.exception);
+ } else {
+ relocate();
+ }
+ };
+
+ /**
+ * A source and destination has been chosen - so time to complete a move.
+ * @method doMove
+ */
+ var doMove = function() {
+ var frameworkid = $('[data-region="filtercompetencies"]').data('frameworkid');
+ var requests = ajax.call([{
+ methodname: 'core_competency_set_parent_competency',
+ args: { competencyid: moveSource, parentid: moveTarget }
+ }, {
+ methodname: 'tool_lp_data_for_competencies_manage_page',
+ args: { competencyframeworkid: frameworkid,
+ search: $('[data-region="filtercompetencies"] input').val() }
+ }]);
+ requests[1].done(reloadPage).fail(notification.exception);
+ };
+
+ /**
+ * Confirms a competency move.
+ *
+ * @method confirmMove
+ */
+ var confirmMove = function() {
+ moveTarget = typeof moveTarget === "undefined" ? 0 : moveTarget;
+ if (moveTarget == moveSource) {
+ // No move to do.
+ return;
+ }
+
+ var targetComp = treeModel.getCompetency(moveTarget) || {},
+ sourceComp = treeModel.getCompetency(moveSource) || {},
+ confirmMessage = 'movecompetencywillresetrules',
+ showConfirm = false;
+
+ // We shouldn't be moving the competency to the same parent.
+ if (sourceComp.parentid == moveTarget) {
+ return;
+ }
+
+ // If we are moving to a child of self.
+ if (targetComp.path && targetComp.path.indexOf('/' + sourceComp.id + '/') >= 0) {
+ confirmMessage = 'movecompetencytochildofselfwillresetrules';
+
+ // Show a confirmation if self has rules, as they'll disappear.
+ showConfirm = showConfirm || treeModel.hasRule(sourceComp.id);
+ }
+
+ // Show a confirmation if the current parent, or the destination have rules.
+ showConfirm = showConfirm || (treeModel.hasRule(targetComp.id) || treeModel.hasRule(sourceComp.parentid));
+
+ // Show confirm, and/or do the things.
+ if (showConfirm) {
+ str.get_strings([
+ { key: 'confirm', component: 'moodle' },
+ { key: confirmMessage, component: 'tool_lp' },
+ { key: 'yes', component: 'moodle' },
+ { key: 'no', component: 'moodle' }
+ ]).done(function (strings) {
+ notification.confirm(
+ strings[0], // Confirm.
+ strings[1], // Delete competency X?
+ strings[2], // Delete.
+ strings[3], // Cancel.
+ doMove
+ );
+ }).fail(notification.exception);
+
+ } else {
+ doMove();
+ }
+ };
+
+ /**
+ * A move competency popup was opened - initialise the aria tree in it.
+ * @method initMovePopup
+ * @param {dialogue} popup The tool_lp/dialogue that was created.
+ */
+ var initMovePopup = function(popup) {
+ var body = $(popup.getContent());
+ var treeRoot = body.find('[data-enhance=movetree]');
+ var tree = new Ariatree(treeRoot, false);
+ tree.on('selectionchanged', function(evt, params) {
+ var target = params.selected;
+ moveTarget = $(target).data('id');
+ });
+ treeRoot.show();
+
+ body.on('click', '[data-action="move"]', function() { popup.close(); confirmMove(); });
+ body.on('click', '[data-action="cancel"]', function() { popup.close(); });
+ };
+
+ /**
+ * Turn a flat list of competencies into a tree structure (recursive).
+ * @method addCompetencyChildren
+ * @param {Object} parent The current parent node in the tree
+ * @param {Object[]} competencies The flat list of competencies
+ */
+ var addCompetencyChildren = function(parent, competencies) {
+ var i;
+
+ for (i = 0; i < competencies.length; i++) {
+ if (competencies[i].parentid == parent.id) {
+ parent.haschildren = true;
+ competencies[i].children = [];
+ competencies[i].haschildren = false;
+ parent.children[parent.children.length] = competencies[i];
+ addCompetencyChildren(competencies[i], competencies);
+ }
+ }
+ };
+
+ /**
+ * A node was chosen and "Move" was selected from the menu. Open a popup to select the target.
+ * @method moveHandler
+ */
+ var moveHandler = function(e) {
+ e.preventDefault();
+ var competency = $('[data-region="competencyactions"]').data('competency');
+
+ // Remember what we are moving.
+ moveSource = competency.id;
+
+ // Load data for the template.
+ var requests = ajax.call([
+ {
+ methodname: 'core_competency_search_competencies',
+ args: {
+ competencyframeworkid: competency.competencyframeworkid,
+ searchtext: ''
+ }
+ },{
+ methodname: 'core_competency_read_competency_framework',
+ args: {
+ id: competency.competencyframeworkid
+ }
+ }
+ ]);
+
+ // When all data has arrived, continue.
+ $.when.apply(null, requests).done(function(competencies, framework) {
+
+ // Expand the list of competencies into a tree.
+ var i, competenciestree = [];
+ for (i = 0; i < competencies.length; i++) {
+ var onecompetency = competencies[i];
+ if (onecompetency.parentid == "0") {
+ onecompetency.children = [];
+ onecompetency.haschildren = 0;
+ competenciestree[competenciestree.length] = onecompetency;
+ addCompetencyChildren(onecompetency, competencies);
+ }
+ }
+
+ str.get_strings([
+ { key: 'movecompetency', component: 'tool_lp', param: competency.shortname },
+ { key: 'move', component: 'tool_lp' },
+ { key: 'cancel', component: 'moodle' }
+ ]).done(function (strings) {
+
+ var context = {
+ framework: framework,
+ competencies: competenciestree
+ };
+
+ templates.render('tool_lp/competencies_move_tree', context)
+ .done(function(tree) {
+ new Dialogue(
+ strings[0], // Move competency x.
+ tree, // The move tree.
+ initMovePopup
+ );
+
+ }).fail(notification.exception);
+
+ }).fail(notification.exception);
+
+ }).fail(notification.exception);
+
+ };
+
+ /**
+ * Edit the selected competency.
+ * @method editHandler
+ */
+ var editHandler = function() {
+ var competency = $('[data-region="competencyactions"]').data('competency');
+
+ var params = {
+ competencyframeworkid : treeModel.getCompetencyFrameworkId(),
+ id : competency.id,
+ parentid: competency.parentid,
+ pagecontextid: pageContextId
+ };
+
+ var queryparams = $.param(params);
+ window.location = url.relativeUrl('/admin/tool/lp/editcompetency.php?' + queryparams);
+ };
+
+ /**
+ * Re-render the page with the latest data.
+ * @method reloadPage
+ */
+ var reloadPage = function(context) {
+ templates.render('tool_lp/manage_competencies_page', context)
+ .done(function(newhtml, newjs) {
+ $('[data-region="managecompetencies"]').replaceWith(newhtml);
+ templates.runTemplateJS(newjs);
+ })
+ .fail(notification.exception);
+ };
+
+ /**
+ * Perform a search and render the page with the new search results.
+ * @method updateSearchHandler
+ */
+ var updateSearchHandler = function(e) {
+ e.preventDefault();
+
+ var frameworkid = $('[data-region="filtercompetencies"]').data('frameworkid');
+
+ var requests = ajax.call([{
+ methodname: 'tool_lp_data_for_competencies_manage_page',
+ args: { competencyframeworkid: frameworkid,
+ search: $('[data-region="filtercompetencies"] input').val() }
+ }]);
+ requests[0].done(reloadPage).fail(notification.exception);
+ };
+
+ /**
+ * Move a competency "up". This only affects the sort order within the same branch of the tree.
+ * @method moveUpHandler
+ */
+ var moveUpHandler = function() {
+ // We are chaining ajax requests here.
+ var competency = $('[data-region="competencyactions"]').data('competency');
+ var requests = ajax.call([{
+ methodname: 'core_competency_move_up_competency',
+ args: { id: competency.id }
+ }, {
+ methodname: 'tool_lp_data_for_competencies_manage_page',
+ args: { competencyframeworkid: competency.competencyframeworkid,
+ search: $('[data-region="filtercompetencies"] input').val() }
+ }]);
+ requests[1].done(reloadPage).fail(notification.exception);
+ };
+
+ /**
+ * Move a competency "down". This only affects the sort order within the same branch of the tree.
+ * @method moveDownHandler
+ */
+ var moveDownHandler = function() {
+ // We are chaining ajax requests here.
+ var competency = $('[data-region="competencyactions"]').data('competency');
+ var requests = ajax.call([{
+ methodname: 'core_competency_move_down_competency',
+ args: { id: competency.id }
+ }, {
+ methodname: 'tool_lp_data_for_competencies_manage_page',
+ args: { competencyframeworkid: competency.competencyframeworkid,
+ search: $('[data-region="filtercompetencies"] input').val() }
+ }]);
+ requests[1].done(reloadPage).fail(notification.exception);
+ };
+
+ /**
+ * Open a dialogue to show all the courses using the selected competency.
+ * @method seeCoursesHandler
+ */
+ var seeCoursesHandler = function() {
+ var competency = $('[data-region="competencyactions"]').data('competency');
+
+ var requests = ajax.call([{
+ methodname: 'tool_lp_list_courses_using_competency',
+ args: { id: competency.id }
+ }]);
+
+ requests[0].done(function(courses) {
+ var context = {
+ courses: courses
+ };
+ templates.render('tool_lp/linked_courses_summary', context).done(function(html) {
+ str.get_string('linkedcourses', 'tool_lp').done(function (linkedcourses) {
+ new Dialogue(
+ linkedcourses, // Title.
+ html, // The linked courses.
+ initMovePopup
+ );
+ }).fail(notification.exception);
+ }).fail(notification.exception);
+ }).fail(notification.exception);
+ };
+
+ /**
+ * Open a competencies popup to relate competencies.
+ *
+ * @method relateCompetenciesHandler
+ */
+ var relateCompetenciesHandler = function() {
+ relatedTarget = $('[data-region="competencyactions"]').data('competency');
+
+ if (!pickerInstance) {
+ pickerInstance = new Picker(pageContextId, relatedTarget.competencyframeworkid);
+ pickerInstance.on('save', function(e, data) {
+ var compIds = data.competencyIds;
+
+ var calls = [];
+ $.each(compIds, function(index, value) {
+ calls.push({
+ methodname: 'core_competency_add_related_competency',
+ args: { competencyid: value, relatedcompetencyid: relatedTarget.id }
+ });
+ });
+
+ calls.push( {
+ methodname: 'tool_lp_data_for_related_competencies_section',
+ args: { competencyid: relatedTarget.id }
+ });
+
+ var promises = ajax.call(calls);
+
+ promises[calls.length - 1].then(function(context) {
+ return templates.render('tool_lp/related_competencies', context).done(function(html, js) {
+ $('[data-region="relatedcompetencies"]').replaceWith(html);
+ templates.runTemplateJS(js);
+ updatedRelatedCompetencies();
+ });
+ }, notification.exception);
+ });
+ }
+
+ pickerInstance.setDisallowedCompetencyIDs([relatedTarget.id]);
+ pickerInstance.display();
+ };
+
+ var ruleConfigHandler = function(e) {
+ e.preventDefault();
+ relatedTarget = $('[data-region="competencyactions"]').data('competency');
+ ruleConfigInstance.setTargetCompetencyId(relatedTarget.id);
+ ruleConfigInstance.display();
+ };
+
+ var ruleConfigSaveHandler = function(e, config) {
+ var update = {
+ id: relatedTarget.id,
+ shortname: relatedTarget.shortname,
+ idnumber: relatedTarget.idnumber,
+ description: relatedTarget.description,
+ descriptionformat: relatedTarget.descriptionformat,
+ ruletype: config.ruletype,
+ ruleoutcome: config.ruleoutcome,
+ ruleconfig: config.ruleconfig
+ };
+ var promise = ajax.call([{
+ methodname: 'core_competency_update_competency',
+ args: { competency: update }
+ }]);
+ promise[0].then(function(result) {
+ if (result) {
+ relatedTarget.ruletype = config.ruletype;
+ relatedTarget.ruleoutcome = config.ruleoutcome;
+ relatedTarget.ruleconfig = config.ruleconfig;
+ renderCompetencySummary(relatedTarget);
+ }
+ }, notification.exception);
+ };
+
+ /**
+ * Delete a competency.
+ * @method doDelete
+ */
+ var doDelete = function() {
+ // We are chaining ajax requests here.
+ var competency = $('[data-region="competencyactions"]').data('competency');
+ var requests = ajax.call([{
+ methodname: 'core_competency_delete_competency',
+ args: { id: competency.id }
+ }, {
+ methodname: 'tool_lp_data_for_competencies_manage_page',
+ args: { competencyframeworkid: competency.competencyframeworkid,
+ search: $('[data-region="filtercompetencies"] input').val() }
+ }]);
+ requests[0].done(function(success) {
+ if (success === false) {
+ str.get_strings([
+ { key: 'competencycannotbedeleted', component: 'tool_lp', param: competency.shortname },
+ { key: 'cancel', component: 'moodle' }
+ ]).done(function (strings) {
+ notification.alert(
+ null,
+ strings[0]
+ );
+ }).fail(notification.exception);
+ }
+ }).fail(notification.exception);
+ requests[1].done(reloadPage).fail(notification.exception);
+ };
+
+ /**
+ * Show a confirm dialogue before deleting a competency.
+ * @method deleteCompetencyHandler
+ */
+ var deleteCompetencyHandler = function() {
+ var competency = $('[data-region="competencyactions"]').data('competency'),
+ confirmMessage = 'deletecompetency';
+
+ if (treeModel.hasRule(competency.parentid)) {
+ confirmMessage = 'deletecompetencyparenthasrule';
+ }
+
+ str.get_strings([
+ { key: 'confirm', component: 'moodle' },
+ { key: confirmMessage, component: 'tool_lp', param: competency.shortname },
+ { key: 'delete', component: 'moodle' },
+ { key: 'cancel', component: 'moodle' }
+ ]).done(function (strings) {
+ notification.confirm(
+ strings[0], // Confirm.
+ strings[1], // Delete competency X?
+ strings[2], // Delete.
+ strings[3], // Cancel.
+ doDelete
+ );
+ }).fail(notification.exception);
+ };
+
+ /**
+ * HTML5 implementation of drag/drop (there is an accesible alternative in the menus).
+ * @method dragStart
+ */
+ var dragStart = function(e) {
+ e.originalEvent.dataTransfer.setData('text', $(e.target).parent().data('id'));
+ };
+
+ /**
+ * HTML5 implementation of drag/drop (there is an accesible alternative in the menus).
+ * @method allowDrop
+ */
+ var allowDrop = function(e) {
+ e.originalEvent.dataTransfer.dropEffect = 'move';
+ e.preventDefault();
+ };
+
+ /**
+ * HTML5 implementation of drag/drop (there is an accesible alternative in the menus).
+ * @method dragEnter
+ */
+ var dragEnter = function(e) {
+ e.preventDefault();
+ $(this).addClass('currentdragtarget');
+ };
+
+ /**
+ * HTML5 implementation of drag/drop (there is an accesible alternative in the menus).
+ * @method dragLeave
+ */
+ var dragLeave = function(e) {
+ e.preventDefault();
+ $(this).removeClass('currentdragtarget');
+ };
+
+ /**
+ * HTML5 implementation of drag/drop (there is an accesible alternative in the menus).
+ * @method dropOver
+ */
+ var dropOver = function(e) {
+ e.preventDefault();
+ moveSource = e.originalEvent.dataTransfer.getData('text');
+ moveTarget = $(e.target).parent().data('id');
+ $(this).removeClass('currentdragtarget');
+
+ confirmMove();
+ };
+
+ /**
+ * Deletes a related competency without confirmation.
+ *
+ * @param {Event} e The event that triggered the action.
+ * @method deleteRelatedHandler
+ */
+ var deleteRelatedHandler = function(e) {
+ e.preventDefault();
+
+ var relatedid = this.id.substr(11);
+ var competency = $('[data-region="competencyactions"]').data('competency');
+ var removeRelated = ajax.call([
+ { methodname: 'core_competency_remove_related_competency',
+ args: { relatedcompetencyid: relatedid, competencyid: competency.id } },
+ { methodname: 'tool_lp_data_for_related_competencies_section',
+ args: { competencyid: competency.id } }
+ ]);
+
+ removeRelated[1].done(function(context) {
+ templates.render('tool_lp/related_competencies', context).done(function(html) {
+ $('[data-region="relatedcompetencies"]').replaceWith(html);
+ updatedRelatedCompetencies();
+ }.bind(this)).fail(notification.exception);
+ }.bind(this)).fail(notification.exception);
+ };
+
+ /**
+ * Updates the competencies list (with relations) and add listeners.
+ *
+ * @method updatedRelatedCompetencies
+ */
+ var updatedRelatedCompetencies = function() {
+
+ // Listeners to newly loaded related competencies.
+ $('[data-action="deleterelation"]').on('click', deleteRelatedHandler);
+
+ };
+
+ /**
+ * Log the competency viewed event.
+ *
+ * @param {Object} The competency.
+ * @method triggerCompetencyViewedEvent
+ */
+ var triggerCompetencyViewedEvent = function(competency) {
+ if (competency.id !== selectedCompetencyId) {
+ // Set the selected competency id.
+ selectedCompetencyId = competency.id;
+ ajax.call([{
+ methodname: 'core_competency_competency_viewed',
+ args: { id: competency.id }
+ }]);
+ }
+ };
+
+ /**
+ * Return if the level has a sub level.
+ *
+ * @param {Number} level The level.
+ * @return {Boolean}
+ * @function hasSubLevel
+ */
+ var hasSubLevel = function(level) {
+ return typeof taxonomiesConstants[level + 1] !== 'undefined';
+ };
+
+ /**
+ * Return the taxonomy constant for a level.
+ *
+ * @param {Number} level The level.
+ * @return {String}
+ * @function getTaxonomyAtLevel
+ */
+ var getTaxonomyAtLevel = function(level) {
+ var constant = taxonomiesConstants[level];
+ if (!constant) {
+ constant = 'competency';
+ }
+ return constant;
+ };
+
+ /**
+ * Render the competency summary.
+ *
+ * @param {Object} competency The competency.
+ */
+ var renderCompetencySummary = function(competency) {
+ var promise = $.Deferred().resolve().promise(),
+ context = {};
+
+ context.competency = competency;
+ context.showdeleterelatedaction = true;
+ context.showrelatedcompetencies = true;
+ context.showrule = false;
+
+ if (competency.ruleoutcome != Outcomes.NONE) {
+ // Get the outcome and rule name.
+ promise = Outcomes.getString(competency.ruleoutcome).then(function(str) {
+ var name;
+ $.each(rulesModules, function(index, modInfo) {
+ if (modInfo.type == competency.ruletype) {
+ name = modInfo.name;
+ }
+ });
+ return [str, name];
+ });
+ }
+
+ promise.then(function(strs) {
+ if (typeof strs !== 'undefined') {
+ context.showrule = true;
+ context.rule = {
+ outcome: strs[0],
+ type: strs[1]
+ };
+ }
+ }).then(function() {
+ return templates.render('tool_lp/competency_summary', context).then(function(html) {
+ $('[data-region="competencyinfo"]').html(html);
+ $('[data-action="deleterelation"]').on('click', deleteRelatedHandler);
+ });
+ }).then(function() {
+ return templates.render('tool_lp/loading', {});
+ }).then(function(html, js) {
+ templates.replaceNodeContents('[data-region="relatedcompetencies"]', html, js);
+ }).done(function() {
+ ajax.call([{
+ methodname: 'tool_lp_data_for_related_competencies_section',
+ args: { competencyid: competency.id },
+ done: function(context) {
+ return templates.render('tool_lp/related_competencies', context).done(function(html, js) {
+ $('[data-region="relatedcompetencies"]').replaceWith(html);
+ templates.runTemplateJS(js);
+ updatedRelatedCompetencies();
+ });
+ }
+ }]);
+ }).fail(notification.exception);
+ };
+
+ /**
+ * Return the string "Add <taxonomy>".
+ *
+ * @param {Number} level The level.
+ * @return {String}
+ * @function strAddTaxonomy
+ */
+ var strAddTaxonomy = function(level) {
+ return str.get_string('taxonomy_add_' + getTaxonomyAtLevel(level), 'tool_lp');
+ };
+
+ /**
+ * Return the string "Selected <taxonomy>".
+ *
+ * @param {Number} level The level.
+ * @return {String}
+ * @function strSelectedTaxonomy
+ */
+ var strSelectedTaxonomy = function(level) {
+ return str.get_string('taxonomy_selected_' + getTaxonomyAtLevel(level), 'tool_lp');
+ };
+
+ /**
+ * Handler when a node in the aria tree is selected.
+ * @method selectionChanged
+ * @param {Event} evt The event that triggered the selection change.
+ * @param {Object} params The parameters for the event. Contains a list of selected nodes.
+ */
+ var selectionChanged = function(evt, params) {
+ var node = params.selected,
+ id = $(node).data('id'),
+ btn = $('[data-region="competencyactions"] [data-action="add"]'),
+ actionMenu = $('[data-region="competencyactionsmenu"]'),
+ selectedTitle = $('[data-region="selected-competency"]'),
+ level = 0,
+ sublevel = 1;
+
+ menubar.closeAll();
+
+ if (typeof id === "undefined") {
+ // Assume this is the root of the tree.
+ // Here we are only getting the text from the top of the tree, to do it we clone the tree,
+ // remove all children and then call text on the result.
+ $('[data-region="competencyinfo"]').html(node.clone().children().remove().end().text());
+ $('[data-region="competencyactions"]').data('competency', null);
+ actionMenu.hide();
+
+ } else {
+ var competency = treeModel.getCompetency(id);
+
+ level = treeModel.getCompetencyLevel(id);
+ if (!hasSubLevel(level)) {
+ sublevel = false;
+ } else {
+ sublevel = level + 1;
+ }
+
+ actionMenu.show();
+ $('[data-region="competencyactions"]').data('competency', competency);
+ renderCompetencySummary(competency);
+ // Log Competency viewed event.
+ triggerCompetencyViewedEvent(competency);
+ }
+
+ strSelectedTaxonomy(level).then(function(str) {
+ selectedTitle.text(str);
+ });
+
+ if (!sublevel) {
+ btn.hide();
+ } else {
+ strAddTaxonomy(sublevel).then(function(str) {
+ btn.show()
+ .find('[data-region="term"]')
+ .text(str);
+ });
+ }
+ // We handled this event so consume it.
+ evt.preventDefault();
+ return false;
+ };
+
+ /**
+ * Return the string "Selected <taxonomy>".
+ *
+ * @function parseTaxonomies
+ * @param {String} Comma separated list of taxonomies.
+ * @return {Array} of level => taxonomystr
+ */
+ var parseTaxonomies = function(taxonomiesstr) {
+ var all = taxonomiesstr.split(',');
+ all.unshift("");
+ delete all[0];
+
+ // Note we don't need to fill holes, because other functions check for empty anyway.
+ return all;
+ };
+
+ return {
+ /**
+ * Initialise this page (attach event handlers etc).
+ *
+ * @method init
+ * @param {Object} model The tree model provides some useful functions for loading and searching competencies.
+ * @param {Number} pagectxid The page context ID.
+ * @param {Object} taxonomies Constants indexed by level.
+ * @param {Object} rulesMods The modules of the rules.
+ */
+ init: function(model, pagectxid, taxonomies, rulesMods) {
+ treeModel = model;
+ pageContextId = pagectxid;
+ taxonomiesConstants = parseTaxonomies(taxonomies);
+ rulesModules = rulesMods;
+
+ $('[data-region="competencyactions"] [data-action="add"]').on('click', addHandler);
+
+ menubar.enhance('.competencyactionsmenu', {
+ '[data-action="edit"]': editHandler,
+ '[data-action="delete"]': deleteCompetencyHandler,
+ '[data-action="move"]': moveHandler,
+ '[data-action="moveup"]': moveUpHandler,
+ '[data-action="movedown"]': moveDownHandler,
+ '[data-action="linkedcourses"]': seeCoursesHandler,
+ '[data-action="relatedcompetencies"]': relateCompetenciesHandler.bind(this),
+ '[data-action="competencyrules"]': ruleConfigHandler.bind(this)
+ });
+ $('[data-region="competencyactionsmenu"]').hide();
+ $('[data-region="competencyactions"] [data-action="add"]').hide();
+
+ $('[data-region="filtercompetencies"]').on('submit', updateSearchHandler);
+ // Simple html5 drag drop because we already added an accessible alternative.
+ var top = $('[data-region="managecompetencies"] [data-enhance="tree"]');
+ top.on('dragstart', 'li>span', dragStart)
+ .on('dragover', 'li>span', allowDrop)
+ .on('dragenter', 'li>span', dragEnter)
+ .on('dragleave', 'li>span', dragLeave)
+ .on('drop', 'li>span', dropOver);
+
+ model.on('selectionchanged', selectionChanged);
+
+ // Prepare the configuration tool.
+ ruleConfigInstance = new RuleConfig(treeModel, rulesModules);
+ ruleConfigInstance.on('save', ruleConfigSaveHandler.bind(this));
+ }
+ };
+});
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Display Competency in dialogue box.
+ *
+ * @module tool_lp/Competencydialogue
+ * @package tool_lp
+ * @copyright 2015 Issam Taboubi <issam.taboubi@umontreal.ca>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery',
+ 'core/notification',
+ 'core/ajax',
+ 'core/templates',
+ 'core/str',
+ 'tool_lp/dialogue'],
+ function($, notification, ajax, templates, str, Dialogue) {
+
+ /**
+ * Constructor for CompetencyDialogue.
+ *
+ * @param {Object} options
+ *
+ */
+ var Competencydialogue = function(options) {
+ this.options = {
+ includerelated: false,
+ includecourses: false
+ };
+ $.extend(this.options, options);
+ };
+
+ /**
+ * Log the competency viewed event.
+ *
+ * @param {Number} The competency ID.
+ * @method triggerCompetencyViewedEvent
+ */
+ Competencydialogue.prototype.triggerCompetencyViewedEvent = function(competencyId) {
+ ajax.call([{
+ methodname: 'core_competency_competency_viewed',
+ args: { id: competencyId }
+ }]);
+ };
+
+ /**
+ * Callback on dialogue display, it apply enhance on competencies dialogue.
+ *
+ * @param {Dialogue} dialogue
+ * @method enhanceDialogue
+ */
+ Competencydialogue.prototype.enhanceDialogue = function(dialogue) {
+ //Apply watch on the related competencies and competencies in the dialogue.
+ var comprelated = new Competencydialogue({includerelated : false});
+ comprelated.watch(dialogue.getContent());
+ };
+
+ /**
+ * Display a dialogue box by competencyid.
+ *
+ * @param {Number} the competency id
+ * @param {Object} Options for tool_lp_data_for_competency_summary service
+ * @param {Object} dataSource data to be used to display dialogue box
+ * @method showDialogue
+ */
+ Competencydialogue.prototype.showDialogue = function(competencyid) {
+
+ var datapromise = this.getCompetencyDataPromise(competencyid);
+ var localthis = this;
+ datapromise.done(function(data) {
+ // Inner Html in the dialogue content.
+ templates.render('tool_lp/competency_summary', data)
+ .done(function(html) {
+ // Log competency viewed event.
+ localthis.triggerCompetencyViewedEvent(competencyid);
+
+ // Show the dialogue.
+ new Dialogue(
+ data.competency.shortname,
+ html,
+ localthis.enhanceDialogue
+ );
+ }).fail(notification.exception);
+ }).fail(notification.exception);
+ };
+
+ /**
+ * Display a dialogue box from data.
+ *
+ * @param {Object} dataSource data to be used to display dialogue box
+ * @method showDialogueFromData
+ */
+ Competencydialogue.prototype.showDialogueFromData = function(dataSource) {
+
+ var localthis = this;
+ // Inner Html in the dialogue content.
+ templates.render('tool_lp/competency_summary', dataSource)
+ .done(function(html) {
+ // Log competency viewed event.
+ localthis.triggerCompetencyViewedEvent(dataSource.id);
+
+ // Show the dialogue.
+ new Dialogue(
+ dataSource.shortname,
+ html,
+ localthis.enhanceDialogue
+ );
+ }).fail(notification.exception);
+ };
+
+ /**
+ * The action on the click event.
+ *
+ * @param {Event} event click
+ * @method clickEventHandler
+ */
+ Competencydialogue.prototype.clickEventHandler = function(e) {
+
+ var compdialogue = e.data.compdialogue;
+ var competencyid = $(e.currentTarget).data('id');
+
+ // Show the dialogue box.
+ compdialogue.showDialogue(competencyid);
+ e.preventDefault();
+ };
+
+ /**
+ * Get a promise on data competency.
+ *
+ * @param {Number} competencyid
+ * @return {Promise} return promise on data request
+ * @method getCompetencyDataPromise
+ */
+ Competencydialogue.prototype.getCompetencyDataPromise = function(competencyid) {
+
+ var requests = ajax.call([
+ { methodname: 'tool_lp_data_for_competency_summary',
+ args: { competencyid: competencyid,
+ includerelated: this.options.includerelated,
+ includecourses: this.options.includecourses
+ }
+ }
+ ]);
+
+ return requests[0].then(function(context) {
+ return context;
+ }).fail(notification.exception);
+ };
+
+ /**
+ * Watch the competencies links in container.
+ *
+ * @param {String} container selector of node containing competencies links
+ * @method watch
+ */
+ Competencydialogue.prototype.watch = function(containerSelector) {
+ $(containerSelector).off('click', '[data-action="competency-dialogue"]', this.clickEventHandler);
+ $(containerSelector).on('click', '[data-action="competency-dialogue"]', { compdialogue: this }, this.clickEventHandler);
+ };
+
+ return Competencydialogue;
+});
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Competency picker.
+ *
+ * To handle 'save' events use: picker.on('save')
+ * This will receive a object with either a single 'competencyId', or an array in 'competencyIds'
+ * depending on the value of multiSelect.
+ *
+ * @package tool_lp
+ * @copyright 2015 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery',
+ 'core/notification',
+ 'core/ajax',
+ 'core/templates',
+ 'tool_lp/dialogue',
+ 'core/str',
+ 'tool_lp/tree'],
+ function($, Notification, Ajax, Templates, Dialogue, Str, Tree) {
+
+ /**
+ * Competency picker class.
+ * @param {Number} pageContextId The page context ID.
+ * @param {Number|false} singleFramework The ID of the framework when limited to one.
+ * @param {String} pageContextIncludes One of 'children', 'parents', 'self'.
+ * @param {Boolean} multiSelect Support multi-select in the tree.
+ */
+ var Picker = function(pageContextId, singleFramework, pageContextIncludes, multiSelect) {
+ var self = this;
+ self._eventNode = $('<div></div>');
+ self._frameworks = [];
+ self._reset();
+
+ self._pageContextId = pageContextId;
+ self._pageContextIncludes = pageContextIncludes || 'children';
+ self._multiSelect = (typeof multiSelect === 'undefined' || multiSelect === true);
+ if (singleFramework) {
+ self._frameworkId = singleFramework;
+ self._singleFramework = true;
+ }
+ };
+
+ /** @type {Array} The competencies fetched. */
+ Picker.prototype._competencies = null;
+ /** @type {Array} The competencies that cannot be picked. */
+ Picker.prototype._disallowedCompetencyIDs = null;
+ /** @type {Node} The node we attach the events to. */
+ Picker.prototype._eventNode = null;
+ /** @type {Array} The list of frameworks fetched. */
+ Picker.prototype._frameworks = null;
+ /** @type {Number} The current framework ID. */
+ Picker.prototype._frameworkId = null;
+ /** @type {Number} The page context ID. */
+ Picker.prototype._pageContextId = null;
+ /** @type {Number} Relevant contexts inclusion. */
+ Picker.prototype._pageContextIncludes = null;
+ /** @type {Dialogue} The reference to the dialogue. */
+ Picker.prototype._popup = null;
+ /** @type {String} The string we filter the competencies with. */
+ Picker.prototype._searchText = '';
+ /** @type {Object} The competency that was selected. */
+ Picker.prototype._selectedCompetencies = null;
+ /** @type {Boolean} Whether we can browse frameworks or not. */
+ Picker.prototype._singleFramework = false;
+ /** @type {Boolean} Do we allow multi select? */
+ Picker.prototype._multiSelect = true;
+ /** @type {Boolean} Do we allow to display hidden framework? */
+ Picker.prototype._onlyVisible = true;
+
+ /**
+ * Hook to executed after the view is rendered.
+ *
+ * @method _afterRender
+ */
+ Picker.prototype._afterRender = function() {
+ var self = this;
+
+ // Initialise the tree.
+ var tree = new Tree(self._find('[data-enhance=linktree]'), self._multiSelect);
+
+ // To prevent jiggling we only show the tree after it is enhanced.
+ self._find('[data-enhance=linktree]').show();
+
+ tree.on('selectionchanged', function(evt, params) {
+ var selected = params.selected;
+ evt.preventDefault();
+ var validIds = [];
+ $.each(selected, function(index, item) {
+ var compId = $(item).data('id'),
+ valid = true;
+
+ if (typeof compId === 'undefined') {
+ // Do not allow picking nodes with no id.
+ valid = false;
+ } else {
+ $.each(self._disallowedCompetencyIDs, function(i, id) {
+ if (id == compId) {
+ valid = false;
+ }
+ });
+ }
+ if (valid) {
+ validIds.push(compId);
+ }
+ }.bind(self));
+
+ self._selectedCompetencies = validIds;
+
+ // TODO Implement disabling of nodes in the tree module somehow.
+ if (!self._selectedCompetencies.length) {
+ self._find('[data-region="competencylinktree"] [data-action="add"]').attr('disabled', 'disabled');
+ } else {
+ self._find('[data-region="competencylinktree"] [data-action="add"]').removeAttr('disabled');
+ }
+ }.bind(self));
+
+ // Add listener for framework change.
+ if (!self._singleFramework) {
+ self._find('[data-action="chooseframework"]').change(function(e) {
+ self._frameworkId = $(e.target).val();
+ self._loadCompetencies().then(self._refresh.bind(self));
+ }.bind(self));
+ }
+
+ // Add listener for search.
+ self._find('[data-region="filtercompetencies"] button').click(function(e) {
+ e.preventDefault();
+ $(e.target).attr('disabled', 'disabled');
+ self._searchText = self._find('[data-region="filtercompetencies"] input').val() || '';
+ return self._refresh().always(function() {
+ $(e.target).removeAttr('disabled');
+ });
+ }.bind(self));
+
+ // Add listener for cancel.
+ self._find('[data-region="competencylinktree"] [data-action="cancel"]').click(function(e) {
+ e.preventDefault();
+ self.close();
+ }.bind(self));
+
+ // Add listener for add.
+ self._find('[data-region="competencylinktree"] [data-action="add"]').click(function(e) {
+ e.preventDefault();
+ if (!self._selectedCompetencies.length) {
+ return;
+ }
+
+ if (self._multiSelect) {
+ self._trigger('save', { competencyIds: self._selectedCompetencies });
+ } else {
+ // We checked above that the array has at least one value.
+ self._trigger('save', { competencyId: self._selectedCompetencies[0] });
+ }
+
+ self.close();
+ }.bind(self));
+
+ // The list of selected competencies will be modified while looping (because of the listeners above).
+ var currentItems = self._selectedCompetencies.slice(0);
+
+ $.each(currentItems, function(index, id) {
+ var node = self._find('[data-id=' + id + ']');
+ if (node.length) {
+ tree.toggleItem(node);
+ tree.updateFocus(node);
+ }
+ }.bind(self));
+
+ };
+
+ /**
+ * Close the dialogue.
+ *
+ * @method close
+ */
+ Picker.prototype.close = function() {
+ var self = this;
+ self._popup.close();
+ self._reset();
+ };
+
+ /**
+ * Opens the picker.
+ *
+ * @method display
+ * @return {Promise}
+ */
+ Picker.prototype.display = function() {
+ var self = this;
+ return self._render().then(function(html) {
+ return Str.get_string('competencypicker', 'tool_lp').then(function(title) {
+ self._popup = new Dialogue(
+ title,
+ html,
+ self._afterRender.bind(self)
+ );
+ }.bind(self));
+ }.bind(self)).fail(Notification.exception);
+ };
+
+ /**
+ * Fetch the competencies.
+ *
+ * @param {Number} frameworkId The frameworkId.
+ * @param {String} searchText Limit the competencies to those matching the text.
+ * @method _fetchCompetencies
+ * @return {Promise}
+ */
+ Picker.prototype._fetchCompetencies = function(frameworkId, searchText) {
+ var self = this;
+
+ return Ajax.call([
+ { methodname: 'core_competency_search_competencies', args: {
+ searchtext: searchText,
+ competencyframeworkid: frameworkId
+ }}
+ ])[0].done(function(competencies) {
+
+ function addCompetencyChildren(parent, competencies) {
+ for (var i = 0; i < competencies.length; i++) {
+ if (competencies[i].parentid == parent.id) {
+ parent.haschildren = true;
+ competencies[i].children = [];
+ competencies[i].haschildren = false;
+ parent.children[parent.children.length] = competencies[i];
+ addCompetencyChildren(competencies[i], competencies);
+ }
+ }
+ }
+
+ // Expand the list of competencies into a tree.
+ var i, tree = [], comp;
+ for (i = 0; i < competencies.length; i++) {
+ comp = competencies[i];
+ if (comp.parentid == "0") { // Loose check for now, because WS returns a string.
+ comp.children = [];
+ comp.haschildren = 0;
+ tree[tree.length] = comp;
+ addCompetencyChildren(comp, competencies);
+ }
+ }
+
+ self._competencies = tree;
+
+ }.bind(self)).fail(Notification.exception);
+ };
+
+ /**
+ * Find a node in the dialogue.
+ *
+ * @param {String} selector
+ * @method _find
+ */
+ Picker.prototype._find = function(selector) {
+ return $(this._popup.getContent()).find(selector);
+ };
+
+ /**
+ * Convenience method to get a framework object.
+ *
+ * @param {Number} fid The framework ID.
+ * @method _getFramework
+ */
+ Picker.prototype._getFramework = function(fid) {
+ var frm;
+ $.each(this._frameworks, function(i, f) {
+ if (f.id == fid) {
+ frm = f;
+ return false;
+ }
+ });
+ return frm;
+ };
+
+ /**
+ * Load the competencies.
+ *
+ * @method _loadCompetencies
+ * @return {Promise}
+ */
+ Picker.prototype._loadCompetencies = function() {
+ return this._fetchCompetencies(this._frameworkId, this._searchText);
+ };
+
+ /**
+ * Load the frameworks.
+ *
+ * @method _loadFrameworks
+ * @return {Promise}
+ */
+ Picker.prototype._loadFrameworks = function() {
+ var promise,
+ self = this;
+
+ // Quit early because we already have the data.
+ if (self._frameworks.length > 0) {
+ return $.when();
+ }
+
+ if (self._singleFramework) {
+ promise = Ajax.call([
+ { methodname: 'core_competency_read_competency_framework', args: {
+ id: this._frameworkId
+ }}
+ ])[0].then(function(framework) {
+ return [framework];
+ });
+ } else {
+ promise = Ajax.call([
+ { methodname: 'core_competency_list_competency_frameworks', args: {
+ sort: 'shortname',
+ context: { contextid: self._pageContextId },
+ includes: self._pageContextIncludes,
+ onlyvisible: self._onlyVisible
+ }}
+ ])[0];
+ }
+
+ return promise.done(function(frameworks) {
+ self._frameworks = frameworks;
+ }).fail(Notification.exception);
+ };
+
+ /**
+ * Register an event listener.
+ *
+ * @param {String} type The event type.
+ * @param {Function} handler The event listener.
+ * @method on
+ */
+ Picker.prototype.on = function(type, handler) {
+ this._eventNode.on(type, handler);
+ };
+
+ /**
+ * Hook to executed before render.
+ *
+ * @method _preRender
+ * @return {Promise}
+ */
+ Picker.prototype._preRender = function() {
+ var self = this;
+ return self._loadFrameworks().then(function() {
+ if (!self._frameworkId && self._frameworks.length > 0) {
+ self._frameworkId = self._frameworks[0].id;
+ }
+
+ // We could not set a framework ID, that probably means there are no frameworks accessible.
+ if (!self._frameworkId) {
+ self._frameworks = [];
+ return $.when();
+ }
+
+ return self._loadCompetencies();
+ }.bind(self));
+ };
+
+ /**
+ * Refresh the view.
+ *
+ * @method _refresh
+ * @return {Promise}
+ */
+ Picker.prototype._refresh = function() {
+ var self = this;
+ return self._render().then(function(html) {
+ self._find('[data-region="competencylinktree"]').replaceWith(html);
+ self._afterRender();
+ }.bind(self));
+ };
+
+ /**
+ * Render the dialogue.
+ *
+ * @method _render
+ * @return {Promise}
+ */
+ Picker.prototype._render = function() {
+ var self = this;
+ return self._preRender().then(function() {
+
+ if (!self._singleFramework) {
+ $.each(self._frameworks, function(i, framework) {
+ if (framework.id == self._frameworkId) {
+ framework.selected = true;
+ } else {
+ framework.selected = false;
+ }
+ });
+ }
+
+ var context = {
+ competencies: self._competencies,
+ framework: self._getFramework(self._frameworkId),
+ frameworks: self._frameworks,
+ search: self._searchText,
+ singleFramework: self._singleFramework,
+ };
+
+ return Templates.render('tool_lp/competency_picker', context);
+ }.bind(self));
+ };
+
+ /**
+ * Reset the dialogue properties.
+ *
+ * This does not reset everything, just enough to reset the UI.
+ *
+ * @method _reset
+ */
+ Picker.prototype._reset = function() {
+ this._competencies = [];
+ this._disallowedCompetencyIDs = [];
+ this._popup = null;
+ this._searchText = '';
+ this._selectedCompetencies = [];
+ };
+
+ /**
+ * Set what competencies cannot be picked.
+ *
+ * This needs to be set after reset/close.
+ *
+ * @params {Number[]} The IDs.
+ * @method _setDisallowedCompetencyIDs
+ */
+ Picker.prototype.setDisallowedCompetencyIDs = function(ids) {
+ this._disallowedCompetencyIDs = ids;
+ };
+
+ /**
+ * Trigger an event.
+ *
+ * @param {String} type The type of event.
+ * @param {Object} The data to pass to the listeners.
+ * @method _reset
+ */
+ Picker.prototype._trigger = function(type, data) {
+ this._eventNode.trigger(type, [data]);
+ };
+
+ return /** @alias module:tool_lp/competencypicker */ Picker;
+
+});
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Competency picker from user plans.
+ *
+ * To handle 'save' events use: picker.on('save').
+ *
+ * This will receive a object with either a single 'competencyId', or an array in 'competencyIds'
+ * depending on the value of multiSelect.
+ *
+ * @package tool_lp
+ * @copyright 2015 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery',
+ 'core/notification',
+ 'core/ajax',
+ 'core/templates',
+ 'core/str',
+ 'tool_lp/tree',
+ 'tool_lp/competencypicker'
+ ],
+ function($, Notification, Ajax, Templates, Str, Tree, PickerBase) {
+
+ /**
+ * Competency picker in plan class.
+ *
+ * @param {Number|false} singlePlan The ID of the plan when limited to one.
+ * @param {String} pageContextIncludes One of 'children', 'parents', 'self'.
+ * @param {Boolean} multiSelect Support multi-select in the tree.
+ */
+ var Picker = function(userId, singlePlan, multiSelect) {
+ PickerBase.prototype.constructor.apply(this, [1, false, 'self', multiSelect]);
+ this._userId = userId;
+ this._plans = [];
+
+ if (singlePlan) {
+ this._planId = singlePlan;
+ this._singlePlan = true;
+ }
+ };
+ Picker.prototype = Object.create(PickerBase.prototype);
+
+ /** @type {Array} The list of plans fetched. */
+ Picker.prototype._plans = null;
+ /** @type {Number} The current plan ID. */
+ Picker.prototype._planId = null;
+ /** @type {Boolean} Whether we can browse plans or not. */
+ Picker.prototype._singlePlan = false;
+ /** @type {Number} The user the plans belongs to. */
+ Picker.prototype._userId = null;
+
+ /**
+ * Hook to executed after the view is rendered.
+ *
+ * @method _afterRender
+ */
+ Picker.prototype._afterRender = function() {
+ var self = this;
+ PickerBase.prototype._afterRender.apply(self, arguments);
+
+ // Add listener for framework change.
+ if (!self._singlePlan) {
+ self._find('[data-action="chooseplan"]').change(function(e) {
+ self._planId = $(e.target).val();
+ self._loadCompetencies().then(self._refresh.bind(self));
+ }.bind(self));
+ }
+ };
+
+ /**
+ * Fetch the competencies.
+ *
+ * @param {Number} planId The planId.
+ * @param {String} searchText Limit the competencies to those matching the text.
+ * @method _fetchCompetencies
+ * @return {Promise}
+ */
+ Picker.prototype._fetchCompetencies = function(planId, searchText) {
+ var self = this;
+
+ return Ajax.call([
+ { methodname: 'core_competency_list_plan_competencies', args: {
+ id: planId
+ }}
+ ])[0].done(function(competencies) {
+
+ // Expand the list of competencies into a fake tree.
+ var i, tree = [], comp;
+ for (i = 0; i < competencies.length; i++) {
+ comp = competencies[i].competency;
+ if (comp.shortname.toLowerCase().indexOf(searchText.toLowerCase()) < 0) {
+ continue;
+ }
+ comp.children = [];
+ comp.haschildren = 0;
+ tree.push(comp);
+ }
+
+ self._competencies = tree;
+
+ }).fail(Notification.exception);
+ };
+
+ /**
+ * Convenience method to get a plan object.
+ *
+ * @param {Number} id The plan ID.
+ * @return {Object|undefined} The plan.
+ * @method _getPlan
+ */
+ Picker.prototype._getPlan = function(id) {
+ var plan;
+ $.each(this._plans, function(i, f) {
+ if (f.id == id) {
+ plan = f;
+ return false;
+ }
+ });
+ return plan;
+ };
+
+ /**
+ * Load the competencies.
+ *
+ * @method _loadCompetencies
+ * @return {Promise}
+ */
+ Picker.prototype._loadCompetencies = function() {
+ return this._fetchCompetencies(this._planId, this._searchText);
+ };
+
+ /**
+ * Load the plans.
+ *
+ * @method _loadPlans
+ * @return {Promise}
+ */
+ Picker.prototype._loadPlans = function() {
+ var promise,
+ self = this;
+
+ // Quit early because we already have the data.
+ if (self._plans.length > 0) {
+ return $.when();
+ }
+
+ if (self._singlePlan) {
+ promise = Ajax.call([
+ { methodname: 'core_competency_read_plan', args: {
+ id: this._planId
+ }}
+ ])[0].then(function(plan) {
+ return [plan];
+ });
+ } else {
+ promise = Ajax.call([
+ { methodname: 'core_competency_list_user_plans', args: {
+ userid: self._userId
+ }}
+ ])[0];
+ }
+
+ return promise.done(function(plans) {
+ self._plans = plans;
+ }).fail(Notification.exception);
+ };
+
+ /**
+ * Hook to executed before render.
+ *
+ * @method _preRender
+ * @return {Promise}
+ */
+ Picker.prototype._preRender = function() {
+ var self = this;
+ return self._loadPlans().then(function() {
+ if (!self._planId && self._plans.length > 0) {
+ self._planId = self._plans[0].id;
+ }
+
+ // We could not set a framework ID, that probably means there are no frameworks accessible.
+ if (!self._planId) {
+ self._plans = [];
+ return $.when();
+ }
+
+ return self._loadCompetencies();
+ }.bind(self));
+ };
+
+ /**
+ * Render the dialogue.
+ *
+ * @method _render
+ * @return {Promise}
+ */
+ Picker.prototype._render = function() {
+ var self = this;
+ return self._preRender().then(function() {
+
+ if (!self._singlePlan) {
+ $.each(self._plans, function(i, plan) {
+ if (plan.id == self._planId) {
+ plan.selected = true;
+ } else {
+ plan.selected = false;
+ }
+ });
+ }
+
+ var context = {
+ competencies: self._competencies,
+ plan: self._getPlan(self._planId),
+ plans: self._plans,
+ search: self._searchText,
+ singlePlan: self._singlePlan,
+ };
+
+ return Templates.render('tool_lp/competency_picker_user_plans', context);
+ }.bind(self));
+ };
+
+ return /** @alias module:tool_lp/competencypicker_user_plans */ Picker;
+
+});
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Competency rule config.
+ *
+ * @package tool_lp
+ * @copyright 2015 Frédéric Massart - FMCorz.net
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery',
+ 'core/notification',
+ 'core/templates',
+ 'tool_lp/dialogue',
+ 'tool_lp/competency_outcomes',
+ 'core/str'],
+ function($, Notification, Templates, Dialogue, Outcomes, Str) {
+
+ /**
+ * Competency rule class.
+ *
+ * When implementing this you should attach a listener to the event 'save'
+ * on the instance. E.g.
+ *
+ * var config = new RuleConfig(tree, modules);
+ * config.on('save', function(e, config) { ... });
+ *
+ * @param {competencytree} tree The competency tree.
+ * @param {Array} rulesModules The modules containing the rules: [{ typeName: { amd: amdModule, name: ruleName }}].
+ */
+ var RuleConfig = function(tree, rulesModules) {
+ this._eventNode = $('<div></div>');
+ this._tree = tree;
+ this._rulesModules = rulesModules;
+ this._setUp();
+ };
+
+ /** @type {Object} The current competency. */
+ RuleConfig.prototype._competency = null;
+ /** @type {Node} The node we attach the events to. */
+ RuleConfig.prototype._eventNode = null;
+ /** @type {Array} Outcomes options. */
+ RuleConfig.prototype._outcomesOption = null;
+ /** @type {Dialogue} The dialogue. */
+ RuleConfig.prototype._popup = null;
+ /** @type {Promise} Resolved when the module is ready. */
+ RuleConfig.prototype._ready = null;
+ /** @type {Array} The rules. */
+ RuleConfig.prototype._rules = null;
+ /** @type {Array} The rules modules. */
+ RuleConfig.prototype._rulesModules = null;
+ /** @type {competencytree} The competency tree. */
+ RuleConfig.prototype._tree = null;
+
+ /**
+ * After change.
+ *
+ * Triggered when a change occured.
+ *
+ * @return {Void}
+ * @method _afterChange
+ * @protected
+ */
+ RuleConfig.prototype._afterChange = function() {
+ if (!this._isValid()) {
+ this._find('[data-action="save"]').prop('disabled', true);
+ } else {
+ this._find('[data-action="save"]').prop('disabled', false);
+ }
+ };
+
+ /**
+ * After change in rule's config.
+ *
+ * Triggered when a change occured in a specific rule config.
+ *
+ * @return {Void}
+ * @method _afterRuleConfigChange
+ * @protected
+ */
+ RuleConfig.prototype._afterRuleConfigChange = function(e, rule) {
+ if (rule != this._getRule()) {
+ // This rule is not the current one any more, we can ignore.
+ return;
+ }
+ this._afterChange();
+ };
+
+ /**
+ * After render hook.
+ *
+ * @return {Promise}
+ * @method _afterRender
+ * @protected
+ */
+ RuleConfig.prototype._afterRender = function() {
+ var self = this;
+
+ self._find('[name="outcome"]').on('change', function() {
+ self._switchedOutcome();
+ }).trigger('change');
+
+ self._find('[name="rule"]').on('change', function() {
+ self._switchedRule();
+ }).trigger('change');
+
+ self._find('[data-action="save"]').on('click', function() {
+ self._trigger('save', self._getConfig());
+ self.close();
+ });
+
+ self._find('[data-action="cancel"]').on('click', function() {
+ self.close();
+ });
+ };
+
+ /**
+ * Whether the current competency can be configured.
+ *
+ * @return {Boolean}
+