diff --git a/admin/settings/competency.php b/admin/settings/competency.php new file mode 100644 index 00000000000..1fffa73271e --- /dev/null +++ b/admin/settings/competency.php @@ -0,0 +1,50 @@ +. + +/** + * 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); + } + +} diff --git a/admin/settings/top.php b/admin/settings/top.php index 4736f06cc8f..223af0cfc59 100644 --- a/admin/settings/top.php +++ b/admin/settings/top.php @@ -30,6 +30,7 @@ if ($hassiteconfig) { $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'))); diff --git a/admin/tool/cohortroles/classes/api.php b/admin/tool/cohortroles/classes/api.php new file mode 100644 index 00000000000..e610e35f442 --- /dev/null +++ b/admin/tool/cohortroles/classes/api.php @@ -0,0 +1,228 @@ +. + +/** + * 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); + } + +} diff --git a/admin/tool/cohortroles/classes/cohort_role_assignment.php b/admin/tool/cohortroles/classes/cohort_role_assignment.php new file mode 100644 index 00000000000..08ac4641076 --- /dev/null +++ b/admin/tool/cohortroles/classes/cohort_role_assignment.php @@ -0,0 +1,107 @@ +. + +/** + * 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; + } + +} diff --git a/admin/tool/cohortroles/classes/form/assign_role_cohort.php b/admin/tool/cohortroles/classes/form/assign_role_cohort.php new file mode 100644 index 00000000000..7cc59aef758 --- /dev/null +++ b/admin/tool/cohortroles/classes/form/assign_role_cohort.php @@ -0,0 +1,86 @@ +. + +/** + * 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')); + } + +} diff --git a/admin/tool/cohortroles/classes/output/cohort_role_assignments_table.php b/admin/tool/cohortroles/classes/output/cohort_role_assignments_table.php new file mode 100644 index 00000000000..08c8c3f7501 --- /dev/null +++ b/admin/tool/cohortroles/classes/output/cohort_role_assignments_table.php @@ -0,0 +1,231 @@ +. + +/** + * 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); + } + } +} diff --git a/admin/tool/cohortroles/classes/output/renderer.php b/admin/tool/cohortroles/classes/output/renderer.php new file mode 100644 index 00000000000..561052f53be --- /dev/null +++ b/admin/tool/cohortroles/classes/output/renderer.php @@ -0,0 +1,76 @@ +. + +/** + * Renderer class for cohort roles + * + * @package tool_cohortroles + * @copyright 2016 Issam Taboubi + * @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 + * @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); + } +} diff --git a/admin/tool/cohortroles/classes/task/cohort_role_sync.php b/admin/tool/cohortroles/classes/task/cohort_role_sync.php new file mode 100644 index 00000000000..a3d628c85d4 --- /dev/null +++ b/admin/tool/cohortroles/classes/task/cohort_role_sync.php @@ -0,0 +1,56 @@ +. + +/** + * 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'])); + } +} diff --git a/admin/tool/cohortroles/db/install.xml b/admin/tool/cohortroles/db/install.xml new file mode 100644 index 00000000000..538ec1fe6a1 --- /dev/null +++ b/admin/tool/cohortroles/db/install.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/admin/tool/cohortroles/db/tasks.php b/admin/tool/cohortroles/db/tasks.php new file mode 100644 index 00000000000..e5f5a627bb9 --- /dev/null +++ b/admin/tool/cohortroles/db/tasks.php @@ -0,0 +1,37 @@ +. + +/** + * 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' => '*' + ), +); diff --git a/admin/tool/cohortroles/index.php b/admin/tool/cohortroles/index.php new file mode 100644 index 00000000000..a202be65feb --- /dev/null +++ b/admin/tool/cohortroles/index.php @@ -0,0 +1,98 @@ +. + +/** + * 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(); + diff --git a/admin/tool/cohortroles/lang/en/tool_cohortroles.php b/admin/tool/cohortroles/lang/en/tool_cohortroles.php new file mode 100644 index 00000000000..e735f7e5925 --- /dev/null +++ b/admin/tool/cohortroles/lang/en/tool_cohortroles.php @@ -0,0 +1,44 @@ +. + +/** + * 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. Manage roles'; +$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'; diff --git a/admin/tool/cohortroles/settings.php b/admin/tool/cohortroles/settings.php new file mode 100644 index 00000000000..98613c15042 --- /dev/null +++ b/admin/tool/cohortroles/settings.php @@ -0,0 +1,27 @@ +. + +/** + * 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')); diff --git a/admin/tool/cohortroles/templates/cohort-in-list.mustache b/admin/tool/cohortroles/templates/cohort-in-list.mustache new file mode 100644 index 00000000000..971065a0da1 --- /dev/null +++ b/admin/tool/cohortroles/templates/cohort-in-list.mustache @@ -0,0 +1,43 @@ +{{! + 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 . +}} +{{! + @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 }} diff --git a/admin/tool/cohortroles/tests/api_test.php b/admin/tool/cohortroles/tests/api_test.php new file mode 100644 index 00000000000..27c4cd8227b --- /dev/null +++ b/admin/tool/cohortroles/tests/api_test.php @@ -0,0 +1,243 @@ +. + +/** + * 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); + } + +} diff --git a/admin/tool/cohortroles/version.php b/admin/tool/cohortroles/version.php new file mode 100644 index 00000000000..bf9fb21f408 --- /dev/null +++ b/admin/tool/cohortroles/version.php @@ -0,0 +1,34 @@ +. + +/** + * 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 +); diff --git a/admin/tool/lp/amd/build/actionselector.min.js b/admin/tool/lp/amd/build/actionselector.min.js new file mode 100644 index 00000000000..90062ca2159 --- /dev/null +++ b/admin/tool/lp/amd/build/actionselector.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/ajax","core/templates","tool_lp/dialogue","tool_lp/event_base"],function(a,b,c,d,e,f){var g=function(a,b,c,d,e){var g=this;f.prototype.constructor.apply(this,[]),g._title=a,g._message=b,g._actions=c,g._confirm=d,g._cancel=e,g._selectedValue=null,g._reset()};return g.prototype=Object.create(f.prototype),g.prototype._selectedValue=null,g.prototype._popup=null,g.prototype._title=null,g.prototype._message=null,g.prototype._actions=null,g.prototype._confirm=null,g.prototype._cancel=null,g.prototype._afterRender=function(){var b=this;b._find('[data-action="action-selector-confirm"]').attr("disabled","disabled"),b._find('[data-region="action-selector-radio-buttons"]').change(function(){b._selectedValue=a("input[type='radio']:checked").val(),b._find('[data-action="action-selector-confirm"]').removeAttr("disabled"),b._refresh.bind(b)}.bind(b)),b._find('[data-action="action-selector-cancel"]').click(function(a){a.preventDefault(),b.close()}.bind(b)),b._find('[data-action="action-selector-confirm"]').click(function(a){a.preventDefault(),b._selectedValue.length&&(b._trigger("save",{action:b._selectedValue}),b.close())}.bind(b))},g.prototype.close=function(){var a=this;a._popup.close(),a._reset()},g.prototype.display=function(){var a=this;return a._render().then(function(b){a._popup=new e(a._title,b,a._afterRender.bind(a))}.bind(a)).fail(b.exception)},g.prototype._find=function(b){return a(this._popup.getContent()).find(b)},g.prototype._refresh=function(){var a=this;return a._render().then(function(b){a._find('[data-region="action-selector"]').replaceWith(b),a._afterRender()}.bind(a))},g.prototype._render=function(){var a=this,b=[];for(var c in a._actions)b.push(a._actions[c]);var e={message:a._message,choices:b,confirm:a._confirm,cancel:a._cancel};return d.render("tool_lp/action_selector",e)},g.prototype._reset=function(){this._popup=null,this._selectedValue=""},g}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competencies.min.js b/admin/tool/lp/amd/build/competencies.min.js new file mode 100644 index 00000000000..cf57df2f6fc --- /dev/null +++ b/admin/tool/lp/amd/build/competencies.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/ajax","core/templates","core/str","tool_lp/competencypicker","tool_lp/dragdrop-reorder"],function(a,b,c,d,e,f,g){var h=function(b,c,d){this.itemid=b,this.itemtype=c,this.pageContextId=d,this.pickerInstance=null,a('[data-region="actions"] button').prop("disabled",!1),this.registerEvents(),this.registerDragDrop()};return h.prototype.registerDragDrop=function(){var a=this;e.get_string("movecompetency","tool_lp").done(function(b){g.dragdrop("movecompetency",b,{identifier:"movecompetency",component:"tool_lp"},{identifier:"movecompetencyafter",component:"tool_lp"},"drag-samenode","drag-parentnode","drag-handlecontainer",function(b,c){a.handleDrop.call(a,b,c)})}).fail(b.exception)},h.prototype.handleDrop=function(d,e){var f=a(d).data("id"),g=a(e).data("id"),h=this,i=[];if("course"==h.itemtype)i=c.call([{methodname:"core_competency_reorder_course_competency",args:{courseid:h.itemid,competencyidfrom:f,competencyidto:g}}]);else if("template"==h.itemtype)i=c.call([{methodname:"core_competency_reorder_template_competency",args:{templateid:h.itemid,competencyidfrom:f,competencyidto:g}}]);else{if("plan"!=h.itemtype)return null;i=c.call([{methodname:"core_competency_reorder_plan_competency",args:{planid:h.itemid,competencyidfrom:f,competencyidto:g}}])}i[0].fail(b.exception)},h.prototype.pickCompetency=function(){var e,g,h,i,j=this;j.pickerInstance||(("template"===j.itemtype||"course"===j.itemtype)&&(i="parents"),j.pickerInstance=new f(j.pageContextId,!1,i),j.pickerInstance.on("save",function(f,i){var k=i.competencyIds;"course"===j.itemtype?(e=[],a.each(k,function(a,b){e.push({methodname:"core_competency_add_competency_to_course",args:{courseid:j.itemid,competencyid:b}})}),e.push({methodname:"tool_lp_data_for_course_competencies_page",args:{courseid:j.itemid}}),g="tool_lp/course_competencies_page",h="coursecompetenciespage"):"template"===j.itemtype?(e=[],a.each(k,function(a,b){e.push({methodname:"core_competency_add_competency_to_template",args:{templateid:j.itemid,competencyid:b}})}),e.push({methodname:"tool_lp_data_for_template_competencies_page",args:{templateid:j.itemid,pagecontext:{contextid:j.pageContextId}}}),g="tool_lp/template_competencies_page",h="templatecompetenciespage"):"plan"===j.itemtype&&(e=[],a.each(k,function(a,b){e.push({methodname:"core_competency_add_competency_to_plan",args:{planid:j.itemid,competencyid:b}})}),e.push({methodname:"tool_lp_data_for_plan_page",args:{planid:j.itemid}}),g="tool_lp/plan_page",h="plan-page"),c.call(e)[e.length-1].then(function(b){return d.render(g,b).done(function(b,c){a('[data-region="'+h+'"]').replaceWith(b),d.runTemplateJS(c)})},b.exception)})),j.pickerInstance.display()},h.prototype.doDelete=function(e){var f=this,g=[],h="",i="";"course"==f.itemtype?(g=c.call([{methodname:"core_competency_remove_competency_from_course",args:{courseid:f.itemid,competencyid:e}},{methodname:"tool_lp_data_for_course_competencies_page",args:{courseid:f.itemid}}]),h="tool_lp/course_competencies_page",i="coursecompetenciespage"):"template"==f.itemtype?(g=c.call([{methodname:"core_competency_remove_competency_from_template",args:{templateid:f.itemid,competencyid:e}},{methodname:"tool_lp_data_for_template_competencies_page",args:{templateid:f.itemid,pagecontext:{contextid:f.pageContextId}}}]),h="tool_lp/template_competencies_page",i="templatecompetenciespage"):"plan"==f.itemtype&&(g=c.call([{methodname:"core_competency_remove_competency_from_plan",args:{planid:f.itemid,competencyid:e}},{methodname:"tool_lp_data_for_plan_page",args:{planid:f.itemid}}]),h="tool_lp/plan_page",i="plan-page"),g[1].done(function(c){d.render(h,c).done(function(b,c){a('[data-region="'+i+'"]').replaceWith(b),d.runTemplateJS(c)}).fail(b.exception)}).fail(b.exception)},h.prototype.deleteHandler=function(a){var d,f=this,g=[];if("course"==f.itemtype)d="unlinkcompetencycourse";else if("template"==f.itemtype)d="unlinkcompetencytemplate";else{if("plan"!=f.itemtype)return;d="unlinkcompetencyplan"}g=c.call([{methodname:"core_competency_read_competency",args:{id:a}}]),g[0].done(function(c){e.get_strings([{key:"confirm",component:"moodle"},{key:d,component:"tool_lp",param:c.shortname},{key:"confirm",component:"moodle"},{key:"cancel",component:"moodle"}]).done(function(c){b.confirm(c[0],c[1],c[2],c[3],function(){f.doDelete(a)})}).fail(b.exception)}).fail(b.exception)},h.prototype.registerEvents=function(){var e=this;"course"==e.itemtype&&a('[data-region="coursecompetenciespage"]').on("change",'select[data-field="ruleoutcome"]',function(f){var g=[],h="tool_lp/course_competencies_page",i="coursecompetenciespage",j=a(f.target).data("id"),k=a(f.target).val();g=c.call([{methodname:"core_competency_set_course_competency_ruleoutcome",args:{coursecompetencyid:j,ruleoutcome:k}},{methodname:"tool_lp_data_for_course_competencies_page",args:{courseid:e.itemid}}]),g[1].done(function(c){d.render(h,c).done(function(b,c){a('[data-region="'+i+'"]').replaceWith(b),d.runTemplateJS(c)}).fail(b.exception)}).fail(b.exception)}),a('[data-region="actions"] button').click(function(a){a.preventDefault(),e.pickCompetency()}),a('[data-action="delete-competency-link"]').click(function(b){b.preventDefault();var c=a(b.target).closest("[data-id]").data("id");e.deleteHandler(c)})},h}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competency_outcomes.min.js b/admin/tool/lp/amd/build/competency_outcomes.min.js new file mode 100644 index 00000000000..2da17456896 --- /dev/null +++ b/admin/tool/lp/amd/build/competency_outcomes.min.js @@ -0,0 +1 @@ +define(["jquery","core/str"],function(a,b){var c=0,d=1,e=2,f=3;return{NONE:c,EVIDENCE:d,COMPLETE:e,RECOMMEND:f,getAll:function(){var a=this;return b.get_strings([{key:"competencyoutcome_none",component:"tool_lp"},{key:"competencyoutcome_evidence",component:"tool_lp"},{key:"competencyoutcome_recommend",component:"tool_lp"},{key:"competencyoutcome_complete",component:"tool_lp"}]).then(function(b){var c={};return c[a.NONE]={code:a.NONE,name:b[0]},c[a.EVIDENCE]={code:a.EVIDENCE,name:b[1]},c[a.RECOMMEND]={code:a.RECOMMEND,name:b[2]},c[a.COMPLETE]={code:a.COMPLETE,name:b[3]},c})},getString:function(b){var c=this,d=c.getAll();return d.then(function(c){return"undefined"==typeof c[b]?a.Deferred().reject().promise():c[b].name})}}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competency_rule.min.js b/admin/tool/lp/amd/build/competency_rule.min.js new file mode 100644 index 00000000000..8939576cf60 --- /dev/null +++ b/admin/tool/lp/amd/build/competency_rule.min.js @@ -0,0 +1 @@ +define(["jquery"],function(a){var b=function(b){this._eventNode=a("
"),this._ready=a.Deferred(),this._tree=b};return b.prototype._competency=null,b.prototype._eventNode=null,b.prototype._ready=null,b.prototype._tree=null,b.prototype.canConfig=function(){return this._tree.hasChildren(this._competency.id)},b.prototype.getConfig=function(){return null},b.prototype.getType=function(){throw new Error("Not implemented")},b.prototype.init=function(){return this._load()},b.prototype.injectTemplate=function(){return a.Deferred().reject().promise()},b.prototype.isValid=function(){return!1},b.prototype._load=function(){return a.when()},b.prototype.on=function(a,b){this._eventNode.on(a,b)},b.prototype.setTargetCompetency=function(a){this._competency=a},b.prototype._trigger=function(a,b){this._eventNode.trigger(a,[b])},b.prototype._triggerChange=function(){this._trigger("change",this)},b}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competency_rule_all.min.js b/admin/tool/lp/amd/build/competency_rule_all.min.js new file mode 100644 index 00000000000..d95f6a0732b --- /dev/null +++ b/admin/tool/lp/amd/build/competency_rule_all.min.js @@ -0,0 +1 @@ +define(["jquery","core/str","tool_lp/competency_rule"],function(a,b,c){var d=function(){c.apply(this,arguments)};return d.prototype=Object.create(c.prototype),d.prototype.getType=function(){return"core_competency\\competency_rule_all"},d.prototype.isValid=function(){return!0},d}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competency_rule_points.min.js b/admin/tool/lp/amd/build/competency_rule_points.min.js new file mode 100644 index 00000000000..39eaef7286f --- /dev/null +++ b/admin/tool/lp/amd/build/competency_rule_points.min.js @@ -0,0 +1 @@ +define(["jquery","core/str","core/templates","tool_lp/competency_rule"],function(a,b,c,d){var e=function(){d.apply(this,arguments)};return e.prototype=Object.create(d.prototype),e.prototype._container=null,e.prototype._templateLoaded=!1,e.prototype.getConfig=function(){return JSON.stringify({base:{points:this._getRequiredPoints()},competencies:this._getCompetenciesConfig()})},e.prototype._getCompetenciesConfig=function(){var b=[];return this._container.find("[data-competency]").each(function(){var c=a(this),d=c.data("competency"),e=parseInt(c.find('[name="points"]').val(),10),f=c.find('[name="required"]').prop("checked");b.push({id:d,points:e,required:f?1:0})}),b},e.prototype._getRequiredPoints=function(){return parseInt(this._container.find('[name="requiredpoints"]').val()||1,10)},e.prototype.getType=function(){return"core_competency\\competency_rule_points"},e.prototype.injectTemplate=function(b){var d,e=this,f=this._tree.getChildren(this._competency.id),g={base:{points:2},competencies:[]};if(this._templateLoaded=!1,e._competency.ruletype==e.getType())try{g=JSON.parse(e._competency.ruleconfig)}catch(h){}return d={requiredpoints:g&&g.base?g.base.points:2,competency:e._competency,children:[]},a.each(f,function(b,c){var e={id:c.id,shortname:c.shortname,required:!1,points:0};g&&a.each(g.competencies,function(a,b){b.id==e.id&&(e.required=b.required?!0:!1,e.points=b.points)}),d.children.push(e)}),c.render("tool_lp/competency_rule_points",d).then(function(a){e._container=b,b.html(a),b.find("input").change(function(){e._triggerChange()}),e._templateLoaded=!0,e._triggerChange()})},e.prototype.isValid=function(){if(!this._templateLoaded)return!1;var b=this._getRequiredPoints(),c=0,d=!0;return a.each(this._getCompetenciesConfig(),function(a,b){b.points<0&&(d=!1),c+=b.points}),d=d&&c>=b},e}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competencyactions.min.js b/admin/tool/lp/amd/build/competencyactions.min.js new file mode 100644 index 00000000000..76bb6777249 --- /dev/null +++ b/admin/tool/lp/amd/build/competencyactions.min.js @@ -0,0 +1 @@ +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(a,b,c,d,e,f,g,h,i,j,k,l,m){var n,o,p,q,r,s,t=null,u=null,v=null,w=null,x=function(){var c=a('[data-region="competencyactions"]').data("competency"),f={competencyframeworkid:t.getCompetencyFrameworkId(),pagecontextid:n};null!==c&&(f.parentid=c.id);var g=function(){var c=a.param(f);window.location=b.relativeUrl("/admin/tool/lp/editcompetency.php?"+c)};null!==c&&t.hasRule(c.id)?e.get_strings([{key:"confirm",component:"moodle"},{key:"addingcompetencywillresetparentrule",component:"tool_lp",param:c.shortname},{key:"yes",component:"core"},{key:"no",component:"core"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],g)}).fail(d.exception):g()},y=function(){var b=a('[data-region="filtercompetencies"]').data("frameworkid"),c=f.call([{methodname:"core_competency_set_parent_competency",args:{competencyid:u,parentid:v}},{methodname:"tool_lp_data_for_competencies_manage_page",args:{competencyframeworkid:b,search:a('[data-region="filtercompetencies"] input').val()}}]);c[1].done(E).fail(d.exception)},z=function(){if(v="undefined"==typeof v?0:v,v!=u){var a=t.getCompetency(v)||{},b=t.getCompetency(u)||{},c="movecompetencywillresetrules",f=!1;b.parentid!=v&&(a.path&&a.path.indexOf("/"+b.id+"/")>=0&&(c="movecompetencytochildofselfwillresetrules",f=f||t.hasRule(b.id)),f=f||t.hasRule(a.id)||t.hasRule(b.parentid),f?e.get_strings([{key:"confirm",component:"moodle"},{key:c,component:"tool_lp"},{key:"yes",component:"moodle"},{key:"no",component:"moodle"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],y)}).fail(d.exception):y())}},A=function(b){var c=a(b.getContent()),d=c.find("[data-enhance=movetree]"),e=new h(d,!1);e.on("selectionchanged",function(b,c){var d=c.selected;v=a(d).data("id")}),d.show(),c.on("click",'[data-action="move"]',function(){b.close(),z()}),c.on("click",'[data-action="cancel"]',function(){b.close()})},B=function(a,b){var c;for(c=0;cspan",O).on("dragover","li>span",P).on("dragenter","li>span",Q).on("dragleave","li>span",R).on("drop","li>span",S),b.on("selectionchanged",_),p=new m(t,s),p.on("save",L.bind(this))}}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competencydialogue.min.js b/admin/tool/lp/amd/build/competencydialogue.min.js new file mode 100644 index 00000000000..b22cdb31c7b --- /dev/null +++ b/admin/tool/lp/amd/build/competencydialogue.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/ajax","core/templates","core/str","tool_lp/dialogue"],function(a,b,c,d,e,f){var g=function(b){this.options={includerelated:!1,includecourses:!1},a.extend(this.options,b)};return g.prototype.triggerCompetencyViewedEvent=function(a){c.call([{methodname:"core_competency_competency_viewed",args:{id:a}}])},g.prototype.enhanceDialogue=function(a){var b=new g({includerelated:!1});b.watch(a.getContent())},g.prototype.showDialogue=function(a){var c=this.getCompetencyDataPromise(a),e=this;c.done(function(c){d.render("tool_lp/competency_summary",c).done(function(b){e.triggerCompetencyViewedEvent(a),new f(c.competency.shortname,b,e.enhanceDialogue)}).fail(b.exception)}).fail(b.exception)},g.prototype.showDialogueFromData=function(a){var c=this;d.render("tool_lp/competency_summary",a).done(function(b){c.triggerCompetencyViewedEvent(a.id),new f(a.shortname,b,c.enhanceDialogue)}).fail(b.exception)},g.prototype.clickEventHandler=function(b){var c=b.data.compdialogue,d=a(b.currentTarget).data("id");c.showDialogue(d),b.preventDefault()},g.prototype.getCompetencyDataPromise=function(a){var d=c.call([{methodname:"tool_lp_data_for_competency_summary",args:{competencyid:a,includerelated:this.options.includerelated,includecourses:this.options.includecourses}}]);return d[0].then(function(a){return a}).fail(b.exception)},g.prototype.watch=function(b){a(b).off("click",'[data-action="competency-dialogue"]',this.clickEventHandler),a(b).on("click",'[data-action="competency-dialogue"]',{compdialogue:this},this.clickEventHandler)},g}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competencypicker.min.js b/admin/tool/lp/amd/build/competencypicker.min.js new file mode 100644 index 00000000000..61eec30e64b --- /dev/null +++ b/admin/tool/lp/amd/build/competencypicker.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/ajax","core/templates","tool_lp/dialogue","core/str","tool_lp/tree"],function(a,b,c,d,e,f,g){var h=function(b,c,d,e){var f=this;f._eventNode=a("
"),f._frameworks=[],f._reset(),f._pageContextId=b,f._pageContextIncludes=d||"children",f._multiSelect="undefined"==typeof e||e===!0,c&&(f._frameworkId=c,f._singleFramework=!0)};return h.prototype._competencies=null,h.prototype._disallowedCompetencyIDs=null,h.prototype._eventNode=null,h.prototype._frameworks=null,h.prototype._frameworkId=null,h.prototype._pageContextId=null,h.prototype._pageContextIncludes=null,h.prototype._popup=null,h.prototype._searchText="",h.prototype._selectedCompetencies=null,h.prototype._singleFramework=!1,h.prototype._multiSelect=!0,h.prototype._onlyVisible=!0,h.prototype._afterRender=function(){var b=this,c=new g(b._find("[data-enhance=linktree]"),b._multiSelect);b._find("[data-enhance=linktree]").show(),c.on("selectionchanged",function(c,d){var e=d.selected;c.preventDefault();var f=[];a.each(e,function(c,d){var e=a(d).data("id"),g=!0;"undefined"==typeof e?g=!1:a.each(b._disallowedCompetencyIDs,function(a,b){b==e&&(g=!1)}),g&&f.push(e)}.bind(b)),b._selectedCompetencies=f,b._selectedCompetencies.length?b._find('[data-region="competencylinktree"] [data-action="add"]').removeAttr("disabled"):b._find('[data-region="competencylinktree"] [data-action="add"]').attr("disabled","disabled")}.bind(b)),b._singleFramework||b._find('[data-action="chooseframework"]').change(function(c){b._frameworkId=a(c.target).val(),b._loadCompetencies().then(b._refresh.bind(b))}.bind(b)),b._find('[data-region="filtercompetencies"] button').click(function(c){return c.preventDefault(),a(c.target).attr("disabled","disabled"),b._searchText=b._find('[data-region="filtercompetencies"] input').val()||"",b._refresh().always(function(){a(c.target).removeAttr("disabled")})}.bind(b)),b._find('[data-region="competencylinktree"] [data-action="cancel"]').click(function(a){a.preventDefault(),b.close()}.bind(b)),b._find('[data-region="competencylinktree"] [data-action="add"]').click(function(a){a.preventDefault(),b._selectedCompetencies.length&&(b._multiSelect?b._trigger("save",{competencyIds:b._selectedCompetencies}):b._trigger("save",{competencyId:b._selectedCompetencies[0]}),b.close())}.bind(b));var d=b._selectedCompetencies.slice(0);a.each(d,function(a,d){var e=b._find("[data-id="+d+"]");e.length&&(c.toggleItem(e),c.updateFocus(e))}.bind(b))},h.prototype.close=function(){var a=this;a._popup.close(),a._reset()},h.prototype.display=function(){var a=this;return a._render().then(function(b){return f.get_string("competencypicker","tool_lp").then(function(c){a._popup=new e(c,b,a._afterRender.bind(a))}.bind(a))}.bind(a)).fail(b.exception)},h.prototype._fetchCompetencies=function(a,d){var e=this;return c.call([{methodname:"core_competency_search_competencies",args:{searchtext:d,competencyframeworkid:a}}])[0].done(function(a){function b(a,c){for(var d=0;d0?a.when():(d=e._singleFramework?c.call([{methodname:"core_competency_read_competency_framework",args:{id:this._frameworkId}}])[0].then(function(a){return[a]}):c.call([{methodname:"core_competency_list_competency_frameworks",args:{sort:"shortname",context:{contextid:e._pageContextId},includes:e._pageContextIncludes,onlyvisible:e._onlyVisible}}])[0],d.done(function(a){e._frameworks=a}).fail(b.exception))},h.prototype.on=function(a,b){this._eventNode.on(a,b)},h.prototype._preRender=function(){var b=this;return b._loadFrameworks().then(function(){return!b._frameworkId&&b._frameworks.length>0&&(b._frameworkId=b._frameworks[0].id),b._frameworkId?b._loadCompetencies():(b._frameworks=[],a.when())}.bind(b))},h.prototype._refresh=function(){var a=this;return a._render().then(function(b){a._find('[data-region="competencylinktree"]').replaceWith(b),a._afterRender()}.bind(a))},h.prototype._render=function(){var b=this;return b._preRender().then(function(){b._singleFramework||a.each(b._frameworks,function(a,c){c.id==b._frameworkId?c.selected=!0:c.selected=!1});var c={competencies:b._competencies,framework:b._getFramework(b._frameworkId),frameworks:b._frameworks,search:b._searchText,singleFramework:b._singleFramework};return d.render("tool_lp/competency_picker",c)}.bind(b))},h.prototype._reset=function(){this._competencies=[],this._disallowedCompetencyIDs=[],this._popup=null,this._searchText="",this._selectedCompetencies=[]},h.prototype.setDisallowedCompetencyIDs=function(a){this._disallowedCompetencyIDs=a},h.prototype._trigger=function(a,b){this._eventNode.trigger(a,[b])},h}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competencypicker_user_plans.min.js b/admin/tool/lp/amd/build/competencypicker_user_plans.min.js new file mode 100644 index 00000000000..faf4bd410bb --- /dev/null +++ b/admin/tool/lp/amd/build/competencypicker_user_plans.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/ajax","core/templates","core/str","tool_lp/tree","tool_lp/competencypicker"],function(a,b,c,d,e,f,g){var h=function(a,b,c){g.prototype.constructor.apply(this,[1,!1,"self",c]),this._userId=a,this._plans=[],b&&(this._planId=b,this._singlePlan=!0)};return h.prototype=Object.create(g.prototype),h.prototype._plans=null,h.prototype._planId=null,h.prototype._singlePlan=!1,h.prototype._userId=null,h.prototype._afterRender=function(){var b=this;g.prototype._afterRender.apply(b,arguments),b._singlePlan||b._find('[data-action="chooseplan"]').change(function(c){b._planId=a(c.target).val(),b._loadCompetencies().then(b._refresh.bind(b))}.bind(b))},h.prototype._fetchCompetencies=function(a,d){var e=this;return c.call([{methodname:"core_competency_list_plan_competencies",args:{id:a}}])[0].done(function(a){var b,c,f=[];for(b=0;b0?a.when():(d=e._singlePlan?c.call([{methodname:"core_competency_read_plan",args:{id:this._planId}}])[0].then(function(a){return[a]}):c.call([{methodname:"core_competency_list_user_plans",args:{userid:e._userId}}])[0],d.done(function(a){e._plans=a}).fail(b.exception))},h.prototype._preRender=function(){var b=this;return b._loadPlans().then(function(){return!b._planId&&b._plans.length>0&&(b._planId=b._plans[0].id),b._planId?b._loadCompetencies():(b._plans=[],a.when())}.bind(b))},h.prototype._render=function(){var b=this;return b._preRender().then(function(){b._singlePlan||a.each(b._plans,function(a,c){c.id==b._planId?c.selected=!0:c.selected=!1});var c={competencies:b._competencies,plan:b._getPlan(b._planId),plans:b._plans,search:b._searchText,singlePlan:b._singlePlan};return d.render("tool_lp/competency_picker_user_plans",c)}.bind(b))},h}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competencyruleconfig.min.js b/admin/tool/lp/amd/build/competencyruleconfig.min.js new file mode 100644 index 00000000000..8a0f0a2d019 --- /dev/null +++ b/admin/tool/lp/amd/build/competencyruleconfig.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/templates","tool_lp/dialogue","tool_lp/competency_outcomes","core/str"],function(a,b,c,d,e,f){var g=function(b,c){this._eventNode=a("
"),this._tree=b,this._rulesModules=c,this._setUp()};return g.prototype._competency=null,g.prototype._eventNode=null,g.prototype._outcomesOption=null,g.prototype._popup=null,g.prototype._ready=null,g.prototype._rules=null,g.prototype._rulesModules=null,g.prototype._tree=null,g.prototype._afterChange=function(){this._isValid()?this._find('[data-action="save"]').prop("disabled",!1):this._find('[data-action="save"]').prop("disabled",!0)},g.prototype._afterRuleConfigChange=function(a,b){b==this._getRule()&&this._afterChange()},g.prototype._afterRender=function(){var a=this;a._find('[name="outcome"]').on("change",function(){a._switchedOutcome()}).trigger("change"),a._find('[name="rule"]').on("change",function(){a._switchedRule()}).trigger("change"),a._find('[data-action="save"]').on("click",function(){a._trigger("save",a._getConfig()),a.close()}),a._find('[data-action="cancel"]').on("click",function(){a.close()})},g.prototype.canBeConfigured=function(){var b=!1;return a.each(this._rules,function(a,c){return c.canConfig()?(b=!0,!1):void 0}),b},g.prototype.close=function(){this._popup.close(),this._popup=null},g.prototype.display=function(){var a=this;if(a._competency)return a._render().then(function(b){return f.get_string("competencyrule","tool_lp").then(function(c){a._popup=new d(c,b,a._afterRender.bind(a))})}).fail(b.exception)},g.prototype._find=function(b){return a(this._popup.getContent()).find(b)},g.prototype._getApplicableOutcomesOptions=function(){var b=this,c=[];return a.each(b._outcomesOption,function(a,d){c.push({code:d.code,name:d.name,selected:d.code==b._competency.ruleoutcome?!0:!1})}),c},g.prototype._getApplicableRulesOptions=function(){var b=this,c=[];return a.each(b._rules,function(a,d){d.canConfig()&&c.push({name:b._getRuleName(d.getType()),type:d.getType(),selected:d.getType()==b._competency.ruletype?!0:!1})}),c},g.prototype._getConfig=function(){var a=this._getRule();return{ruletype:a?a.getType():null,ruleconfig:a?a.getConfig():null,ruleoutcome:this._getOutcome()}},g.prototype._getOutcome=function(){return this._find('[name="outcome"]').val()},g.prototype._getRule=function(){var b,c=this._find('[name="rule"]').val();return a.each(this._rules,function(a,d){return d.getType()==c?(b=d,!1):void 0}),b},g.prototype._getRuleName=function(b){var c,d=this;return a.each(d._rulesModules,function(a,d){return d.type==b?(c=d.name,!1):void 0}),c},g.prototype._initOutcomes=function(){var a=this;return e.getAll().then(function(b){a._outcomesOption=b})},g.prototype._initRules=function(){var b=this,c=[];return a.each(b._rules,function(d,e){var f=e.init().then(function(){e.setTargetCompetency(b._competency),e.on("change",b._afterRuleConfigChange.bind(b))},function(){return b._rules.splice(d,1),a.when()});c.push(f)}),a.when.apply(a.when,c)},g.prototype._isValid=function(){var a=this._getOutcome(),b=this._getRule();return a==e.NONE?!0:b?b.isValid():!1},g.prototype.on=function(a,b){this._eventNode.on(a,b)},g.prototype._preRender=function(){return this.ready()},g.prototype.ready=function(){return this._ready.promise()},g.prototype._render=function(){var a=this;return this._preRender().then(function(){var b;a.canBeConfigured()?(b={},b.outcomes=a._getApplicableOutcomesOptions(),b.rules=a._getApplicableRulesOptions()):b=!1;var d={competencyshortname:a._competency.shortname,config:b};return c.render("tool_lp/competency_rule_config",d)})},g.prototype.setTargetCompetencyId=function(b){var c=this;c._competency=c._tree.getCompetency(b),a.each(c._rules,function(a,b){b.setTargetCompetency(c._competency)})},g.prototype._setUp=function(){var b=this,c=[],d=[];b._ready=a.Deferred(),b._rules=[],a.each(b._rulesModules,function(a,b){d.push(b.amd)}),require(d,function(){a.each(arguments,function(a,c){var d=new c(b._tree);b._rules.push(d)}),c.push(b._initRules()),c.push(b._initOutcomes()),a.when.apply(a.when,c).always(function(){b._ready.resolve()})})},g.prototype._switchedOutcome=function(){var a=this,b=a._getOutcome();return b==e.NONE?(a._find('[data-region="rule-type"]').hide().find('[name="rule"]').val(-1),a._find('[data-region="rule-config"]').empty().hide(),void a._afterChange()):(a._find('[data-region="rule-type"]').show(),a._find('[data-region="rule-config"]').show(),void a._afterChange())},g.prototype._switchedRule=function(){var a=this,b=a._find('[data-region="rule-config"]'),c=a._getRule();return c?void c.injectTemplate(b).then(function(){b.show()},function(){b.empty().hide()}).always(function(){a._afterChange()}):(b.empty().hide(),void a._afterChange())},g.prototype._trigger=function(a,b){this._eventNode.trigger(a,[b])},g}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/competencytree.min.js b/admin/tool/lp/amd/build/competencytree.min.js new file mode 100644 index 00000000000..4955f991808 --- /dev/null +++ b/admin/tool/lp/amd/build/competencytree.min.js @@ -0,0 +1 @@ +define(["core/ajax","core/notification","core/templates","tool_lp/tree","tool_lp/competency_outcomes","jquery"],function(a,b,c,d,e,f){var g={},h=0,i="",j="",k="",l=!1,m=function(a,b){var c=0,d=!1;for(a.haschildren=!1,a.children=[],c=0;c0},hasRule:function(a){var b=this.getCompetency(a);return b?b.ruleoutcome!=e.OUTCOME_NONE&&b.ruletype:!1},reloadCompetencies:function(){return n("").fail(b.exception)},listCompetencies:function(){return g}}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/course_competency_settings.min.js b/admin/tool/lp/amd/build/course_competency_settings.min.js new file mode 100644 index 00000000000..5fe246e85db --- /dev/null +++ b/admin/tool/lp/amd/build/course_competency_settings.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","tool_lp/dialogue","core/str","core/ajax","core/templates"],function(a,b,c,d,e,f){var g=function(b){a(b).on("click",this.configureSettings.bind(this))};return g.prototype._dialogue=null,g.prototype.configureSettings=function(e){var g=a(e.target).closest("a").data("courseid"),h=a(e.target).closest("a").data("pushratingstouserplans"),i={courseid:g,settings:{pushratingstouserplans:h}};e.preventDefault(),f.render("tool_lp/course_competency_settings",i).done(function(a){d.get_string("configurecoursecompetencysettings","tool_lp").done(function(b){this._dialogue=new c(b,a,this.addListeners.bind(this))}.bind(this)).fail(b.exception)}.bind(this)).fail(b.exception)},g.prototype.addListeners=function(){var a=this._find('[data-action="save"]');a.on("click",this.saveSettings.bind(this));var b=this._find('[data-action="cancel"]');b.on("click",this.cancelChanges.bind(this))},g.prototype.cancelChanges=function(a){a.preventDefault(),this._dialogue.close()},g.prototype._find=function(b){return a('[data-region="coursecompetencysettings"]').find(b)},g.prototype.saveSettings=function(a){a.preventDefault();var c=this._find('input[name="pushratingstouserplans"]:checked').val(),d=this._find('input[name="courseid"]').val(),f={pushratingstouserplans:c};e.call([{methodname:"core_competency_update_course_competency_settings",args:{courseid:d,settings:f}}])[0].done(function(){this.refreshCourseCompetenciesPage()}.bind(this)).fail(b.exception)},g.prototype.refreshCourseCompetenciesPage=function(){var c=this._find('input[name="courseid"]').val();e.call([{methodname:"tool_lp_data_for_course_competencies_page",args:{courseid:c}}])[0].done(function(c){f.render("tool_lp/course_competencies_page",c).done(function(b,c){a('[data-region="coursecompetenciespage"]').replaceWith(b),f.runTemplateJS(c),this._dialogue.close()}.bind(this)).fail(b.exception)}.bind(this)).fail(b.exception)},g}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/coursecompetencies.min.js b/admin/tool/lp/amd/build/coursecompetencies.min.js new file mode 100644 index 00000000000..2fe295faff0 --- /dev/null +++ b/admin/tool/lp/amd/build/coursecompetencies.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/ajax","core/templates","tool_lp/dialogue","core/str","tool_lp/tree","tool_lp/dragdrop-reorder"],function(a,b,c,d,e,f,g,h){var i=function(e){this.courseid=e,this.selectedCompetency=0;var f=this,g=c.call([{methodname:"core_competency_list_competency_frameworks",args:{filters:{},sort:"sortorder"}}]);g[0].done(function(c){return f.frameworks=c,0===c.length?void d.render("tool_lp/no_frameworks_warning",{}).done(function(b){a('[data-region="actions"]').append(b),a('[data-region="actions"] button').hide()}).fail(b.exception):(a('[data-region="actions"] button').show(),f.registerEvents(),void f.registerDragDrop())}).fail(b.exception)};return i.prototype.registerDragDrop=function(){var a=this;f.get_string("movecoursecompetency","tool_lp").done(function(b){h.dragdrop("movecoursecompetency",b,{identifier:"movecoursecompetency",component:"tool_lp"},{identifier:"movecoursecompetencyafter",component:"tool_lp"},"drag-samenode","drag-parentnode","drag-handlecontainer",function(b,c){a.handleDrop.call(a,b,c)})}).fail(b.exception)},i.prototype.handleDrop=function(d,e){var f=a(d).data("id"),g=a(e).data("id"),h=this,i=c.call([{methodname:"core_competency_reorder_course_competency",args:{courseid:h.courseid,competencyidfrom:f,competencyidto:g}}]);i[0].fail(b.exception)},i.prototype.applyFilter=function(c){c.preventDefault();var e=this,f=a('[data-region="filtercompetencies"] input'),g=f.val(),h=a('[data-action="chooseframework"]'),i=h.val();this.searchCompetencies().done(function(c){var f=0,h=e.frameworks[0];for(f=0;f
")};return b.prototype._eventNode=null,b.prototype.on=function(a,b){this._eventNode.on(a,b)},b.prototype._trigger=function(a,b){this._eventNode.trigger(a,[b])},b}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/evidence_delete.min.js b/admin/tool/lp/amd/build/evidence_delete.min.js new file mode 100644 index 00000000000..aebb3f1b58e --- /dev/null +++ b/admin/tool/lp/amd/build/evidence_delete.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/ajax","core/str","core/log"],function(a,b,c,d,e){var f={},g=function(g,h){"undefined"==typeof f[g]&&(f[g]=a("body").delegate(g,"click",function(f){var g=a(f.currentTarget).parents(h);if(!g.length||g.length>1)return void e.error("None or too many evidence container were found.");var i=g.data("id");return i?(f.preventDefault(),f.stopPropagation(),void d.get_strings([{key:"confirm",component:"moodle"},{key:"areyousure",component:"moodle"},{key:"delete",component:"moodle"},{key:"cancel",component:"moodle"}]).done(function(a){b.confirm(a[0],a[1],a[2],a[3],function(){var a=c.call([{methodname:"core_competency_delete_evidence",args:{id:i}}]);a[0].then(function(){g.remove()}).fail(b.exception)})}).fail(b.exception)):void e.error("Evidence ID was not found.")}))};return{register:g}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/form-cohort-selector.min.js b/admin/tool/lp/amd/build/form-cohort-selector.min.js new file mode 100644 index 00000000000..b582a15777b --- /dev/null +++ b/admin/tool/lp/amd/build/form-cohort-selector.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/templates"],function(a,b,c){return{processResults:function(b,c){var d=[];return a.each(c,function(a,b){d.push({value:b.id,label:b._label})}),d},transport:function(d,e,f,g){var h,i=parseInt(a(d).data("contextid"),10),j=a(d).data("includes");h=b.call([{methodname:"tool_lp_search_cohorts",args:{query:e,context:{contextid:i},includes:j}}]),h[0].then(function(b){var d=[],e=0;return a.each(b.cohorts,function(a,b){d.push(c.render("tool_lp/form-cohort-selector-suggestion",b))}),a.when.apply(a.when,d).then(function(){var c=arguments;a.each(b.cohorts,function(a,b){b._label=c[e],e++}),f(b.cohorts)})},g)}}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/form-user-selector.min.js b/admin/tool/lp/amd/build/form-user-selector.min.js new file mode 100644 index 00000000000..d228806883a --- /dev/null +++ b/admin/tool/lp/amd/build/form-user-selector.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/templates"],function(a,b,c){return{processResults:function(b,c){var d=[];return a.each(c,function(a,b){d.push({value:b.id,label:b._label})}),d},transport:function(d,e,f,g){var h,i=a(d).data("capability");"undefined"==typeof i&&(i=""),h=b.call([{methodname:"tool_lp_search_users",args:{query:e,capability:i}}]),h[0].then(function(b){var d=[],e=0;return a.each(b.users,function(b,e){var f=e,g=[];a.each(["idnumber","email","phone1","phone2","department","institution"],function(a,b){"undefined"!=typeof e[b]&&""!==e[b]&&(f.hasidentity=!0,g.push(e[b]))}),f.identity=g.join(", "),d.push(c.render("tool_lp/form-user-selector-suggestion",f))}),a.when.apply(a.when,d).then(function(){var c=arguments;a.each(b.users,function(a,b){b._label=c[e],e++}),f(b.users)})},g)}}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/frameworkactions.min.js b/admin/tool/lp/amd/build/frameworkactions.min.js new file mode 100644 index 00000000000..32c694da748 --- /dev/null +++ b/admin/tool/lp/amd/build/frameworkactions.min.js @@ -0,0 +1 @@ +define(["jquery","core/templates","core/ajax","core/notification","core/str"],function(a,b,c,d,e){var f=0,g=0,h=function(c,d){a('[data-region="managecompetencies"]').replaceWith(c),b.runTemplateJS(d)},i=function(a){b.render("tool_lp/manage_competency_frameworks_page",a).done(h).fail(d.exception)},j=function(b){b.preventDefault(),g=a(this).attr("data-frameworkid");var e=c.call([{methodname:"core_competency_duplicate_competency_framework",args:{id:g}},{methodname:"tool_lp_data_for_competency_frameworks_manage_page",args:{pagecontext:{contextid:f}}}]);e[1].done(i).fail(d.exception)},k=function(){var a=c.call([{methodname:"core_competency_delete_competency_framework",args:{id:g}},{methodname:"tool_lp_data_for_competency_frameworks_manage_page",args:{pagecontext:{contextid:f}}}]);a[0].done(function(a){if(a===!1){var b=c.call([{methodname:"core_competency_read_competency_framework",args:{id:g}}]);b[0].done(function(a){e.get_strings([{key:"frameworkcannotbedeleted",component:"tool_lp",param:a.shortname},{key:"cancel",component:"moodle"}]).done(function(a){d.alert(null,a[0])}).fail(d.exception)})}}).fail(d.exception),a[1].done(i).fail(d.exception)},l=function(b){b.preventDefault();var f=a(this).attr("data-frameworkid");g=f;var h=c.call([{methodname:"core_competency_read_competency_framework",args:{id:g}}]);h[0].done(function(a){e.get_strings([{key:"confirm",component:"moodle"},{key:"deletecompetencyframework",component:"tool_lp",param:a.shortname},{key:"delete",component:"moodle"},{key:"cancel",component:"moodle"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],k)}).fail(d.exception)}).fail(d.exception)};return{deleteHandler:l,duplicateHandler:j,init:function(a){f=a}}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/frameworkdelete.min.js b/admin/tool/lp/amd/build/frameworkdelete.min.js new file mode 100644 index 00000000000..d1a3e8c420d --- /dev/null +++ b/admin/tool/lp/amd/build/frameworkdelete.min.js @@ -0,0 +1 @@ +define(["jquery","core/templates","core/ajax","core/notification","core/str"],function(a,b,c,d,e){var f=0,g=0,h=function(c,d){a('[data-region="managecompetencies"]').replaceWith(c),b.runTemplateJS(d)},i=function(a){b.render("tool_lp/manage_competency_frameworks_page",a).done(h).fail(d.exception)},j=function(){var a=c.call([{methodname:"core_competency_delete_competency_framework",args:{id:g}},{methodname:"tool_lp_data_for_competency_frameworks_manage_page",args:{pagecontext:{contextid:f}}}]);a[1].done(i).fail(d.exception)},k=function(b){b.preventDefault();var f=a(this).attr("data-frameworkid");g=f;var h=c.call([{methodname:"core_competency_read_competency_framework",args:{id:g}}]);h[0].done(function(a){e.get_strings([{key:"confirm",component:"moodle"},{key:"deletecompetencyframework",component:"tool_lp",param:a.shortname},{key:"delete",component:"moodle"},{key:"cancel",component:"moodle"}]).done(function(a){d.confirm(a[0],a[1],a[2],a[3],j)}).fail(d.exception)}).fail(d.exception)};return{deleteHandler:k,init:function(a){f=a}}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/frameworks_datasource.min.js b/admin/tool/lp/amd/build/frameworks_datasource.min.js new file mode 100644 index 00000000000..cfc5c70bddb --- /dev/null +++ b/admin/tool/lp/amd/build/frameworks_datasource.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/notification"],function(a,b,c){return{list:function(d,e){var f,g={context:{contextid:d}};return a.extend(g,"undefined"==typeof e?{}:e),f=b.call([{methodname:"core_competency_list_competency_frameworks",args:g}])[0],f.fail(c.exception)},processResults:function(b,c){var d=[];return a.each(c,function(a,b){d.push({value:b.id,label:b.shortname+" "+b.idnumber})}),d},transport:function(b,c,d){var e=a(b),f=e.data("contextid"),g=e.data("onlyvisible");if(!f)throw new Error("The attribute data-contextid is required on "+b);this.list(f,{query:c,onlyvisible:g}).then(d)}}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/grade_dialogue.min.js b/admin/tool/lp/amd/build/grade_dialogue.min.js new file mode 100644 index 00000000000..6bfa3dfeec8 --- /dev/null +++ b/admin/tool/lp/amd/build/grade_dialogue.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/templates","tool_lp/dialogue","tool_lp/event_base","core/str"],function(a,b,c,d,e,f){var g=function(a){e.prototype.constructor.apply(this,[]),this._ratingOptions=a};return g.prototype=Object.create(e.prototype),g.prototype._popup=null,g.prototype._ratingOptions=null,g.prototype._afterRender=function(){var b=this._find('[data-action="rate"]'),c=this._find('[name="rating"]'),d=this._find('[name="comment"]');this._find('[data-action="cancel"]').click(function(a){a.preventDefault(),this._trigger("cancelled"),this.close()}.bind(this)),c.change(function(){var c=a(this);c.val()?b.prop("disabled",!1):b.prop("disabled",!0)}).change(),b.click(function(a){a.preventDefault();var b=c.val();b&&(this._trigger("rated",{rating:b,note:d.val()}),this.close())}.bind(this))},g.prototype.close=function(){this._popup.close(),this._popup=null},g.prototype.display=function(){return this._render().then(function(a){return f.get_string("rate","tool_lp").then(function(b){this._popup=new d(b,a,this._afterRender.bind(this))}.bind(this))}.bind(this)).fail(b.exception)},g.prototype._find=function(b){return a(this._popup.getContent()).find(b)},g.prototype._render=function(){var a={cangrade:this._canGrade,ratings:this._ratingOptions};return c.render("tool_lp/competency_grader",a)},g}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/grade_user_competency_inline.min.js b/admin/tool/lp/amd/build/grade_user_competency_inline.min.js new file mode 100644 index 00000000000..ac0c28fcfff --- /dev/null +++ b/admin/tool/lp/amd/build/grade_user_competency_inline.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/ajax","core/log","tool_lp/grade_dialogue","tool_lp/event_base","tool_lp/scalevalues"],function(a,b,c,d,e,f,g){var h=function(b,c,d,e,g,h,i){f.prototype.constructor.apply(this,[]);var j=a(b);if(!j.length)throw new Error("Could not find the trigger");this._scaleId=c,this._competencyId=d,this._userId=e,this._planId=g,this._courseId=h,this._chooseStr=i,this._setUp(),j.click(function(a){a.preventDefault(),this._dialogue.display()}.bind(this)),this._planId?(this._methodName="core_competency_grade_competency_in_plan",this._args={competencyid:this._competencyId,planid:this._planId}):this._courseId?(this._methodName="core_competency_grade_competency_in_course",this._args={competencyid:this._competencyId,courseid:this._courseId,userid:this._userId}):(this._methodName="core_competency_grade_competency",this._args={userid:this._userId,competencyid:this._competencyId})};return h.prototype=Object.create(f.prototype),h.prototype._setUp=function(){var a=[],d=this,f=g.get_values(d._scaleId);f.done(function(f){a.push({value:"",name:d._chooseStr});for(var g=0;g0){var h=a.proxy(d,e);g=h(f)===!1||f.isDefaultPrevented()}}),g||"#"===e.attr("href")||(window.location.href=e.attr("href"))}return!1},e.prototype.handleFocus=function(a){if(null===this.activeItem)this.activeItem=a;else if(a[0]!=this.activeItem[0])return!0;var b=this.activeItem.parentsUntil("ul.tool-lp-menu").filter("li");if(this.allItems.removeClass("menu-focus"),this.activeItem.addClass("menu-focus"),b.addClass("menu-focus"),this.isChildOpen===!0){var c=a.parent();c.is(".tool-lp-menu")&&"true"==a.attr("aria-haspopup")&&this.openSubMenu(a.children("ul").first())}return!0},e.prototype.handleBlur=function(a){return a.removeClass("menu-focus"),!0},e.prototype.setOpenDirection=function(){var b=this.menuRoot.offset(),c=a(document.body).hasClass("dir-rtl"),d=160,e=!1;c?b.left-d<0&&(e=!0):b.left+d>a(window).width()&&(e=!0),e?this.menuRoot.addClass("tool-lp-menu-open-left"):this.menuRoot.removeClass("tool-lp-menu-open-left")},e.prototype.handleKeyDown=function(a,b){if(b.altKey||b.ctrlKey)return!0;switch(b.keyCode){case this.keys.tab:this.menuRoot.find("ul").attr("aria-hidden","true"),this.allItems.removeClass("menu-focus"),this.activeItem=null,this.isChildOpen=!1;break;case this.keys.esc:var c=a.parent();return c.is(".tool-lp-menu")?a.children("ul").first().attr("aria-hidden","true"):(this.activeItem=c.parent(),this.isChildOpen=!1,this.activeItem.focus(),c.attr("aria-hidden","true")),b.stopPropagation(),!1;case this.keys.enter:case this.keys.space:return this.handleClick(a,b);case this.keys.left:return this.activeItem=this.moveToPrevious(a),this.activeItem.focus(),b.stopPropagation(),!1;case this.keys.right:return this.activeItem=this.moveToNext(a),this.activeItem.focus(),b.stopPropagation(),!1;case this.keys.up:return this.activeItem=this.moveUp(a),this.activeItem.focus(),b.stopPropagation(),!1;case this.keys.down:return this.activeItem=this.moveDown(a),this.activeItem.focus(),b.stopPropagation(),!1}return!0},e.prototype.moveToNext=function(a){var b=a.parent(),c=b.children("li"),d=c.length,e=c.index(a),f=null,g=null;if(b.is(".tool-lp-menu"))f=d-1>e?a.next():c.first(),"true"==a.attr("aria-haspopup")&&(g=a.children("ul").first(),"false"==g.attr("aria-hidden")&&(g.attr("aria-hidden","true"),this.isChildOpen=!0)),a.removeClass("menu-focus"),"true"===f.attr("aria-haspopup")&&this.isChildOpen===!0&&(g=f.children("ul").first(),this.openSubMenu(g));else if("true"==a.attr("aria-haspopup"))g=a.children("ul").first(),f=g.children("li").first(),this.openSubMenu(g);else{var h=null,i=null;h=a.parentsUntil("ul.tool-lp-menu").filter("ul").not(".tool-lp-menu"),h.attr("aria-hidden","true"),h.find("li").removeClass("menu-focus"),h.last().parent().removeClass("menu-focus"),i=h.last().parent(),e=this.rootMenus.index(i),f=e0?a.prev():c.last(),"true"==a.attr("aria-haspopup")&&(f=a.children("ul").first(),"false"==f.attr("aria-hidden")&&(f.attr("aria-hidden","true"),this.isChildOpen=!0)),a.removeClass("menu-focus"),"true"===e.attr("aria-haspopup")&&this.isChildOpen===!0&&(f=e.children("ul").first(),this.openSubMenu(f));else{var g=b.parent(),h=g.parent();h.is(".tool-lp-menu")?(b.attr("aria-hidden","true"),a.removeClass("menu-focus"),g.removeClass("menu-focus"),d=this.rootMenus.index(g),e=d>0?g.prev():this.rootMenus.last(),e.addClass("menu-focus"),"true"==e.attr("aria-haspopup")&&(f=e.children("ul").first(),this.openSubMenu(f),this.isChildOpen=!0,e=f.children("li").first())):(e=b.parent(),b.attr("aria-hidden","true"),a.removeClass("menu-focus"))}return e},e.prototype.moveDown=function(a,b){var c=a.parent(),d=c.children("li").not(".separator"),e=d.length,f=d.index(a),g=null,h=null;if(c.is(".tool-lp-menu"))return"true"!=a.attr("aria-haspopup")?a:(h=a.children("ul").first(),g=h.children("li").first(),this.openSubMenu(h),g);if(b){var i=!1,j=f+1;for(j==e&&(j=0);j!=f;){var k=d.eq(j).html().charAt(0);if(k.toLowerCase()==b){i=!0;break}j+=1,j==e&&(j=0)}return i===!0?(g=d.eq(j),a.removeClass("menu-focus"),g):a}return g=e-1>f?d.eq(f+1):d.first(),a.removeClass("menu-focus"),g},e.prototype.moveUp=function(a){var b=a.parent(),c=b.children("li").not(".separator"),d=c.index(a),e=null;return b.is(".tool-lp-menu")?a:(e=d>0?c.eq(d-1):c.last(),a.removeClass("menu-focus"),e)},e.prototype.addAriaAttributes=function(){this.menuRoot.attr("role","menubar"),this.rootMenus.attr("role","menuitem"),this.rootMenus.attr("tabindex","0"),this.rootMenus.attr("aria-haspopup","true"),this.subMenus.attr("role","menu"),this.subMenus.attr("aria-hidden","true"),this.subMenuItems.attr("role","menuitem"),this.subMenuItems.attr("tabindex","-1"),this.menuRoot.addClass("tool-lp-menu"),this.allItems.addClass("tool-lp-menu-item"),this.rootMenus.addClass("tool-lp-root-menu"),this.subMenus.addClass("tool-lp-sub-menu")},{enhance:function(b,c){a(b).each(function(b,d){var f=a(d);f.data("menubarEnhanced")!==!0&&(new e(f,c),f.data("menubarEnhanced",!0))})},closeAll:d}}); \ No newline at end of file diff --git a/admin/tool/lp/amd/build/parentcompetency_form.min.js b/admin/tool/lp/amd/build/parentcompetency_form.min.js new file mode 100644 index 00000000000..01610536296 --- /dev/null +++ b/admin/tool/lp/amd/build/parentcompetency_form.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/str","tool_lp/competencypicker","core/templates","core/notification"],function(a,b,c,d,e,f){var g=function(a,b,c,d,e,f){this.buttonSelector=a,this.inputHiddenSelector=b,this.staticElementSelector=c,this.frameworkId=d,this.frameworkMaxLevel=e,this.pageContextId=f,this.registerEvents()};return g.prototype.buttonSelector=null,g.prototype.inputHiddenSelector=null,g.prototype.staticElementSelector=null,g.prototype.frameworkId=null,g.prototype.frameworkMaxLevel=null,g.prototype.pageContextId=null,g.prototype.setParent=function(d){var e=this;0!==d.competencyId?b.call([{methodname:"core_competency_read_competency",args:{id:d.competencyId}}])[0].done(function(b){a(e.staticElementSelector).html(b.shortname),a(e.inputHiddenSelector).val(b.id)}).fail(f.exception):c.get_string("competencyframeworkroot","tool_lp").then(function(b){a(e.staticElementSelector).html(b),a(e.inputHiddenSelector).val(d.competencyId)}).fail(f.exception)},g.prototype.registerEvents=function(){var c=this;a(c.buttonSelector).on("click",function(a){a.preventDefault();var g=new d(c.pageContextId,c.frameworkId,"self",!1),h=c.frameworkMaxLevel;g._fetchCompetencies=function(a,c){var d=this;return b.call([{methodname:"core_competency_search_competencies",args:{searchtext:c,competencyframeworkid:a}}])[0].done(function(a){function b(a,c){for(var d=0;d=h&&"0"!==c[d].id&&f.push(c[d].id),c[d].parentid==a.id&&(a.haschildren=!0,c[d].children=[],c[d].haschildren=!1,a.children[a.children.length]=c[d],b(c[d],c))}}var c,e,f=[],g=[];for(c=0;c'),e=a(''),f=function(b,c){this.treeRoot=a(b),this.multiSelect="undefined"==typeof c||c===!0,this.items=this.treeRoot.find("li"),this.expandAll=this.items.length<20,this.parents=this.treeRoot.find("li:has(ul)"),c&&this.treeRoot.attr("aria-multiselectable","true"),this.items.attr("aria-selected","false"),this.visibleItems=null,this.activeItem=null,this.lastActiveItem=null,this.keys={tab:9,enter:13,space:32,pageup:33,pagedown:34,end:35,home:36,left:37,up:38,right:39,down:40,asterisk:106},this.init(),this.bindEventHandlers()};return f.prototype.init=function(){this.parents.attr("aria-expanded","true"),this.parents.prepend(d.clone()),this.items.attr("role","tree-item"),this.items.attr("tabindex","-1"),this.parents.attr("role","group"),this.treeRoot.attr("role","tree"),this.visibleItems=this.treeRoot.find("li");var b=this;this.expandAll||(this.parents.each(function(){b.collapseGroup(a(this))}),this.expandGroup(this.parents.first()))},f.prototype.expandGroup=function(a){var b=a.children("ul");b.show().attr("aria-hidden","false"),a.attr("aria-expanded","true"),a.children("img").attr("src",d.attr("src")),this.visibleItems=this.treeRoot.find("li:visible")},f.prototype.collapseGroup=function(a){var b=a.children("ul");b.hide().attr("aria-hidden","true"),a.attr("aria-expanded","false"),a.children("img").attr("src",e.attr("src")),this.visibleItems=this.treeRoot.find("li:visible")},f.prototype.toggleGroup=function(a){"true"==a.attr("aria-expanded")?this.collapseGroup(a):this.expandGroup(a)},f.prototype.triggerChange=function(){var a=this.items.filter("[aria-selected=true]");this.multiSelect||(a=a.first()),this.treeRoot.trigger("selectionchanged",{selected:a})},f.prototype.multiSelectItem=function(b){if(this.multiSelect){if(null!==this.lastActiveItem){for(var c=this.visibleItems.index(this.lastActiveItem),d=this.visibleItems.index(this.activeItem),e=null;d>c;)e=a(this.visibleItems.get(c)),e.attr("aria-selected","true"),c++;for(;c>d;)e=a(this.visibleItems.get(c)),e.attr("aria-selected","true"),c--}}else this.items.attr("aria-selected","false");b.attr("aria-selected","true"),this.triggerChange()},f.prototype.selectItem=function(a){for(var b=a.parent();"tree"!=b.attr("role");)b=b.parent(),"false"==b.attr("aria-expanded")&&this.expandGroup(b),b=b.parent();this.items.attr("aria-selected","false"),a.attr("aria-selected","true"),this.triggerChange()},f.prototype.toggleItem=function(a){if(!this.multiSelect)return this.selectItem(a);var b=a.attr("aria-selected");b="true"===b?"false":"true",a.attr("aria-selected",b),this.triggerChange()},f.prototype.updateFocus=function(a){this.lastActiveItem=this.activeItem,this.activeItem=a;for(var b=a.parent();"tree"!=b.attr("role");)b=b.parent(),"false"==b.attr("aria-expanded")&&this.expandGroup(b),b=b.parent();this.items.attr("tabindex","-1"),a.attr("tabindex",0)},f.prototype.handleKeyDown=function(b,c){var d=this.visibleItems.index(b),e=null;switch(c.keyCode){case this.keys.home:return e=this.parents.first(),e.focus(),c.shiftKey?this.multiSelectItem(e):c.ctrlKey||this.selectItem(e),c.stopPropagation(),!1;case this.keys.end:return e=this.visibleItems.last(),e.focus(),c.shiftKey?this.multiSelectItem(e):c.ctrlKey||this.selectItem(e),c.stopPropagation(),!1;case this.keys.enter:case this.keys.space:return c.shiftKey?this.multiSelectItem(b):c.ctrlKey?this.toggleItem(b):this.selectItem(b),c.stopPropagation(),!1;case this.keys.left:if(b.has("ul")&&"true"==b.attr("aria-expanded"))this.collapseGroup(b);else{var f=b.parent(),g=f.parent();g.is("li")&&(g.focus(),c.shiftKey?this.multiSelectItem(g):c.ctrlKey||this.selectItem(g))}return c.stopPropagation(),!1;case this.keys.right:return b.has("ul")&&"false"==b.attr("aria-expanded")?this.expandGroup(b):(e=b.children("ul").children("li").first(),e.length>0&&(e.focus(),c.shiftKey?this.multiSelectItem(e):c.ctrlKey||this.selectItem(e))),c.stopPropagation(),!1;case this.keys.up:if(d>0){var h=this.visibleItems.eq(d-1);h.focus(),c.shiftKey?this.multiSelectItem(h):c.ctrlKey||this.selectItem(h)}return c.stopPropagation(),!1;case this.keys.down:if(d. + +/** + * 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 - + * @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; + +}); diff --git a/admin/tool/lp/amd/src/competencies.js b/admin/tool/lp/amd/src/competencies.js new file mode 100644 index 00000000000..6e1973ceec0 --- /dev/null +++ b/admin/tool/lp/amd/src/competencies.js @@ -0,0 +1,338 @@ +// 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 . + +/** + * Handle add/remove competency links. + * + * @module tool_lp/competencies + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @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; +}); diff --git a/admin/tool/lp/amd/src/competency_outcomes.js b/admin/tool/lp/amd/src/competency_outcomes.js new file mode 100644 index 00000000000..1ff679e2f38 --- /dev/null +++ b/admin/tool/lp/amd/src/competency_outcomes.js @@ -0,0 +1,83 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * 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; + }); + } + }; + +}); diff --git a/admin/tool/lp/amd/src/competency_rule.js b/admin/tool/lp/amd/src/competency_rule.js new file mode 100644 index 00000000000..35def4ad7d9 --- /dev/null +++ b/admin/tool/lp/amd/src/competency_rule.js @@ -0,0 +1,175 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * 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 = $('
'); + 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; + +}); diff --git a/admin/tool/lp/amd/src/competency_rule_all.js b/admin/tool/lp/amd/src/competency_rule_all.js new file mode 100644 index 00000000000..db46330a503 --- /dev/null +++ b/admin/tool/lp/amd/src/competency_rule_all.js @@ -0,0 +1,60 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * 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; + +}); diff --git a/admin/tool/lp/amd/src/competency_rule_points.js b/admin/tool/lp/amd/src/competency_rule_points.js new file mode 100644 index 00000000000..470bb5df916 --- /dev/null +++ b/admin/tool/lp/amd/src/competency_rule_points.js @@ -0,0 +1,199 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * 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; + +}); diff --git a/admin/tool/lp/amd/src/competencyactions.js b/admin/tool/lp/amd/src/competencyactions.js new file mode 100644 index 00000000000..2c1c4c35a18 --- /dev/null +++ b/admin/tool/lp/amd/src/competencyactions.js @@ -0,0 +1,861 @@ +// 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 . + +/** + * Handle selection changes and actions on the competency tree. + * + * @module tool_lp/competencyactions + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @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 ". + * + * @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 ". + * + * @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 ". + * + * @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)); + } + }; +}); diff --git a/admin/tool/lp/amd/src/competencydialogue.js b/admin/tool/lp/amd/src/competencydialogue.js new file mode 100644 index 00000000000..747b9b0d1bf --- /dev/null +++ b/admin/tool/lp/amd/src/competencydialogue.js @@ -0,0 +1,175 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Display Competency in dialogue box. + * + * @module tool_lp/Competencydialogue + * @package tool_lp + * @copyright 2015 Issam Taboubi + * @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; +}); diff --git a/admin/tool/lp/amd/src/competencypicker.js b/admin/tool/lp/amd/src/competencypicker.js new file mode 100644 index 00000000000..d6247fe4e10 --- /dev/null +++ b/admin/tool/lp/amd/src/competencypicker.js @@ -0,0 +1,460 @@ +// 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 . + +/** + * 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 = $('
'); + 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; + +}); diff --git a/admin/tool/lp/amd/src/competencypicker_user_plans.js b/admin/tool/lp/amd/src/competencypicker_user_plans.js new file mode 100644 index 00000000000..7291829f135 --- /dev/null +++ b/admin/tool/lp/amd/src/competencypicker_user_plans.js @@ -0,0 +1,240 @@ +// 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 . + +/** + * 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; + +}); diff --git a/admin/tool/lp/amd/src/competencyruleconfig.js b/admin/tool/lp/amd/src/competencyruleconfig.js new file mode 100644 index 00000000000..4894d3689f3 --- /dev/null +++ b/admin/tool/lp/amd/src/competencyruleconfig.js @@ -0,0 +1,548 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * 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 = $('
'); + this._tree = tree; + this._rulesModules = rulesModules; + this._setUp(); + }; + + /** @type {Object} The current competency. */ + RuleConfig.prototype._competency = null; + /** @type {Node} The node we attach the events to. */ + RuleConfig.prototype._eventNode = null; + /** @type {Array} Outcomes options. */ + RuleConfig.prototype._outcomesOption = null; + /** @type {Dialogue} The dialogue. */ + RuleConfig.prototype._popup = null; + /** @type {Promise} Resolved when the module is ready. */ + RuleConfig.prototype._ready = null; + /** @type {Array} The rules. */ + RuleConfig.prototype._rules = null; + /** @type {Array} The rules modules. */ + RuleConfig.prototype._rulesModules = null; + /** @type {competencytree} The competency tree. */ + RuleConfig.prototype._tree = null; + + /** + * After change. + * + * Triggered when a change occured. + * + * @return {Void} + * @method _afterChange + * @protected + */ + RuleConfig.prototype._afterChange = function() { + if (!this._isValid()) { + this._find('[data-action="save"]').prop('disabled', true); + } else { + this._find('[data-action="save"]').prop('disabled', false); + } + }; + + /** + * After change in rule's config. + * + * Triggered when a change occured in a specific rule config. + * + * @return {Void} + * @method _afterRuleConfigChange + * @protected + */ + RuleConfig.prototype._afterRuleConfigChange = function(e, rule) { + if (rule != this._getRule()) { + // This rule is not the current one any more, we can ignore. + return; + } + this._afterChange(); + }; + + /** + * After render hook. + * + * @return {Promise} + * @method _afterRender + * @protected + */ + RuleConfig.prototype._afterRender = function() { + var self = this; + + self._find('[name="outcome"]').on('change', function() { + self._switchedOutcome(); + }).trigger('change'); + + self._find('[name="rule"]').on('change', function() { + self._switchedRule(); + }).trigger('change'); + + self._find('[data-action="save"]').on('click', function() { + self._trigger('save', self._getConfig()); + self.close(); + }); + + self._find('[data-action="cancel"]').on('click', function() { + self.close(); + }); + }; + + /** + * Whether the current competency can be configured. + * + * @return {Boolean} + * @method canBeConfigured + */ + RuleConfig.prototype.canBeConfigured = function() { + var can = false; + $.each(this._rules, function(index, rule) { + if (rule.canConfig()) { + can = true; + return false; + } + }); + return can; + }; + + /** + * Close the dialogue. + * + * @method close + */ + RuleConfig.prototype.close = function() { + this._popup.close(); + this._popup = null; + }; + + /** + * Opens the picker. + * + * @param {Number} competencyId The competency ID of the competency to work on. + * @method display + * @return {Promise} + */ + RuleConfig.prototype.display = function() { + var self = this; + if (!self._competency) { + return; + } + return self._render().then(function(html) { + return Str.get_string('competencyrule', 'tool_lp').then(function(title) { + self._popup = new Dialogue( + title, + html, + self._afterRender.bind(self) + ); + }); + }).fail(Notification.exception); + }; + + /** + * Find a node in the dialogue. + * + * @param {String} selector + * @method _find + * @protected + */ + RuleConfig.prototype._find = function(selector) { + return $(this._popup.getContent()).find(selector); + }; + + /** + * Get the applicable outcome options. + * + * @return {Array} + * @method _getApplicableOutcomesOptions + * @protected + */ + RuleConfig.prototype._getApplicableOutcomesOptions = function() { + var self = this, + options = []; + + $.each(self._outcomesOption, function(index, outcome) { + options.push({ + code: outcome.code, + name: outcome.name, + selected: (outcome.code == self._competency.ruleoutcome) ? true : false, + }); + }); + + return options; + }; + + /** + * Get the applicable rules options. + * + * @return {Array} + * @method _getApplicableRulesOptions + * @protected + */ + RuleConfig.prototype._getApplicableRulesOptions = function() { + var self = this, + options = []; + + $.each(self._rules, function(index, rule) { + if (!rule.canConfig()) { + return; + } + options.push({ + name: self._getRuleName(rule.getType()), + type: rule.getType(), + selected: (rule.getType() == self._competency.ruletype) ? true : false, + }); + }); + + return options; + }; + + /** + * Get the full config for the competency. + * + * @return {Object} Contains rule, ruleoutcome and ruleconfig. + * @method _getConfig + * @protected + */ + RuleConfig.prototype._getConfig = function() { + var rule = this._getRule(); + return { + ruletype: rule ? rule.getType() : null, + ruleconfig: rule ? rule.getConfig() : null, + ruleoutcome: this._getOutcome() + }; + }; + + /** + * Get the selected outcome code. + * + * @return {String} + * @method _getOutcome + * @protected + */ + RuleConfig.prototype._getOutcome = function() { + return this._find('[name="outcome"]').val(); + }; + + /** + * Get the selected rule. + * + * @return {null|Rule} + * @method _getRule + * @protected + */ + RuleConfig.prototype._getRule = function() { + var result, + type = this._find('[name="rule"]').val(); + + $.each(this._rules, function(index, rule) { + if (rule.getType() == type) { + result = rule; + return false; + } + }); + + return result; + }; + + /** + * Return the name of a rule. + * + * @param {String} type The type of a rule. + * @return {String} + * @method _getRuleName + * @protected + */ + RuleConfig.prototype._getRuleName = function(type) { + var self = this, + name; + $.each(self._rulesModules, function(index, modInfo) { + if (modInfo.type == type) { + name = modInfo.name; + return false; + } + }); + return name; + }; + + /** + * Initialise the outcomes. + * + * @return {Promise} + * @method _initOutcomes + * @protected + */ + RuleConfig.prototype._initOutcomes = function() { + var self = this; + + return Outcomes.getAll().then(function(outcomes) { + self._outcomesOption = outcomes; + }); + }; + + /** + * Initialise the rules. + * + * @return {Promise} + * @method _initRules + * @protected + */ + RuleConfig.prototype._initRules = function() { + var self = this, + promises = []; + + $.each(self._rules, function(index, rule) { + var promise = rule.init().then(function() { + rule.setTargetCompetency(self._competency); + rule.on('change', self._afterRuleConfigChange.bind(self)); + }, function() { + // Upon failure remove the rule, and resolve the promise. + self._rules.splice(index, 1); + return $.when(); + }); + promises.push(promise); + }); + + return $.when.apply($.when, promises); + }; + + /** + * Whether or not the current config is valid. + * + * @return {Boolean} + * @method _isValid + * @protected + */ + RuleConfig.prototype._isValid = function() { + var outcome = this._getOutcome(), + rule = this._getRule(); + + if (outcome == Outcomes.NONE) { + return true; + } else if (!rule) { + return false; + } + + return rule.isValid(); + }; + + /** + * Register an event listener. + * + * @param {String} type The event type. + * @param {Function} handler The event listener. + * @method on + */ + RuleConfig.prototype.on = function(type, handler) { + this._eventNode.on(type, handler); + }; + + /** + * Hook to executed before render. + * + * @method _preRender + * @protected + * @return {Promise} + */ + RuleConfig.prototype._preRender = function() { + // We need to have all the information about the rule plugins first. + return this.ready(); + }; + + /** + * Returns a promise that is resolved when the module is ready. + * + * @return {Promise} + * @method ready + * @protected + */ + RuleConfig.prototype.ready = function() { + return this._ready.promise(); + }; + + /** + * Render the dialogue. + * + * @method _render + * @protected + * @return {Promise} + */ + RuleConfig.prototype._render = function() { + var self = this; + return this._preRender().then(function() { + var config; + + if (!self.canBeConfigured()) { + config = false; + } else { + config = {}; + config.outcomes = self._getApplicableOutcomesOptions(); + config.rules = self._getApplicableRulesOptions(); + } + + var context = { + competencyshortname: self._competency.shortname, + config: config + }; + + return Templates.render('tool_lp/competency_rule_config', context); + }); + }; + + /** + * Set the target competency. + * + * @param {Number} competencyId The target competency Id. + * @method setTargetCompetencyId + */ + RuleConfig.prototype.setTargetCompetencyId = function(competencyId) { + var self = this; + self._competency = self._tree.getCompetency(competencyId); + $.each(self._rules, function(index, rule) { + rule.setTargetCompetency(self._competency); + }); + }; + + /** + * Set up the instance. + * + * @return {Void} + * @method _setUp + * @protected + */ + RuleConfig.prototype._setUp = function() { + var self = this, + promises = [], + modules = []; + + self._ready = $.Deferred(); + self._rules = []; + + $.each(self._rulesModules, function(index, rule) { + modules.push(rule.amd); + }); + + // Load all the modules. + require(modules, function() { + $.each(arguments, function(index, Module) { + // Instantiate the rule and listen to it. + var rule = new Module(self._tree); + self._rules.push(rule); + }); + + // Load all the option values. + promises.push(self._initRules()); + promises.push(self._initOutcomes()); + + // Ready when everything is done. + $.when.apply($.when, promises).always(function() { + self._ready.resolve(); + }); + }); + }; + + /** + * Called when the user switches outcome. + * + * @return {Void} + * @method _switchedOutcome + * @protected + */ + RuleConfig.prototype._switchedOutcome = function() { + var self = this, + type = self._getOutcome(); + + if (type == Outcomes.NONE) { + // Reset to defaults. + self._find('[data-region="rule-type"]').hide() + .find('[name="rule"]').val(-1); + self._find('[data-region="rule-config"]').empty().hide(); + self._afterChange(); + return; + } + + self._find('[data-region="rule-type"]').show(); + self._find('[data-region="rule-config"]').show(); + self._afterChange(); + }; + + /** + * Called when the user switches rule. + * + * @return {Void} + * @method _switchedRule + * @protected + */ + RuleConfig.prototype._switchedRule = function() { + var self = this, + container = self._find('[data-region="rule-config"]'), + rule = self._getRule(); + + if (!rule) { + container.empty().hide(); + self._afterChange(); + return; + } + + rule.injectTemplate(container).then(function() { + container.show(); + }, function() { + container.empty().hide(); + }).always(function() { + self._afterChange(); + }); + }; + + /** + * Trigger an event. + * + * @param {String} type The type of event. + * @param {Object} The data to pass to the listeners. + * @method _trigger + * @protected + */ + RuleConfig.prototype._trigger = function(type, data) { + this._eventNode.trigger(type, [data]); + }; + + return /** @alias module:tool_lp/competencyruleconfig */ RuleConfig; + +}); diff --git a/admin/tool/lp/amd/src/competencytree.js b/admin/tool/lp/amd/src/competencytree.js new file mode 100644 index 00000000000..f912b5936cd --- /dev/null +++ b/admin/tool/lp/amd/src/competencytree.js @@ -0,0 +1,262 @@ +// 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 . + +/** + * Handle selection changes on the competency tree. + * + * @module tool_lp/competencyselect + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['core/ajax', 'core/notification', 'core/templates', 'tool_lp/tree', 'tool_lp/competency_outcomes', 'jquery'], + function(ajax, notification, templates, Ariatree, CompOutcomes, $) { + + // Private variables and functions. + /** @var {Object[]} competencies - Cached list of competencies */ + var competencies = {}; + + /** @var {Number} competencyFrameworkId - The current framework id */ + var competencyFrameworkId = 0; + + /** @var {String} competencyFrameworkShortName - The current framework short name */ + var competencyFrameworkShortName = ''; + + /** @var {String} treeSelector - The selector for the root of the tree. */ + var treeSelector = ''; + + /** @var {String} currentNodeId - The data-id of the current node in the tree. */ + var currentNodeId = ''; + + /** @var {Boolean} competencyFramworkCanManage - Can manage the competencies framework */ + var competencyFramworkCanManage = false; + + /** + * Build a tree from the flat list of competencies. + * @param {Object} parent The parent competency. + * @param {Array} all The list of all competencies. + */ + var addChildren = function(parent, all) { + var i = 0; + var current = false; + parent.haschildren = false; + parent.children = []; + for (i = 0; i < all.length; i++) { + current = all[i]; + if (current.parentid == parent.id) { + parent.haschildren = true; + parent.children.push(current); + addChildren(current, all); + } + } + }; + + /** + * Load the list of competencies via ajax. Competencies are filtered by the searchtext. + * @param {String} searchtext The text to filter on. + * @return {promise} + */ + var loadCompetencies = function(searchtext) { + var deferred = $.Deferred(); + + templates.render('tool_lp/loading', {}).done(function(loadinghtml, loadingjs) { + templates.replaceNodeContents($(treeSelector), loadinghtml, loadingjs); + + var promises = ajax.call([{ + methodname: 'core_competency_search_competencies', + args: { + searchtext: searchtext, + competencyframeworkid: competencyFrameworkId + } + }]); + promises[0].done(function(result) { + competencies = {}; + var i = 0; + for (i = 0; i < result.length; i++) { + competencies[result[i].id] = result[i]; + } + + var children = []; + var competency = false; + for (i = 0; i < result.length; i++) { + competency = result[i]; + if (parseInt(competency.parentid, 10) === 0) { + children.push(competency); + addChildren(competency, result); + } + } + var context = { + shortname: competencyFrameworkShortName, + canmanage: competencyFramworkCanManage, + competencies: children + }; + templates.render('tool_lp/competencies_tree_root', context).done(function(html, js) { + templates.replaceNodeContents($(treeSelector), $(html).html(), js); + var tree = new Ariatree(treeSelector, false); + + if (currentNodeId) { + var node = $(treeSelector).find('[data-id=' + currentNodeId + ']'); + if (node.length) { + tree.selectItem(node); + tree.updateFocus(node); + } + } + deferred.resolve(competencies); + }).fail(deferred.reject); + }).fail(deferred.reject); + }); + + return deferred.promise(); + }; + + /** + * Whenever the current item in the tree is changed - remember the "id". + * @param {Event} evt + * @param {Object} params The parameters for the event (This is the selected node). + */ + var rememberCurrent = function(evt, params) { + var node = params.selected; + currentNodeId = node.attr('data-id'); + }; + + return /** @alias module:tool_lp/competencytree */ { + // Public variables and functions. + /** + * Initialise the tree. + * + * @param {Number} id The competency id. + * @param {String} shortname The framework shortname + * @param {String} search The current search string + * @param {String} selector The selector for the tree div + * @param {Boolean} canmanage Can manage the competencies + */ + init: function(id, shortname, search, selector, canmanage) { + competencyFrameworkId = id; + competencyFrameworkShortName = shortname; + competencyFramworkCanManage = canmanage; + treeSelector = selector; + loadCompetencies(search).fail(notification.exception); + + this.on('selectionchanged', rememberCurrent); + }, + + /** + * Add an event handler for custom events emitted by the tree. + * + * @param {String} eventname The name of the event - only "selectionchanged" for now + * @param {Function} handler The handler for the event. + */ + on: function(eventname, handler) { + // We can't use the tree on function directly + // because the tree gets rebuilt whenever the search string changes, + // instead we attach the listner to the root node of the tree which never + // gets destroyed (same as "on()" code in the tree.js). + $(treeSelector).on(eventname, handler); + }, + + /** + * Get the children of a competency. + * + * @param {Number} id The competency ID. + * @return {Array} + * @method getChildren + */ + getChildren: function(id) { + var children = []; + $.each(competencies, function(index, competency) { + if (competency.parentid == id) { + children.push(competency); + } + }); + return children; + }, + + /** + * Get the competency framework id this model was initiliased with. + * + * @return {Number} + */ + getCompetencyFrameworkId: function() { + return competencyFrameworkId; + }, + + /** + * Get a competency by id + * + * @param {Number} id The competency id + * @return {Object} + */ + getCompetency: function(id) { + return competencies[id]; + }, + + /** + * Get the competency level. + * + * @param {Number} id The competency ID. + * @return {Number} + */ + getCompetencyLevel: function(id) { + var competency = this.getCompetency(id), + level = competency.path.replace(/^\/|\/$/g, '').split('/').length; + return level; + }, + + /** + * Whether a competency has children. + * + * @param {Number} id The competency ID. + * @return {Boolean} + * @method hasChildren + */ + hasChildren: function(id) { + return this.getChildren(id).length > 0; + }, + + /** + * Does the competency have a rule? + * + * @param {Number} id The competency ID. + * @return {Boolean} + */ + hasRule: function(id) { + var comp = this.getCompetency(id); + if (comp) { + return comp.ruleoutcome != CompOutcomes.OUTCOME_NONE + && comp.ruletype; + } + return false; + }, + + /** + * Reload all the page competencies framework competencies. + * @method reloadCompetencies + * @return {Promise} + */ + reloadCompetencies: function() { + return loadCompetencies('').fail(notification.exception); + }, + + /** + * Get all competencies for this framework. + * + * @return {Object[]} + */ + listCompetencies: function() { + return competencies; + }, + + }; + }); diff --git a/admin/tool/lp/amd/src/course_competency_settings.js b/admin/tool/lp/amd/src/course_competency_settings.js new file mode 100644 index 00000000000..bff7349814e --- /dev/null +++ b/admin/tool/lp/amd/src/course_competency_settings.js @@ -0,0 +1,149 @@ +// 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 . + +/** + * Change the course competency settings in a popup. + * + * @module tool_lp/configurecoursecompetencysettings + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', + 'core/notification', + 'tool_lp/dialogue', + 'core/str', + 'core/ajax', + 'core/templates'], + function($, notification, Dialogue, str, ajax, templates) { + + /** + * Constructor + * + * @param {String} selector - selector for the links to open the dialogue. + */ + var settingsMod = function(selector) { + $(selector).on('click', this.configureSettings.bind(this)); + }; + + /** @type {Dialogue} Reference to the dialogue that we opened. */ + settingsMod.prototype._dialogue = null; + + /** + * Open the configure settings dialogue. + * + * @param {Event} e + * @method configureSettings + */ + settingsMod.prototype.configureSettings = function(e) { + var courseid = $(e.target).closest('a').data('courseid'); + var currentValue = $(e.target).closest('a').data('pushratingstouserplans'); + var context = { + courseid: courseid, + settings: { pushratingstouserplans: currentValue } + }; + e.preventDefault(); + + templates.render('tool_lp/course_competency_settings', context).done(function(html) { + str.get_string('configurecoursecompetencysettings', 'tool_lp').done(function (title) { + this._dialogue = new Dialogue( + title, + html, + this.addListeners.bind(this) + ); + }.bind(this)).fail(notification.exception); + }.bind(this)).fail(notification.exception); + + }; + + /** + * Add the save listener to the form. + * + * @method addSaveListener + */ + settingsMod.prototype.addListeners = function() { + var save = this._find('[data-action="save"]'); + save.on('click', this.saveSettings.bind(this)); + var cancel = this._find('[data-action="cancel"]'); + cancel.on('click', this.cancelChanges.bind(this)); + }; + + /** + * Cancel the changes. + * + * @param {Event} e + * @method cancelChanges + */ + settingsMod.prototype.cancelChanges = function(e) { + e.preventDefault(); + this._dialogue.close(); + }; + + /** + * Cancel the changes. + * + * @param {String} selector + * @return {JQuery} + */ + settingsMod.prototype._find = function(selector) { + return $('[data-region="coursecompetencysettings"]').find(selector); + }; + + /** + * Save the settings. + * + * @param {Event} e + * @method saveSettings + */ + settingsMod.prototype.saveSettings = function(e) { + e.preventDefault(); + + var newValue = this._find('input[name="pushratingstouserplans"]:checked').val(); + var courseId = this._find('input[name="courseid"]').val(); + var settings = { pushratingstouserplans: newValue }; + + ajax.call([ + { methodname: 'core_competency_update_course_competency_settings', + args: { courseid: courseId, settings: settings } } + ])[0].done(function() { + this.refreshCourseCompetenciesPage(); + }.bind(this)).fail(notification.exception); + + }; + + /** + * Refresh the course competencies page. + * + * @param {Event} e + * @method saveSettings + */ + settingsMod.prototype.refreshCourseCompetenciesPage = function() { + var courseId = this._find('input[name="courseid"]').val(); + + ajax.call([ + { methodname: 'tool_lp_data_for_course_competencies_page', + args: { courseid: courseId } } + ])[0].done(function(context) { + templates.render('tool_lp/course_competencies_page', context).done(function(html, js) { + $('[data-region="coursecompetenciespage"]').replaceWith(html); + templates.runTemplateJS(js); + this._dialogue.close(); + }.bind(this)).fail(notification.exception); + }.bind(this)).fail(notification.exception); + + }; + + return /** @alias module:tool_lp/configurecoursecompetencysettings */ settingsMod; +}); diff --git a/admin/tool/lp/amd/src/dialogue.js b/admin/tool/lp/amd/src/dialogue.js new file mode 100644 index 00000000000..882ee5b6054 --- /dev/null +++ b/admin/tool/lp/amd/src/dialogue.js @@ -0,0 +1,101 @@ +// 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 . + +/** + * Wrapper for the YUI M.core.notification class. Allows us to + * use the YUI version in AMD code until it is replaced. + * + * @module tool_lp/dialogue + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['core/yui'], function(Y) { + + // Private variables and functions. + /** + * Constructor + * + * @param {String} title Title for the window. + * @param {String} content The content for the window. + * @param {function} afterShow Callback executed after the window is opened. + * @param {function} afterHide Callback executed after the window is closed. + * @param {Boolean} wide Specify we want an extra wide dialogue (the size is standard, but wider than the default). + */ + var dialogue = function(title, content, afterShow, afterHide, wide) { + this.yuiDialogue = null; + var parent = this; + + // Default for wide is false. + if (typeof wide == 'undefined') { + wide = false; + } + + Y.use('moodle-core-notification', 'timers', function () { + var width = '480px'; + if (wide) { + width = '800px'; + } + + parent.yuiDialogue = new M.core.dialogue({ + headerContent: title, + bodyContent: content, + draggable: true, + visible: false, + center: true, + modal: true, + width: width + }); + + parent.yuiDialogue.after('visibleChange', function(e) { + if (e.newVal) { + // Delay the callback call to the next tick, otherwise it can happen that it is + // executed before the dialogue constructor returns. + if ((typeof afterShow !== 'undefined')) { + Y.soon(function() { + afterShow(parent); + parent.yuiDialogue.centerDialogue(); + }); + } + } else { + if ((typeof afterHide !== 'undefined')) { + Y.soon(function() { + afterHide(parent); + }); + } + } + }); + + parent.yuiDialogue.show(); + }); + }; + + /** + * Close this window. + */ + dialogue.prototype.close = function() { + this.yuiDialogue.hide(); + this.yuiDialogue.destroy(); + }; + + /** + * Get content. + */ + dialogue.prototype.getContent = function() { + return this.yuiDialogue.bodyNode.getDOMNode(); + }; + + return /** @alias module:tool_lp/dialogue */ dialogue; +}); diff --git a/admin/tool/lp/amd/src/dragdrop-reorder.js b/admin/tool/lp/amd/src/dragdrop-reorder.js new file mode 100644 index 00000000000..433604dd341 --- /dev/null +++ b/admin/tool/lp/amd/src/dragdrop-reorder.js @@ -0,0 +1,88 @@ +// 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 . + +/** + * Drag and drop reorder via HTML5. + * + * @module tool_lp/dragdrop-reorder + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['core/str', 'core/yui'], function(str, Y) { + + // Private variables and functions. + + /** + * Translate the drophit event from YUI + * into simple drag and drop nodes. + * @param {Y.Event} e The yui drop event. + */ + var proxyCallback = function(e) { + var dragNode = e.drag.get('node'); + var dropNode = e.drop.get('node'); + this.callback(dragNode.getDOMNode(), dropNode.getDOMNode()); + }; + + return /** @alias module:tool_lp/dragdrop-reorder */ { + // Public variables and functions. + /** + * Create an instance of M.tool_lp.dragdrop + * + * @param {String} group Unique string to identify this interaction. + * @param {String} dragHandleText Alt text for the drag handle. + * @param {String} sameNodeText Used in keyboard drag drop for the list of items target. + * @param {String} parentNodeText Used in keyboard drag drop for the parent target. + * @param {String} sameNodeClass class used to find the each of the list of items. + * @param {String} parentNodeClass class used to find the container for the list of items. + * @param {String} dragHandleInsertClass class used to find the location to insert the drag handles. + * @param {function} callback Drop hit handler. + */ + dragdrop: function(group, + dragHandleText, + sameNodeText, + parentNodeText, + sameNodeClass, + parentNodeClass, + dragHandleInsertClass, + callback) { + // Here we are wrapping YUI. This allows us to start transitioning, but + // wait for a good alternative without having inconsistent UIs. + str.get_strings([ + { key: 'emptydragdropregion', component: 'moodle' }, + { key: 'movecontent', component: 'moodle' }, + { key: 'tocontent', component: 'moodle' }, + ]).done( function () { + Y.use('moodle-tool_lp-dragdrop-reorder', function () { + + var context = { + callback: callback + }; + M.tool_lp.dragdrop_reorder({ + group: group, + dragHandleText: dragHandleText, + sameNodeText: sameNodeText, + parentNodeText: parentNodeText, + sameNodeClass: sameNodeClass, + parentNodeClass: parentNodeClass, + dragHandleInsertClass: dragHandleInsertClass, + callback: Y.bind(proxyCallback, context) + }); + }); + }); + } + + }; +}); diff --git a/admin/tool/lp/amd/src/event_base.js b/admin/tool/lp/amd/src/event_base.js new file mode 100644 index 00000000000..1bda7edac9e --- /dev/null +++ b/admin/tool/lp/amd/src/event_base.js @@ -0,0 +1,59 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Event base javascript module. + * + * @module tool_lp/event_base + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery'], function($) { + + /** + * Base class. + */ + var Base = function() { + this._eventNode = $('
'); + }; + + /** @type {Node} The node we attach the events to. */ + Base.prototype._eventNode = null; + + /** + * Register an event listener. + * + * @param {String} type The event type. + * @param {Function} handler The event listener. + * @method on + */ + Base.prototype.on = function(type, handler) { + this._eventNode.on(type, handler); + }; + + /** + * Trigger an event. + * + * @param {String} type The type of event. + * @param {Object} The data to pass to the listeners. + * @method _trigger + */ + Base.prototype._trigger = function(type, data) { + this._eventNode.trigger(type, [data]); + }; + + return /** @alias module:tool_lp/event_base */ Base; +}); diff --git a/admin/tool/lp/amd/src/evidence_delete.js b/admin/tool/lp/amd/src/evidence_delete.js new file mode 100644 index 00000000000..19a5534aabc --- /dev/null +++ b/admin/tool/lp/amd/src/evidence_delete.js @@ -0,0 +1,101 @@ +// 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 . + +/** + * Evidence delete. + * + * @package tool_lp + * @copyright 2016 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/str', + 'core/log'], + function($, Notification, Ajax, Str, Log) { + + var selectors = {}; + + /** + * Register an event listener. + * + * @param {String} triggerSelector The node on which the click will happen. + * @param {String} containerSelector The parent node that will be removed and contains the evidence ID. + * @return {Void} + */ + var register = function(triggerSelector, containerSelector) { + if (typeof selectors[triggerSelector] !== 'undefined') { + return; + } + + selectors[triggerSelector] = $('body').delegate(triggerSelector, 'click', function(e) { + var parent = $(e.currentTarget).parents(containerSelector); + if (!parent.length || parent.length > 1) { + Log.error('None or too many evidence container were found.'); + return; + } + var evidenceId = parent.data('id'); + if (!evidenceId) { + Log.error('Evidence ID was not found.'); + return; + } + + e.preventDefault(); + e.stopPropagation(); + + Str.get_strings([ + { key: 'confirm', component: 'moodle' }, + { key: 'areyousure', component: 'moodle' }, + { key: 'delete', component: 'moodle' }, + { key: 'cancel', component: 'moodle' } + ]).done(function (strings) { + Notification.confirm( + strings[0], // Confirm. + strings[1], // Are you sure? + strings[2], // Delete. + strings[3], // Cancel. + function() { + var promise = Ajax.call([{ + methodname: 'core_competency_delete_evidence', + args: { + id: evidenceId + } + }]); + promise[0].then(function() { + parent.remove(); + }).fail(Notification.exception); + } + ); + }).fail(Notification.exception); + + + }); + }; + + return /** @alias module:tool_lp/evidence_delete */ { + + /** + * Register an event listener. + * + * @param {String} triggerSelector The node on which the click will happen. + * @param {String} containerSelector The parent node that will be removed and contains the evidence ID. + * @return {Void} + */ + register: register + }; + +}); diff --git a/admin/tool/lp/amd/src/form-cohort-selector.js b/admin/tool/lp/amd/src/form-cohort-selector.js new file mode 100644 index 00000000000..9172c19fad6 --- /dev/null +++ b/admin/tool/lp/amd/src/form-cohort-selector.js @@ -0,0 +1,79 @@ +// 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 . + +/** + * Cohort selector module. + * + * @module tool_lp/form-cohort-selector + * @class form-cohort-selector + * @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/ajax', 'core/templates'], function($, Ajax, Templates) { + + return /** @alias module:tool_lp/form-cohort-selector */ { + + processResults: function(selector, results) { + var cohorts = []; + $.each(results, function(index, cohort) { + cohorts.push({ + value: cohort.id, + label: cohort._label + }); + }); + return cohorts; + }, + + transport: function(selector, query, success, failure) { + var promise, + contextid = parseInt($(selector).data('contextid'), 10), + includes = $(selector).data('includes'); + + promise = Ajax.call([{ + methodname: 'tool_lp_search_cohorts', + args: { + query: query, + context: { contextid: contextid }, + includes: includes + } + }]); + + promise[0].then(function(results) { + var promises = [], + i = 0; + + // Render the label. + $.each(results.cohorts, function(index, cohort) { + promises.push(Templates.render('tool_lp/form-cohort-selector-suggestion', cohort)); + }); + + // Apply the label to the results. + return $.when.apply($.when, promises).then(function() { + var args = arguments; + $.each(results.cohorts, function(index, cohort) { + cohort._label = args[i]; + i++; + }); + success(results.cohorts); + }); + + }, failure); + } + + }; + +}); diff --git a/admin/tool/lp/amd/src/form-user-selector.js b/admin/tool/lp/amd/src/form-user-selector.js new file mode 100644 index 00000000000..8713d069d6a --- /dev/null +++ b/admin/tool/lp/amd/src/form-user-selector.js @@ -0,0 +1,89 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * User selector module. + * + * @module tool_lp/form-user-selector + * @class form-user-selector + * @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/ajax', 'core/templates'], function($, Ajax, Templates) { + + return /** @alias module:tool_lp/form-user-selector */ { + + processResults: function(selector, results) { + var users = []; + $.each(results, function(index, user) { + users.push({ + value: user.id, + label: user._label + }); + }); + return users; + }, + + transport: function(selector, query, success, failure) { + var promise; + var capability = $(selector).data('capability'); + if (typeof capability === "undefined") { + capability = ''; + } + + promise = Ajax.call([{ + methodname: 'tool_lp_search_users', + args: { + query: query, + capability: capability + } + }]); + + promise[0].then(function(results) { + var promises = [], + i = 0; + + // Render the label. + $.each(results.users, function(index, user) { + var ctx = user, + identity = []; + $.each(['idnumber', 'email', 'phone1', 'phone2', 'department', 'institution'], function(i, k) { + if (typeof user[k] !== 'undefined' && user[k] !== '') { + ctx.hasidentity = true; + identity.push(user[k]); + } + }); + ctx.identity = identity.join(', '); + promises.push(Templates.render('tool_lp/form-user-selector-suggestion', ctx)); + }); + + // Apply the label to the results. + return $.when.apply($.when, promises).then(function() { + var args = arguments; + $.each(results.users, function(index, user) { + user._label = args[i]; + i++; + }); + success(results.users); + }); + + }, failure); + } + + }; + +}); diff --git a/admin/tool/lp/amd/src/frameworkactions.js b/admin/tool/lp/amd/src/frameworkactions.js new file mode 100644 index 00000000000..a783033a91e --- /dev/null +++ b/admin/tool/lp/amd/src/frameworkactions.js @@ -0,0 +1,179 @@ +// 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 . + +/** + * Competency frameworks actions via ajax. + * + * @module tool_lp/frameworkactions + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/templates', 'core/ajax', 'core/notification', 'core/str'], function($, templates, ajax, notification, str) { + // Private variables and functions. + + /** @var {Number} pagecontextid The id of the context */ + var pagecontextid = 0; + + /** @var {Number} frameworkid The id of the framework */ + var frameworkid = 0; + + /** + * Callback to replace the dom element with the rendered template. + * + * @param {String} newhtml The new html to insert. + * @param {String} newjs The new js to run. + */ + var updatePage = function(newhtml, newjs) { + $('[data-region="managecompetencies"]').replaceWith(newhtml); + templates.runTemplateJS(newjs); + }; + + /** + * Callback to render the page template again and update the page. + * + * @param {Object} context The context for the template. + */ + var reloadList = function(context) { + templates.render('tool_lp/manage_competency_frameworks_page', context) + .done(updatePage) + .fail(notification.exception); + }; + + /** + * Duplicate a framework and reload the page. + * @method doDuplicate + * @param {Event} e + */ + var doDuplicate = function(e) { + e.preventDefault(); + + frameworkid = $(this).attr('data-frameworkid'); + + // We are chaining ajax requests here. + var requests = ajax.call([{ + methodname: 'core_competency_duplicate_competency_framework', + args: { id: frameworkid } + }, { + methodname: 'tool_lp_data_for_competency_frameworks_manage_page', + args: { + pagecontext: { + contextid: pagecontextid + } + } + }]); + requests[1].done(reloadList).fail(notification.exception); + }; + /** + * Delete a framework and reload the page. + */ + var doDelete = function() { + + // We are chaining ajax requests here. + var requests = ajax.call([{ + methodname: 'core_competency_delete_competency_framework', + args: { id: frameworkid } + }, { + methodname: 'tool_lp_data_for_competency_frameworks_manage_page', + args: { + pagecontext: { + contextid: pagecontextid + } + } + }]); + requests[0].done(function (success) { + if (success === false) { + var req = ajax.call([{ + methodname: 'core_competency_read_competency_framework', + args: { id: frameworkid } + }]); + req[0].done(function (framework) { + str.get_strings([ + { key: 'frameworkcannotbedeleted', component: 'tool_lp', param: framework.shortname }, + { key: 'cancel', component: 'moodle' } + ]).done(function (strings) { + notification.alert( + null, + strings[0] + ); + }).fail(notification.exception); + }); + } + }).fail(notification.exception); + requests[1].done(reloadList).fail(notification.exception); + }; + + /** + * Handler for "Delete competency framework" actions. + * @param {Event} e + */ + var confirmDelete = function(e) { + e.preventDefault(); + + var id = $(this).attr('data-frameworkid'); + frameworkid = id; + + var requests = ajax.call([{ + methodname: 'core_competency_read_competency_framework', + args: { id: frameworkid } + }]); + + requests[0].done(function(framework) { + str.get_strings([ + { key: 'confirm', component: 'moodle' }, + { key: 'deletecompetencyframework', component: 'tool_lp', param: framework.shortname }, + { key: 'delete', component: 'moodle' }, + { key: 'cancel', component: 'moodle' } + ]).done(function (strings) { + notification.confirm( + strings[0], // Confirm. + strings[1], // Delete competency framework X? + strings[2], // Delete. + strings[3], // Cancel. + doDelete + ); + }).fail(notification.exception); + }).fail(notification.exception); + + }; + + + return /** @alias module:tool_lp/frameworkactions */ { + // Public variables and functions. + + /** + * Expose the event handler for delete. + * @method deleteHandler + * @param {Event} e + */ + deleteHandler: confirmDelete, + + /** + * Expose the event handler for duplicate. + * @method duplicateHandler + * @param {Event} e + */ + duplicateHandler: doDuplicate, + + /** + * Initialise the module. + * @method init + * @param {Number} contextid The context id of the page. + */ + init: function(contextid) { + pagecontextid = contextid; + } + }; +}); diff --git a/admin/tool/lp/amd/src/frameworks_datasource.js b/admin/tool/lp/amd/src/frameworks_datasource.js new file mode 100644 index 00000000000..a4c9274dd6a --- /dev/null +++ b/admin/tool/lp/amd/src/frameworks_datasource.js @@ -0,0 +1,96 @@ +// 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 . + +/** + * Frameworks datasource. + * + * This module is compatible with core/form-autocomplete. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) { + + return /** @alias module:tool_lpmigrate/frameworks_datasource */ { + + /** + * List frameworks. + * + * @param {Number} contextId The context ID. + * @param {Object} options Additional parameters to pass to the external function. + * @return {Promise} + */ + list: function(contextId, options) { + var promise, + args = { + context: { + contextid: contextId + } + }; + + $.extend(args, typeof options === 'undefined' ? {} : options); + promise = Ajax.call([{ + methodname: 'core_competency_list_competency_frameworks', + args: args + }])[0]; + + return promise.fail(Notification.exception); + }, + + /** + * Process the results for auto complete elements. + * + * @param {String} selector The selector of the auto complete element. + * @param {Array} results An array or results. + * @return {Array} New array of results. + */ + processResults: function(selector, results) { + var options = []; + $.each(results, function(index, data) { + options.push({ + value: data.id, + label: data.shortname + ' ' + data.idnumber + }); + }); + return options; + }, + + /** + * Source of data for Ajax element. + * + * @param {String} selector The selector of the auto complete element. + * @param {String} query The query string. + * @param {Function} callback A callback function receiving an array of results. + * @return {Void} + */ + transport: function(selector, query, callback) { + var el = $(selector), + contextId = el.data('contextid'), + onlyVisible = el.data('onlyvisible'); + + if (!contextId) { + throw new Error('The attribute data-contextid is required on ' + selector); + } + + this.list(contextId, { + query: query, + onlyvisible: onlyVisible, + }).then(callback); + } + }; + +}); diff --git a/admin/tool/lp/amd/src/grade_dialogue.js b/admin/tool/lp/amd/src/grade_dialogue.js new file mode 100644 index 00000000000..9063b508eaa --- /dev/null +++ b/admin/tool/lp/amd/src/grade_dialogue.js @@ -0,0 +1,144 @@ +// 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 . + +/** + * Grade dialogue. + * + * @package tool_lp + * @copyright 2016 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/event_base', + 'core/str'], + function($, Notification, Templates, Dialogue, EventBase, Str) { + + /** + * Grade dialogue class. + */ + var Grade = function(ratingOptions) { + EventBase.prototype.constructor.apply(this, []); + this._ratingOptions = ratingOptions; + }; + Grade.prototype = Object.create(EventBase.prototype); + + /** @type {Dialogue} The dialogue. */ + Grade.prototype._popup = null; + /** @type {Array} Array of objects containing, 'value', 'name' and optionally 'selected'. */ + Grade.prototype._ratingOptions = null; + + /** + * After render hook. + * + * @return {Promise} + * @method _afterRender + * @protected + */ + Grade.prototype._afterRender = function() { + var btnRate = this._find('[data-action="rate"]'), + lstRating = this._find('[name="rating"]'), + txtComment = this._find('[name="comment"]'); + + this._find('[data-action="cancel"]').click(function(e) { + e.preventDefault(); + this._trigger('cancelled'); + this.close(); + }.bind(this)); + + lstRating.change(function() { + var node = $(this); + if (!node.val()) { + btnRate.prop('disabled', true); + } else { + btnRate.prop('disabled', false); + } + }).change(); + + btnRate.click(function(e) { + e.preventDefault(); + var val = lstRating.val(); + if (!val) { + return; + } + this._trigger('rated', { + 'rating': val, + 'note': txtComment.val() + }); + this.close(); + }.bind(this)); + }; + + /** + * Close the dialogue. + * + * @method close + */ + Grade.prototype.close = function() { + this._popup.close(); + this._popup = null; + }; + + /** + * Opens the picker. + * + * @param {Number} competencyId The competency ID of the competency to work on. + * @method display + * @return {Promise} + */ + Grade.prototype.display = function() { + return this._render().then(function(html) { + return Str.get_string('rate', 'tool_lp').then(function(title) { + this._popup = new Dialogue( + title, + html, + this._afterRender.bind(this) + ); + }.bind(this)); + }.bind(this)).fail(Notification.exception); + }; + + /** + * Find a node in the dialogue. + * + * @param {String} selector + * @method _find + * @protected + */ + Grade.prototype._find = function(selector) { + return $(this._popup.getContent()).find(selector); + }; + + /** + * Render the dialogue. + * + * @method _render + * @protected + * @return {Promise} + */ + Grade.prototype._render = function() { + var context = { + cangrade: this._canGrade, + ratings: this._ratingOptions + }; + return Templates.render('tool_lp/competency_grader', context); + }; + + return /** @alias module:tool_lp/grade_dialogue */ Grade; + +}); diff --git a/admin/tool/lp/amd/src/grade_user_competency_inline.js b/admin/tool/lp/amd/src/grade_user_competency_inline.js new file mode 100644 index 00000000000..fd9d6ac76ea --- /dev/null +++ b/admin/tool/lp/amd/src/grade_user_competency_inline.js @@ -0,0 +1,146 @@ +// 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 . + +/** + * Module to enable inline editing of a comptency grade. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', + 'core/notification', + 'core/ajax', + 'core/log', + 'tool_lp/grade_dialogue', + 'tool_lp/event_base', + 'tool_lp/scalevalues', + ], function($, notification, ajax, log, GradeDialogue, EventBase, ScaleValues) { + + /** + * InlineEditor + * + * @param {String} selector The selector to trigger the grading. + * @param {Number} The id of the scale for this competency. + * @param {Number} The id of the competency. + * @param {Number} The id of the user. + * @param {Number} The id of the plan. + * @param {Number} The id of the course. + * @param {String} Language string for choose a rating. + */ + var InlineEditor = function(selector, scaleId, competencyId, userId, planId, courseId, chooseStr) { + EventBase.prototype.constructor.apply(this, []); + + var trigger = $(selector); + if (!trigger.length) { + throw new Error('Could not find the trigger'); + } + + this._scaleId = scaleId; + this._competencyId = competencyId; + this._userId = userId; + this._planId = planId; + this._courseId = courseId; + this._chooseStr = chooseStr; + this._setUp(); + + trigger.click(function(e) { + e.preventDefault(); + this._dialogue.display(); + }.bind(this)); + + if (this._planId) { + this._methodName = 'core_competency_grade_competency_in_plan'; + this._args = { + competencyid: this._competencyId, + planid: this._planId + }; + } else if (this._courseId) { + this._methodName = 'core_competency_grade_competency_in_course'; + this._args = { + competencyid: this._competencyId, + courseid: this._courseId, + userid: this._userId + }; + } else { + this._methodName = 'core_competency_grade_competency'; + this._args = { + userid: this._userId, + competencyid: this._competencyId + }; + } + }; + InlineEditor.prototype = Object.create(EventBase.prototype); + + /** + * Setup. + * + * @method _setUp + */ + InlineEditor.prototype._setUp = function() { + var options = [], + self = this; + + var promise = ScaleValues.get_values(self._scaleId); + promise.done(function(scalevalues) { + options.push({ + value: '', + name: self._chooseStr + }); + + for (var i = 0; i < scalevalues.length; i++) { + var optionConfig = scalevalues[i]; + options.push({ + value: optionConfig.id, + name: optionConfig.name + }); + } + + self._dialogue = new GradeDialogue(options); + self._dialogue.on('rated', function(e, data) { + var args = self._args; + args.grade = data.rating; + args.note = data.note; + ajax.call([{ + methodname: self._methodName, + args: args, + done: function(evidence) { + self._trigger('competencyupdated', { args: args, evidence: evidence }); + }.bind(self), + fail: notification.exception + }]); + }.bind(self)); + }).fail(notification.exception); + }; + + /** @type {Number} The scale id for this competency. */ + InlineEditor.prototype._scaleId = null; + /** @type {Number} The id of the competency. */ + InlineEditor.prototype._competencyId = null; + /** @type {Number} The id of the user. */ + InlineEditor.prototype._userId = null; + /** @type {Number} The id of the plan. */ + InlineEditor.prototype._planId = null; + /** @type {Number} The id of the course. */ + InlineEditor.prototype._courseId = null; + /** @type {String} The text for Choose rating. */ + InlineEditor.prototype._chooseStr = null; + /** @type {GradeDialogue} The grading dialogue. */ + InlineEditor.prototype._dialogue = null; + + return /** @alias module:tool_lp/grade_user_competency_inline */ InlineEditor; + +}); diff --git a/admin/tool/lp/amd/src/menubar.js b/admin/tool/lp/amd/src/menubar.js new file mode 100644 index 00000000000..cf1b773583e --- /dev/null +++ b/admin/tool/lp/amd/src/menubar.js @@ -0,0 +1,817 @@ +// 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 . + +/** + * Aria menubar functionality. Enhances a simple nested list structure into a full aria widget. + * Based on the open ajax example: http://oaa-accessibility.org/example/26/ + * + * @module tool_lp/menubar + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery'], function($) { + + /** @property {boolean} Flag to indicate if we have already registered a click event handler for the document. */ + var documentClickHandlerRegistered = false; + + /** @property {boolean} Flag to indicate whether there's an active, open menu. */ + var menuActive = false; + + /** + * Close all open submenus anywhere in the page (there should only ever be one open at a time). + * + * @method closeAllSubMenus + */ + var closeAllSubMenus = function() { + $('.tool-lp-menu .tool-lp-sub-menu').attr('aria-hidden', 'true'); + // Every menu's closed at this point, so set the menu active flag to false. + menuActive = false; + }; + + /** + * Constructor + * + * @param {$} Jquery collection matching the root of the menu. + * @param {Function[]} handlers, called when a menu item is chosen. + */ + var Menubar = function(menuRoot, handlers) { + // Setup private class variables. + this.menuRoot = menuRoot; + this.handlers = handlers; + this.rootMenus = this.menuRoot.children('li'); + this.subMenus = this.rootMenus.children('ul'); + this.subMenuItems = this.subMenus.children('li'); + this.allItems = this.rootMenus.add(this.subMenuItems); + this.activeItem = null; + this.isChildOpen = false; + + this.keys = { + tab: 9, + enter: 13, + esc: 27, + space: 32, + left: 37, + up: 38, + right: 39, + down: 40 + }; + + this.addAriaAttributes(); + // Add the event listeners. + this.addEventListeners(); + }; + + /** + * Open a submenu, first it closes all other sub-menus and sets the open direction. + * @method openSubMenu + * @param {Node} menu + */ + Menubar.prototype.openSubMenu = function(menu) { + this.setOpenDirection(); + closeAllSubMenus(); + menu.attr('aria-hidden', 'false'); + // Set menu active flag to true when a menu is opened. + menuActive = true; + }; + + + /** + * Bind the event listeners to the DOM + * @method addEventListeners + */ + Menubar.prototype.addEventListeners = function() { + var currentThis = this; + + // When clicking outside the menubar. + if (documentClickHandlerRegistered === false) { + $(document).click(function() { + // Check if a menu is opened. + if (menuActive) { + // Close menu. + closeAllSubMenus(); + } + }); + // Set this flag to true so that we won't need to add a document click handler for the other Menubar instances. + documentClickHandlerRegistered = true; + } + + // Hovers. + this.subMenuItems.mouseenter(function() { + $(this).addClass('menu-hover'); + return true; + }); + + this.subMenuItems.mouseout(function() { + $(this).removeClass('menu-hover'); + return true; + }); + + // Mouse listeners. + this.allItems.click(function(e) { + return currentThis.handleClick($(this), e); + }); + + // Key listeners. + this.allItems.keydown(function(e) { + return currentThis.handleKeyDown($(this), e); + }); + + this.allItems.focus(function() { + return currentThis.handleFocus($(this)); + }); + + this.allItems.blur(function() { + return currentThis.handleBlur($(this)); + }); + }; + + /** + * Process click events for the top menus. + * + * @method handleClick + * @param {Object} item is the jquery object of the item firing the event + * @param {Event} e is the associated event object + * @return boolean Returns false + */ + Menubar.prototype.handleClick = function(item, e) { + e.stopPropagation(); + + var parentUL = item.parent(); + + if (parentUL.is('.tool-lp-menu')) { + // Toggle the child menu open/closed. + if (item.children('ul').first().attr('aria-hidden') == 'true') { + this.openSubMenu(item.children('ul').first()); + } else { + item.children('ul').first().attr('aria-hidden', 'true'); + } + } else { + // Remove hover and focus styling. + this.allItems.removeClass('menu-hover menu-focus'); + + // Clear the active item. + this.activeItem = null; + + // Close the menu. + this.menuRoot.find('ul').not('.root-level').attr('aria-hidden','true'); + // Follow any link, or call the click handlers. + var anchor = item.find('a').first(); + var clickEvent = new $.Event('click'); + clickEvent.target = anchor; + var eventHandled = false; + if (this.handlers) { + $.each(this.handlers, function(selector, handler) { + if (eventHandled) { + return false; + } + if (item.find(selector).length > 0) { + var callable = $.proxy(handler, anchor); + // False means stop propogatting events. + eventHandled = (callable(clickEvent) === false) || clickEvent.isDefaultPrevented(); + } + }); + } + // If we didn't find a handler, and the HREF is # that probably means that + // we are handling it from somewhere else. Let's just do nothing in that case. + if (!eventHandled && anchor.attr('href') !== '#') { + window.location.href = anchor.attr('href'); + } + } + return false; + }; + + /* + * Process focus events for the menu. + * + * @method handleFocus + * @param {Object} item is the jquery object of the item firing the event + * @return boolean Returns false + */ + Menubar.prototype.handleFocus = function(item) { + + // If activeItem is null, we are getting focus from outside the menu. Store + // the item that triggered the event. + if (this.activeItem === null) { + this.activeItem = item; + } else if (item[0] != this.activeItem[0]) { + return true; + } + + // Get the set of jquery objects for all the parent items of the active item. + var parentItems = this.activeItem.parentsUntil('ul.tool-lp-menu').filter('li'); + + // Remove focus styling from all other menu items. + this.allItems.removeClass('menu-focus'); + + // Add focus styling to the active item. + this.activeItem.addClass('menu-focus'); + + // Add focus styling to all parent items. + parentItems.addClass('menu-focus'); + + // If the bChildOpen flag has been set, open the active item's child menu (if applicable). + if (this.isChildOpen === true) { + + var itemUL = item.parent(); + + // If the itemUL is a root-level menu and item is a parent item, + // show the child menu. + if (itemUL.is('.tool-lp-menu') && (item.attr('aria-haspopup') == 'true')) { + this.openSubMenu(item.children('ul').first()); + } + } + + return true; + }; + + /* + * Process blur events for the menu. + * + * @method handleBlur + * @param {Object} item is the jquery object of the item firing the event + * @return boolean Returns false + */ + Menubar.prototype.handleBlur = function(item) { + item.removeClass('menu-focus'); + + return true; + }; + + /* + * Determine if the menu should open to the left, or the right, + * based on the screen size and menu position. + * @method setOpenDirection + */ + Menubar.prototype.setOpenDirection = function() { + var pos = this.menuRoot.offset(); + var isRTL = $(document.body).hasClass('dir-rtl'); + var menuMinWidth = 160; + var openLeft = false; + + if (isRTL) { + if (pos.left - menuMinWidth < 0) { + openLeft = true; + } + } else { + if (pos.left + menuMinWidth > $( window ).width()) { + openLeft = true; + } + } + + if (openLeft) { + this.menuRoot.addClass('tool-lp-menu-open-left'); + } else { + this.menuRoot.removeClass('tool-lp-menu-open-left'); + } + + }; + + /* + * Process keyDown events for the menu. + * + * @method handleKeyDown + * @param {Object} item is the jquery object of the item firing the event + * @param {Event} e is the associated event object + * @return boolean Returns false if consuming the event + */ + Menubar.prototype.handleKeyDown = function(item, e) { + + if (e.altKey || e.ctrlKey) { + // Modifier key pressed: Do not process. + return true; + } + + switch(e.keyCode) { + case this.keys.tab: { + + // Hide all menu items and update their aria attributes. + this.menuRoot.find('ul').attr('aria-hidden', 'true'); + + // Remove focus styling from all menu items. + this.allItems.removeClass('menu-focus'); + + this.activeItem = null; + + this.isChildOpen = false; + + break; + } + case this.keys.esc: { + var itemUL = item.parent(); + + if (itemUL.is('.tool-lp-menu')) { + // Hide the child menu and update the aria attributes. + item.children('ul').first().attr('aria-hidden', 'true'); + } else { + + // Move up one level. + this.activeItem = itemUL.parent(); + + // Reset the isChildOpen flag. + this.isChildOpen = false; + + // Set focus on the new item. + this.activeItem.focus(); + + // Hide the active menu and update the aria attributes. + itemUL.attr('aria-hidden', 'true'); + } + + e.stopPropagation(); + return false; + } + case this.keys.enter: + case this.keys.space: { + // Trigger click handler. + return this.handleClick(item, e); + } + + case this.keys.left: { + + this.activeItem = this.moveToPrevious(item); + + this.activeItem.focus(); + + e.stopPropagation(); + return false; + } + case this.keys.right: { + + this.activeItem = this.moveToNext(item); + + this.activeItem.focus(); + + e.stopPropagation(); + return false; + } + case this.keys.up: { + + this.activeItem = this.moveUp(item); + + this.activeItem.focus(); + + e.stopPropagation(); + return false; + } + case this.keys.down: { + + this.activeItem = this.moveDown(item); + + this.activeItem.focus(); + + e.stopPropagation(); + return false; + } + } + + return true; + + }; + + + /** + * Move to the next menu level. + * This will be either the next root-level menu or the child of a menu parent. If + * at the root level and the active item is the last in the menu, this function will loop + * to the first menu item. + * + * If the menu is a horizontal menu, the first child element of the newly selected menu will + * be selected + * + * @method moveToNext + * @param {Object} item is the active menu item + * @return {Object} Returns the item to move to. Returns item is no move is possible + */ + Menubar.prototype.moveToNext = function(item) { + // Item's containing menu. + var itemUL = item.parent(); + + // The items in the currently active menu. + var menuItems = itemUL.children('li'); + + // The number of items in the active menu. + var menuNum = menuItems.length; + // The items index in its menu. + var menuIndex = menuItems.index(item); + var newItem = null; + var childMenu = null; + + if (itemUL.is('.tool-lp-menu')) { + // This is the root level move to next sibling. This will require closing + // the current child menu and opening the new one. + + if (menuIndex < menuNum-1) { + // Not the last root menu. + newItem = item.next(); + } else { // Wrap to first item. + newItem = menuItems.first(); + } + + // Close the current child menu (if applicable). + if (item.attr('aria-haspopup') == 'true') { + + childMenu = item.children('ul').first(); + + if (childMenu.attr('aria-hidden') == 'false') { + // Update the child menu's aria-hidden attribute. + childMenu.attr('aria-hidden', 'true'); + this.isChildOpen = true; + } + } + + // Remove the focus styling from the current menu. + item.removeClass('menu-focus'); + + // Open the new child menu (if applicable). + if ((newItem.attr('aria-haspopup') === 'true') && (this.isChildOpen === true)) { + + childMenu = newItem.children('ul').first(); + + // Update the child's aria-hidden attribute. + this.openSubMenu(childMenu); + } + } else { + // This is not the root level. If there is a child menu to be moved into, do that; + // otherwise, move to the next root-level menu if there is one. + if (item.attr('aria-haspopup') == 'true') { + + childMenu = item.children('ul').first(); + + newItem = childMenu.children('li').first(); + + // Show the child menu and update its aria attributes. + this.openSubMenu(childMenu); + } else { + // At deepest level, move to the next root-level menu. + + var parentMenus = null; + var rootItem = null; + + // Get list of all parent menus for item, up to the root level. + parentMenus = item.parentsUntil('ul.tool-lp-menu').filter('ul').not('.tool-lp-menu'); + + // Hide the current menu and update its aria attributes accordingly. + parentMenus.attr('aria-hidden', 'true'); + + // Remove the focus styling from the active menu. + parentMenus.find('li').removeClass('menu-focus'); + parentMenus.last().parent().removeClass('menu-focus'); + + // The containing root for the menu. + rootItem = parentMenus.last().parent(); + + menuIndex = this.rootMenus.index(rootItem); + + // If this is not the last root menu item, move to the next one. + if (menuIndex < this.rootMenus.length-1) { + newItem = rootItem.next(); + } else { + // Loop. + newItem = this.rootMenus.first(); + } + + // Add the focus styling to the new menu. + newItem.addClass('menu-focus'); + + if (newItem.attr('aria-haspopup') == 'true') { + childMenu = newItem.children('ul').first(); + + newItem = childMenu.children('li').first(); + + // Show the child menu and update it's aria attributes. + this.openSubMenu(childMenu); + this.isChildOpen = true; + } + } + } + + return newItem; + }; + + /** + * Member function to move to the previous menu level. + * This will be either the previous root-level menu or the child of a menu parent. If + * at the root level and the active item is the first in the menu, this function will loop + * to the last menu item. + * + * If the menu is a horizontal menu, the first child element of the newly selected menu will + * be selected + * + * @method moveToPrevious + * @param {Object} item is the active menu item + * @return {Object} Returns the item to move to. Returns item is no move is possible + */ + Menubar.prototype.moveToPrevious = function(item) { + // Item's containing menu. + var itemUL = item.parent(); + // The items in the currently active menu. + var menuItems = itemUL.children('li'); + // The items index in its menu. + var menuIndex = menuItems.index(item); + var newItem = null; + var childMenu = null; + + if (itemUL.is('.tool-lp-menu')) { + // This is the root level move to previous sibling. This will require closing + // the current child menu and opening the new one. + + if (menuIndex > 0) { + // Not the first root menu. + newItem = item.prev(); + } else { + // Wrap to last item. + newItem = menuItems.last(); + } + + // Close the current child menu (if applicable). + if (item.attr('aria-haspopup') == 'true') { + childMenu = item.children('ul').first(); + + if (childMenu.attr('aria-hidden') == 'false') { + // Update the child menu's aria-hidden attribute. + childMenu.attr('aria-hidden', 'true'); + this.isChildOpen = true; + } + } + + // Remove the focus styling from the current menu. + item.removeClass('menu-focus'); + + // Open the new child menu (if applicable). + if ((newItem.attr('aria-haspopup') === 'true') && (this.isChildOpen === true)) { + + childMenu = newItem.children('ul').first(); + + // Update the child's aria-hidden attribute. + this.openSubMenu(childMenu); + + } + } else { + // This is not the root level. If there is a parent menu that is not the + // root menu, move up one level; otherwise, move to first item of the previous + // root menu. + + var parentLI = itemUL.parent(); + var parentUL = parentLI.parent(); + + // If this is a vertical menu or is not the first child menu + // of the root-level menu, move up one level. + if (!parentUL.is('.tool-lp-menu')) { + + newItem = itemUL.parent(); + + // Hide the active menu and update aria-hidden. + itemUL.attr('aria-hidden', 'true'); + + // Remove the focus highlight from the item. + item.removeClass('menu-focus'); + + } else { + // Move to previous root-level menu. + + // Hide the current menu and update the aria attributes accordingly. + itemUL.attr('aria-hidden', 'true'); + + // Remove the focus styling from the active menu. + item.removeClass('menu-focus'); + parentLI.removeClass('menu-focus'); + + menuIndex = this.rootMenus.index(parentLI); + + if (menuIndex > 0) { + // Move to the previous root-level menu. + newItem = parentLI.prev(); + } else { + // Loop to last root-level menu. + newItem = this.rootMenus.last(); + } + + // Add the focus styling to the new menu. + newItem.addClass('menu-focus'); + + if (newItem.attr('aria-haspopup') == 'true') { + childMenu = newItem.children('ul').first(); + + // Show the child menu and update it's aria attributes. + this.openSubMenu(childMenu); + this.isChildOpen = true; + + newItem = childMenu.children('li').first(); + } + } + } + + return newItem; + }; + + /** + * Member function to select the next item in a menu. + * If the active item is the last in the menu, this function will loop to the + * first menu item. + * + * @method moveDown + * @param {Object} item is the active menu item + * @param {String} startChr is the character to attempt to match against the beginning of the + * menu item titles. If found, focus moves to the next menu item beginning with that character. + * @return {Object} Returns the item to move to. Returns item is no move is possible + */ + Menubar.prototype.moveDown = function(item, startChr) { + // Item's containing menu. + var itemUL = item.parent(); + // The items in the currently active menu. + var menuItems = itemUL.children('li').not('.separator'); + // The number of items in the active menu. + var menuNum = menuItems.length; + // The items index in its menu. + var menuIndex = menuItems.index(item); + var newItem = null; + var newItemUL = null; + + if (itemUL.is('.tool-lp-menu')) { + // This is the root level menu. + + if (item.attr('aria-haspopup') != 'true') { + // No child menu to move to. + return item; + } + + // Move to the first item in the child menu. + newItemUL = item.children('ul').first(); + newItem = newItemUL.children('li').first(); + + // Make sure the child menu is visible. + this.openSubMenu(newItemUL); + + return newItem; + } + + // If $item is not the last item in its menu, move to the next item. If startChr is specified, move + // to the next item with a title that begins with that character. + if (startChr) { + var match = false; + var curNdx = menuIndex+1; + + // Check if the active item was the last one on the list. + if (curNdx == menuNum) { + curNdx = 0; + } + + // Iterate through the menu items (starting from the current item and wrapping) until a match is found + // or the loop returns to the current menu item. + while (curNdx != menuIndex) { + + var titleChr = menuItems.eq(curNdx).html().charAt(0); + + if (titleChr.toLowerCase() == startChr) { + match = true; + break; + } + + curNdx = curNdx+1; + + if (curNdx == menuNum) { + // Reached the end of the list, start again at the beginning. + curNdx = 0; + } + } + + if (match === true) { + newItem = menuItems.eq(curNdx); + + // Remove the focus styling from the current item. + item.removeClass('menu-focus'); + + return newItem; + } else { + return item; + } + } else { + if (menuIndex < menuNum-1) { + newItem = menuItems.eq(menuIndex+1); + } else { + newItem = menuItems.first(); + } + } + + // Remove the focus styling from the current item. + item.removeClass('menu-focus'); + + return newItem; + }; + + /** + * Function moveUp() is a member function to select the previous item in a menu. + * If the active item is the first in the menu, this function will loop to the + * last menu item. + * + * @method moveUp + * @param {Object} item is the active menu item + * @return {Object} Returns the item to move to. Returns item is no move is possible + */ + Menubar.prototype.moveUp = function(item) { + // Item's containing menu. + var itemUL = item.parent(); + // The items in the currently active menu. + var menuItems = itemUL.children('li').not('.separator'); + // The items index in its menu. + var menuIndex = menuItems.index(item); + var newItem = null; + + if (itemUL.is('.tool-lp-menu')) { + // This is the root level menu. + // Nothing to do. + return item; + } + + // If item is not the first item in its menu, move to the previous item. + if (menuIndex > 0) { + newItem = menuItems.eq(menuIndex-1); + } else { + // Loop to top of menu. + newItem = menuItems.last(); + } + + // Remove the focus styling from the current item. + item.removeClass('menu-focus'); + + return newItem; + }; + + /** + * Enhance the dom with aria attributes. + * @method addAriaAttributes + */ + Menubar.prototype.addAriaAttributes = function() { + this.menuRoot.attr('role', 'menubar'); + this.rootMenus.attr('role', 'menuitem'); + this.rootMenus.attr('tabindex', '0'); + this.rootMenus.attr('aria-haspopup', 'true'); + this.subMenus.attr('role', 'menu'); + this.subMenus.attr('aria-hidden', 'true'); + this.subMenuItems.attr('role', 'menuitem'); + this.subMenuItems.attr('tabindex', '-1'); + + // For CSS styling and effects. + this.menuRoot.addClass('tool-lp-menu'); + this.allItems.addClass('tool-lp-menu-item'); + this.rootMenus.addClass('tool-lp-root-menu'); + this.subMenus.addClass('tool-lp-sub-menu'); + }; + + return /** @alias module:tool_lp/menubar */ { + /** + * Create a menu bar object for every node matching the selector. + * + * The expected DOM structure is shown below. + *
    <- This is the target of the selector parameter. + *
  • <- This is repeated for each top level menu. + * Text <- This is the text for the top level menu. + *
      <- This is a list of the entries in this top level menu. + *
    • <- This is repeated for each menu entry. + * Choice 1 <- The anchor for the menu. + *
    • + *
    + *
  • + *
+ * + * @method enhance + * @param {String} selector - The selector for the outer most menu node. + * @param {Function} handler - Javascript handler for when a menu item was chosen. If the + * handler returns true (or does not exist), the + * menu will look for an anchor with a link to follow. + * For example, if the menu entry has a "data-action" attribute + * and we want to call a javascript function when that entry is chosen, + * we could pass a list of handlers like this: + * { "[data-action='add']" : callAddFunction } + */ + enhance: function(selector, handler) { + $(selector).each(function (index, element) { + var menuRoot = $(element); + // Don't enhance the same menu twice. + if (menuRoot.data("menubarEnhanced") !== true) { + (new Menubar(menuRoot, handler)); + menuRoot.data("menubarEnhanced", true); + } + }); + }, + + /** + * Handy function to close all open menus anywhere on the page. + * @method closeAll + */ + closeAll: closeAllSubMenus + }; +}); diff --git a/admin/tool/lp/amd/src/parentcompetency_form.js b/admin/tool/lp/amd/src/parentcompetency_form.js new file mode 100644 index 00000000000..1e0e3d7f3ce --- /dev/null +++ b/admin/tool/lp/amd/src/parentcompetency_form.js @@ -0,0 +1,207 @@ +// 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 . + +/** + * Handle selecting parent competency in competency form. + * + * @module tool_lp/parentcompetency_form + * @package tool_lp + * @copyright 2015 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/ajax', 'core/str', 'tool_lp/competencypicker', 'core/templates', 'core/notification'], + function($, ajax, Str, Picker, Templates, Notification) { + + /** + * Parent Competency Form object. + * @param {String} buttonSelector The parent competency button selector. + * @param {String} inputHiddenSelector The hidden input field selector. + * @param {String} staticElementSelector The static element displaying the parent competency. + * @param {Number} frameworkId The competency framework ID. + * @param {Number} frameworkMaxLevel The framework max level. + * @param {Number} pageContextId The page context ID. + */ + var ParentCompetencyForm = function(buttonSelector, + inputHiddenSelector, + staticElementSelector, + frameworkId, + frameworkMaxLevel, + pageContextId) { + this.buttonSelector = buttonSelector; + this.inputHiddenSelector = inputHiddenSelector; + this.staticElementSelector = staticElementSelector; + this.frameworkId = frameworkId; + this.frameworkMaxLevel = frameworkMaxLevel; + this.pageContextId = pageContextId; + + // Register the events. + this.registerEvents(); + }; + + /** @var {String} The parent competency button selector. */ + ParentCompetencyForm.prototype.buttonSelector = null; + /** @var {String} The hidden input field selector. */ + ParentCompetencyForm.prototype.inputHiddenSelector = null; + /** @var {String} The static element displaying the parent competency. */ + ParentCompetencyForm.prototype.staticElementSelector = null; + /** @var {Number} The competency framework ID. */ + ParentCompetencyForm.prototype.frameworkId = null; + /** @var {Number} The framework max level. */ + ParentCompetencyForm.prototype.frameworkMaxLevel = null; + /** @var {Number} The page context ID. */ + ParentCompetencyForm.prototype.pageContextId = null; + + /** + * Set the parent competency in the competency form. + * + * @param {Object} Data containing selected cmpetency. + * @method setParent + */ + ParentCompetencyForm.prototype.setParent = function(data) { + var self = this; + + if (data.competencyId !== 0) { + ajax.call([ + { methodname: 'core_competency_read_competency', args: { + id: data.competencyId + }} + ])[0].done(function(competency) { + $(self.staticElementSelector).html(competency.shortname); + $(self.inputHiddenSelector).val(competency.id); + }).fail(Notification.exception); + } else { + // Root of competency framework selected. + Str.get_string('competencyframeworkroot', 'tool_lp').then(function(rootframework) { + $(self.staticElementSelector).html(rootframework); + $(self.inputHiddenSelector).val(data.competencyId); + }).fail(Notification.exception); + } + }; + + /** + * Register the events of parent competency button click. + * + * @method registerEvents + */ + ParentCompetencyForm.prototype.registerEvents = function() { + var self = this; + + // Event on edit parent button. + $(self.buttonSelector).on('click', function(e) { + e.preventDefault(); + + var picker = new Picker(self.pageContextId, self.frameworkId, 'self', false); + var maxlevel = self.frameworkMaxLevel; + // Override the fetchcompetencies method to filter by max level. + picker._fetchCompetencies = function(frameworkId, searchText) { + var self = this; + + return ajax.call([ + { methodname: 'core_competency_search_competencies', args: { + searchtext: searchText, + competencyframeworkid: frameworkId + }} + ])[0].done(function(competencies) { + + var disabledcompetencies = []; + function addCompetencyChildren(parent, competencies) { + for (var i = 0; i < competencies.length; i++) { + // Check if competency does not exceed the framework max level. + var path = String(competencies[i].path), + level = path.split('/').length - 2; + if (level >= maxlevel && competencies[i].id !== "0") { + disabledcompetencies.push(competencies[i].id); + } + + 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; + self.setDisallowedCompetencyIDs(disabledcompetencies); + + }.bind(self)).fail(Notification.exception); + }; + // Override the render method to make framework selectable. + picker._render = function() { + var self = this; + return self._preRender().then(function() { + 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_competencyform', context); + }.bind(self)); + }; + + // On selected competency. + picker.on('save', function(e, data) { + self.setParent(data); + }.bind(self)); + + picker.display(); + }); + }; + + return { + + /** + * Main initialisation. + * @param {String} buttonSelector The parent competency button selector. + * @param {String} inputHiddenSelector The hidden input field selector. + * @param {String} staticElementSelector The static element displaying the parent competency. + * @param {Number} frameworkId The competency framework ID. + * @param {Number} frameworkMaxLevel The framework max level. + * @param {Number} pageContextId The page context ID. + * @method init + */ + init: function(buttonSelector, + inputSelector, + staticElementSelector, + frameworkId, + frameworkMaxLevel, + pageContextId) { + // Create instance. + new ParentCompetencyForm(buttonSelector, + inputSelector, + staticElementSelector, + frameworkId, + frameworkMaxLevel, + pageContextId); + } + }; +}); diff --git a/admin/tool/lp/amd/src/planactions.js b/admin/tool/lp/amd/src/planactions.js new file mode 100644 index 00000000000..8d3e05043f2 --- /dev/null +++ b/admin/tool/lp/amd/src/planactions.js @@ -0,0 +1,600 @@ +// 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 . + +/** + * Plan actions via ajax. + * + * @module tool_lp/planactions + * @package tool_lp + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', + 'core/templates', + 'core/ajax', + 'core/notification', + 'core/str', + 'tool_lp/menubar', + 'tool_lp/dialogue'], + function($, templates, ajax, notification, str, Menubar, Dialogue) { + + /** + * PlanActions class. + * + * Note that presently this cannot be instantiated more than once per page. + * + * @param {String} type The type of page we're in. + */ + var PlanActions = function(type) { + this._type = type; + + if (type === 'plan') { + // This is the page to view one plan. + this._region = '[data-region="plan-page"]'; + this._planNode = '[data-region="plan-page"]'; + this._template = 'tool_lp/plan_page'; + this._contextMethod = 'tool_lp_data_for_plan_page'; + + } else if (type === 'plans') { + // This is the page to view a list of plans. + this._region = '[data-region="plans"]'; + this._planNode = '[data-region="plan-node"]'; + this._template = 'tool_lp/plans_page'; + this._contextMethod = 'tool_lp_data_for_plans_page'; + + } else { + throw new TypeError('Unexpected type.'); + } + }; + + /** @type {String} Ajax method to fetch the page data from. */ + PlanActions.prototype._contextMethod = null; + /** @type {String} Selector to find the node describing the plan. */ + PlanActions.prototype._planNode = null; + /** @type {String} Selector mapping to the region to update. Usually similar to wrapper. */ + PlanActions.prototype._region = null; + /** @type {String} Name of the template used to render the region. */ + PlanActions.prototype._template = null; + /** @type {String} Type of page/region we're in. */ + PlanActions.prototype._type = null; + + /** + * Resolve the arguments to refresh the region. + * + * @param {Object} planData Plan data from plan node. + * @return {Object} List of arguments. + */ + PlanActions.prototype._getContextArgs = function(planData) { + var self = this, + args = {}; + + if (self._type === 'plan') { + args = { + planid: planData.id + }; + + } else if (self._type === 'plans') { + args = { + userid: planData.userid + }; + } + + return args; + }; + + /** + * Refresh the plan view. + * + * This is useful when you only want to refresh the view. + * + * @param {String} selector The node to search the plan data from. + */ + PlanActions.prototype.refresh = function(selector) { + var planData = this._findPlanData($(selector)); + this._callAndRefresh([], planData); + }; + + /** + * Callback to render the region template. + * + * @param {Object} context The context for the template. + */ + PlanActions.prototype._renderView = function(context) { + var self = this; + templates.render(self._template, context) + .done(function(newhtml, newjs) { + $(self._region).replaceWith(newhtml); + templates.runTemplateJS(newjs); + }.bind(self)) + .fail(notification.exception); + }; + + /** + * Call multiple ajax methods, and refresh. + * + * @param {Array} calls List of Ajax calls. + * @param {Object} planData Plan data from plan node. + * @return {Promise} + */ + PlanActions.prototype._callAndRefresh = function(calls, planData) { + var self = this; + + calls.push({ + methodname: self._contextMethod, + args: self._getContextArgs(planData) + }); + + // Apply all the promises, and refresh when the last one is resolved. + return $.when.apply($.when, ajax.call(calls)) + .then(function() { + self._renderView.call(self, arguments[arguments.length - 1]); + }) + .fail(notification.exception); + }; + + /** + * Delete a plan and reload the region. + * + * @param {Object} planData Plan data from plan node. + */ + PlanActions.prototype._doDelete = function(planData) { + var self = this, + calls = [{ + methodname: 'core_competency_delete_plan', + args: { id: planData.id } + }]; + self._callAndRefresh(calls, planData); + }; + + /** + * Delete a plan. + * + * @param {Object} planData Plan data from plan node. + */ + PlanActions.prototype.deletePlan = function(planData) { + var self = this, + requests; + + requests = ajax.call([{ + methodname: 'core_competency_read_plan', + args: { id: planData.id } + }]); + + requests[0].done(function(plan) { + str.get_strings([ + { key: 'confirm', component: 'moodle' }, + { key: 'deleteplan', component: 'tool_lp', param: plan.name }, + { key: 'delete', component: 'moodle' }, + { key: 'cancel', component: 'moodle' } + ]).done(function (strings) { + notification.confirm( + strings[0], // Confirm. + strings[1], // Delete plan X? + strings[2], // Delete. + strings[3], // Cancel. + function() { + self._doDelete(planData); + }.bind(self) + ); + }).fail(notification.exception); + }).fail(notification.exception); + + }; + + /** + * Reopen plan and reload the region. + * + * @param {Object} planData Plan data from plan node. + */ + PlanActions.prototype._doReopenPlan = function(planData) { + var self = this, + calls = [{ + methodname: 'core_competency_reopen_plan', + args: { planid: planData.id} + }]; + self._callAndRefresh(calls, planData); + }; + + /** + * Reopen a plan. + * + * @param {Object} planData Plan data from plan node. + */ + PlanActions.prototype.reopenPlan = function(planData) { + var self = this, + requests = ajax.call([{ + methodname: 'core_competency_read_plan', + args: { id: planData.id } + }]); + + requests[0].done(function(plan) { + str.get_strings([ + { key: 'confirm', component: 'moodle' }, + { key: 'reopenplanconfirm', component: 'tool_lp', param: plan.name }, + { key: 'reopenplan', component: 'tool_lp' }, + { key: 'cancel', component: 'moodle' } + ]).done(function (strings) { + notification.confirm( + strings[0], // Confirm. + strings[1], // Reopen plan X? + strings[2], // reopen. + strings[3], // Cancel. + function() { + self._doReopenPlan(planData); + }.bind(self) + ); + }).fail(notification.exception); + }).fail(notification.exception); + + }; + + /** + * Complete plan and reload the region. + * + * @param {Object} planData Plan data from plan node. + */ + PlanActions.prototype._doCompletePlan = function(planData) { + var self = this, + calls = [{ + methodname: 'core_competency_complete_plan', + args: { planid: planData.id} + }]; + self._callAndRefresh(calls, planData); + }; + + /** + * Complete a plan process. + * + * @param {Object} planData Plan data from plan node. + */ + PlanActions.prototype.completePlan = function(planData) { + var self = this, + requests = ajax.call([{ + methodname: 'core_competency_read_plan', + args: { id: planData.id } + }]); + + requests[0].done(function(plan) { + str.get_strings([ + { key: 'confirm', component: 'moodle' }, + { key: 'completeplanconfirm', component: 'tool_lp', param: plan.name }, + { key: 'completeplan', component: 'tool_lp' }, + { key: 'cancel', component: 'moodle' } + ]).done(function (strings) { + notification.confirm( + strings[0], // Confirm. + strings[1], // Complete plan X? + strings[2], // Complete. + strings[3], // Cancel. + function() { + self._doCompletePlan(planData); + }.bind(self) + ); + }).fail(notification.exception); + }).fail(notification.exception); + }; + + /** + * Unlink plan and reload the region. + * + * @param {Object} planData Plan data from plan node. + */ + PlanActions.prototype._doUnlinkPlan = function(planData) { + var self = this, + calls = [{ + methodname: 'core_competency_unlink_plan_from_template', + args: { planid: planData.id} + }]; + self._callAndRefresh(calls, planData); + }; + + /** + * Unlink a plan process. + * + * @param {Object} planData Plan data from plan node. + */ + PlanActions.prototype.unlinkPlan = function(planData) { + var self = this, + requests = ajax.call([{ + methodname: 'core_competency_read_plan', + args: { id: planData.id } + }]); + + requests[0].done(function(plan) { + str.get_strings([ + { key: 'confirm', component: 'moodle' }, + { key: 'unlinkplantemplateconfirm', component: 'tool_lp', param: plan.name }, + { key: 'unlinkplantemplate', component: 'tool_lp' }, + { key: 'cancel', component: 'moodle' } + ]).done(function (strings) { + notification.confirm( + strings[0], // Confirm. + strings[1], // Unlink plan X? + strings[2], // Unlink. + strings[3], // Cancel. + function() { + self._doUnlinkPlan(planData); + }.bind(self) + ); + }).fail(notification.exception); + }).fail(notification.exception); + }; + + /** + * Request review of a plan. + * + * @param {Object} planData Plan data from plan node. + * @method _doRequestReview + */ + PlanActions.prototype._doRequestReview = function(planData) { + var calls = [{ + methodname: 'core_competency_plan_request_review', + args: { + id: planData.id + } + }]; + this._callAndRefresh(calls, planData); + }; + + /** + * Request review of a plan. + * + * @param {Object} planData Plan data from plan node. + * @method requestReview + */ + PlanActions.prototype.requestReview = function(planData) { + this._doRequestReview(planData); + }; + + /** + * Cancel review request of a plan. + * + * @param {Object} planData Plan data from plan node. + * @method _doCancelReviewRequest + */ + PlanActions.prototype._doCancelReviewRequest = function(planData) { + var calls = [{ + methodname: 'core_competency_plan_cancel_review_request', + args: { + id: planData.id + } + }]; + this._callAndRefresh(calls, planData); + }; + + /** + * Cancel review request of a plan. + * + * @param {Object} planData Plan data from plan node. + * @method cancelReviewRequest + */ + PlanActions.prototype.cancelReviewRequest = function(planData) { + this._doCancelReviewRequest(planData); + }; + + /** + * Start review of a plan. + * + * @param {Object} planData Plan data from plan node. + * @method _doStartReview + */ + PlanActions.prototype._doStartReview = function(planData) { + var calls = [{ + methodname: 'core_competency_plan_start_review', + args: { + id: planData.id + } + }]; + this._callAndRefresh(calls, planData); + }; + + /** + * Start review of a plan. + * + * @param {Object} planData Plan data from plan node. + * @method startReview + */ + PlanActions.prototype.startReview = function(planData) { + this._doStartReview(planData); + }; + + /** + * Stop review of a plan. + * + * @param {Object} planData Plan data from plan node. + * @method _doStopReview + */ + PlanActions.prototype._doStopReview = function(planData) { + var calls = [{ + methodname: 'core_competency_plan_stop_review', + args: { + id: planData.id + } + }]; + this._callAndRefresh(calls, planData); + }; + + /** + * Stop review of a plan. + * + * @param {Object} planData Plan data from plan node. + * @method stopReview + */ + PlanActions.prototype.stopReview = function(planData) { + this._doStopReview(planData); + }; + + /** + * Approve a plan. + * + * @param {Object} planData Plan data from plan node. + * @method _doApprove + */ + PlanActions.prototype._doApprove = function(planData) { + var calls = [{ + methodname: 'core_competency_approve_plan', + args: { + id: planData.id + } + }]; + this._callAndRefresh(calls, planData); + }; + + /** + * Approve a plan. + * + * @param {Object} planData Plan data from plan node. + * @method approve + */ + PlanActions.prototype.approve = function(planData) { + this._doApprove(planData); + }; + + /** + * Unapprove a plan. + * + * @param {Object} planData Plan data from plan node. + * @method _doUnapprove + */ + PlanActions.prototype._doUnapprove = function(planData) { + var calls = [{ + methodname: 'core_competency_unapprove_plan', + args: { + id: planData.id + } + }]; + this._callAndRefresh(calls, planData); + }; + + /** + * Unapprove a plan. + * + * @param {Object} planData Plan data from plan node. + * @method unapprove + */ + PlanActions.prototype.unapprove = function(planData) { + this._doUnapprove(planData); + }; + + /** + * Display list of linked courses on a modal dialogue. + * + * @param {Event} e The event. + */ + PlanActions.prototype._showLinkedCoursesHandler = function(e) { + e.preventDefault(); + + var competencyid = $(e.target).data('id'); + var requests = ajax.call([{ + methodname: 'tool_lp_list_courses_using_competency', + args: { id: competencyid } + }]); + + 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. + ); + }).fail(notification.exception); + }).fail(notification.exception); + }).fail(notification.exception); + }; + + /** + * Plan event handler. + * + * @param {String} method The method to call. + * @param {Event} e The event. + * @method _eventHandler + */ + PlanActions.prototype._eventHandler = function(method, e) { + e.preventDefault(); + var data = this._findPlanData($(e.target)); + this[method](data); + }; + + /** + * Find the plan data from the plan node. + * + * @param {Node} node The node to search from. + * @return {Object} Plan data. + */ + PlanActions.prototype._findPlanData = function(node) { + var parent = node.parentsUntil($(this._region).parent(), this._planNode), + data; + + if (parent.length != 1) { + throw new Error('The plan node was not located.'); + } + + data = parent.data(); + if (typeof data === 'undefined' || typeof data.id === 'undefined') { + throw new Error('Plan data could not be found.'); + } + + return data; + }; + + /** + * Enhance a menu bar. + * + * @param {String} selector Menubar selector. + */ + PlanActions.prototype.enhanceMenubar = function(selector) { + Menubar.enhance(selector, { + '[data-action="plan-delete"]': this._eventHandler.bind(this, 'deletePlan'), + '[data-action="plan-complete"]': this._eventHandler.bind(this, 'completePlan'), + '[data-action="plan-reopen"]': this._eventHandler.bind(this, 'reopenPlan'), + '[data-action="plan-unlink"]': this._eventHandler.bind(this, 'unlinkPlan'), + '[data-action="plan-request-review"]': this._eventHandler.bind(this, 'requestReview'), + '[data-action="plan-cancel-review-request"]': this._eventHandler.bind(this, 'cancelReviewRequest'), + '[data-action="plan-start-review"]': this._eventHandler.bind(this, 'startReview'), + '[data-action="plan-stop-review"]': this._eventHandler.bind(this, 'stopReview'), + '[data-action="plan-approve"]': this._eventHandler.bind(this, 'approve'), + '[data-action="plan-unapprove"]': this._eventHandler.bind(this, 'unapprove'), + }); + }; + + /** + * Register the events in the region. + * + * At this stage this cannot be used with enhanceMenubar or multiple handlers + * will be added to the same node. + */ + PlanActions.prototype.registerEvents = function() { + var wrapper = $(this._region); + + wrapper.find('[data-action="plan-delete"]').click(this._eventHandler.bind(this, 'deletePlan')); + wrapper.find('[data-action="plan-complete"]').click(this._eventHandler.bind(this, 'completePlan')); + wrapper.find('[data-action="plan-reopen"]').click(this._eventHandler.bind(this, 'reopenPlan')); + wrapper.find('[data-action="plan-unlink"]').click(this._eventHandler.bind(this, 'unlinkPlan')); + + wrapper.find('[data-action="plan-request-review"]').click(this._eventHandler.bind(this, 'requestReview')); + wrapper.find('[data-action="plan-cancel-review-request"]').click(this._eventHandler.bind(this, 'cancelReviewRequest')); + wrapper.find('[data-action="plan-start-review"]').click(this._eventHandler.bind(this, 'startReview')); + wrapper.find('[data-action="plan-stop-review"]').click(this._eventHandler.bind(this, 'stopReview')); + wrapper.find('[data-action="plan-approve"]').click(this._eventHandler.bind(this, 'approve')); + wrapper.find('[data-action="plan-unapprove"]').click(this._eventHandler.bind(this, 'unapprove')); + + wrapper.find('[data-action="find-courses-link"]').click(this._showLinkedCoursesHandler.bind(this)); + }; + + return PlanActions; +}); diff --git a/admin/tool/lp/amd/src/scaleconfig.js b/admin/tool/lp/amd/src/scaleconfig.js new file mode 100644 index 00000000000..11c8568fa00 --- /dev/null +++ b/admin/tool/lp/amd/src/scaleconfig.js @@ -0,0 +1,228 @@ +// 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 . + +/** + * Handle opening a dialogue to configure scale data. + * + * @module tool_lp/scaleconfig + * @package tool_lp + * @copyright 2015 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/notification', 'core/templates', 'core/ajax', 'tool_lp/dialogue', 'tool_lp/scalevalues'], + function($, notification, templates, ajax, Dialogue, ModScaleValues) { + + /** + * Scale config object. + * @param {String} selectSelector The select box selector. + * @param {String} inputSelector The hidden input field selector. + * @param {String} triggerSelector The trigger selector. + */ + var ScaleConfig = function(selectSelector, inputSelector, triggerSelector) { + this.selectSelector = selectSelector; + this.inputSelector = inputSelector; + this.triggerSelector = triggerSelector; + + // Get the current scale ID. + this.originalscaleid = $(selectSelector).val(); + $(selectSelector).on('change', this.scaleChangeHandler.bind(this)).change(); + $(triggerSelector).click(this.showConfig.bind(this)); + }; + + /** @var {String} The select box selector. */ + ScaleConfig.prototype.selectSelector = null; + /** @var {String} The hidden field selector. */ + ScaleConfig.prototype.inputSelector = null; + /** @var {String} The trigger selector. */ + ScaleConfig.prototype.triggerSelector = null; + /** @var {Array} scalevalues ID and name of the scales. */ + ScaleConfig.prototype.scalevalues = null; + /** @var {Number) originalscaleid Original scale ID when the page loads. */ + ScaleConfig.prototype.originalscaleid = 0; + /** @var {Number} scaleid Current scale ID. */ + ScaleConfig.prototype.scaleid = 0; + /** @var {Dialogue} Reference to the popup. */ + ScaleConfig.prototype.popup = null; + + /** + * Displays the scale configuration dialogue. + * + * @method showConfig + */ + ScaleConfig.prototype.showConfig = function() { + var self = this; + + this.scaleid = $(this.selectSelector).val(); + if (this.scaleid <= 0) { + // This should not happen. + return; + } + + var scalename = $(this.selectSelector).find("option:selected").text(); + this.getScaleValues(this.scaleid).done(function() { + + var context = { + scalename: scalename, + scales: self.scalevalues + }; + + // Dish up the form. + templates.render('tool_lp/scale_configuration_page', context) + .done(function(html) { + new Dialogue( + scalename, + html, + self.initScaleConfig.bind(self) + ); + }).fail(notification.exception); + }).fail(notification.exception); + }; + + /** + * Gets the original scale configuration if it was set. + * + * @method retrieveOriginalScaleConfig + * @return {Object|String} scale configuration or empty string. + */ + ScaleConfig.prototype.retrieveOriginalScaleConfig = function() { + var jsonstring = $(this.inputSelector).val(); + if (jsonstring !== '') { + var scaleconfiguration = $.parseJSON(jsonstring); + // The first object should contain the scale ID for the configuration. + var scaledetail = scaleconfiguration.shift(); + // Check that this scale id matches the one from the page before returning the configuration. + if (scaledetail.scaleid === this.originalscaleid) { + return scaleconfiguration; + } + } + return ''; + }; + + /** + * Initialises the scale configuration dialogue. + * + * @method initScaleConfig + * @param {Dialogue} popup Dialogue object to initialise. + */ + ScaleConfig.prototype.initScaleConfig = function(popup) { + this.popup = popup; + var body = $(popup.getContent()); + if (this.originalscaleid === this.scaleid) { + // Set up the popup to show the current configuration. + var currentconfig = this.retrieveOriginalScaleConfig(); + // Set up the form only if there is configuration settings to set. + if (currentconfig !== '') { + currentconfig.forEach(function(value) { + if (value.scaledefault === 1) { + body.find('[data-field="tool_lp_scale_default_' + value.id + '"]').attr('checked', true); + } + if (value.proficient === 1) { + body.find('[data-field="tool_lp_scale_proficient_' + value.id + '"]').attr('checked', true); + } + }); + } + } + body.on('click', '[data-action="close"]', function() { + this.setScaleConfig(); + popup.close(); + }.bind(this)); + body.on('click', '[data-action="cancel"]', function() { + popup.close(); + }.bind(this)); + }; + + /** + * Set the scale configuration back into a JSON string in the hidden element. + * + * @method setScaleConfig + */ + ScaleConfig.prototype.setScaleConfig = function() { + var body = $(this.popup.getContent()); + // Get the data. + var data = [{ scaleid: this.scaleid}]; + this.scalevalues.forEach(function(value) { + var scaledefault = 0; + var proficient = 0; + if (body.find('[data-field="tool_lp_scale_default_' + value.id + '"]').is(':checked')) { + scaledefault = 1; + } + if (body.find('[data-field="tool_lp_scale_proficient_' + value.id + '"]').is(':checked')) { + proficient = 1; + } + + if (!scaledefault && !proficient) { + return; + } + + data.push({ + id: value.id, + scaledefault: scaledefault, + proficient: proficient + }); + }); + var datastring = JSON.stringify(data); + // Send to the hidden field on the form. + $(this.inputSelector).val(datastring); + // Once the configuration has been saved then the original scale ID is set to the current scale ID. + this.originalscaleid = this.scaleid; + }; + + /** + * Get the scale values for the selected scale. + * + * @method getScaleValues + * @param {Number} scaleid The scale ID of the selected scale. + * @return {Promise} A deffered object with the scale values. + */ + ScaleConfig.prototype.getScaleValues = function(scaleid) { + return ModScaleValues.get_values(scaleid).then(function(values) { + this.scalevalues = values; + return values; + }.bind(this)); + }; + + /** + * Triggered when a scale is selected. + * + * @name scaleChangeHandler + * @param {Event} e + * @return {Void} + * @function + */ + ScaleConfig.prototype.scaleChangeHandler = function(e) { + if ($(e.target).val() <= 0) { + $(this.triggerSelector).prop('disabled', true); + } else { + $(this.triggerSelector).prop('disabled', false); + } + + }; + + return { + + /** + * Main initialisation. + * + * @param {String} selectSelector The select box selector. + * @param {String} inputSelector The hidden input field selector. + * @param {String} triggerSelector The trigger selector. + * @return {ScaleConfig} A new instance of ScaleConfig. + * @method init + */ + init: function(selectSelector, inputSelector, triggerSelector) { + return new ScaleConfig(selectSelector, inputSelector, triggerSelector); + } + }; +}); diff --git a/admin/tool/lp/amd/src/scalevalues.js b/admin/tool/lp/amd/src/scalevalues.js new file mode 100644 index 00000000000..d27abd307d2 --- /dev/null +++ b/admin/tool/lp/amd/src/scalevalues.js @@ -0,0 +1,57 @@ +// 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 . + +/** + * Module to get the scale values. + * + * @package tool_lp + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/ajax'], function($, ajax) { + var localCache = []; + + return /** @alias module:tool_lp/scalevalues */ { + + /** + * Return a promise object that will be resolved into a string eventually (maybe immediately). + * + * @method get_values + * @param {Number} scaleid The scale id + * @return [] {Promise} + */ + + get_values: function(scaleid) { + + var deferred = $.Deferred(); + + if (typeof localCache[scaleid] === 'undefined') { + ajax.call([{ + methodname: 'core_competency_get_scale_values', + args: {scaleid : scaleid}, + done: function(scaleinfo) { + localCache[scaleid] = scaleinfo; + deferred.resolve(scaleinfo); + }, + fail: (deferred.reject) + }]); + } else { + deferred.resolve(localCache[scaleid]); + } + + return deferred.promise(); + } + }; +}); diff --git a/admin/tool/lp/amd/src/templateactions.js b/admin/tool/lp/amd/src/templateactions.js new file mode 100644 index 00000000000..0966cc254d1 --- /dev/null +++ b/admin/tool/lp/amd/src/templateactions.js @@ -0,0 +1,202 @@ +// 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 . + +/** + * Handle actions on learning plan templates via ajax. + * + * @module tool_lp/templateactions + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/templates', 'core/ajax', 'core/notification', 'core/str','tool_lp/actionselector'], + function($, templates, ajax, notification, str, Actionselector) { + // Private variables and functions. + + /** @var {Number} pagecontextid The id of the context */ + var pagecontextid = 0; + + /** @var {Number} templateid The id of the template */ + var templateid = 0; + + /** @var {Boolean} Action to apply to plans when deleting a template */ + var deleteplans = true; + + /** + * Callback to replace the dom element with the rendered template. + * + * @method updatePage + * @param {String} newhtml The new html to insert. + * @param {String} newjs The new js to run. + */ + var updatePage = function(newhtml, newjs) { + $('[data-region="managetemplates"]').replaceWith(newhtml); + templates.runTemplateJS(newjs); + }; + + /** + * Callback to render the page template again and update the page. + * + * @method reloadList + * @param {Object} context The context for the template. + */ + var reloadList = function(context) { + templates.render('tool_lp/manage_templates_page', context) + .done(updatePage) + .fail(notification.exception); + }; + + /** + * Delete a template and reload the page. + * @method doDelete + */ + var doDelete = function() { + + // We are chaining ajax requests here. + var requests = ajax.call([{ + methodname: 'core_competency_delete_template', + args: { id: templateid, + deleteplans: deleteplans } + }, { + methodname: 'tool_lp_data_for_templates_manage_page', + args: { + pagecontext: { + contextid: pagecontextid + } + } + }]); + requests[1].done(reloadList).fail(notification.exception); + }; + + /** + * Duplicate a template and reload the page. + * @method doDuplicate + * @param {Event} e + */ + var doDuplicate = function(e) { + e.preventDefault(); + + templateid = $(this).attr('data-templateid'); + + // We are chaining ajax requests here. + var requests = ajax.call([{ + methodname: 'core_competency_duplicate_template', + args: { id: templateid } + }, { + methodname: 'tool_lp_data_for_templates_manage_page', + args: { + pagecontext: { + contextid: pagecontextid + } + } + }]); + requests[1].done(reloadList).fail(notification.exception); + }; + + /** + * Handler for "Delete learning plan template" actions. + * @method confirmDelete + * @param {Event} e + */ + var confirmDelete = function(e) { + e.preventDefault(); + + var id = $(this).attr('data-templateid'); + templateid = id; + deleteplans = true; + + var requests = ajax.call([{ + methodname: 'core_competency_read_template', + args: { id: templateid } + }, { + methodname: 'core_competency_template_has_related_data', + args: { id: templateid } + }]); + + requests[0].done(function(template) { + requests[1].done(function(templatehasrelateddata) { + if (templatehasrelateddata) { + str.get_strings([ + { key: 'deletetemplate', component: 'tool_lp', param: template.shortname }, + { key: 'deletetemplatewithplans', component: 'tool_lp' }, + { key: 'deleteplans', component: 'tool_lp' }, + { key: 'unlinkplanstemplate', component: 'tool_lp' }, + { key: 'confirm', component: 'moodle' }, + { key: 'cancel', component: 'moodle' } + ]).done(function (strings) { + var actions = [{'text': strings[2], 'value' : 'delete'}, + {'text': strings[3], 'value' : 'unlink'}]; + var actionselector = new Actionselector( + strings[0], // Title. + strings[1], // Message + actions, // Radio button options. + strings[4], // Confirm. + strings[5]); // Cancel. + actionselector.display(); + actionselector.on('save', function(e, data) { + if (data.action != 'delete') { + deleteplans = false; + } + doDelete(); + }); + }).fail(notification.exception); + } + else { + str.get_strings([ + { key: 'confirm', component: 'moodle' }, + { key: 'deletetemplate', component: 'tool_lp', param: template.shortname }, + { key: 'delete', component: 'moodle' }, + { key: 'cancel', component: 'moodle' } + ]).done(function (strings) { + notification.confirm( + strings[0], // Confirm. + strings[1], // Delete learning plan template X? + strings[2], // Delete. + strings[3], // Cancel. + doDelete + ); + }).fail(notification.exception); + } + }).fail(notification.exception); + }).fail(notification.exception); + + }; + + return /** @alias module:tool_lp/templateactions */ { + // Public variables and functions. + /** + * Expose the event handler for the delete. + * @method deleteHandler + * @param {Event} e + */ + deleteHandler: confirmDelete, + + /** + * Expose the event handler for the duplicate. + * @method duplicateHandler + * @param {Event} e + */ + duplicateHandler: doDuplicate, + + /** + * Initialise the module. + * @method init + * @param {Number} contextid The context id of the page. + */ + init: function(contextid) { + pagecontextid = contextid; + } + }; +}); diff --git a/admin/tool/lp/amd/src/tree.js b/admin/tool/lp/amd/src/tree.js new file mode 100644 index 00000000000..a8ac21b6b65 --- /dev/null +++ b/admin/tool/lp/amd/src/tree.js @@ -0,0 +1,624 @@ +// 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 . + +/** + * Implement an accessible aria tree widget, from a nested unordered list. + * Based on http://oaa-accessibility.org/example/41/ + * + * To respond to selection changed events - use tree.on("selectionchanged", handler). + * The handler will receive an array of nodes, which are the list items that are currently + * selected. (Or a single node if multiselect is disabled). + * + * @module tool_lp/tree + * @package core + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/url', 'core/log'], function($, url, log) { + // Private variables and functions. + /** @var {String} expandedImage The html for an expanded tree node twistie. */ + var expandedImage = $(''); + /** @var {String} collapsedImage The html for a collapsed tree node twistie. */ + var collapsedImage = $(''); + + /** + * Constructor + * + * @param {String} selector + * @param {Boolean} multiSelect + */ + var Tree = function(selector, multiSelect) { + this.treeRoot = $(selector); + this.multiSelect = (typeof multiSelect === 'undefined' || multiSelect === true); + + this.items = this.treeRoot.find('li'); + this.expandAll = this.items.length < 20; + this.parents = this.treeRoot.find('li:has(ul)'); + + if (multiSelect) { + this.treeRoot.attr('aria-multiselectable', 'true'); + } + + this.items.attr('aria-selected', 'false'); + + this.visibleItems = null; + this.activeItem = null; + this.lastActiveItem = null; + + this.keys = { + tab: 9, + enter: 13, + space: 32, + pageup: 33, + pagedown: 34, + end: 35, + home: 36, + left: 37, + up: 38, + right: 39, + down: 40, + asterisk: 106 + }; + + this.init(); + + this.bindEventHandlers(); + }; + // Public variables and functions. + + /** + * Init this tree + * @method init + */ + Tree.prototype.init = function() { + this.parents.attr('aria-expanded', 'true'); + this.parents.prepend(expandedImage.clone()); + + this.items.attr('role', 'tree-item'); + this.items.attr('tabindex', '-1'); + this.parents.attr('role', 'group'); + this.treeRoot.attr('role', 'tree'); + + this.visibleItems = this.treeRoot.find('li'); + + var thisObj = this; + if (!this.expandAll) { + this.parents.each(function() { + thisObj.collapseGroup($(this)); + }); + this.expandGroup(this.parents.first()); + } + }; + + /** + * Expand a collapsed group. + * + * @method expandGroup + * @param {Object} item is the jquery id of the parent item of the group + */ + Tree.prototype.expandGroup = function(item) { + // Find the first child ul node. + var group = item.children('ul'); + + // Expand the group. + group.show().attr('aria-hidden', 'false'); + + item.attr('aria-expanded', 'true'); + + item.children('img').attr('src', expandedImage.attr('src')); + + // Update the list of visible items. + this.visibleItems = this.treeRoot.find('li:visible'); + }; + + /** + * Collapse an expanded group. + * + * @method collapseGroup + * @param {Object} item is the jquery id of the parent item of the group + */ + Tree.prototype.collapseGroup = function(item) { + var group = item.children('ul'); + + // Collapse the group. + group.hide().attr('aria-hidden', 'true'); + + item.attr('aria-expanded', 'false'); + + item.children('img').attr('src', collapsedImage.attr('src')); + + // Update the list of visible items. + this.visibleItems = this.treeRoot.find('li:visible'); + }; + + /** + * Expand or collapse a group. + * + * @method toggleGroup + * @param {Object} item is the jquery id of the parent item of the group + */ + Tree.prototype.toggleGroup = function(item) { + if (item.attr('aria-expanded') == 'true') { + this.collapseGroup(item); + } else { + this.expandGroup(item); + } + }; + + /** + * Whenever the currently selected node has changed, trigger an event using this function. + * + * @method triggerChange + */ + Tree.prototype.triggerChange = function() { + var allSelected = this.items.filter('[aria-selected=true]'); + if (!this.multiSelect) { + allSelected = allSelected.first(); + } + this.treeRoot.trigger('selectionchanged', { selected: allSelected }); + }; + + /** + * Select all the items between the last focused item and this currently focused item. + * + * @method multiSelectItem + * @param {Object} item is the jquery id of the newly selected item. + */ + Tree.prototype.multiSelectItem = function(item) { + if (!this.multiSelect) { + this.items.attr('aria-selected', 'false'); + } else if (this.lastActiveItem !== null) { + var lastIndex = this.visibleItems.index(this.lastActiveItem); + var currentIndex = this.visibleItems.index(this.activeItem); + var oneItem = null; + + while (lastIndex < currentIndex) { + oneItem = $(this.visibleItems.get(lastIndex)); + oneItem.attr('aria-selected', 'true'); + lastIndex++; + } + while (lastIndex > currentIndex) { + oneItem = $(this.visibleItems.get(lastIndex)); + oneItem.attr('aria-selected', 'true'); + lastIndex--; + } + } + + item.attr('aria-selected', 'true'); + this.triggerChange(); + }; + + /** + * Select a single item. Make sure all the parents are expanded. De-select all other items. + * + * @method selectItem + * @param {Object} item is the jquery id of the newly selected item. + */ + Tree.prototype.selectItem = function(item) { + // Expand all nodes up the tree. + var walk = item.parent(); + while (walk.attr('role') != 'tree') { + walk = walk.parent(); + if (walk.attr('aria-expanded') == 'false') { + this.expandGroup(walk); + } + walk = walk.parent(); + } + this.items.attr('aria-selected', 'false'); + item.attr('aria-selected', 'true'); + this.triggerChange(); + }; + + /** + * Toggle the selected state for an item back and forth. + * + * @method toggleItem + * @param {Object} item is the jquery id of the item to toggle. + */ + Tree.prototype.toggleItem = function(item) { + if (!this.multiSelect) { + return this.selectItem(item); + } + + var current = item.attr('aria-selected'); + if (current === 'true') { + current = 'false'; + } else { + current = 'true'; + } + item.attr('aria-selected', current); + this.triggerChange(); + }; + + /** + * Set the focus to this item. + * + * @method updateFocus + * @param {Object} item is the jquery id of the parent item of the group + */ + Tree.prototype.updateFocus = function(item) { + this.lastActiveItem = this.activeItem; + this.activeItem = item; + // Expand all nodes up the tree. + var walk = item.parent(); + while (walk.attr('role') != 'tree') { + walk = walk.parent(); + if (walk.attr('aria-expanded') == 'false') { + this.expandGroup(walk); + } + walk = walk.parent(); + } + this.items.attr('tabindex', '-1'); + item.attr('tabindex', 0); + }; + + /** + * Handle a key down event - ie navigate the tree. + * + * @method handleKeyDown + * @param {Object} item is the jquery id of the parent item of the group + * @param {Event} e The event. + */ + Tree.prototype.handleKeyDown = function(item, e) { + var currentIndex = this.visibleItems.index(item); + var newItem = null; + + switch (e.keyCode) { + case this.keys.home: { + // Jump to first item in tree. + newItem = this.parents.first(); + newItem.focus(); + if (e.shiftKey) { + this.multiSelectItem(newItem); + } else if (!e.ctrlKey) { + this.selectItem(newItem); + } + + e.stopPropagation(); + return false; + } + case this.keys.end: { + // Jump to last visible item. + newItem = this.visibleItems.last(); + newItem.focus(); + if (e.shiftKey) { + this.multiSelectItem(newItem); + } else if (!e.ctrlKey) { + this.selectItem(newItem); + } + + e.stopPropagation(); + return false; + } + case this.keys.enter: + case this.keys.space: { + + if (e.shiftKey) { + this.multiSelectItem(item); + } else if (e.ctrlKey) { + this.toggleItem(item); + } else { + this.selectItem(item); + } + + e.stopPropagation(); + return false; + } + case this.keys.left: { + if (item.has('ul') && item.attr('aria-expanded') == 'true') { + this.collapseGroup(item); + } else { + // Move up to the parent. + var itemUL = item.parent(); + var itemParent = itemUL.parent(); + if (itemParent.is('li')) { + itemParent.focus(); + if (e.shiftKey) { + this.multiSelectItem(itemParent); + } else if (!e.ctrlKey) { + this.selectItem(itemParent); + } + } + } + + e.stopPropagation(); + return false; + } + case this.keys.right: { + if (item.has('ul') && item.attr('aria-expanded') == 'false') { + this.expandGroup(item); + } else { + // Move to the first item in the child group. + newItem = item.children('ul').children('li').first(); + if (newItem.length > 0) { + newItem.focus(); + if (e.shiftKey) { + this.multiSelectItem(newItem); + } else if (!e.ctrlKey) { + this.selectItem(newItem); + } + } + } + + e.stopPropagation(); + return false; + } + case this.keys.up: { + + if (currentIndex > 0) { + var prev = this.visibleItems.eq(currentIndex - 1); + prev.focus(); + if (e.shiftKey) { + this.multiSelectItem(prev); + } else if (!e.ctrlKey) { + this.selectItem(prev); + } + } + + e.stopPropagation(); + return false; + } + case this.keys.down: { + + if (currentIndex < this.visibleItems.length - 1) { + var next = this.visibleItems.eq(currentIndex + 1); + next.focus(); + if (e.shiftKey) { + this.multiSelectItem(next); + } else if (!e.ctrlKey) { + this.selectItem(next); + } + } + e.stopPropagation(); + return false; + } + case this.keys.asterisk: { + // Expand all groups. + + var thisObj = this; + + this.parents.each(function() { + thisObj.expandGroup($(this)); + }); + + e.stopPropagation(); + return false; + } + } + + return true; + }; + + /** + * Handle a key press event - ie navigate the tree. + * + * @method handleKeyPress + * @param {Object} item is the jquery id of the parent item of the group + * @param {Event} e The event. + */ + Tree.prototype.handleKeyPress = function(item, e) { + if (e.altKey || e.ctrlKey || e.shiftKey) { + // Do nothing. + return true; + } + + switch (e.keyCode) { + case this.keys.tab: { + return true; + } + case this.keys.enter: + case this.keys.home: + case this.keys.end: + case this.keys.left: + case this.keys.right: + case this.keys.up: + case this.keys.down: { + e.stopPropagation(); + return false; + } + default : { + var chr = String.fromCharCode(e.which); + var match = false; + var itemIndex = this.visibleItems.index(item); + var itemCount = this.visibleItems.length; + var currentIndex = itemIndex + 1; + + // Check if the active item was the last one on the list. + if (currentIndex == itemCount) { + currentIndex = 0; + } + + // Iterate through the menu items (starting from the current item and wrapping) until a match is found + // or the loop returns to the current menu item. + while (currentIndex != itemIndex) { + + var currentItem = this.visibleItems.eq(currentIndex); + var titleChr = currentItem.text().charAt(0); + + if (currentItem.has('ul')) { + titleChr = currentItem.find('span').text().charAt(0); + } + + if (titleChr.toLowerCase() == chr) { + match = true; + break; + } + + currentIndex = currentIndex+1; + if (currentIndex == itemCount) { + // Reached the end of the list, start again at the beginning. + currentIndex = 0; + } + } + + if (match === true) { + this.updateFocus(this.visibleItems.eq(currentIndex)); + } + e.stopPropagation(); + return false; + } + } + + return true; + }; + + /** + * Attach an event listener to the tree. + * + * @method on + * @param {String} eventname This is the name of the event to listen for. Only 'selectionchanged' is supported for now. + * @param {Function} handler The function to call when the event is triggered. + */ + Tree.prototype.on = function(eventname, handler) { + if (eventname !== 'selectionchanged') { + log.warning('Invalid custom event name for tree. Only "selectionchanged" is supported.'); + } else { + this.treeRoot.on(eventname, handler); + } + }; + + /** + * Handle a double click (expand/collapse). + * + * @method handleDblClick + * @param {Object} item is the jquery id of the parent item of the group + * @param {Event} e The event. + */ + Tree.prototype.handleDblClick = function(item, e) { + + if (e.altKey || e.ctrlKey || e.shiftKey) { + // Do nothing. + return true; + } + + // Apply the focus markup. + this.updateFocus(item); + + // Expand or collapse the group. + this.toggleGroup(item); + + e.stopPropagation(); + return false; + }; + + /** + * Handle a click (select). + * + * @method handleExpandCollapseClick + * @param {Object} item is the jquery id of the parent item of the group + * @param {Event} e The event. + */ + Tree.prototype.handleExpandCollapseClick = function(item, e) { + + // Do not shift the focus. + this.toggleGroup(item); + e.stopPropagation(); + return false; + }; + + + /** + * Handle a click (select). + * + * @method handleClick + * @param {Object} item is the jquery id of the parent item of the group + * @param {Event} e The event. + */ + Tree.prototype.handleClick = function(item, e) { + + if (e.shiftKey) { + this.multiSelectItem(item); + } else if (e.ctrlKey) { + this.toggleItem(item); + } else { + this.selectItem(item); + } + this.updateFocus(item); + e.stopPropagation(); + return false; + }; + + /** + * Handle a blur event + * + * @method handleBlur + * @param {Object} item item is the jquery id of the parent item of the group + * @param {Event} e The event. + */ + Tree.prototype.handleBlur = function() { + return true; + }; + + /** + * Handle a focus event + * + * @method handleFocus + * @param {Object} item item is the jquery id of the parent item of the group + * @param {Event} e The event. + */ + Tree.prototype.handleFocus = function(item) { + + this.updateFocus(item); + + return true; + }; + + /** + * Bind the event listeners we require. + * + * @method bindEventHandlers + */ + Tree.prototype.bindEventHandlers = function() { + var thisObj = this; + + // Bind a dblclick handler to the parent items. + this.parents.dblclick(function(e) { + return thisObj.handleDblClick($(this), e); + }); + + // Bind a click handler. + this.items.click(function(e) { + return thisObj.handleClick($(this), e); + }); + + // Bind a toggle handler to the expand/collapse icons. + this.items.children('img').click(function(e) { + return thisObj.handleExpandCollapseClick($(this).parent(), e); + }); + + // Bind a keydown handler. + this.items.keydown(function(e) { + return thisObj.handleKeyDown($(this), e); + }); + + // Bind a keypress handler. + this.items.keypress(function(e) { + return thisObj.handleKeyPress($(this), e); + }); + + // Bind a focus handler. + this.items.focus(function(e) { + return thisObj.handleFocus($(this), e); + }); + + // Bind a blur handler. + this.items.blur(function(e) { + return thisObj.handleBlur($(this), e); + }); + + }; + + return /** @alias module:tool_lp/tree */ Tree; +}); diff --git a/admin/tool/lp/amd/src/user_competency_course_navigation.js b/admin/tool/lp/amd/src/user_competency_course_navigation.js new file mode 100644 index 00000000000..7cfa93dc8f0 --- /dev/null +++ b/admin/tool/lp/amd/src/user_competency_course_navigation.js @@ -0,0 +1,96 @@ +// 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 . + +/** + * Module to enable inline editing of a comptency grade. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery'], function($) { + + /** + * UserCompetencyCourseNavigation + * + * @param {String} The selector of the user element. + * @param {String} The selector of the competency element. + * @param {String} The base url for the page (no params). + * @param {Number} The course id + * @param {Number} The user id + * @param {Number} The competency id + */ + var UserCompetencyCourseNavigation = function(userSelector, competencySelector, baseUrl, userId, competencyId, courseId) { + this._baseUrl = baseUrl; + this._userId = userId + ''; + this._competencyId = competencyId + ''; + this._courseId = courseId; + this._ignoreFirstUser = true; + this._ignoreFirstCompetency = true; + + $(userSelector).on('change', this._userChanged.bind(this)); + $(competencySelector).on('change', this._competencyChanged.bind(this)); + }; + + /** + * The user was changed in the select list. + * + * @method _userChanged + * @param {Event} e + */ + UserCompetencyCourseNavigation.prototype._userChanged = function(e) { + if (this._ignoreFirstUser) { + this._ignoreFirstUser = false; + return; + } + + var newUserId = $(e.target).val(); + var queryStr = '?userid=' + newUserId + '&courseid=' + this._courseId + '&competencyid=' + this._competencyId; + document.location = this._baseUrl + queryStr; + }; + + /** + * The competency was changed in the select list. + * + * @method _competencyChanged + * @param {Event} e + */ + UserCompetencyCourseNavigation.prototype._competencyChanged = function(e) { + if (this._ignoreFirstCompetency) { + this._ignoreFirstCompetency = false; + return; + } + var newCompetencyId = $(e.target).val(); + var queryStr = '?userid=' + this._userId + '&courseid=' + this._courseId + '&competencyid=' + newCompetencyId; + document.location = this._baseUrl + queryStr; + }; + + /** @type {Number} The id of the competency. */ + UserCompetencyCourseNavigation.prototype._competencyId = null; + /** @type {Number} The id of the user. */ + UserCompetencyCourseNavigation.prototype._userId = null; + /** @type {Number} The id of the course. */ + UserCompetencyCourseNavigation.prototype._courseId = null; + /** @type {String} Plugin base url. */ + UserCompetencyCourseNavigation.prototype._baseUrl = null; + /** @type {Boolean} Ignore the first change event for users. */ + UserCompetencyCourseNavigation.prototype._ignoreFirstUser = null; + /** @type {Boolean} Ignore the first change event for competencies. */ + UserCompetencyCourseNavigation.prototype._ignoreFirstCompetency = null; + + return /** @alias module:tool_lp/user_competency_course_navigation */ UserCompetencyCourseNavigation; + +}); diff --git a/admin/tool/lp/amd/src/user_competency_info.js b/admin/tool/lp/amd/src/user_competency_info.js new file mode 100644 index 00000000000..2234414f055 --- /dev/null +++ b/admin/tool/lp/amd/src/user_competency_info.js @@ -0,0 +1,112 @@ +// 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 . + +/** + * Module to refresh a user competency summary in a page. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', 'core/notification', 'core/ajax', 'core/templates'], function($, notification, ajax, templates) { + + /** + * Info + * + * @param {JQuery} Selector to replace when the information needs updating. + * @param {Number} The id of the competency. + * @param {Number} The id of the user. + * @param {Number} The id of the plan. + * @param {Number} The id of the course. + * @param {Boolean} If we should display the user info. + */ + var Info = function(rootElement, competencyId, userId, planId, courseId, displayuser) { + this._rootElement = rootElement; + this._competencyId = competencyId; + this._userId = userId; + this._planId = planId; + this._courseId = courseId; + this._valid = true; + this._displayuser = (typeof displayuser !== 'undefined') ? displayuser : false; + + if (this._planId) { + this._methodName = 'tool_lp_data_for_user_competency_summary_in_plan'; + this._args = { competencyid: this._competencyId, planid: this._planId }; + this._templateName = 'tool_lp/user_competency_summary_in_plan'; + } else if (this._courseId) { + this._methodName = 'tool_lp_data_for_user_competency_summary_in_course'; + this._args = { userid: this._userId, competencyid: this._competencyId, courseid: this._courseId }; + this._templateName = 'tool_lp/user_competency_summary_in_course'; + } else { + this._methodName = 'tool_lp_data_for_user_competency_summary'; + this._args = { userid: this._userId, competencyid: this._competencyId }; + this._templateName = 'tool_lp/user_competency_summary'; + } + }; + + /** + * Reload the info for this user competency. + * + * @method reload + */ + Info.prototype.reload = function() { + var self = this, + promises = []; + + if (!this._valid) { + return; + } + + promises = ajax.call([{ + methodname: this._methodName, + args: this._args + }]); + + promises[0].done(function(context) { + // Check if we should also the user info. + if (self._displayuser) { + context.displayuser = true; + } + templates.render(self._templateName, context).done(function(html, js) { + templates.replaceNode(self._rootElement, html, js); + }).fail(notification.exception); + }).fail(notification.exception); + }; + + /** @type {JQuery} The root element to replace in the DOM. */ + Info.prototype._rootElement = null; + /** @type {Number} The id of the course. */ + Info.prototype._courseId = null; + /** @type {Boolean} Is this module valid? */ + Info.prototype._valid = null; + /** @type {Number} The id of the plan. */ + Info.prototype._planId = null; + /** @type {Number} The id of the competency. */ + Info.prototype._competencyId = null; + /** @type {Number} The id of the user. */ + Info.prototype._userId = null; + /** @type {String} The method name to load the data. */ + Info.prototype._methodName = null; + /** @type {Object} The arguments to load the data. */ + Info.prototype._args = null; + /** @type {String} The template to reload the fragment. */ + Info.prototype._templateName = null; + /** @type {Boolean} If we should display the user info? */ + Info.prototype._displayuser = false; + + return /** @alias module:tool_lp/user_competency_info */ Info; + +}); diff --git a/admin/tool/lp/amd/src/user_competency_workflow.js b/admin/tool/lp/amd/src/user_competency_workflow.js new file mode 100644 index 00000000000..6c4376a4a9e --- /dev/null +++ b/admin/tool/lp/amd/src/user_competency_workflow.js @@ -0,0 +1,301 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * User competency workflow. + * + * @module tool_lp/user_competency_workflow + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', + 'core/templates', + 'core/ajax', + 'core/notification', + 'core/str', + 'tool_lp/menubar', + 'tool_lp/event_base'], + function($, Templates, Ajax, Notification, Str, Menubar, EventBase) { + + /** + * UserCompetencyWorkflow class. + * + * @param {String} selector The node containing the buttons to switch mode. + */ + var UserCompetencyWorkflow = function() { + EventBase.prototype.constructor.apply(this, []); + }; + UserCompetencyWorkflow.prototype = Object.create(EventBase.prototype); + + /** @type {String} The selector to find the user competency data. */ + UserCompetencyWorkflow.prototype._nodeSelector = '[data-node="user-competency"]'; + + /** + * Cancel a review request and refresh the view. + * + * @param {Object} data The user competency data. + * @return {Void} + * @method _cancelReviewRequest + */ + UserCompetencyWorkflow.prototype._cancelReviewRequest = function(data) { + var call = { + methodname: 'core_competency_user_competency_cancel_review_request', + args: { + userid: data.userid, + competencyid: data.competencyid + } + }; + + Ajax.call([call])[0].then(function() { + this._trigger('review-request-cancelled', data); + this._trigger('status-changed', data); + }.bind(this), function() { + this._trigger('error-occured', data); + }.bind(this)); + }; + + /** + * Cancel a review request an refresh the view. + * + * @param {Object} data The user competency data. + * @return {Void} + * @method cancelReviewRequest + */ + UserCompetencyWorkflow.prototype.cancelReviewRequest = function(data) { + this._cancelReviewRequest(data); + }; + + /** + * Cancel a review request handler. + * + * @param {Event} e The event. + * @return {Void} + * @method _cancelReviewRequestHandler + */ + UserCompetencyWorkflow.prototype._cancelReviewRequestHandler = function(e) { + e.preventDefault(); + var data = this._findUserCompetencyData($(e.target)); + this.cancelReviewRequest(data); + }; + + /** + * Request a review and refresh the view. + * + * @param {Object} data The user competency data. + * @return {Void} + * @method _requestReview + */ + UserCompetencyWorkflow.prototype._requestReview = function(data) { + var call = { + methodname: 'core_competency_user_competency_request_review', + args: { + userid: data.userid, + competencyid: data.competencyid + } + }; + + Ajax.call([call])[0].then(function() { + this._trigger('review-requested', data); + this._trigger('status-changed', data); + }.bind(this), function() { + this._trigger('error-occured', data); + }.bind(this)); + }; + + /** + * Request a review. + * + * @param {Object} data The user competency data. + * @return {Void} + * @method requestReview + */ + UserCompetencyWorkflow.prototype.requestReview = function(data) { + this._requestReview(data); + }; + + /** + * Request a review handler. + * + * @param {Event} e The event. + * @return {Void} + * @method _requestReviewHandler + */ + UserCompetencyWorkflow.prototype._requestReviewHandler = function(e) { + e.preventDefault(); + var data = this._findUserCompetencyData($(e.target)); + this.requestReview(data); + }; + + /** + * Start a review and refresh the view. + * + * @param {Object} data The user competency data. + * @return {Void} + * @method _startReview + */ + UserCompetencyWorkflow.prototype._startReview = function(data) { + var call = { + methodname: 'core_competency_user_competency_start_review', + args: { + userid: data.userid, + competencyid: data.competencyid + } + }; + + Ajax.call([call])[0].then(function() { + this._trigger('review-started', data); + this._trigger('status-changed', data); + }.bind(this), function() { + this._trigger('error-occured', data); + }.bind(this)); + }; + + /** + * Start a review. + * + * @param {Object} data The user competency data. + * @return {Void} + * @method startReview + */ + UserCompetencyWorkflow.prototype.startReview = function(data) { + this._startReview(data); + }; + + /** + * Start a review handler. + * + * @param {Event} e The event. + * @return {Void} + * @method _startReviewHandler + */ + UserCompetencyWorkflow.prototype._startReviewHandler = function(e) { + e.preventDefault(); + var data = this._findUserCompetencyData($(e.target)); + this.startReview(data); + }; + + /** + * Stop a review and refresh the view. + * + * @param {Object} data The user competency data. + * @return {Void} + * @method _stopReview + */ + UserCompetencyWorkflow.prototype._stopReview = function(data) { + var call = { + methodname: 'core_competency_user_competency_stop_review', + args: { + userid: data.userid, + competencyid: data.competencyid + } + }; + + Ajax.call([call])[0].then(function() { + this._trigger('review-stopped', data); + this._trigger('status-changed', data); + }.bind(this), function() { + this._trigger('error-occured', data); + }.bind(this)); + }; + + /** + * Stop a review. + * + * @param {Object} data The user competency data. + * @return {Void} + * @method stopReview + */ + UserCompetencyWorkflow.prototype.stopReview = function(data) { + this._stopReview(data); + }; + + /** + * Stop a review handler. + * + * @param {Event} e The event. + * @return {Void} + * @method _stopReviewHandler + */ + UserCompetencyWorkflow.prototype._stopReviewHandler = function(e) { + e.preventDefault(); + var data = this._findUserCompetencyData($(e.target)); + this.stopReview(data); + }; + + /** + * Enhance a menu bar. + * + * @param {String} selector Menubar selector. + */ + UserCompetencyWorkflow.prototype.enhanceMenubar = function(selector) { + Menubar.enhance(selector, { + '[data-action="request-review"]': this._requestReviewHandler.bind(this), + '[data-action="cancel-review-request"]': this._cancelReviewRequestHandler.bind(this), + }); + }; + + /** + * Find the user competency data from a node. + * + * @param {Node} node The node to search from. + * @return {Object} User competency data. + */ + UserCompetencyWorkflow.prototype._findUserCompetencyData = function(node) { + var parent = node.parents(this._nodeSelector), + data; + + if (parent.length != 1) { + throw new Error('The evidence node was not located.'); + } + + data = parent.data(); + if (typeof data === 'undefined' || typeof data.userid === 'undefined' || typeof data.competencyid === 'undefined') { + throw new Error('User competency data could not be found.'); + } + + return data; + }; + + /** + * Enhance a menu bar. + * + * @param {String} selector Menubar selector. + */ + UserCompetencyWorkflow.prototype.enhanceMenubar = function(selector) { + Menubar.enhance(selector, { + '[data-action="request-review"]': this._requestReviewHandler.bind(this), + '[data-action="cancel-review-request"]': this._cancelReviewRequestHandler.bind(this), + '[data-action="start-review"]': this._startReviewHandler.bind(this), + '[data-action="stop-review"]': this._stopReviewHandler.bind(this), + }); + }; + + /** + * Register the events in the region. + * + * @param {String} selector The base selector to search nodes in and attach events. + */ + UserCompetencyWorkflow.prototype.registerEvents = function(selector) { + var wrapper = $(selector); + + wrapper.find('[data-action="request-review"]').click(this._requestReviewHandler.bind(this)); + wrapper.find('[data-action="cancel-review-request"]').click(this._cancelReviewRequestHandler.bind(this)); + wrapper.find('[data-action="start-review"]').click(this._startReviewHandler.bind(this)); + wrapper.find('[data-action="stop-review"]').click(this._stopReviewHandler.bind(this)); + }; + + return /** @alias module:tool_lp/user_competency_actions */ UserCompetencyWorkflow; +}); diff --git a/admin/tool/lp/amd/src/user_evidence_actions.js b/admin/tool/lp/amd/src/user_evidence_actions.js new file mode 100644 index 00000000000..8c9e47e0466 --- /dev/null +++ b/admin/tool/lp/amd/src/user_evidence_actions.js @@ -0,0 +1,402 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * User evidence actions. + * + * @module tool_lp/user_evidence_actions + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', + 'core/templates', + 'core/ajax', + 'core/notification', + 'core/str', + 'tool_lp/menubar', + 'tool_lp/competencypicker_user_plans'], + function($, templates, ajax, notification, str, Menubar, PickerUserPlans) { + + /** + * UserEvidenceActions class. + * + * Note that presently this cannot be instantiated more than once per page. + * + * @param {String} type The type of page we're in. + */ + var UserEvidenceActions = function(type) { + this._type = type; + + if (type === 'evidence') { + // This is the page to view one evidence. + this._region = '[data-region="user-evidence-page"]'; + this._evidenceNode = '[data-region="user-evidence-page"]'; + this._template = 'tool_lp/user_evidence_page'; + this._contextMethod = 'tool_lp_data_for_user_evidence_page'; + + } else if (type === 'list') { + // This is the page to view a list of evidence. + this._region = '[data-region="user-evidence-list"]'; + this._evidenceNode = '[data-region="user-evidence-node"]'; + this._template = 'tool_lp/user_evidence_list_page'; + this._contextMethod = 'tool_lp_data_for_user_evidence_list_page'; + + } else { + throw new TypeError('Unexpected type.'); + } + }; + + /** @type {String} Ajax method to fetch the page data from. */ + UserEvidenceActions.prototype._contextMethod = null; + /** @type {String} Selector to find the node describing the evidence. */ + UserEvidenceActions.prototype._evidenceNode = null; + /** @type {String} Selector mapping to the region to update. Usually similar to wrapper. */ + UserEvidenceActions.prototype._region = null; + /** @type {String} Name of the template used to render the region. */ + UserEvidenceActions.prototype._template = null; + /** @type {String} Type of page/region we're in. */ + UserEvidenceActions.prototype._type = null; + + /** + * Resolve the arguments to refresh the region. + * + * @param {Object} evidenceData Evidence data from evidence node. + * @return {Object} List of arguments. + */ + UserEvidenceActions.prototype._getContextArgs = function(evidenceData) { + var self = this, + args = {}; + + if (self._type === 'evidence') { + args = { + id: evidenceData.id + }; + + } else if (self._type === 'list') { + args = { + userid: evidenceData.userid + }; + } + + return args; + }; + + /** + * Callback to render the region template. + * + * @param {Object} context The context for the template. + */ + UserEvidenceActions.prototype._renderView = function(context) { + var self = this; + templates.render(self._template, context) + .done(function(newhtml, newjs) { + templates.replaceNode($(self._region), newhtml, newjs); + }.bind(self)) + .fail(notification.exception); + }; + + /** + * Call multiple ajax methods, and refresh. + * + * @param {Array} calls List of Ajax calls. + * @param {Object} evidenceData Evidence data from evidence node. + * @return {Promise} + */ + UserEvidenceActions.prototype._callAndRefresh = function(calls, evidenceData) { + var self = this; + + calls.push({ + methodname: self._contextMethod, + args: self._getContextArgs(evidenceData) + }); + + // Apply all the promises, and refresh when the last one is resolved. + return $.when.apply($.when, ajax.call(calls)) + .then(function() { + self._renderView.call(self, arguments[arguments.length - 1]); + }) + .fail(notification.exception); + }; + + /** + * Delete a plan and reload the region. + * + * @param {Object} evidenceData Evidence data from evidence node. + */ + UserEvidenceActions.prototype._doDelete = function(evidenceData) { + var self = this, + calls = [{ + methodname: 'core_competency_delete_user_evidence', + args: { id: evidenceData.id } + }]; + self._callAndRefresh(calls, evidenceData); + }; + + /** + * Delete a plan. + * + * @param {Object} evidenceData Evidence data from evidence node. + */ + UserEvidenceActions.prototype.deleteEvidence = function(evidenceData) { + var self = this, + requests; + + requests = ajax.call([{ + methodname: 'core_competency_read_user_evidence', + args: { id: evidenceData.id } + }]); + + requests[0].done(function(evidence) { + str.get_strings([ + { key: 'confirm', component: 'moodle' }, + { key: 'deleteuserevidence', component: 'tool_lp', param: evidence.name }, + { key: 'delete', component: 'moodle' }, + { key: 'cancel', component: 'moodle' } + ]).done(function (strings) { + notification.confirm( + strings[0], // Confirm. + strings[1], // Delete evidence X? + strings[2], // Delete. + strings[3], // Cancel. + function() { + self._doDelete(evidenceData); + }.bind(self) + ); + }).fail(notification.exception); + }).fail(notification.exception); + + }; + + /** + * Delete evidence handler. + * + * @param {Event} e The event. + */ + UserEvidenceActions.prototype._deleteEvidenceHandler = function(e) { + e.preventDefault(); + var data = this._findEvidenceData($(e.target)); + this.deleteEvidence(data); + }; + + /** + * Link a competency and reload. + * + * @param {Object} evidenceData Evidence data from evidence node. + * @param {Number} competencyIds The competency IDs. + * @param {Boolean} requestReview Send competencies to review. + */ + UserEvidenceActions.prototype._doCreateUserEvidenceCompetency = function(evidenceData, competencyIds) { + var self = this, + calls = []; + + $.each(competencyIds, function(index, competencyId) { + calls.push({ + methodname: 'core_competency_create_user_evidence_competency', + args: { + userevidenceid: evidenceData.id, + competencyid: competencyId, + } + }); + }); + + self._callAndRefresh(calls, evidenceData); + }; + + /** + * Create a user evidence competency. + * + * @param {Object} evidenceData Evidence data from evidence node. + */ + UserEvidenceActions.prototype.createUserEvidenceCompetency = function(evidenceData) { + var self = this, + picker = new PickerUserPlans(evidenceData.userid); + + picker.on('save', function(e, data) { + var competencyIds = data.competencyIds; + self._doCreateUserEvidenceCompetency(evidenceData, competencyIds, data.requestReview); + }.bind(self)); + + picker.display(); + }; + + /** + * Create user evidence competency handler. + * + * @param {Event} e The event. + */ + UserEvidenceActions.prototype._createUserEvidenceCompetencyHandler = function(e) { + e.preventDefault(); + var data = this._findEvidenceData($(e.target)); + this.createUserEvidenceCompetency(data); + }; + + /** + * Remove a linked competency and reload. + * + * @param {Object} evidenceData Evidence data from evidence node. + * @param {Number} competencyId The competency ID. + */ + UserEvidenceActions.prototype._doDeleteUserEvidenceCompetency = function(evidenceData, competencyId) { + var self = this, + calls = []; + + calls.push({ + methodname: 'core_competency_delete_user_evidence_competency', + args: { + userevidenceid: evidenceData.id, + competencyid: competencyId, + } + }); + + self._callAndRefresh(calls, evidenceData); + }; + + /** + * Delete a user evidence competency. + * + * @param {Object} evidenceData Evidence data from evidence node. + * @param {Number} competencyId The competency ID. + */ + UserEvidenceActions.prototype.deleteUserEvidenceCompetency = function(evidenceData, competencyId) { + this._doDeleteUserEvidenceCompetency(evidenceData, competencyId); + }; + + /** + * Delete user evidence competency handler. + * + * @param {Event} e The event. + */ + UserEvidenceActions.prototype._deleteUserEvidenceCompetencyHandler = function(e) { + var data = this._findEvidenceData($(e.currentTarget)), + competencyId = $(e.currentTarget).data('id'); + e.preventDefault(); + this.deleteUserEvidenceCompetency(data, competencyId); + }; + + /** + * Send request review for user evidence competencies and reload the region. + * + * @param {Object} evidenceData Evidence data from evidence node. + */ + UserEvidenceActions.prototype._doReviewUserEvidenceCompetencies = function(evidenceData) { + var self = this, + calls = [{ + methodname: 'core_competency_request_review_of_user_evidence_linked_competencies', + args: { id: evidenceData.id } + }]; + self._callAndRefresh(calls, evidenceData); + }; + + /** + * Send request review for user evidence competencies. + * + * @param {Object} evidenceData Evidence data from evidence node. + */ + UserEvidenceActions.prototype.reviewUserEvidenceCompetencies = function(evidenceData) { + var self = this, + requests; + + requests = ajax.call([{ + methodname: 'core_competency_read_user_evidence', + args: { id: evidenceData.id } + }]); + + requests[0].done(function(evidence) { + str.get_strings([ + { key: 'confirm', component: 'moodle' }, + { key: 'sendallcompetenciestoreview', component: 'tool_lp', param: evidence.name }, + { key: 'confirm', component: 'moodle' }, + { key: 'cancel', component: 'moodle' } + ]).done(function (strings) { + notification.confirm( + strings[0], // Confirm. + strings[1], // Send all competencies in review for X? + strings[2], // Confirm. + strings[3], // Cancel. + function() { + self._doReviewUserEvidenceCompetencies(evidenceData); + }.bind(self) + ); + }).fail(notification.exception); + }).fail(notification.exception); + + }; + + /** + * Send request review for user evidence competencies handler. + * + * @param {Event} e The event. + */ + UserEvidenceActions.prototype._reviewUserEvidenceCompetenciesHandler = function(e) { + e.preventDefault(); + var data = this._findEvidenceData($(e.target)); + this.reviewUserEvidenceCompetencies(data); + }; + + /** + * Find the evidence data from the evidence node. + * + * @param {Node} node The node to search from. + * @return {Object} Evidence data. + */ + UserEvidenceActions.prototype._findEvidenceData = function(node) { + var parent = node.parentsUntil($(this._region).parent(), this._evidenceNode), + data; + + if (parent.length != 1) { + throw new Error('The evidence node was not located.'); + } + + data = parent.data(); + if (typeof data === 'undefined' || typeof data.id === 'undefined') { + throw new Error('Evidence data could not be found.'); + } + + return data; + }; + + /** + * Enhance a menu bar. + * + * @param {String} selector Menubar selector. + */ + UserEvidenceActions.prototype.enhanceMenubar = function(selector) { + var self = this; + Menubar.enhance(selector, { + '[data-action="user-evidence-delete"]': self._deleteEvidenceHandler.bind(self), + '[data-action="link-competency"]': self._createUserEvidenceCompetencyHandler.bind(self), + '[data-action="send-competencies-review"]': self._reviewUserEvidenceCompetenciesHandler.bind(self), + }); + }; + + /** + * Register the events in the region. + * + * At this stage this cannot be used with enhanceMenubar or multiple handlers + * will be added to the same node. + */ + UserEvidenceActions.prototype.registerEvents = function() { + var wrapper = $(this._region), + self = this; + + wrapper.find('[data-action="user-evidence-delete"]').click(self._deleteEvidenceHandler.bind(self)); + wrapper.find('[data-action="link-competency"]').click(self._createUserEvidenceCompetencyHandler.bind(self)); + wrapper.find('[data-action="delete-competency-link"]').click(self._deleteUserEvidenceCompetencyHandler.bind(self)); + wrapper.find('[data-action="send-competencies-review"]').click(self._reviewUserEvidenceCompetenciesHandler.bind(self)); + }; + + return /** @alias module:tool_lp/user_evidence_actions */ UserEvidenceActions; +}); diff --git a/admin/tool/lp/classes/course_competencies_form_element.php b/admin/tool/lp/classes/course_competencies_form_element.php new file mode 100644 index 00000000000..035d2cdd2f0 --- /dev/null +++ b/admin/tool/lp/classes/course_competencies_form_element.php @@ -0,0 +1,87 @@ +. + + +/** + * Course competencies element. + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use core_competency\api; +use core_competency\external\competency_exporter; +require_once($CFG->libdir . '/form/autocomplete.php'); + +/** + * Course competencies element. + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_lp_course_competencies_form_element extends MoodleQuickForm_autocomplete { + + /** + * Constructor + * + * @param string $elementName Element name + * @param mixed $elementLabel Label(s) for an element + * @param array $options Options to control the element's display + * @param mixed $attributes Either a typical HTML attribute string or an associative array. + */ + public function __construct($elementName=null, $elementLabel=null, $options=array(), $attributes=null) { + global $OUTPUT; + + if ($elementName == null) { + // This is broken quickforms messing with the constructors. + return; + } + + if (!isset($options['courseid'])) { + throw new coding_exception('Course id is required for the course_competencies form element'); + } + $courseid = $options['courseid']; + + if (!empty($options['cmid'])) { + $current = \core_competency\api::list_course_module_competencies_in_course_module($options['cmid']); + $ids = array(); + foreach ($current as $coursemodulecompetency) { + array_push($ids, $coursemodulecompetency->get_competencyid()); + } + $this->setValue($ids); + } + + $competencies = api::list_course_competencies($courseid); + $validoptions = array(); + + $context = context_course::instance($courseid); + foreach ($competencies as $competency) { + $exporter = new competency_exporter($competency['competency'], array('context' => $context)); + $templatecontext = array('competency' => $exporter->export($OUTPUT)); + $id = $competency['competency']->get_id(); + $validoptions[$id] = $OUTPUT->render_from_template('tool_lp/competency_summary', $templatecontext); + } + $attributes['tags'] = false; + $attributes['multiple'] = 'multiple'; + parent::__construct($elementName, $elementLabel, $validoptions, $attributes); + } +} diff --git a/admin/tool/lp/classes/course_competency_rule_form_element.php b/admin/tool/lp/classes/course_competency_rule_form_element.php new file mode 100644 index 00000000000..29fc72fe878 --- /dev/null +++ b/admin/tool/lp/classes/course_competency_rule_form_element.php @@ -0,0 +1,74 @@ +. + + +/** + * Course competency rule element. + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use core_competency\api; +use core_competency\external\competency_exporter; +use core_competency\course_module_competency; + +require_once($CFG->libdir . '/form/select.php'); + +/** + * Course competency rule element. + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_lp_course_competency_rule_form_element extends MoodleQuickForm_select { + + /** + * Constructor + * + * @param string $elementName Element name + * @param mixed $elementLabel Label(s) for an element + * @param array $options Options to control the element's display + * @param mixed $attributes Either a typical HTML attribute string or an associative array. + */ + public function __construct($elementName=null, $elementLabel=null, $options=array(), $attributes=null) { + if ($elementName == null) { + // This is broken quickforms messing with the constructors. + return; + } + + if (!empty($options['cmid'])) { + $cmid = $options['cmid']; + + $current = \core_competency\api::list_course_module_competencies_in_course_module($cmid); + + // Note: We just pick the outcome set on the first course_module_competency - because in our UI are are + // forcing them to be all the same for each activity. + if (!empty($current)) { + $one = array_pop($current); + $this->setValue($one->get_ruleoutcome()); + } + } + $validoptions = course_module_competency::get_ruleoutcome_list(); + parent::__construct($elementName, $elementLabel, $validoptions, $attributes); + } +} diff --git a/admin/tool/lp/classes/course_competency_statistics.php b/admin/tool/lp/classes/course_competency_statistics.php new file mode 100644 index 00000000000..9d2d95e4d9b --- /dev/null +++ b/admin/tool/lp/classes/course_competency_statistics.php @@ -0,0 +1,60 @@ +. + +/** + * Course competency statistics class + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp; +defined('MOODLE_INTERNAL') || die(); + +use core_competency\api; + +/** + * Course competency statistics class. + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_competency_statistics { + + /** @var $competencycount The number of competencies in the course */ + public $competencycount = 0; + + /** @var $proficientcompetencycount The number of proficient competencies for the current user */ + public $proficientcompetencycount = 0; + + /** @var $leastproficientcompetencies The competencies in this course that were proficient the least times */ + public $leastproficientcompetencies = array(); + + /** + * Return the custom definition of the properties of this model. + * + * @param int $courseid The course we want to generate statistics for. + */ + public function __construct($courseid) { + global $USER; + + $this->competencycount = api::count_competencies_in_course($courseid); + $this->proficientcompetencycount = api::count_proficient_competencies_in_course_for_user($courseid, $USER->id); + $this->leastproficientcompetencies = api::get_least_proficient_competencies_for_course($courseid, 0, 3); + } +} diff --git a/admin/tool/lp/classes/external.php b/admin/tool/lp/classes/external.php new file mode 100644 index 00000000000..61a922f9c6b --- /dev/null +++ b/admin/tool/lp/classes/external.php @@ -0,0 +1,1207 @@ +. + +/** + * This is the external API for this tool. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp; +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->libdir/externallib.php"); +require_once("$CFG->libdir/grade/grade_scale.php"); + +use context; +use context_system; +use context_course; +use context_helper; +use context_user; +use coding_exception; +use external_api; +use external_function_parameters; +use external_value; +use external_format_value; +use external_single_structure; +use external_multiple_structure; +use invalid_parameter_exception; +use required_capability_exception; + +use tool_lp\external\cohort_summary_exporter; +use tool_lp\external\competency_path_exporter; +use tool_lp\external\competency_summary_exporter; +use tool_lp\external\course_competency_statistics_exporter; +use tool_lp\external\course_module_summary_exporter; +use tool_lp\external\course_summary_exporter; +use tool_lp\external\template_statistics_exporter; +use tool_lp\external\user_competency_summary_exporter; +use tool_lp\external\user_competency_summary_in_course_exporter; +use tool_lp\external\user_competency_summary_in_plan_exporter; +use tool_lp\external\user_evidence_summary_exporter; +use tool_lp\output\user_competency_summary_in_plan; +use tool_lp\output\user_competency_summary_in_course; + +use core_competency\api; +use core_competency\external\competency_exporter; +use core_competency\external\competency_framework_exporter; +use core_competency\external\course_competency_exporter; +use core_competency\external\course_competency_settings_exporter; +use core_competency\external\plan_exporter; +use core_competency\external\template_exporter; +use core_competency\external\user_competency_course_exporter; +use core_competency\external\user_competency_exporter; +use core_competency\external\user_competency_plan_exporter; +use core_competency\external\user_summary_exporter; + +/** + * This is the external API for this tool. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class external extends external_api { + + /** + * Returns a prepared structure to use a context parameters. + * @return external_single_structure + */ + protected static function get_context_parameters() { + $id = new external_value( + PARAM_INT, + 'Context ID. Either use this value, or level and instanceid.', + VALUE_DEFAULT, + 0 + ); + $level = new external_value( + PARAM_ALPHA, + 'Context level. To be used with instanceid.', + VALUE_DEFAULT, + '' + ); + $instanceid = new external_value( + PARAM_INT, + 'Context instance ID. To be used with level', + VALUE_DEFAULT, + 0 + ); + return new external_single_structure(array( + 'contextid' => $id, + 'contextlevel' => $level, + 'instanceid' => $instanceid, + )); + } + + /** + * Returns description of data_for_competency_frameworks_manage_page() parameters. + * + * @return \external_function_parameters + */ + public static function data_for_competency_frameworks_manage_page_parameters() { + $params = array('pagecontext' => self::get_context_parameters()); + return new external_function_parameters($params); + } + + /** + * Loads the data required to render the competency_frameworks_manage_page template. + * + * @param context $pagecontext The page context + * @return \stdClass + */ + public static function data_for_competency_frameworks_manage_page($pagecontext) { + global $PAGE; + + $params = self::validate_parameters( + self::data_for_competency_frameworks_manage_page_parameters(), + array( + 'pagecontext' => $pagecontext + ) + ); + $context = self::get_context_from_params($params['pagecontext']); + self::validate_context($context); + + $renderable = new output\manage_competency_frameworks_page($context); + $renderer = $PAGE->get_renderer('tool_lp'); + + $data = $renderable->export_for_template($renderer); + + return $data; + } + + /** + * Returns description of data_for_competency_frameworks_manage_page() result value. + * + * @return \external_description + */ + public static function data_for_competency_frameworks_manage_page_returns() { + return new external_single_structure(array ( + 'competencyframeworks' => new external_multiple_structure( + competency_framework_exporter::get_read_structure() + ), + 'pluginbaseurl' => new external_value(PARAM_LOCALURL, 'Url to the tool_lp plugin folder on this Moodle site'), + 'navigation' => new external_multiple_structure( + new external_value(PARAM_RAW, 'HTML for a navigation item that should be on this page') + ), + 'pagecontextid' => new external_value(PARAM_INT, 'The page context id') + )); + + } + + /** + * Returns description of data_for_competencies_manage_page() parameters. + * + * @return \external_function_parameters + */ + public static function data_for_competencies_manage_page_parameters() { + $competencyframeworkid = new external_value( + PARAM_INT, + 'The competency framework id', + VALUE_REQUIRED + ); + $search = new external_value( + PARAM_RAW, + 'A search string', + VALUE_DEFAULT, + '' + ); + $params = array( + 'competencyframeworkid' => $competencyframeworkid, + 'search' => $search + ); + return new external_function_parameters($params); + } + + /** + * Loads the data required to render the competencies_manage_page template. + * + * @param int $competencyframeworkid Framework id. + * @param string $search Text to search. + * + * @return boolean + */ + public static function data_for_competencies_manage_page($competencyframeworkid, $search) { + global $PAGE; + + $params = self::validate_parameters(self::data_for_competencies_manage_page_parameters(), array( + 'competencyframeworkid' => $competencyframeworkid, + 'search' => $search + )); + + $framework = api::read_framework($params['competencyframeworkid']); + self::validate_context($framework->get_context()); + $output = $PAGE->get_renderer('tool_lp'); + + $renderable = new output\manage_competencies_page($framework, $params['search'], $framework->get_context()); + + $data = $renderable->export_for_template($output); + + return $data; + } + + /** + * Returns description of data_for_competencies_manage_page() result value. + * + * @return \external_description + */ + public static function data_for_competencies_manage_page_returns() { + return new external_single_structure(array ( + 'framework' => competency_framework_exporter::get_read_structure(), + 'canmanage' => new external_value(PARAM_BOOL, 'True if this user has permission to manage competency frameworks'), + 'pagecontextid' => new external_value(PARAM_INT, 'Context id for the framework'), + 'search' => new external_value(PARAM_RAW, 'Current search string'), + 'rulesmodules' => new external_value(PARAM_RAW, 'JSON encoded data for rules'), + 'pluginbaseurl' => new external_value(PARAM_RAW, 'Plugin base url') + )); + + } + + /** + * Returns description of data_for_competency_summary() parameters. + * + * @return \external_function_parameters + */ + public static function data_for_competency_summary_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $includerelated = new external_value( + PARAM_BOOL, + 'Include or not related competencies', + VALUE_DEFAULT, + false + ); + $includecourses = new external_value( + PARAM_BOOL, + 'Include or not competency courses', + VALUE_DEFAULT, + false + ); + $params = array( + 'competencyid' => $competencyid, + 'includerelated' => $includerelated, + 'includecourses' => $includecourses + ); + return new external_function_parameters($params); + } + + /** + * Loads the data required to render the competency_page template. + * + * @param int $competencyid Competency id. + * @param boolean $includerelated Include or not related competencies. + * @param boolean $includecourses Include or not competency courses. + * + * @return \stdClass + */ + public static function data_for_competency_summary($competencyid, $includerelated = false, $includecourses = false) { + global $PAGE; + $params = self::validate_parameters(self::data_for_competency_summary_parameters(), array( + 'competencyid' => $competencyid, + 'includerelated' => $includerelated, + 'includecourses' => $includecourses + )); + + $competency = api::read_competency($params['competencyid']); + $framework = api::read_framework($competency->get_competencyframeworkid()); + self::validate_context($framework->get_context()); + $renderable = new output\competency_summary($competency, $framework, $params['includerelated'], $params['includecourses']); + $renderer = $PAGE->get_renderer('tool_lp'); + + $data = $renderable->export_for_template($renderer); + + return $data; + } + + /** + * Returns description of data_for_competency_summary_() result value. + * + * @return \external_description + */ + public static function data_for_competency_summary_returns() { + return competency_summary_exporter::get_read_structure(); + } + + /** + * Returns description of list_courses_using_competency() parameters. + * + * @return \external_function_parameters + */ + public static function list_courses_using_competency_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'id' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * Count the courses (visible to this user) that use this competency. + * + * @param int $competencyid Competency id. + * @return array + */ + public static function list_courses_using_competency($competencyid) { + global $PAGE; + + $params = self::validate_parameters(self::list_courses_using_competency_parameters(), array( + 'id' => $competencyid, + )); + + $competency = api::read_competency($params['id']); + self::validate_context($competency->get_context()); + $output = $PAGE->get_renderer('tool_lp'); + + $results = array(); + $courses = api::list_courses_using_competency($params['id']); + foreach ($courses as $course) { + $context = context_course::instance($course->id); + $exporter = new course_summary_exporter($course, array('context' => $context)); + $result = $exporter->export($output); + array_push($results, $result); + } + return $results; + } + + /** + * Returns description of list_courses_using_competency() result value. + * + * @return \external_description + */ + public static function list_courses_using_competency_returns() { + return new external_multiple_structure(course_summary_exporter::get_read_structure()); + } + + + /** + * Returns description of data_for_course_competenies_page() parameters. + * + * @return \external_function_parameters + */ + public static function data_for_course_competencies_page_parameters() { + $courseid = new external_value( + PARAM_INT, + 'The course id', + VALUE_REQUIRED + ); + $params = array('courseid' => $courseid); + return new external_function_parameters($params); + } + + /** + * Loads the data required to render the course_competencies_page template. + * + * @param int $courseid The course id to check. + * @return boolean + */ + public static function data_for_course_competencies_page($courseid) { + global $PAGE; + $params = self::validate_parameters(self::data_for_course_competencies_page_parameters(), array( + 'courseid' => $courseid, + )); + self::validate_context(context_course::instance($params['courseid'])); + + $renderable = new output\course_competencies_page($params['courseid']); + $renderer = $PAGE->get_renderer('tool_lp'); + + $data = $renderable->export_for_template($renderer); + + return $data; + } + + /** + * Returns description of data_for_course_competencies_page() result value. + * + * @return \external_description + */ + public static function data_for_course_competencies_page_returns() { + $ucc = user_competency_course_exporter::get_read_structure(); + $ucc->required = VALUE_OPTIONAL; + + return new external_single_structure(array ( + 'courseid' => new external_value(PARAM_INT, 'The current course id'), + 'pagecontextid' => new external_value(PARAM_INT, 'The current page context ID.'), + 'gradableuserid' => new external_value(PARAM_INT, 'Current user id, if the user is a gradable user.', VALUE_OPTIONAL), + 'canmanagecompetencyframeworks' => new external_value(PARAM_BOOL, 'User can manage competency frameworks'), + 'canmanagecoursecompetencies' => new external_value(PARAM_BOOL, 'User can manage linked course competencies'), + 'canconfigurecoursecompetencies' => new external_value(PARAM_BOOL, 'User can configure course competency settings'), + 'settings' => course_competency_settings_exporter::get_read_structure(), + 'statistics' => course_competency_statistics_exporter::get_read_structure(), + 'competencies' => new external_multiple_structure(new external_single_structure(array( + 'competency' => competency_exporter::get_read_structure(), + 'coursecompetency' => course_competency_exporter::get_read_structure(), + 'coursemodules' => new external_multiple_structure(course_module_summary_exporter::get_read_structure()), + 'usercompetencycourse' => $ucc, + 'ruleoutcomeoptions' => new external_multiple_structure( + new external_single_structure(array( + 'value' => new external_value(PARAM_INT, 'The option value'), + 'text' => new external_value(PARAM_NOTAGS, 'The name of the option'), + 'selected' => new external_value(PARAM_BOOL, 'If this is the currently selected option'), + )) + ), + 'comppath' => competency_path_exporter::get_read_structure(), + ))), + 'manageurl' => new external_value(PARAM_LOCALURL, 'Url to the manage competencies page.'), + )); + + } + + /** + * Returns description of data_for_templates_manage_page() parameters. + * + * @return \external_function_parameters + */ + public static function data_for_templates_manage_page_parameters() { + $params = array('pagecontext' => self::get_context_parameters()); + return new external_function_parameters($params); + } + + /** + * Loads the data required to render the templates_manage_page template. + * + * @param array $pagecontext The page context info. + * @return boolean + */ + public static function data_for_templates_manage_page($pagecontext) { + global $PAGE; + + $params = self::validate_parameters(self::data_for_templates_manage_page_parameters(), array( + 'pagecontext' => $pagecontext + )); + $context = self::get_context_from_params($params['pagecontext']); + self::validate_context($context); + + $renderable = new output\manage_templates_page($context); + $renderer = $PAGE->get_renderer('tool_lp'); + + $data = $renderable->export_for_template($renderer); + + return $data; + } + + /** + * Returns description of data_for_templates_manage_page() result value. + * + * @return \external_description + */ + public static function data_for_templates_manage_page_returns() { + return new external_single_structure(array ( + 'templates' => new external_multiple_structure( + template_exporter::get_read_structure() + ), + 'pluginbaseurl' => new external_value(PARAM_LOCALURL, 'Url to the tool_lp plugin folder on this Moodle site'), + 'navigation' => new external_multiple_structure( + new external_value(PARAM_RAW, 'HTML for a navigation item that should be on this page') + ), + 'pagecontextid' => new external_value(PARAM_INT, 'The page context id'), + 'canmanage' => new external_value(PARAM_BOOL, 'Whether the user manage the templates') + )); + + } + + /** + * Returns description of data_for_template_competenies_page() parameters. + * + * @return \external_function_parameters + */ + public static function data_for_template_competencies_page_parameters() { + $templateid = new external_value( + PARAM_INT, + 'The template id', + VALUE_REQUIRED + ); + $params = array('templateid' => $templateid, 'pagecontext' => self::get_context_parameters()); + return new external_function_parameters($params); + } + + /** + * Loads the data required to render the template_competencies_page template. + * + * @param int $templateid Template id. + * @param array $pagecontext The page context info. + * @return boolean + */ + public static function data_for_template_competencies_page($templateid, $pagecontext) { + global $PAGE; + $params = self::validate_parameters(self::data_for_template_competencies_page_parameters(), array( + 'templateid' => $templateid, + 'pagecontext' => $pagecontext + )); + + $context = self::get_context_from_params($params['pagecontext']); + self::validate_context($context); + + $template = api::read_template($params['templateid']); + $renderable = new output\template_competencies_page($template, $context); + $renderer = $PAGE->get_renderer('tool_lp'); + + $data = $renderable->export_for_template($renderer); + + return $data; + } + + /** + * Returns description of data_for_template_competencies_page() result value. + * + * @return \external_description + */ + public static function data_for_template_competencies_page_returns() { + return new external_single_structure(array ( + 'template' => template_exporter::get_read_structure(), + 'pagecontextid' => new external_value(PARAM_INT, 'Context ID'), + 'canmanagecompetencyframeworks' => new external_value(PARAM_BOOL, 'User can manage competency frameworks'), + 'canmanagetemplatecompetencies' => new external_value(PARAM_BOOL, 'User can manage learning plan templates'), + 'competencies' => new external_multiple_structure( + competency_summary_exporter::get_read_structure() + ), + 'manageurl' => new external_value(PARAM_LOCALURL, 'Url to the manage competencies page.'), + 'pluginbaseurl' => new external_value(PARAM_LOCALURL, 'Base URL of the plugin.'), + 'statistics' => template_statistics_exporter::get_read_structure() + )); + + } + + /** + * Returns description of data_for_plan_competenies_page() parameters. + * + * @return \external_function_parameters + */ + public static function data_for_plan_page_parameters() { + $planid = new external_value( + PARAM_INT, + 'The plan id', + VALUE_REQUIRED + ); + $params = array('planid' => $planid); + return new external_function_parameters($params); + } + + /** + * Loads the data required to render the plan_page template. + * + * @param int $planid Learning Plan id. + * @return boolean + */ + public static function data_for_plan_page($planid) { + global $PAGE; + $params = self::validate_parameters(self::data_for_plan_page_parameters(), array( + 'planid' => $planid + )); + $plan = api::read_plan($params['planid']); + self::validate_context($plan->get_context()); + + $renderable = new output\plan_page($plan); + $renderer = $PAGE->get_renderer('tool_lp'); + + $data = $renderable->export_for_template($renderer); + + return $data; + } + + /** + * Returns description of data_for_plan_page() result value. + * + * @return \external_description + */ + public static function data_for_plan_page_returns() { + $uc = user_competency_exporter::get_read_structure(); + $ucp = user_competency_plan_exporter::get_read_structure(); + + $uc->required = VALUE_OPTIONAL; + $ucp->required = VALUE_OPTIONAL; + + return new external_single_structure(array ( + 'plan' => plan_exporter::get_read_structure(), + 'contextid' => new external_value(PARAM_INT, 'Context ID.'), + 'pluginbaseurl' => new external_value(PARAM_URL, 'Plugin base URL.'), + 'competencies' => new external_multiple_structure( + new external_single_structure(array( + 'competency' => competency_exporter::get_read_structure(), + 'comppath' => competency_path_exporter::get_read_structure(), + 'usercompetency' => $uc, + 'usercompetencyplan' => $ucp + )) + ), + 'competencycount' => new external_value(PARAM_INT, 'Count of competencies'), + 'proficientcompetencycount' => new external_value(PARAM_INT, 'Count of proficientcompetencies'), + 'proficientcompetencypercentage' => new external_value(PARAM_FLOAT, 'Percentage of competencies proficient'), + 'proficientcompetencypercentageformatted' => new external_value(PARAM_RAW, 'Displayable percentage'), + )); + } + + /** + * Returns description of data_for_plans_page() parameters. + * + * @return \external_function_parameters + */ + public static function data_for_plans_page_parameters() { + $userid = new external_value( + PARAM_INT, + 'The user id', + VALUE_REQUIRED + ); + $params = array('userid' => $userid); + return new external_function_parameters($params); + } + + /** + * Loads the data required to render the plans_page template. + * + * @param int $userid User id. + * @return boolean + */ + public static function data_for_plans_page($userid) { + global $PAGE; + + $params = self::validate_parameters(self::data_for_plans_page_parameters(), array( + 'userid' => $userid, + )); + + $context = context_user::instance($params['userid']); + self::validate_context($context); + $output = $PAGE->get_renderer('tool_lp'); + + $renderable = new \tool_lp\output\plans_page($params['userid']); + + return $renderable->export_for_template($output); + } + + /** + * Returns description of data_for_plans_page() result value. + * + * @return \external_description + */ + public static function data_for_plans_page_returns() { + return new external_single_structure(array ( + 'userid' => new external_value(PARAM_INT, 'The learning plan user id'), + 'plans' => new external_multiple_structure( + plan_exporter::get_read_structure() + ), + 'pluginbaseurl' => new external_value(PARAM_LOCALURL, 'Url to the tool_lp plugin folder on this Moodle site'), + 'navigation' => new external_multiple_structure( + new external_value(PARAM_RAW, 'HTML for a navigation item that should be on this page') + ), + 'canreaduserevidence' => new external_value(PARAM_BOOL, 'Can the current user view the user\'s evidence'), + 'canmanageuserplans' => new external_value(PARAM_BOOL, 'Can the current user manage the user\'s plans'), + )); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function data_for_user_evidence_list_page_parameters() { + return new external_function_parameters(array( + 'userid' => new external_value(PARAM_INT, 'The user ID') + )); + } + + /** + * Loads the data required to render the user_evidence_list_page template. + * + * @param int $userid User id. + * @return boolean + */ + public static function data_for_user_evidence_list_page($userid) { + global $PAGE; + $params = self::validate_parameters(self::data_for_user_evidence_list_page_parameters(), + array('userid' => $userid)); + + $context = context_user::instance($params['userid']); + self::validate_context($context); + $output = $PAGE->get_renderer('tool_lp'); + + $renderable = new \tool_lp\output\user_evidence_list_page($params['userid']); + return $renderable->export_for_template($output); + } + + /** + * Returns description of external function result value. + * + * @return \external_description + */ + public static function data_for_user_evidence_list_page_returns() { + return new external_single_structure(array ( + 'canmanage' => new external_value(PARAM_BOOL, 'Can the current user manage the user\'s evidence'), + 'userid' => new external_value(PARAM_INT, 'The user ID'), + 'pluginbaseurl' => new external_value(PARAM_LOCALURL, 'Url to the tool_lp plugin folder on this Moodle site'), + 'evidence' => new external_multiple_structure(user_evidence_summary_exporter::get_read_structure()), + 'navigation' => new external_multiple_structure( + new external_value(PARAM_RAW, 'HTML for a navigation item that should be on this page') + ), + )); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function data_for_user_evidence_page_parameters() { + return new external_function_parameters(array( + 'id' => new external_value(PARAM_INT, 'The user evidence ID') + )); + } + + /** + * Loads the data required to render the user_evidence_page template. + * + * @param int $id User id. + * @return boolean + */ + public static function data_for_user_evidence_page($id) { + global $PAGE; + $params = self::validate_parameters(self::data_for_user_evidence_page_parameters(), + array('id' => $id)); + + $userevidence = api::read_user_evidence($id); + self::validate_context($userevidence->get_context()); + $output = $PAGE->get_renderer('tool_lp'); + + $renderable = new \tool_lp\output\user_evidence_page($userevidence); + return $renderable->export_for_template($output); + } + + /** + * Returns description of external function result value. + * + * @return \external_description + */ + public static function data_for_user_evidence_page_returns() { + return new external_single_structure(array( + 'userevidence' => user_evidence_summary_exporter::get_read_structure(), + 'pluginbaseurl' => new external_value(PARAM_LOCALURL, 'Url to the tool_lp plugin folder on this Moodle site') + )); + } + + /** + * Returns the description of the data_for_related_competencies_section_parameters() parameters. + * + * @return external_function_parameters. + */ + public static function data_for_related_competencies_section_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + return new external_function_parameters(array('competencyid' => $competencyid)); + } + + /** + * Data to render in the related competencies section. + * + * @param int $competencyid + * @return array Related competencies and whether to show delete action button or not. + */ + public static function data_for_related_competencies_section($competencyid) { + global $PAGE; + + $params = self::validate_parameters(self::data_for_related_competencies_section_parameters(), array( + 'competencyid' => $competencyid, + )); + $competency = api::read_competency($params['competencyid']); + self::validate_context($competency->get_context()); + + $renderable = new \tool_lp\output\related_competencies($params['competencyid']); + $renderer = $PAGE->get_renderer('tool_lp'); + + return $renderable->export_for_template($renderer); + } + + /** + * Returns description of data_for_related_competencies_section_returns() result value. + * + * @return external_description + */ + public static function data_for_related_competencies_section_returns() { + return new external_single_structure(array( + 'relatedcompetencies' => new external_multiple_structure(competency_exporter::get_read_structure()), + 'showdeleterelatedaction' => new external_value(PARAM_BOOL, 'Whether to show the delete relation link or not') + )); + } + + /** + * Returns the description of external function parameters. + * + * @return external_function_parameters. + */ + public static function search_users_parameters() { + $query = new external_value( + PARAM_RAW, + 'Query string' + ); + $capability = new external_value( + PARAM_RAW, + 'Required capability' + ); + $limitfrom = new external_value( + PARAM_INT, + 'Number of records to skip', + VALUE_DEFAULT, + 0 + ); + $limitnum = new external_value( + PARAM_RAW, + 'Number of records to fetch', + VALUE_DEFAULT, + 100 + ); + return new external_function_parameters(array( + 'query' => $query, + 'capability' => $capability, + 'limitfrom' => $limitfrom, + 'limitnum' => $limitnum + )); + } + + /** + * Search users. + * + * @param string $query + * @param string $capability + * @param int $limitfrom + * @param int $limitnum + * @return array + */ + public static function search_users($query, $capability = '', $limitfrom = 0, $limitnum = 100) { + global $DB, $CFG, $PAGE, $USER; + + $params = self::validate_parameters(self::search_users_parameters(), array( + 'query' => $query, + 'capability' => $capability, + 'limitfrom' => $limitfrom, + 'limitnum' => $limitnum, + )); + $query = $params['query']; + $cap = $params['capability']; + $limitfrom = $params['limitfrom']; + $limitnum = $params['limitnum']; + + $context = context_system::instance(); + self::validate_context($context); + $output = $PAGE->get_renderer('tool_lp'); + + list($filtercapsql, $filtercapparams) = api::filter_users_with_capability_on_user_context_sql($cap, + $USER->id, SQL_PARAMS_NAMED); + + $extrasearchfields = array(); + if (!empty($CFG->showuseridentity) && has_capability('moodle/site:viewuseridentity', $context)) { + $extrasearchfields = explode(',', $CFG->showuseridentity); + } + $fields = \user_picture::fields('u', $extrasearchfields); + + list($wheresql, $whereparams) = users_search_sql($query, 'u', true, $extrasearchfields); + list($sortsql, $sortparams) = users_order_by_sql('u', $query, $context); + + $countsql = "SELECT COUNT('x') FROM {user} u WHERE $wheresql AND u.id $filtercapsql"; + $countparams = $whereparams + $filtercapparams; + $sql = "SELECT $fields FROM {user} u WHERE $wheresql AND u.id $filtercapsql ORDER BY $sortsql"; + $params = $whereparams + $filtercapparams + $sortparams; + + $count = $DB->count_records_sql($countsql, $countparams); + $result = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum); + + $users = array(); + foreach ($result as $key => $user) { + // Make sure all required fields are set. + foreach (user_summary_exporter::define_properties() as $propertykey => $definition) { + if (empty($user->$propertykey) || !in_array($propertykey, $extrasearchfields)) { + if ($propertykey != 'id') { + $user->$propertykey = ''; + } + } + } + $exporter = new user_summary_exporter($user); + $newuser = $exporter->export($output); + + $users[$key] = $newuser; + } + $result->close(); + + return array( + 'users' => $users, + 'count' => $count + ); + } + + /** + * Returns description of external function result value. + * + * @return external_description + */ + public static function search_users_returns() { + global $CFG; + require_once($CFG->dirroot . '/user/externallib.php'); + return new external_single_structure(array( + 'users' => new external_multiple_structure(user_summary_exporter::get_read_structure()), + 'count' => new external_value(PARAM_INT, 'Total number of results.') + )); + } + + /** + * Returns the description of external function parameters. + * + * @return external_function_parameters + */ + public static function search_cohorts_parameters() { + $query = new external_value( + PARAM_RAW, + 'Query string' + ); + $includes = new external_value( + PARAM_ALPHA, + 'What other contexts to fetch the frameworks from. (all, parents, self)', + VALUE_DEFAULT, + 'parents' + ); + $limitfrom = new external_value( + PARAM_INT, + 'limitfrom we are fetching the records from', + VALUE_DEFAULT, + 0 + ); + $limitnum = new external_value( + PARAM_INT, + 'Number of records to fetch', + VALUE_DEFAULT, + 25 + ); + return new external_function_parameters(array( + 'query' => $query, + 'context' => self::get_context_parameters(), + 'includes' => $includes, + 'limitfrom' => $limitfrom, + 'limitnum' => $limitnum + )); + } + + /** + * Search cohorts. + * TODO: MDL-52243 Move this function to cohorts/externallib.php + * + * @param string $query + * @param array $context + * @param string $includes + * @param int $limitfrom + * @param int $limitnum + * @return array + */ + public static function search_cohorts($query, $context, $includes = 'parents', $limitfrom = 0, $limitnum = 25) { + global $DB, $CFG, $PAGE; + require_once($CFG->dirroot . '/cohort/lib.php'); + + $params = self::validate_parameters(self::search_cohorts_parameters(), array( + 'query' => $query, + 'context' => $context, + 'includes' => $includes, + 'limitfrom' => $limitfrom, + 'limitnum' => $limitnum, + )); + $query = $params['query']; + $includes = $params['includes']; + $context = self::get_context_from_params($params['context']); + $limitfrom = $params['limitfrom']; + $limitnum = $params['limitnum']; + + self::validate_context($context); + $output = $PAGE->get_renderer('tool_lp'); + + $manager = has_capability('moodle/cohort:manage', $context); + if (!$manager) { + require_capability('moodle/cohort:view', $context); + } + + // TODO Make this more efficient. + if ($includes == 'self') { + $results = cohort_get_cohorts($context->id, $limitfrom, $limitnum, $query); + $results = $results['cohorts']; + } else if ($includes == 'parents') { + $results = cohort_get_cohorts($context->id, $limitfrom, $limitnum, $query); + $results = $results['cohorts']; + if (!$context instanceof context_system) { + $results = array_merge($results, cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query)); + } + } else if ($includes == 'all') { + $results = cohort_get_all_cohorts($limitfrom, $limitnum, $query); + $results = $results['cohorts']; + } else { + throw new coding_exception('Invalid parameter value for \'includes\'.'); + } + + $cohorts = array(); + foreach ($results as $key => $cohort) { + $cohortcontext = context::instance_by_id($cohort->contextid); + $exporter = new cohort_summary_exporter($cohort, array('context' => $cohortcontext)); + $newcohort = $exporter->export($output); + + $cohorts[$key] = $newcohort; + } + + return array('cohorts' => $cohorts); + } + + /** + * Returns description of external function result value. + * + * @return external_description + */ + public static function search_cohorts_returns() { + return new external_single_structure(array( + 'cohorts' => new external_multiple_structure(cohort_summary_exporter::get_read_structure()) + )); + } + + /** + * Returns description of external function. + * + * @return \external_function_parameters + */ + public static function data_for_user_competency_summary_parameters() { + $userid = new external_value( + PARAM_INT, + 'Data base record id for the user', + VALUE_REQUIRED + ); + $competencyid = new external_value( + PARAM_INT, + 'Data base record id for the competency', + VALUE_REQUIRED + ); + $params = array( + 'userid' => $userid, + 'competencyid' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * Data for user competency summary. + * + * @param int $userid The user ID + * @param int $competencyid The competency ID + * @return \stdClass + */ + public static function data_for_user_competency_summary($userid, $competencyid) { + global $PAGE; + $params = self::validate_parameters(self::data_for_user_competency_summary_parameters(), array( + 'userid' => $userid, + 'competencyid' => $competencyid, + )); + + $uc = api::get_user_competency($params['userid'], $params['competencyid']); + self::validate_context($uc->get_context()); + $output = $PAGE->get_renderer('tool_lp'); + + $renderable = new \tool_lp\output\user_competency_summary($uc); + return $renderable->export_for_template($output); + } + + /** + * Returns description of external function. + * + * @return \external_description + */ + public static function data_for_user_competency_summary_returns() { + return user_competency_summary_exporter::get_read_structure(); + } + + /** + * Returns description of data_for_user_competency_summary_in_plan() parameters. + * + * @return \external_function_parameters + */ + public static function data_for_user_competency_summary_in_plan_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'Data base record id for the competency', + VALUE_REQUIRED + ); + $planid = new external_value( + PARAM_INT, + 'Data base record id for the plan', + VALUE_REQUIRED + ); + + $params = array( + 'competencyid' => $competencyid, + 'planid' => $planid, + ); + return new external_function_parameters($params); + } + + /** + * Read a user competency summary. + * + * @param int $competencyid The competency id + * @param int $planid The plan id + * @return \stdClass + */ + public static function data_for_user_competency_summary_in_plan($competencyid, $planid) { + global $PAGE; + $params = self::validate_parameters(self::data_for_user_competency_summary_in_plan_parameters(), array( + 'competencyid' => $competencyid, + 'planid' => $planid + )); + + $plan = api::read_plan($params['planid']); + $context = $plan->get_context(); + self::validate_context($context); + $output = $PAGE->get_renderer('tool_lp'); + + $renderable = new user_competency_summary_in_plan($params['competencyid'], $params['planid']); + return $renderable->export_for_template($output); + } + + /** + * Returns description of data_for_user_competency_summary_in_plan() result value. + * + * @return \external_description + */ + public static function data_for_user_competency_summary_in_plan_returns() { + return user_competency_summary_in_plan_exporter::get_read_structure(); + } + + /** + * Returns description of data_for_user_competency_summary_in_course() parameters. + * + * @return \external_function_parameters + */ + public static function data_for_user_competency_summary_in_course_parameters() { + $userid = new external_value( + PARAM_INT, + 'Data base record id for the user', + VALUE_REQUIRED + ); + $competencyid = new external_value( + PARAM_INT, + 'Data base record id for the competency', + VALUE_REQUIRED + ); + $courseid = new external_value( + PARAM_INT, + 'Data base record id for the course', + VALUE_REQUIRED + ); + + $params = array( + 'userid' => $userid, + 'competencyid' => $competencyid, + 'courseid' => $courseid, + ); + return new external_function_parameters($params); + } + + /** + * Read a user competency summary. + * + * @param int $userid The user id + * @param int $competencyid The competency id + * @param int $courseid The course id + * @return \stdClass + */ + public static function data_for_user_competency_summary_in_course($userid, $competencyid, $courseid) { + global $PAGE; + $params = self::validate_parameters(self::data_for_user_competency_summary_in_course_parameters(), array( + 'userid' => $userid, + 'competencyid' => $competencyid, + 'courseid' => $courseid + )); + $context = context_user::instance($params['userid']); + self::validate_context($context); + $output = $PAGE->get_renderer('tool_lp'); + + $renderable = new user_competency_summary_in_course($params['userid'], $params['competencyid'], $params['courseid']); + return $renderable->export_for_template($output); + } + + /** + * Returns description of data_for_user_competency_summary_in_course() result value. + * + * @return \external_description + */ + public static function data_for_user_competency_summary_in_course_returns() { + return user_competency_summary_in_course_exporter::get_read_structure(); + } + +} diff --git a/admin/tool/lp/classes/external/cohort_summary_exporter.php b/admin/tool/lp/classes/external/cohort_summary_exporter.php new file mode 100644 index 00000000000..a22ac9f53bc --- /dev/null +++ b/admin/tool/lp/classes/external/cohort_summary_exporter.php @@ -0,0 +1,74 @@ +. + +/** + * Class for exporting a cohort summary from an stdClass. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; +defined('MOODLE_INTERNAL') || die(); + +use renderer_base; +use moodle_url; + +/** + * Class for exporting a cohort summary from an stdClass. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cohort_summary_exporter extends \core_competency\external\exporter { + + protected static function define_related() { + // Cohorts can exist on a category context. + return array('context' => '\\context'); + } + + public static function define_properties() { + return array( + 'id' => array( + 'type' => PARAM_INT, + ), + 'name' => array( + 'type' => PARAM_TEXT, + ), + 'idnumber' => array( + 'type' => PARAM_RAW, // ID numbers are plain text. + 'default' => '' + ), + 'visible' => array( + 'type' => PARAM_BOOL, + ) + ); + } + + public static function define_other_properties() { + return array( + 'contextname' => array( + 'type' => PARAM_TEXT + ), + ); + } + + protected function get_other_values(renderer_base $output) { + return array( + 'contextname' => $this->related['context']->get_context_name() + ); + } +} diff --git a/admin/tool/lp/classes/external/competency_path_exporter.php b/admin/tool/lp/classes/external/competency_path_exporter.php new file mode 100644 index 00000000000..1e9e1cf2850 --- /dev/null +++ b/admin/tool/lp/classes/external/competency_path_exporter.php @@ -0,0 +1,124 @@ +. + +/** + * Class for exporting competency_path data. + * + * @package tool_lp + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; +defined('MOODLE_INTERNAL') || die(); + +use renderer_base; +use moodle_url; + +/** + * Class for exporting competency_path data. + * + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_path_exporter extends \core_competency\external\exporter { + + /** + * Constructor. + * + * @param array $related - related objects. + */ + public function __construct($related) { + parent::__construct([], $related); + } + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_related() { + return [ + 'ancestors' => 'core_competency\\competency[]', + 'framework' => 'core_competency\\competency_framework', + 'context' => 'context' + ]; + } + + /** + * Return the list of additional properties used only for display. + * + * @return array - Keys with their types. + */ + protected static function define_other_properties() { + return [ + 'ancestors' => [ + 'type' => path_node_exporter::read_properties_definition(), + 'multiple' => true, + ], + 'framework' => [ + 'type' => path_node_exporter::read_properties_definition() + ], + 'pluginbaseurl' => [ + 'type' => PARAM_TEXT + ], + 'pagecontextid' => [ + 'type' => PARAM_INT + ] + ]; + } + + /** + * Get the additional values to inject while exporting. + * + * @param renderer_base $output The renderer. + * @return array Keys are the property names, values are their values. + */ + protected function get_other_values(renderer_base $output) { + $result = new \stdClass(); + $ancestors = []; + $nodescount = count($this->related['ancestors']); + $i = 1; + foreach ($this->related['ancestors'] as $competency) { + $exporter = new path_node_exporter([ + 'id' => $competency->get_id(), + 'name' => $competency->get_idnumber(), + 'position' => $i, + 'first' => $i == 1, + 'last' => $i == $nodescount + ], [ + 'context' => $this->related['context'], + ] + ); + $ancestors[] = $exporter->export($output); + $i++; + } + $result->ancestors = $ancestors; + $exporter = new path_node_exporter([ + 'id' => $this->related['framework']->get_id(), + 'name' => $this->related['framework']->get_shortname(), + 'first' => 0, + 'last' => 0, + 'position' => -1 + ], [ + 'context' => $this->related['context'] + ] + ); + $result->framework = $exporter->export($output); + $result->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true); + $result->pagecontextid = $this->related['context']->id; + return (array) $result; + } +} diff --git a/admin/tool/lp/classes/external/competency_summary_exporter.php b/admin/tool/lp/classes/external/competency_summary_exporter.php new file mode 100644 index 00000000000..ff174712a6b --- /dev/null +++ b/admin/tool/lp/classes/external/competency_summary_exporter.php @@ -0,0 +1,141 @@ +. + +/** + * Class for exporting competency data with the set of linked courses. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; +defined('MOODLE_INTERNAL') || die(); + +use context_course; +use renderer_base; +use stdClass; +use core_competency\competency_framework; +use core_competency\external\competency_exporter; +use core_competency\external\competency_framework_exporter; + +/** + * Class for exporting competency data with additional related data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_summary_exporter extends \core_competency\external\exporter { + + protected static function define_related() { + // We cache the context so it does not need to be retrieved from the framework every time. + return array('context' => '\\context', + 'competency' => '\\core_competency\\competency', + 'framework' => '\\core_competency\\competency_framework', + 'linkedcourses' => '\\stdClass[]', + 'relatedcompetencies' => '\\core_competency\\competency[]'); + } + + protected static function define_other_properties() { + return array( + 'linkedcourses' => array( + 'type' => course_summary_exporter::read_properties_definition(), + 'multiple' => true + ), + 'relatedcompetencies' => array( + 'type' => competency_exporter::read_properties_definition(), + 'multiple' => true + ), + 'competency' => array( + 'type' => competency_exporter::read_properties_definition() + ), + 'framework' => array( + 'type' => competency_framework_exporter::read_properties_definition() + ), + 'hascourses' => array( + 'type' => PARAM_BOOL + ), + 'hasrelatedcompetencies' => array( + 'type' => PARAM_BOOL + ), + 'scaleid' => array( + 'type' => PARAM_INT + ), + 'scaleconfiguration' => array( + 'type' => PARAM_RAW + ), + 'taxonomyterm' => array( + 'type' => PARAM_TEXT + ), + 'comppath' => array( + 'type' => competency_path_exporter::read_properties_definition(), + ) + ); + } + + protected function get_other_values(renderer_base $output) { + $result = new stdClass(); + $context = $this->related['context']; + + $courses = $this->related['linkedcourses']; + $linkedcourses = array(); + foreach ($courses as $course) { + $context = context_course::instance($course->id); + $exporter = new course_summary_exporter($course, array('context' => $context)); + $courseexport = $exporter->export($output); + array_push($linkedcourses, $courseexport); + } + $result->linkedcourses = $linkedcourses; + $result->hascourses = count($linkedcourses) > 0; + + $relatedcompetencies = array(); + foreach ($this->related['relatedcompetencies'] as $competency) { + $exporter = new competency_exporter($competency, array('context' => $context)); + $competencyexport = $exporter->export($output); + array_push($relatedcompetencies, $competencyexport); + } + $result->relatedcompetencies = $relatedcompetencies; + $result->hasrelatedcompetencies = count($relatedcompetencies) > 0; + + $competency = $this->related['competency']; + $exporter = new competency_exporter($competency, array('context' => $context)); + $result->competency = $exporter->export($output); + + $exporter = new competency_framework_exporter($this->related['framework']); + $result->framework = $exporter->export($output); + $scaleconfiguration = $this->related['framework']->get_scaleconfiguration(); + $scaleid = $this->related['framework']->get_scaleid(); + if ($competency->get_scaleid()) { + $scaleconfiguration = $competency->get_scaleconfiguration(); + $scaleid = $competency->get_scaleid(); + } + $result->scaleconfiguration = $scaleconfiguration; + $result->scaleid = $scaleid; + + $level = $competency->get_level(); + $taxonomy = $this->related['framework']->get_taxonomy($level); + $result->taxonomyterm = (string) (competency_framework::get_taxonomies_list()[$taxonomy]); + + // Competency path. + $exporter = new competency_path_exporter([ + 'ancestors' => $competency->get_ancestors(), + 'framework' => $this->related['framework'], + 'context' => $context + ]); + $result->comppath = $exporter->export($output); + + return (array) $result; + } +} diff --git a/admin/tool/lp/classes/external/course_competency_statistics_exporter.php b/admin/tool/lp/classes/external/course_competency_statistics_exporter.php new file mode 100644 index 00000000000..1e603e00e67 --- /dev/null +++ b/admin/tool/lp/classes/external/course_competency_statistics_exporter.php @@ -0,0 +1,105 @@ +. + +/** + * Class for exporting a course competency statistics summary. + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; +defined('MOODLE_INTERNAL') || die(); + +use renderer_base; +use moodle_url; +use core_competency\external\competency_exporter; + +/** + * Class for exporting a course competency statistics summary. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_competency_statistics_exporter extends \core_competency\external\exporter { + + public static function define_properties() { + return array( + 'competencycount' => array( + 'type' => PARAM_INT, + ), + 'proficientcompetencycount' => array( + 'type' => PARAM_INT, + ), + ); + } + + public static function define_other_properties() { + return array( + 'proficientcompetencypercentage' => array( + 'type' => PARAM_FLOAT + ), + 'proficientcompetencypercentageformatted' => array( + 'type' => PARAM_RAW + ), + 'leastproficient' => array( + 'type' => competency_exporter::read_properties_definition(), + 'multiple' => true + ), + 'leastproficientcount' => array( + 'type' => PARAM_INT + ), + 'canbegradedincourse' => array( + 'type' => PARAM_BOOL + ), + 'canmanagecoursecompetencies' => array( + 'type' => PARAM_BOOL + ), + ); + } + + protected static function define_related() { + return array('context' => 'context'); + } + + protected function get_other_values(renderer_base $output) { + $proficientcompetencypercentage = 0; + $proficientcompetencypercentageformatted = ''; + if ($this->data->competencycount > 0) { + $proficientcompetencypercentage = ((float) $this->data->proficientcompetencycount + / (float) $this->data->competencycount) * 100.0; + $proficientcompetencypercentageformatted = format_float($proficientcompetencypercentage); + } + $competencies = array(); + $contextcache = array(); + foreach ($this->data->leastproficientcompetencies as $competency) { + if (!isset($contextcache[$competency->get_competencyframeworkid()])) { + $contextcache[$competency->get_competencyframeworkid()] = $competency->get_context(); + } + $context = $contextcache[$competency->get_competencyframeworkid()]; + $exporter = new competency_exporter($competency, array('context' => $context)); + $competencies[] = $exporter->export($output); + } + return array( + 'proficientcompetencypercentage' => $proficientcompetencypercentage, + 'proficientcompetencypercentageformatted' => $proficientcompetencypercentageformatted, + 'leastproficient' => $competencies, + 'leastproficientcount' => count($competencies), + 'canbegradedincourse' => has_capability('moodle/competency:coursecompetencygradable', $this->related['context']), + 'canmanagecoursecompetencies' => has_capability('moodle/competency:coursecompetencymanage', $this->related['context']) + ); + } +} diff --git a/admin/tool/lp/classes/external/course_module_summary_exporter.php b/admin/tool/lp/classes/external/course_module_summary_exporter.php new file mode 100644 index 00000000000..dfd24fd772b --- /dev/null +++ b/admin/tool/lp/classes/external/course_module_summary_exporter.php @@ -0,0 +1,70 @@ +. + +/** + * Class for exporting a course module summary from an stdClass. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; +defined('MOODLE_INTERNAL') || die(); + +use renderer_base; + + +/** + * Class for exporting a course module summary from a cm_info class. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_summary_exporter extends \core_competency\external\exporter { + + protected static function define_related() { + return array('cm' => 'cm_info'); + } + + protected function get_other_values(renderer_base $output) { + $context = $this->related['cm']->context; + + return array( + 'id' => $this->related['cm']->id, + 'name' => external_format_string($this->related['cm']->name, $context->id), + 'url' => $this->related['cm']->url->out(), + 'iconurl' => $this->related['cm']->get_icon_url()->out() + ); + } + + + public static function define_other_properties() { + return array( + 'id' => array( + 'type' => PARAM_INT, + ), + 'name' => array( + 'type' => PARAM_TEXT + ), + 'url' => array( + 'type' => PARAM_URL + ), + 'iconurl' => array( + 'type' => PARAM_URL + ) + ); + } +} diff --git a/admin/tool/lp/classes/external/course_summary_exporter.php b/admin/tool/lp/classes/external/course_summary_exporter.php new file mode 100644 index 00000000000..4f44a7d9631 --- /dev/null +++ b/admin/tool/lp/classes/external/course_summary_exporter.php @@ -0,0 +1,73 @@ +. + +/** + * Class for exporting a course summary from an stdClass. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; +defined('MOODLE_INTERNAL') || die(); + +use renderer_base; +use moodle_url; + +/** + * Class for exporting a course summary from an stdClass. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_summary_exporter extends \core_competency\external\exporter { + + protected static function define_related() { + // We cache the context so it does not need to be retrieved from the course. + return array('context' => '\\context'); + } + + protected function get_other_values(renderer_base $output) { + return array( + 'viewurl' => (new moodle_url('/course/view.php', array('id' => $this->data->id)))->out(false) + ); + } + + public static function define_properties() { + return array( + 'id' => array( + 'type' => PARAM_INT, + ), + 'fullname' => array( + 'type' => PARAM_TEXT, + ), + 'shortname' => array( + 'type' => PARAM_TEXT, + ), + 'idnumber' => array( + 'type' => PARAM_TEXT, + ) + ); + } + + public static function define_other_properties() { + return array( + 'viewurl' => array( + 'type' => PARAM_URL, + ) + ); + } +} diff --git a/admin/tool/lp/classes/external/path_node_exporter.php b/admin/tool/lp/classes/external/path_node_exporter.php new file mode 100644 index 00000000000..2b25ed24863 --- /dev/null +++ b/admin/tool/lp/classes/external/path_node_exporter.php @@ -0,0 +1,61 @@ +. + +/** + * Class for exporting path_node data. + * + * @package tool_lp + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; +defined('MOODLE_INTERNAL') || die(); + + +/** + * Class for exporting path_node data. + * + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class path_node_exporter extends \core_competency\external\exporter { + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_properties() { + return [ + 'id' => [ + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED + ], + 'name' => [ + 'type' => PARAM_TEXT + ], + 'first' => [ + 'type' => PARAM_BOOL + ], + 'last' => [ + 'type' => PARAM_BOOL + ], + 'position' => [ + 'type' => PARAM_INT + ] + ]; + } +} diff --git a/admin/tool/lp/classes/external/template_statistics_exporter.php b/admin/tool/lp/classes/external/template_statistics_exporter.php new file mode 100644 index 00000000000..e9f401b4126 --- /dev/null +++ b/admin/tool/lp/classes/external/template_statistics_exporter.php @@ -0,0 +1,142 @@ +. + +/** + * Class for exporting a template statistics summary. + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; +defined('MOODLE_INTERNAL') || die(); + +use renderer_base; +use moodle_url; +use core_competency\external\competency_exporter; + +/** + * Class for exporting a cohort summary from an stdClass. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_statistics_exporter extends \core_competency\external\exporter { + + public static function define_properties() { + return array( + 'competencycount' => array( + 'type' => PARAM_INT, + ), + 'unlinkedcompetencycount' => array( + 'type' => PARAM_INT, + ), + 'plancount' => array( + 'type' => PARAM_INT, + ), + 'completedplancount' => array( + 'type' => PARAM_INT, + ), + 'usercompetencyplancount' => array( + 'type' => PARAM_INT, + ), + 'proficientusercompetencyplancount' => array( + 'type' => PARAM_INT, + ) + ); + } + + public static function define_other_properties() { + return array( + 'linkedcompetencypercentage' => array( + 'type' => PARAM_FLOAT + ), + 'linkedcompetencypercentageformatted' => array( + 'type' => PARAM_RAW + ), + 'linkedcompetencycount' => array( + 'type' => PARAM_INT + ), + 'completedplanpercentage' => array( + 'type' => PARAM_FLOAT + ), + 'completedplanpercentageformatted' => array( + 'type' => PARAM_RAW + ), + 'proficientusercompetencyplanpercentage' => array( + 'type' => PARAM_FLOAT + ), + 'proficientusercompetencyplanpercentageformatted' => array( + 'type' => PARAM_RAW + ), + 'leastproficient' => array( + 'type' => competency_exporter::read_properties_definition(), + 'multiple' => true + ), + 'leastproficientcount' => array( + 'type' => PARAM_INT + ), + ); + } + + protected function get_other_values(renderer_base $output) { + $linkedcompetencycount = $this->data->competencycount - $this->data->unlinkedcompetencycount; + if ($linkedcompetencycount < 0) { + // Should never happen. + $linkedcompetencycount = 0; + } + $linkedcompetencypercentage = 0; + $linkedcompetencypercentageformatted = ''; + if ($this->data->competencycount > 0) { + $linkedcompetencypercentage = ((float) $linkedcompetencycount / (float) $this->data->competencycount) * 100.0; + $linkedcompetencypercentageformatted = format_float($linkedcompetencypercentage); + } + $completedplanpercentage = 0; + $completedplanpercentageformatted = ''; + if ($this->data->plancount > 0) { + $completedplanpercentage = ((float) $this->data->completedplancount / (float) $this->data->plancount) * 100.0; + $completedplanpercentageformatted = format_float($completedplanpercentage); + } + $proficientusercompetencyplanpercentage = 0; + $proficientusercompetencyplanpercentageformatted = ''; + if ($this->data->usercompetencyplancount > 0) { + $proficientusercompetencyplanpercentage = ((float) $this->data->proficientusercompetencyplancount + / (float) $this->data->usercompetencyplancount) * 100.0; + $proficientusercompetencyplanpercentageformatted = format_float($proficientusercompetencyplanpercentage); + } + $competencies = array(); + $contextcache = array(); + foreach ($this->data->leastproficientcompetencies as $competency) { + if (!isset($contextcache[$competency->get_competencyframeworkid()])) { + $contextcache[$competency->get_competencyframeworkid()] = $competency->get_context(); + } + $context = $contextcache[$competency->get_competencyframeworkid()]; + $exporter = new competency_exporter($competency, array('context' => $context)); + $competencies[] = $exporter->export($output); + } + return array( + 'linkedcompetencycount' => $linkedcompetencycount, + 'linkedcompetencypercentage' => $linkedcompetencypercentage, + 'linkedcompetencypercentageformatted' => $linkedcompetencypercentageformatted, + 'completedplanpercentage' => $completedplanpercentage, + 'completedplanpercentageformatted' => $completedplanpercentageformatted, + 'proficientusercompetencyplanpercentage' => $proficientusercompetencyplanpercentage, + 'proficientusercompetencyplanpercentageformatted' => $proficientusercompetencyplanpercentageformatted, + 'leastproficient' => $competencies, + 'leastproficientcount' => count($competencies) + ); + } +} diff --git a/admin/tool/lp/classes/external/user_competency_summary_exporter.php b/admin/tool/lp/classes/external/user_competency_summary_exporter.php new file mode 100644 index 00000000000..cc433771363 --- /dev/null +++ b/admin/tool/lp/classes/external/user_competency_summary_exporter.php @@ -0,0 +1,173 @@ +. + +/** + * Class for exporting user competency data with all the evidence + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; + +use context_user; +use renderer_base; +use stdClass; +use core_competency\external\comment_area_exporter; +use core_competency\external\evidence_exporter; +use core_competency\external\user_competency_exporter; +use core_competency\external\user_competency_plan_exporter; +use core_competency\external\user_competency_course_exporter; +use core_competency\external\user_summary_exporter; +use core_competency\user_competency; + +/** + * Class for exporting user competency data with additional related data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency_summary_exporter extends \core_competency\external\exporter { + + protected static function define_related() { + // We cache the context so it does not need to be retrieved from the framework every time. + return array('competency' => '\\core_competency\\competency', + 'relatedcompetencies' => '\\core_competency\\competency[]', + 'user' => '\\stdClass', + 'usercompetency' => '\\core_competency\\user_competency?', + 'usercompetencyplan' => '\\core_competency\\user_competency_plan?', + 'usercompetencycourse' => '\\core_competency\\user_competency_course?', + 'evidence' => '\\core_competency\\evidence[]'); + } + + protected static function define_other_properties() { + return array( + 'showrelatedcompetencies' => array( + 'type' => PARAM_BOOL + ), + 'cangrade' => array( + 'type' => PARAM_BOOL + ), + 'competency' => array( + 'type' => competency_summary_exporter::read_properties_definition() + ), + 'user' => array( + 'type' => user_summary_exporter::read_properties_definition(), + ), + 'usercompetency' => array( + 'type' => user_competency_exporter::read_properties_definition(), + 'optional' => true + ), + 'usercompetencyplan' => array( + 'type' => user_competency_exporter::read_properties_definition(), + 'optional' => true + ), + 'usercompetencycourse' => array( + 'type' => user_competency_course_exporter::read_properties_definition(), + 'optional' => true + ), + 'evidence' => array( + 'type' => evidence_exporter::read_properties_definition(), + 'multiple' => true + ), + 'commentarea' => array( + 'type' => comment_area_exporter::read_properties_definition(), + 'optional' => true + ), + ); + } + + protected function get_other_values(renderer_base $output) { + global $DB; + $result = new stdClass(); + + $result->showrelatedcompetencies = true; + + $competency = $this->related['competency']; + $exporter = new competency_summary_exporter(null, array( + 'competency' => $competency, + 'context' => $competency->get_context(), + 'framework' => $competency->get_framework(), + 'linkedcourses' => array(), + 'relatedcompetencies' => $this->related['relatedcompetencies'] + )); + $result->competency = $exporter->export($output); + + $result->cangrade = user_competency::can_grade_user($this->related['user']->id); + if ($this->related['user']) { + $exporter = new user_summary_exporter($this->related['user']); + $result->user = $exporter->export($output); + } + $related = array('scale' => $competency->get_scale()); + if ($this->related['usercompetency']) { + $exporter = new user_competency_exporter($this->related['usercompetency'], $related); + $result->usercompetency = $exporter->export($output); + } + if ($this->related['usercompetencyplan']) { + $exporter = new user_competency_plan_exporter($this->related['usercompetencyplan'], $related); + $result->usercompetencyplan = $exporter->export($output); + } + if ($this->related['usercompetencycourse']) { + $exporter = new user_competency_course_exporter($this->related['usercompetencycourse'], $related); + $result->usercompetencycourse = $exporter->export($output); + } + + $allevidence = array(); + $usercache = array(); + $scale = $competency->get_scale(); + + $result->evidence = array(); + if (count($this->related['evidence'])) { + foreach ($this->related['evidence'] as $evidence) { + $actionuserid = $evidence->get_actionuserid(); + if (!empty($actionuserid)) { + $usercache[$evidence->get_actionuserid()] = true; + } + } + $users = array(); + if (!empty($usercache)) { + list($sql, $params) = $DB->get_in_or_equal(array_keys($usercache)); + $users = $DB->get_records_select('user', 'id ' . $sql, $params); + } + + foreach ($users as $user) { + $usercache[$user->id] = $user; + } + + foreach ($this->related['evidence'] as $evidence) { + $actionuserid = $evidence->get_actionuserid(); + $related = array( + 'scale' => $scale, + 'usercompetency' => ($this->related['usercompetency'] ? $this->related['usercompetency'] : null), + 'usercompetencyplan' => ($this->related['usercompetencyplan'] ? $this->related['usercompetencyplan'] : null), + ); + $related['actionuser'] = !empty($actionuserid) ? $usercache[$actionuserid] : null; + $exporter = new evidence_exporter($evidence, $related); + $allevidence[] = $exporter->export($output); + } + $result->evidence = $allevidence; + } + + $usercompetency = !empty($this->related['usercompetency']) ? $this->related['usercompetency'] : null; + + if (!empty($usercompetency) && $usercompetency->can_read_comments()) { + $commentareaexporter = new comment_area_exporter($usercompetency->get_comment_object()); + $result->commentarea = $commentareaexporter->export($output); + } + + return (array) $result; + } +} diff --git a/admin/tool/lp/classes/external/user_competency_summary_in_course_exporter.php b/admin/tool/lp/classes/external/user_competency_summary_in_course_exporter.php new file mode 100644 index 00000000000..0055734b64f --- /dev/null +++ b/admin/tool/lp/classes/external/user_competency_summary_in_course_exporter.php @@ -0,0 +1,94 @@ +. + +/** + * Class for exporting user competency data with all the evidence in a course + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; + +use core_competency\api; +use context_course; +use renderer_base; +use stdClass; + +/** + * Class for exporting user competency data with additional related data in a plan. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency_summary_in_course_exporter extends \core_competency\external\exporter { + + protected static function define_related() { + // We cache the context so it does not need to be retrieved from the framework every time. + return array('competency' => '\\core_competency\\competency', + 'relatedcompetencies' => '\\core_competency\\competency[]', + 'user' => '\\stdClass', + 'course' => '\\stdClass', + 'usercompetencycourse' => '\\core_competency\\user_competency_course?', + 'evidence' => '\\core_competency\\evidence[]', + 'scale' => '\\grade_scale'); + } + + protected static function define_other_properties() { + return array( + 'usercompetencysummary' => array( + 'type' => user_competency_summary_exporter::read_properties_definition() + ), + 'course' => array( + 'type' => course_summary_exporter::read_properties_definition(), + ), + 'coursemodules' => array( + 'type' => course_module_summary_exporter::read_properties_definition(), + 'multiple' => true + ) + ); + } + + protected function get_other_values(renderer_base $output) { + // Arrays are copy on assign. + $related = $this->related; + $result = new stdClass(); + // Remove course from related as it is not wanted by the user_competency_summary_exporter. + unset($related['course']); + $related['usercompetencyplan'] = null; + $related['usercompetency'] = null; + $exporter = new user_competency_summary_exporter(null, $related); + $result->usercompetencysummary = $exporter->export($output); + + $context = context_course::instance($this->related['course']->id); + $exporter = new course_summary_exporter($this->related['course'], array('context' => $context)); + $result->course = $exporter->export($output); + + $coursemodules = api::list_course_modules_using_competency($this->related['competency']->get_id(), + $this->related['course']->id); + + $fastmodinfo = get_fast_modinfo($this->related['course']->id); + $exportedmodules = array(); + foreach ($coursemodules as $cm) { + $cminfo = $fastmodinfo->cms[$cm]; + $cmexporter = new course_module_summary_exporter(null, array('cm' => $cminfo)); + $exportedmodules[] = $cmexporter->export($output); + } + $result->coursemodules = $exportedmodules; + + return (array) $result; + } +} diff --git a/admin/tool/lp/classes/external/user_competency_summary_in_plan_exporter.php b/admin/tool/lp/classes/external/user_competency_summary_in_plan_exporter.php new file mode 100644 index 00000000000..bf91e343cfd --- /dev/null +++ b/admin/tool/lp/classes/external/user_competency_summary_in_plan_exporter.php @@ -0,0 +1,77 @@ +. + +/** + * Class for exporting user competency data with all the evidence in a plan + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; + +use context_user; +use renderer_base; +use stdClass; +use core_competency\external\plan_exporter; + +/** + * Class for exporting user competency data with additional related data in a plan. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency_summary_in_plan_exporter extends \core_competency\external\exporter { + + protected static function define_related() { + // We cache the context so it does not need to be retrieved from the framework every time. + return array('competency' => '\\core_competency\\competency', + 'relatedcompetencies' => '\\core_competency\\competency[]', + 'user' => '\\stdClass', + 'plan' => '\\core_competency\\plan', + 'usercompetency' => '\\core_competency\\user_competency?', + 'usercompetencyplan' => '\\core_competency\\user_competency_plan?', + 'evidence' => '\\core_competency\\evidence[]'); + } + + protected static function define_other_properties() { + return array( + 'usercompetencysummary' => array( + 'type' => user_competency_summary_exporter::read_properties_definition() + ), + 'plan' => array( + 'type' => plan_exporter::read_properties_definition(), + ) + ); + } + + protected function get_other_values(renderer_base $output) { + // Arrays are copy on assign. + $related = $this->related; + // Remove plan from related as it is not wanted by the user_competency_summary_exporter. + unset($related['plan']); + // We do not need user_competency_course in user_competency_summary_exporter. + $related['usercompetencycourse'] = null; + $exporter = new user_competency_summary_exporter(null, $related); + $result = new stdClass(); + $result->usercompetencysummary = $exporter->export($output); + + $exporter = new plan_exporter($this->related['plan'], array('template' => $this->related['plan']->get_template())); + $result->plan = $exporter->export($output); + + return (array) $result; + } +} diff --git a/admin/tool/lp/classes/external/user_evidence_competency_summary_exporter.php b/admin/tool/lp/classes/external/user_evidence_competency_summary_exporter.php new file mode 100644 index 00000000000..aa97b0ba43a --- /dev/null +++ b/admin/tool/lp/classes/external/user_evidence_competency_summary_exporter.php @@ -0,0 +1,72 @@ +. + +/** + * Class for exporting user evidence competency data. + * + * @package tool_lp + * @copyright 2016 Serge Gauthier - + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; + +use moodle_url; +use renderer_base; +use core_competency\external\competency_exporter; +use core_competency\external\user_competency_exporter; + +/** + * Class for exporting user evidence competency data. + * + * @copyright 2016 Serge Gauthier - + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_evidence_competency_summary_exporter extends \core_competency\external\exporter { + + protected static function define_related() { + return array('competency' => '\\core_competency\\competency', + 'usercompetency' => '\\core_competency\\user_competency', + 'scale' => 'grade_scale', + 'context' => '\\context' + ); + } + + protected static function define_other_properties() { + return array( + 'competency' => array( + 'type' => competency_exporter::read_properties_definition() + ), + 'usercompetency' => array( + 'type' => user_competency_exporter::read_properties_definition(), + ) + ); + } + + protected function get_other_values(renderer_base $output) { + $competencyexporter = new competency_exporter($this->related['competency'], + array('context' => $this->related['context'])); + $usercompetencyexporter = new user_competency_exporter($this->related['usercompetency'], + array('scale' => $this->related['scale'])); + + $values = array( + 'competency' => $competencyexporter->export($output), + 'usercompetency' => $usercompetencyexporter->export($output) + ); + + return $values; + } + +} diff --git a/admin/tool/lp/classes/external/user_evidence_summary_exporter.php b/admin/tool/lp/classes/external/user_evidence_summary_exporter.php new file mode 100644 index 00000000000..828724d9757 --- /dev/null +++ b/admin/tool/lp/classes/external/user_evidence_summary_exporter.php @@ -0,0 +1,136 @@ +. + +/** + * Class for exporting user evidence with all competencies. + * + * @package tool_lp + * @copyright 2016 Serge Gauthier - + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\external; +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; +use renderer_base; +use core_competency\external\stored_file_exporter; + +/** + * Class for exporting user evidence with all competencies. + * + * @copyright 2016 Serge Gauthier - + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_evidence_summary_exporter extends \core_competency\external\persistent_exporter { + + protected static function define_class() { + return 'core_competency\\user_evidence'; + } + + protected static function define_other_properties() { + return array( + 'canmanage' => array( + 'type' => PARAM_BOOL + ), + 'filecount' => array( + 'type' => PARAM_INT + ), + 'files' => array( + 'type' => stored_file_exporter::read_properties_definition(), + 'multiple' => true + ), + 'hasurlorfiles' => array( + 'type' => PARAM_BOOL + ), + 'urlshort' => array( + 'type' => PARAM_TEXT + ), + 'competencycount' => array( + 'type' => PARAM_INT + ), + 'usercompetencies' => array( + 'type' => user_evidence_competency_summary_exporter::read_properties_definition(), + 'optional' => true, + 'multiple' => true + ), + ); + } + + protected function get_other_values(renderer_base $output) { + $urlshort = ''; + $url = $this->persistent->get_url(); + if (!empty($url)) { + $murl = new moodle_url($url); + $shorturl = preg_replace('@^https?://(www\.)?@', '', $murl->out(false)); + $urlshort = shorten_text($shorturl, 30, true); + } + + $files = array(); + $storedfiles = $this->persistent->get_files(); + if (!empty($storedfiles)) { + foreach ($storedfiles as $storedfile) { + $fileexporter = new stored_file_exporter($storedfile, array('context' => $this->related['context'])); + $files[] = $fileexporter->export($output); + } + } + + $userevidencecompetencies = array(); + $frameworks = array(); + $scales = array(); + $usercompetencies = $this->persistent->get_user_competencies(); + foreach ($usercompetencies as $usercompetency) { + $competency = $usercompetency->get_competency(); + + // Get the framework. + if (!isset($frameworks[$competency->get_competencyframeworkid()])) { + $frameworks[$competency->get_competencyframeworkid()] = $competency->get_framework(); + } + $framework = $frameworks[$competency->get_competencyframeworkid()]; + + // Get the scale. + $scaleid = $competency->get_scaleid(); + if ($scaleid === null) { + $scaleid = $framework->get_scaleid(); + } + if (!isset($scales[$framework->get_scaleid()])) { + $scales[$framework->get_scaleid()] = $framework->get_scale(); + } + $scale = $scales[$framework->get_scaleid()]; + + $related = array('competency' => $competency, + 'usercompetency' => $usercompetency, + 'scale' => $scale, + 'context' => $framework->get_context()); + + $userevidencecompetencysummaryexporter = new user_evidence_competency_summary_exporter(null, $related); + + $userevidencecompetencies[] = $userevidencecompetencysummaryexporter->export($output); + } + + $values = array( + 'canmanage' => $this->persistent->can_manage(), + 'filecount' => count($files), + 'files' => $files, + 'hasurlorfiles' => !empty($files) || !empty($url), + 'urlshort' => $urlshort, + 'competencycount' => count($userevidencecompetencies), + 'usercompetencies' => $userevidencecompetencies + ); + + return $values; + } + +} diff --git a/admin/tool/lp/classes/form/competency.php b/admin/tool/lp/classes/form/competency.php new file mode 100644 index 00000000000..7073f22ff7c --- /dev/null +++ b/admin/tool/lp/classes/form/competency.php @@ -0,0 +1,165 @@ +. + +/** + * This file contains the form add/update a competency framework. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\form; +defined('MOODLE_INTERNAL') || die(); + +use stdClass; + +/** + * Competency framework form. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency extends persistent { + + /** @var core_competency\competency persistent class for form */ + protected static $persistentclass = 'core_competency\\competency'; + + /** + * Define the form - called by parent constructor + */ + public function definition() { + global $PAGE, $OUTPUT; + + $mform = $this->_form; + $framework = $this->_customdata['competencyframework']; + $parent = $this->_customdata['parent']; + $pagecontextid = $this->_customdata['pagecontextid']; + $competency = $this->get_persistent(); + + $mform->addElement('hidden', 'competencyframeworkid'); + $mform->setType('competencyframeworkid', PARAM_INT); + $mform->setConstant('competencyframeworkid', $framework->get_id()); + + $mform->addElement('header', 'generalhdr', get_string('general')); + + $mform->addElement('static', + 'frameworkdesc', + get_string('competencyframework', 'tool_lp'), + s($framework->get_shortname())); + + $mform->addElement('hidden', 'parentid', '', array('id' => 'tool_lp_parentcompetency')); + + $mform->setType('parentid', PARAM_INT); + $mform->setConstant('parentid', ($parent) ? $parent->get_id() : 0); + $parentlevel = ($parent) ? $parent->get_level() : 0; + $parentname = ($parent) ? $parent->get_shortname() : get_string('competencyframeworkroot', 'tool_lp'); + $parentlabel = ($competency->get_id()) ? + get_string('taxonomy_parent_' . $framework->get_taxonomy($parentlevel), 'tool_lp') : + get_string('parentcompetency', 'tool_lp'); + $editaction = ''; + if (!$competency->get_id()) { + $icon = $OUTPUT->pix_icon('t/editinline', get_string('parentcompetency_edit', 'tool_lp')); + $editaction = $OUTPUT->action_link('#', $icon, null, array('id' => 'id_parentcompetencybutton')); + } + + $mform->addElement('static', + 'parentdesc', + $parentlabel, + "$parentname ".$editaction); + // Set the picker competency when adding new competency. + if (!$competency->get_id()) { + // Call the parentcompetency_form init to initialize the competency picker for parent competency. + $PAGE->requires->js_call_amd('tool_lp/parentcompetency_form', 'init', array('#id_parentcompetencybutton', + '#tool_lp_parentcompetency', + '#id_parentdesc', + $framework->get_id(), + \core_competency\competency_framework::get_taxonomies_max_level(), + $pagecontextid)); + } + + $mform->addElement('text', 'shortname', + get_string('shortname', 'tool_lp')); + $mform->setType('shortname', PARAM_TEXT); + $mform->addRule('shortname', null, 'required', null, 'client'); + $mform->addElement('editor', 'description', + get_string('description', 'tool_lp'), array('rows' => 4)); + $mform->setType('description', PARAM_RAW); + $mform->addElement('text', 'idnumber', + get_string('idnumber', 'tool_lp')); + $mform->setType('idnumber', PARAM_TEXT); + $mform->addRule('idnumber', null, 'required', null, 'client'); + + $scales = array(null => get_string('inheritfromframework', 'tool_lp')) + get_scales_menu(); + $scaleid = $mform->addElement('select', 'scaleid', get_string('scale', 'tool_lp'), $scales); + $mform->setType('scaleid', PARAM_INT); + $mform->addHelpButton('scaleid', 'scale', 'tool_lp'); + + $mform->addElement('hidden', 'scaleconfiguration', '', array('id' => 'tool_lp_scaleconfiguration')); + $mform->setType('scaleconfiguration', PARAM_RAW); + + $mform->addElement('button', 'scaleconfigbutton', get_string('configurescale', 'tool_lp')); + $PAGE->requires->js_call_amd('tool_lp/scaleconfig', 'init', array('#id_scaleid', + '#tool_lp_scaleconfiguration', '#id_scaleconfigbutton')); + + if ($competency && $competency->has_user_competencies()) { + // The scale is used so we "freeze" the element. Though, the javascript code for the scale + // configuration requires this field so we only disable it. It is fine as setting the value + // as a constant will ensure that nobody can change it. And it's validated in the persistent anyway. + $scaleid->updateAttributes(array('disabled' => 'disabled')); + $mform->setConstant('scaleid', $competency->get_scaleid()); + } + + // Disable short forms. + $mform->setDisableShortforms(); + $this->add_action_buttons(true, get_string('savechanges', 'tool_lp')); + } + + /** + * Convert some fields. + * + * @param stdClass $data + * @return object + */ + protected static function convert_fields(stdClass $data) { + $data = parent::convert_fields($data); + if (empty($data->scaleid)) { + $data->scaleid = null; + $data->scaleconfiguration = null; + } + return $data; + } + + /** + * Extra validation. + * + * @param stdClass $data Data to validate. + * @param array $files Array of files. + * @param array $errors Currently reported errors. + * @return array of additional errors, or overridden errors. + */ + protected function extra_validation($data, $files, array &$errors) { + $newerrors = array(); + // Move the error from scaleconfiguration to the form element scale ID. + if (isset($errors['scaleconfiguration']) && !isset($errors['scaleid'])) { + $newerrors['scaleid'] = $errors['scaleconfiguration']; + unset($errors['scaleconfiguration']); + } + return $newerrors; + } + +} diff --git a/admin/tool/lp/classes/form/competency_framework.php b/admin/tool/lp/classes/form/competency_framework.php new file mode 100644 index 00000000000..c60ecdd774e --- /dev/null +++ b/admin/tool/lp/classes/form/competency_framework.php @@ -0,0 +1,152 @@ +. + +/** + * This file contains the form add/update a competency framework. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\form; +defined('MOODLE_INTERNAL') || die(); + +use stdClass; + +/** + * Competency framework form. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_framework extends persistent { + + protected static $persistentclass = 'core_competency\\competency_framework'; + + /** + * Define the form - called by parent constructor + */ + public function definition() { + global $PAGE; + + $mform = $this->_form; + $context = $this->_customdata['context']; + $framework = $this->get_persistent(); + + $mform->addElement('hidden', 'contextid'); + $mform->setType('contextid', PARAM_INT); + $mform->setConstant('contextid', $context->id); + + $mform->addElement('header', 'generalhdr', get_string('general')); + + $mform->addElement('text', 'shortname', + get_string('shortname', 'tool_lp')); + $mform->setType('shortname', PARAM_TEXT); + $mform->addRule('shortname', null, 'required', null, 'client'); + $mform->addElement('editor', 'description', + get_string('description', 'tool_lp'), array('rows' => 4)); + $mform->setType('description', PARAM_RAW); + $mform->addElement('text', 'idnumber', + get_string('idnumber', 'tool_lp')); + $mform->setType('idnumber', PARAM_TEXT); + $mform->addRule('idnumber', null, 'required', null, 'client'); + + $scales = get_scales_menu(); + $scaleid = $mform->addElement('select', 'scaleid', get_string('scale', 'tool_lp'), $scales); + $mform->setType('scaleid', PARAM_INT); + $mform->addHelpButton('scaleid', 'scale', 'tool_lp'); + $mform->addRule('scaleid', null, 'required', null, 'client'); + if ($framework && $framework->has_user_competencies()) { + // The scale is used so we "freeze" the element. Though, the javascript code for the scale + // configuration requires this field so we only disable it. It is fine as setting the value + // as a constant will ensure that nobody can change it. And it's validated in the persistent anyway. + $scaleid->updateAttributes(array('readonly' => 'readonly')); + $mform->setConstant('scaleid', $framework->get_scaleid()); + } + + $mform->addElement('button', 'scaleconfigbutton', get_string('configurescale', 'tool_lp')); + // Add js. + $mform->addElement('hidden', 'scaleconfiguration', '', array('id' => 'tool_lp_scaleconfiguration')); + $mform->setType('scaleconfiguration', PARAM_RAW); + $PAGE->requires->js_call_amd('tool_lp/scaleconfig', 'init', array('#id_scaleid', + '#tool_lp_scaleconfiguration', '#id_scaleconfigbutton')); + + $mform->addElement('selectyesno', 'visible', + get_string('visible', 'tool_lp')); + $mform->setDefault('visible', true); + $mform->addHelpButton('visible', 'visible', 'tool_lp'); + + $mform->addElement('static', 'context', get_string('category', 'tool_lp')); + $mform->setDefault('context', $context->get_context_name(false)); + + $mform->addElement('header', 'taxonomyhdr', get_string('taxonomies', 'tool_lp')); + $taxonomies = \core_competency\competency_framework::get_taxonomies_list(); + $taxdefaults = array(); + for ($i = 1; $i <= \core_competency\competency_framework::get_taxonomies_max_level(); $i++) { + $mform->addElement('select', "taxonomies[$i]", get_string('levela', 'tool_lp', $i), $taxonomies); + $taxdefaults[$i] = \core_competency\competency_framework::TAXONOMY_COMPETENCY; + } + // Not using taxonomies[n] here or it would takes precedence over set_data(array('taxonomies' => ...)). + $mform->setDefault('taxonomies', $taxdefaults); + + $this->add_action_buttons(true, get_string('savechanges', 'tool_lp')); + } + + /** + * Convert some fields. + * + * @param stdClass $data + * @return object + */ + protected static function convert_fields(stdClass $data) { + $data = parent::convert_fields($data); + $data->taxonomies = implode(',', $data->taxonomies); + return $data; + } + + /** + * Extra validation. + * + * @param stdClass $data Data to validate. + * @param array $files Array of files. + * @param array $errors Currently reported errors. + * @return array of additional errors, or overridden errors. + */ + protected function extra_validation($data, $files, array &$errors) { + $newerrors = array(); + // Move the error from scaleconfiguration to the form element scale ID. + if (isset($errors['scaleconfiguration']) && !isset($errors['scaleid'])) { + $newerrors['scaleid'] = $errors['scaleconfiguration']; + unset($errors['scaleconfiguration']); + } + return $newerrors; + } + + /** + * Get the default data. + * + * @return stdClass + */ + protected function get_default_data() { + $data = parent::get_default_data(); + $data->taxonomies = $this->get_persistent()->get_taxonomies(); + return $data; + } + +} + diff --git a/admin/tool/lp/classes/form/framework_autocomplete.php b/admin/tool/lp/classes/form/framework_autocomplete.php new file mode 100644 index 00000000000..61070477854 --- /dev/null +++ b/admin/tool/lp/classes/form/framework_autocomplete.php @@ -0,0 +1,121 @@ +. + + +/** + * Framework selector field. + * + * @package tool_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\form; + +use coding_exception; +use MoodleQuickForm_autocomplete; +use \core_competency\competency_framework; + +global $CFG; +require_once($CFG->libdir . '/form/autocomplete.php'); + + +/** + * Form field type for choosing a framework. + * + * @package tool_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class framework_autocomplete extends MoodleQuickForm_autocomplete { + + /** @var bool Only visible frameworks? */ + protected $onlyvisible = false; + + /** + * Constructor. + * + * @param string $elementName Element name + * @param mixed $elementLabel Label(s) for an element + * @param array $options Options to control the element's display + * Valid options are: + * - context context The context. + * - contextid int The context id. + * - multiple bool Whether or not the field accepts more than one values. + * - onlyvisible bool Whether or not only visible framework can be listed. + */ + public function __construct($elementName = null, $elementLabel = null, $options = array()) { + $contextid = null; + if (!empty($options['contextid'])) { + $contextid = $options['contextid']; + } else if (!empty($options['context'])) { + $contextid = $options['context']->id; + } + + $this->onlyvisible = !empty($options['onlyvisible']); + + $validattributes = array( + 'ajax' => 'tool_lp/frameworks_datasource', + 'data-contextid' => $contextid, + 'data-onlyvisible' => $this->onlyvisible ? '1' : '0', + ); + if (!empty($options['multiple'])) { + $validattributes['multiple'] = 'multiple'; + } + + parent::__construct($elementName, $elementLabel, array(), $validattributes); + } + + /** + * Set the value of this element. + * + * @param string|array $value The value to set. + * @return boolean + */ + public function setValue($value) { + global $DB; + $values = (array) $value; + $ids = array(); + + foreach ($values as $onevalue) { + if ((!$this->optionExists($onevalue)) && + ($onevalue !== '_qf__force_multiselect_submission')) { + array_push($ids, $onevalue); + } + } + + if (empty($ids)) { + return $this->setSelected(array()); + } + + // Logic here is simulating API. + $toselect = array(); + list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'param'); + $frameworks = competency_framework::get_records_select("id $insql", $inparams, 'shortname'); + foreach ($frameworks as $framework) { + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), + $framework->get_context())) { + continue; + } else if ($this->onlyvisible && !$framework->get_visible()) { + continue; + } + $this->addOption($framework->get_shortname() . ' ' . $framework->get_idnumber(), $framework->get_id()); + array_push($toselect, $framework->get_id()); + } + + return $this->setSelected($toselect); + } +} diff --git a/admin/tool/lp/classes/form/persistent.php b/admin/tool/lp/classes/form/persistent.php new file mode 100644 index 00000000000..9b7dc85f4ff --- /dev/null +++ b/admin/tool/lp/classes/form/persistent.php @@ -0,0 +1,275 @@ +. + +/** + * Persistent form abstract. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\form; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use moodleform; +use stdClass; + +require_once($CFG->libdir.'/formslib.php'); + +/** + * Persistent form abstract class. + * + * This provides some shortcuts to validate objects based on the persistent model. + * + * Note that all mandatory fields (non-optional) of your model should be included in the + * form definition. Mandatory fields which are not editable by the user should be + * as hidden and constant. + * + * $mform->addElement('hidden', 'userid'); + * $mform->setType('userid', PARAM_INT); + * $mform->setConstant('userid', $this->_customdata['userid']); + * + * You may exclude some fields from the validation should your form include other + * properties such as files. To do so use the $foreignfields property. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class persistent extends moodleform { + + /** @var string The fully qualified classname. */ + protected static $persistentclass = null; + + /** @var array Fields to remove when getting the final data. */ + protected static $fieldstoremove = array('submitbutton'); + + /** @var array Fields to remove from the persistent validation. */ + protected static $foreignfields = array(); + + /** @var \tool_lp\peristent Reference to the persistent. */ + private $persistent = null; + + /** + * Constructor. + * + * The 'persistent' has to be passed as custom data when 'editing'. + * + * Note that in order for your persistent to be reloaded after form submission you should + * either override the URL to include the ID to your resource, or add the ID to the form + * fields. + * + * @param mixed $action + * @param mixed $customdata + * @param string $method + * @param string $target + * @param mixed $attributes + * @param bool $editable + */ + public function __construct($action = null, $customdata = null, $method = 'post', $target = '', + $attributes = null, $editable = true) { + if (empty(static::$persistentclass)) { + throw new coding_exception('Static property $persistentclass must be set.'); + } else if (!is_subclass_of(static::$persistentclass, 'core_competency\\persistent')) { + throw new coding_exception('Static property $persistentclass is not valid.'); + } else if (!array_key_exists('persistent', $customdata)) { + throw new coding_exception('The custom data \'persistent\' key must be set, even if it is null.'); + } + + // Make a copy of the persistent passed, this ensures validation and object reference issues. + $persistendata = new stdClass(); + $persistent = isset($customdata['persistent']) ? $customdata['persistent'] : null; + if ($persistent) { + if (!($persistent instanceof static::$persistentclass)) { + throw new coding_exception('Invalid persistent'); + } + $persistendata = $persistent->to_record(); + unset($persistent); + } + + $this->persistent = new static::$persistentclass(); + $this->persistent->from_record($persistendata); + + unset($customdata['persistent']); + parent::__construct($action, $customdata, $method, $target, $attributes, $editable); + + // Load the defaults. + $this->set_data($this->get_default_data()); + } + + /** + * Convert some fields. + * + * @param stdClass $data The whole data set. + * @return stdClass The amended data set. + */ + protected static function convert_fields(stdClass $data) { + $class = static::$persistentclass; + $properties = $class::get_formatted_properties(); + + foreach ($data as $field => $value) { + // Replace formatted properties. + if (isset($properties[$field])) { + $formatfield = $properties[$field]; + $data->$formatfield = $data->{$field}['format']; + $data->$field = $data->{$field}['text']; + } + } + + return $data; + } + + /** + * Define extra validation mechanims. + * + * The data here: + * - does not include {@link self::$fieldstoremove}. + * - does include {@link self::$foreignfields}. + * - was converted to map persistent-like data, e.g. array $description to string $description + int $descriptionformat. + * + * You can modify the $errors parameter in order to remove some validation errors should you + * need to. However, the best practice is to return new or overriden errors. Only modify the + * errors passed by reference when you have no other option. + * + * Do not add any logic here, it is only intended to be used by child classes. + * + * @param stdClass $data Data to validate. + * @param array $files Array of files. + * @param array $errors Currently reported errors. + * @return array of additional errors, or overridden errors. + */ + protected function extra_validation($data, $files, array &$errors) { + return array(); + } + + /** + * Filter out the foreign fields of the persistent. + * + * This can be overridden to filter out more complex fields. + * + * @param stdClass $data The data to filter the fields out of. + * @return stdClass. + */ + protected function filter_data_for_persistent($data) { + return (object) array_diff_key((array) $data, array_flip((array) static::$foreignfields)); + } + + /** + * Get the default data. + * + * This is the data that is prepopulated in the form at it loads, we automatically + * fetch all the properties of the persistent however some needs to be converted + * to map the form structure. + * + * Extend this class if you need to add more conversion. + * + * @return stdClass + */ + protected function get_default_data() { + $data = $this->get_persistent()->to_record(); + $class = static::$persistentclass; + $properties = $class::get_formatted_properties(); + + foreach ($data as $field => $value) { + // Convert formatted properties. + if (isset($properties[$field])) { + $data->$field = array( + 'text' => $data->$field, + 'format' => $data->{$properties[$field]} + ); + unset($data->{$properties[$field]}); + } + } + + return $data; + } + + /** + * Get form data. + * + * Conveniently removes non-desired properties and add the ID property. + * + * @return object|null + */ + public function get_data() { + $data = parent::get_data(); + if (is_object($data)) { + foreach (static::$fieldstoremove as $field) { + unset($data->{$field}); + } + $data = static::convert_fields($data); + + // Ensure that the ID is set. + $data->id = $this->persistent->get_id(); + } + return $data; + } + + /** + * Return the persistent object associated with this form instance. + * + * @return tool_lp\persistent + */ + final protected function get_persistent() { + return $this->persistent; + } + + /** + * Get the submitted form data. + * + * Conveniently removes non-desired properties. + * + * @return object|null + */ + public function get_submitted_data() { + $data = parent::get_submitted_data(); + if (is_object($data)) { + foreach (static::$fieldstoremove as $field) { + unset($data->{$field}); + } + $data = static::convert_fields($data); + } + return $data; + } + + /** + * Form validation. + * + * If you need extra validation, use {@link self::extra_validation()}. + * + * @param array $data + * @param array $files + * @return array + */ + public final function validation($data, $files) { + $errors = parent::validation($data, $files); + $data = $this->get_submitted_data(); + + // Only validate compatible fields. + $persistentdata = $this->filter_data_for_persistent($data); + $persistent = $this->get_persistent(); + $persistent->from_record((object) $persistentdata); + $errors = array_merge($errors, $persistent->get_errors()); + + // Apply extra validation. + $extraerrors = $this->extra_validation($data, $files, $errors); + $errors = array_merge($errors, (array) $extraerrors); + + return $errors; + } +} diff --git a/admin/tool/lp/classes/form/plan.php b/admin/tool/lp/classes/form/plan.php new file mode 100644 index 00000000000..54604ee93e0 --- /dev/null +++ b/admin/tool/lp/classes/form/plan.php @@ -0,0 +1,86 @@ +. + +/** + * This file contains the form add/update a learning plan. + * + * @package tool_lp + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\form; +defined('MOODLE_INTERNAL') || die(); + +use core_competency\plan as planpersistent; +use required_capability_exception; + +/** + * Learning plan form. + * + * @package tool_lp + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plan extends persistent { + + protected static $persistentclass = 'core_competency\\plan'; + + /** + * Define the form - called by parent constructor + */ + public function definition() { + $mform = $this->_form; + $context = $this->_customdata['context']; + + $mform->addElement('hidden', 'userid'); + $mform->setType('userid', PARAM_INT); + $mform->setConstant('userid', $this->_customdata['userid']); + + $mform->addElement('header', 'generalhdr', get_string('general')); + + $mform->addElement('text', 'name', get_string('planname', 'tool_lp')); + $mform->setType('name', PARAM_TEXT); + $mform->addRule('name', null, 'required', null, 'client'); + $mform->addElement('editor', 'description', get_string('plandescription', 'tool_lp'), array('rows' => 4)); + $mform->setType('description', PARAM_RAW); + + $mform->addElement('date_time_selector', 'duedate', get_string('duedate', 'tool_lp'), array('optional' => true)); + $mform->addHelpButton('duedate', 'duedate', 'tool_lp'); + + // Display status selector in form. + // When the plan was already saved then the status can not be changed via this form. + $status = planpersistent::get_status_list($this->_customdata['userid']); + $plan = $this->get_persistent(); + if ($plan->get_id()) { + // The current status is not selectable (workflow status probably), we just display it. + $mform->addElement('static', 'staticstatus', get_string('status', 'tool_lp'), $plan->get_statusname()); + } else if (!empty($status) && count($status) > 1) { + // There is more than one status to select from. + $mform->addElement('select', 'status', get_string('status', 'tool_lp'), $status); + } else if (count($status) === 1) { + // There is only one status to select from. + $mform->addElement('static', 'staticstatus', get_string('status', 'tool_lp'), current($status)); + } else { + throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); + } + + // Disable short forms. + $mform->setDisableShortforms(); + $this->add_action_buttons(true, get_string('savechanges', 'tool_lp')); + } + +} diff --git a/admin/tool/lp/classes/form/template.php b/admin/tool/lp/classes/form/template.php new file mode 100644 index 00000000000..e5f69408c2f --- /dev/null +++ b/admin/tool/lp/classes/form/template.php @@ -0,0 +1,80 @@ +. + +/** + * This file contains the form add/update a competency framework. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\form; +defined('MOODLE_INTERNAL') || die(); + +/** + * Learning plan template form. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template extends persistent { + + protected static $persistentclass = 'core_competency\\template'; + + /** + * Define the form - called by parent constructor + */ + public function definition() { + $mform = $this->_form; + + $context = $this->_customdata['context']; + + $mform->addElement('hidden', 'contextid'); + $mform->setType('contextid', PARAM_INT); + $mform->setConstant('contextid', $context->id); + + $mform->addElement('header', 'generalhdr', get_string('general')); + + $mform->addElement('text', 'shortname', + get_string('shortname', 'tool_lp')); + $mform->setType('shortname', PARAM_TEXT); + $mform->addRule('shortname', null, 'required', null, 'client'); + $mform->addElement('editor', 'description', + get_string('description', 'tool_lp'), array('rows' => 4)); + $mform->setType('description', PARAM_RAW); + $mform->addElement('selectyesno', 'visible', + get_string('visible', 'tool_lp')); + $mform->addElement('date_time_selector', + 'duedate', + get_string('duedate', 'tool_lp'), + array('optional' => true)); + $mform->addHelpButton('duedate', 'duedate', 'tool_lp'); + + $mform->setDefault('visible', true); + $mform->addHelpButton('visible', 'visible', 'tool_lp'); + + $mform->addElement('static', 'context', get_string('category', 'tool_lp')); + $mform->setDefault('context', $context->get_context_name(false)); + // Disable short forms. + $mform->setDisableShortforms(); + + $this->add_action_buttons(true, get_string('savechanges', 'tool_lp')); + + } + +} diff --git a/admin/tool/lp/classes/form/template_cohorts.php b/admin/tool/lp/classes/form/template_cohorts.php new file mode 100644 index 00000000000..438c9335d35 --- /dev/null +++ b/admin/tool/lp/classes/form/template_cohorts.php @@ -0,0 +1,54 @@ +. + +/** + * Template cohorts form. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\form; +defined('MOODLE_INTERNAL') || die(); + +use moodleform; + +require_once($CFG->libdir . '/formslib.php'); + +/** + * Template cohorts form class. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_cohorts extends moodleform { + + public function definition() { + $mform = $this->_form; + + $options = array( + 'ajax' => 'tool_lp/form-cohort-selector', + 'multiple' => true, + 'data-contextid' => $this->_customdata['pagecontextid'], + 'data-includes' => 'parents' + ); + $mform->addElement('autocomplete', 'cohorts', get_string('selectcohortstosync', 'tool_lp'), array(), $options); + $mform->addElement('submit', 'submit', get_string('addcohorts', 'tool_lp')); + } + +} diff --git a/admin/tool/lp/classes/form/template_plans.php b/admin/tool/lp/classes/form/template_plans.php new file mode 100644 index 00000000000..862a3836f97 --- /dev/null +++ b/admin/tool/lp/classes/form/template_plans.php @@ -0,0 +1,53 @@ +. + +/** + * Template plans form. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\form; +defined('MOODLE_INTERNAL') || die(); + +use moodleform; + +require_once($CFG->libdir . '/formslib.php'); + +/** + * Template plans form class. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_plans extends moodleform { + + public function definition() { + $mform = $this->_form; + + $options = array( + 'ajax' => 'tool_lp/form-user-selector', + 'multiple' => true, + 'data-capability' => 'moodle/competency:planmanage' + ); + $mform->addElement('autocomplete', 'users', get_string('selectuserstocreateplansfor', 'tool_lp'), array(), $options); + $mform->addElement('submit', 'submit', get_string('createplans', 'tool_lp')); + } + +} diff --git a/admin/tool/lp/classes/form/user_evidence.php b/admin/tool/lp/classes/form/user_evidence.php new file mode 100644 index 00000000000..7741444a45c --- /dev/null +++ b/admin/tool/lp/classes/form/user_evidence.php @@ -0,0 +1,68 @@ +. + +/** + * User evidence form. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\form; +defined('MOODLE_INTERNAL') || die(); + +/** + * User evidence form class. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_evidence extends persistent { + + protected static $persistentclass = 'core_competency\\user_evidence'; + + protected static $foreignfields = array('files'); + + public function definition() { + $mform = $this->_form; + + $mform->addElement('hidden', 'userid'); + $mform->setType('userid', PARAM_INT); + $mform->setConstant('userid', $this->_customdata['userid']); + + $mform->addElement('header', 'generalhdr', get_string('general')); + + $mform->addElement('text', 'name', get_string('userevidencename', 'tool_lp'), 'maxlength="100"'); + $mform->setType('name', PARAM_TEXT); + $mform->addRule('name', null, 'required', null, 'client'); + + $mform->addElement('editor', 'description', get_string('userevidencedescription', 'tool_lp'), array('rows' => 10)); + $mform->setType('description', PARAM_RAW); + + $mform->addElement('url', 'url', get_string('userevidenceurl', 'tool_lp'), array(), array('usefilepicker' => false)); + $mform->setType('url', PARAM_RAW_TRIMMED); // Can not use PARAM_URL, it silently converts bad URLs to ''. + + $mform->addElement('filemanager', 'files', get_string('userevidencefiles', 'tool_lp'), array(), + $this->_customdata['fileareaoptions']); + // Disable short forms. + $mform->setDisableShortforms(); + + $this->add_action_buttons(); + } + +} diff --git a/admin/tool/lp/classes/output/competency_summary.php b/admin/tool/lp/classes/output/competency_summary.php new file mode 100644 index 00000000000..05480029cde --- /dev/null +++ b/admin/tool/lp/classes/output/competency_summary.php @@ -0,0 +1,94 @@ +. + +/** + * Class containing data for competency_page page + * + * @package tool_lp + * @copyright 2015 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use renderer_base; +use stdClass; +use core_competency\api; +use tool_lp\external\competency_summary_exporter; + +/** + * Class containing data for competency summary + * + * @copyright 2015 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_summary implements renderable, templatable { + + /** @var \core_competency\competency_framework $framework This competency framework. */ + protected $framework = null; + + /** @var \core_competency\competency $competency. */ + protected $competency = null; + + /** @var \core_competency\competency[] $relatedcompetencies List of competencies. */ + protected $relatedcompetencies = array(); + + /** @var course[] $courses List of courses. */ + protected $courses = array(); + + /** + * Construct this renderable. + * + * @param \core_competency\competency $competency Competency persistent. + * @param \core_competency\competency_framework $framework framework persistent. + * @param boolean $includerelated Include or not related competencies. + * @param boolean $includecourses Include or not competency courses. + */ + public function __construct($competency, $framework, $includerelated, $includecourses) { + $this->competency = $competency; + $this->framework = $framework; + if ($includerelated) { + $this->relatedcompetencies = api::list_related_competencies($competency->get_id()); + } + + if ($includecourses) { + $this->courses = api::list_courses_using_competency($competency->get_id()); + } + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output Renderer base. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $related = array( + 'context' => $this->framework->get_context(), + 'framework' => $this->framework, + 'linkedcourses' => $this->courses, + 'relatedcompetencies' => $this->relatedcompetencies, + 'competency' => $this->competency + ); + + $exporter = new competency_summary_exporter($this->competency, $related); + $data = $exporter->export($output); + + return $data; + } +} diff --git a/admin/tool/lp/classes/output/course_competencies_page.php b/admin/tool/lp/classes/output/course_competencies_page.php new file mode 100644 index 00000000000..45c5cbf4eea --- /dev/null +++ b/admin/tool/lp/classes/output/course_competencies_page.php @@ -0,0 +1,199 @@ +. + +/** + * Class containing data for course competencies page + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use renderer_base; +use stdClass; +use moodle_url; +use context_system; +use context_course; +use core_competency\api; +use tool_lp\course_competency_statistics; +use core_competency\competency; +use core_competency\course_competency; +use core_competency\external\competency_exporter; +use core_competency\external\course_competency_exporter; +use core_competency\external\course_competency_settings_exporter; +use core_competency\external\user_competency_course_exporter; +use core_competency\external\user_competency_exporter; +use tool_lp\external\competency_path_exporter; +use tool_lp\external\course_competency_statistics_exporter; +use tool_lp\external\course_module_summary_exporter; + +/** + * Class containing data for course competencies page + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_competencies_page implements renderable, templatable { + + /** @var int $courseid Course id for this page. */ + protected $courseid = null; + + /** @var context $context The context for this page. */ + protected $context = null; + + /** @var \core_competency\course_competency[] $competencies List of competencies. */ + protected $coursecompetencylist = array(); + + /** @var bool $canmanagecompetencyframeworks Can the current user manage competency frameworks. */ + protected $canmanagecompetencyframeworks = false; + + /** @var bool $canmanagecoursecompetencies Can the current user manage course competency frameworks.. */ + protected $canmanagecoursecompetencies = false; + + /** @var string $manageurl manage url. */ + protected $manageurl = null; + + /** + * Construct this renderable. + * @param int $courseid The course record for this page. + */ + public function __construct($courseid) { + $this->context = context_course::instance($courseid); + $this->courseid = $courseid; + $this->coursecompetencylist = api::list_course_competencies($courseid); + $this->canmanagecoursecompetencies = has_capability('moodle/competency:coursecompetencymanage', $this->context); + $this->canconfigurecoursecompetencies = has_capability('moodle/competency:coursecompetencyconfigure', $this->context); + $this->coursecompetencysettings = api::read_course_competency_settings($courseid); + $this->coursecompetencystatistics = new course_competency_statistics($courseid); + + // Check the lowest level in which the user can manage the competencies. + $this->manageurl = null; + $this->canmanagecompetencyframeworks = false; + $contexts = array_reverse($this->context->get_parent_contexts(true)); + foreach ($contexts as $context) { + $canmanage = has_capability('moodle/competency:competencymanage', $context); + if ($canmanage) { + $this->manageurl = new moodle_url('/admin/tool/lp/competencyframeworks.php', + array('pagecontextid' => $context->id)); + $this->canmanagecompetencyframeworks = true; + break; + } + } + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output Renderer base. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + global $USER; + + $data = new stdClass(); + $data->courseid = $this->courseid; + $data->pagecontextid = $this->context->id; + $data->competencies = array(); + $contextcache = array(); + + $gradable = is_enrolled($this->context, $USER, 'moodle/competency:coursecompetencygradable'); + if ($gradable) { + $usercompetencycourses = api::list_user_competencies_in_course($this->courseid, $USER->id); + $data->gradableuserid = $USER->id; + } + + $ruleoutcomelist = course_competency::get_ruleoutcome_list(); + $ruleoutcomeoptions = array(); + foreach ($ruleoutcomelist as $value => $text) { + $ruleoutcomeoptions[$value] = array('value' => $value, 'text' => (string) $text, 'selected' => false); + } + + foreach ($this->coursecompetencylist as $coursecompetencyelement) { + $coursecompetency = $coursecompetencyelement['coursecompetency']; + $competency = $coursecompetencyelement['competency']; + if (!isset($contextcache[$competency->get_competencyframeworkid()])) { + $contextcache[$competency->get_competencyframeworkid()] = $competency->get_context(); + } + $context = $contextcache[$competency->get_competencyframeworkid()]; + + $compexporter = new competency_exporter($competency, array('context' => $context)); + $ccexporter = new course_competency_exporter($coursecompetency, array('context' => $context)); + + $ccoutcomeoptions = (array) (object) $ruleoutcomeoptions; + $ccoutcomeoptions[$coursecompetency->get_ruleoutcome()]['selected'] = true; + + $coursemodules = api::list_course_modules_using_competency($competency->get_id(), $this->courseid); + + $fastmodinfo = get_fast_modinfo($this->courseid); + $exportedmodules = array(); + foreach ($coursemodules as $cmid) { + $cminfo = $fastmodinfo->cms[$cmid]; + $cmexporter = new course_module_summary_exporter(null, array('cm' => $cminfo)); + $exportedmodules[] = $cmexporter->export($output); + } + // Competency path. + $pathexporter = new competency_path_exporter([ + 'ancestors' => $competency->get_ancestors(), + 'framework' => $competency->get_framework(), + 'context' => $context + ]); + + $onerow = array( + 'competency' => $compexporter->export($output), + 'coursecompetency' => $ccexporter->export($output), + 'ruleoutcomeoptions' => $ccoutcomeoptions, + 'coursemodules' => $exportedmodules, + 'comppath' => $pathexporter->export($output) + ); + if ($gradable) { + $foundusercompetencycourse = false; + foreach ($usercompetencycourses as $usercompetencycourse) { + if ($usercompetencycourse->get_competencyid() == $competency->get_id()) { + $foundusercompetencycourse = $usercompetencycourse; + } + } + if ($foundusercompetencycourse) { + $related = array( + 'scale' => $competency->get_scale() + ); + $exporter = new user_competency_course_exporter($foundusercompetencycourse, $related); + $onerow['usercompetencycourse'] = $exporter->export($output); + } + } + array_push($data->competencies, $onerow); + } + + $data->canmanagecompetencyframeworks = $this->canmanagecompetencyframeworks; + $data->canmanagecoursecompetencies = $this->canmanagecoursecompetencies; + $data->canconfigurecoursecompetencies = $this->canconfigurecoursecompetencies; + $exporter = new course_competency_settings_exporter($this->coursecompetencysettings); + $data->settings = $exporter->export($output); + $related = array('context' => $this->context); + $exporter = new course_competency_statistics_exporter($this->coursecompetencystatistics, $related); + $data->statistics = $exporter->export($output); + $data->manageurl = null; + if ($this->canmanagecompetencyframeworks) { + $data->manageurl = $this->manageurl->out(true); + } + + return $data; + } + +} diff --git a/admin/tool/lp/classes/output/manage_competencies_page.php b/admin/tool/lp/classes/output/manage_competencies_page.php new file mode 100644 index 00000000000..c7785341f3c --- /dev/null +++ b/admin/tool/lp/classes/output/manage_competencies_page.php @@ -0,0 +1,124 @@ +. + +/** + * Class containing data for managecompetencyframeworks page + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use renderer_base; +use single_button; +use stdClass; +use moodle_url; +use context_system; +use core_competency\api; +use core_competency\competency; +use core_competency\competency_framework; +use core_competency\external\competency_framework_exporter; + +/** + * Class containing data for managecompetencies page + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manage_competencies_page implements renderable, templatable { + + /** @var \core_competency\competency_framework $framework This competency framework. */ + protected $framework = null; + + /** @var \core_competency\competency[] $competencies List of competencies. */ + protected $competencies = array(); + + /** @var string $search Text to search for. */ + protected $search = ''; + + /** @var bool $canmanage Result of permissions checks. */ + protected $canmanage = false; + + /** @var moodle_url $pluginurlbase Base url to use constructing links. */ + protected $pluginbaseurl = null; + + /** @var context $pagecontext The page context. */ + protected $pagecontext = null; + + /** + * Construct this renderable. + * + * @param \core_competency\competency_framework $framework Competency framework. + * @param string $search Search string. + * @param context $pagecontext The page context. + */ + public function __construct($framework, $search, $pagecontext) { + $this->framework = $framework; + $this->pagecontext = $pagecontext; + $this->search = $search; + $addpage = new single_button( + new moodle_url('/admin/tool/lp/editcompetencyframework.php'), + get_string('addnewcompetency', 'tool_lp') + ); + $this->navigation[] = $addpage; + + $this->canmanage = has_capability('moodle/competency:competencymanage', $framework->get_context()); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output Renderer base. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $exporter = new competency_framework_exporter($this->framework); + $data->framework = $exporter->export($output); + $data->canmanage = $this->canmanage; + $data->search = $this->search; + $data->pagecontextid = $this->pagecontext->id; + $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true); + + $rulesmodules = array(); + $rules = competency::get_available_rules(); + foreach ($rules as $type => $rulename) { + + $amd = null; + if ($type == 'core_competency\\competency_rule_all') { + $amd = 'tool_lp/competency_rule_all'; + } else if ($type == 'core_competency\\competency_rule_points') { + $amd = 'tool_lp/competency_rule_points'; + } else { + // We do not know how to display that rule. + continue; + } + + $rulesmodules[] = [ + 'name' => (string) $rulename, + 'type' => $type, + 'amd' => $amd, + ]; + } + $data->rulesmodules = json_encode(array_values($rulesmodules)); + + return $data; + } +} diff --git a/admin/tool/lp/classes/output/manage_competency_frameworks_page.php b/admin/tool/lp/classes/output/manage_competency_frameworks_page.php new file mode 100644 index 00000000000..3864cbf10d7 --- /dev/null +++ b/admin/tool/lp/classes/output/manage_competency_frameworks_page.php @@ -0,0 +1,104 @@ +. + +/** + * Class containing data for managecompetencyframeworks page + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use renderer_base; +use single_button; +use stdClass; +use moodle_url; +use context; +use context_system; +use core_competency\api; +use core_competency\competency_framework; +use core_competency\external\competency_framework_exporter; + +/** + * Class containing data for managecompetencyframeworks page + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manage_competency_frameworks_page implements renderable, templatable { + + /** @var context The context in which everything is happening. */ + protected $pagecontext; + + /** @var array $navigation List of links to display on the page. Each link contains a url and a title. */ + protected $navigation = array(); + + /** @var array $competencyframeworks List of competency frameworks. */ + protected $competencyframeworks = array(); + + /** @var bool $canmanage Result of permissions checks. */ + protected $canmanage = false; + + /** @var moodle_url $pluginurlbase Base url to use constructing links. */ + protected $pluginbaseurl = null; + + /** + * Construct this renderable. + * + * @param context $pagecontext The page context + */ + public function __construct(context $pagecontext) { + $this->pagecontext = $pagecontext; + + if (competency_framework::can_manage_context($this->pagecontext)) { + $addpage = new single_button( + new moodle_url('/admin/tool/lp/editcompetencyframework.php', array('pagecontextid' => $this->pagecontext->id)), + get_string('addnewcompetencyframework', 'tool_lp'), + 'get' + ); + $this->navigation[] = $addpage; + } + + $this->competencyframeworks = api::list_frameworks('shortname', 'ASC', 0, 0, $this->pagecontext); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output Renderer base. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $data->competencyframeworks = array(); + $data->pagecontextid = $this->pagecontext->id; + foreach ($this->competencyframeworks as $framework) { + $exporter = new competency_framework_exporter($framework); + $data->competencyframeworks[] = $exporter->export($output); + } + $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true); + $data->navigation = array(); + foreach ($this->navigation as $button) { + $data->navigation[] = $output->render($button); + } + + return $data; + } +} diff --git a/admin/tool/lp/classes/output/manage_templates_page.php b/admin/tool/lp/classes/output/manage_templates_page.php new file mode 100644 index 00000000000..36130ffadb5 --- /dev/null +++ b/admin/tool/lp/classes/output/manage_templates_page.php @@ -0,0 +1,98 @@ +. + +/** + * Class containing data for managelearningplans page + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use context; +use renderable; +use templatable; +use renderer_base; +use single_button; +use stdClass; +use moodle_url; +use context_system; +use core_competency\api; +use core_competency\template; +use core_competency\external\template_exporter; + +/** + * Class containing data for managecompetencyframeworks page + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manage_templates_page implements renderable, templatable { + + /** @var context The context in which everything is happening. */ + protected $pagecontext; + + /** @var array $navigation List of links to display on the page. Each link contains a url and a title. */ + protected $navigation = array(); + + /** @var array $templates List of learning plan templates. */ + protected $templates = array(); + + /** + * Construct this renderable. + * @param context $pagecontext + */ + public function __construct(context $pagecontext) { + $this->pagecontext = $pagecontext; + + if (template::can_manage_context($this->pagecontext)) { + $addpage = new single_button( + new moodle_url('/admin/tool/lp/edittemplate.php', array('pagecontextid' => $this->pagecontext->id)), + get_string('addnewtemplate', 'tool_lp'), + 'get' + ); + $this->navigation[] = $addpage; + } + + $this->templates = api::list_templates('shortname', 'ASC', 0, 0, $this->pagecontext); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output Renderer base. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $data->pagecontextid = $this->pagecontext->id; + $data->templates = array(); + foreach ($this->templates as $template) { + $exporter = new template_exporter($template); + $data->templates[] = $exporter->export($output); + } + $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true); + $data->navigation = array(); + foreach ($this->navigation as $button) { + $data->navigation[] = $output->render($button); + } + $data->canmanage = template::can_manage_context($this->pagecontext); + + return $data; + } +} diff --git a/admin/tool/lp/classes/output/plan_page.php b/admin/tool/lp/classes/output/plan_page.php new file mode 100644 index 00000000000..e156a975b31 --- /dev/null +++ b/admin/tool/lp/classes/output/plan_page.php @@ -0,0 +1,137 @@ +. + +/** + * Plan page output. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use stdClass; +use moodle_url; +use core_competency\api; +use core_competency\plan; +use core_competency\external\competency_exporter; +use core_competency\external\plan_exporter; +use tool_lp\external\competency_path_exporter; + +/** + * Plan page class. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plan_page implements renderable, templatable { + + /** @var plan */ + protected $plan; + + /** + * Construct. + * + * @param plan $plan + */ + public function __construct($plan) { + $this->plan = $plan; + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(\renderer_base $output) { + $frameworks = array(); + $scales = array(); + + $planexporter = new plan_exporter($this->plan, array('template' => $this->plan->get_template())); + + $data = new stdClass(); + $data->plan = $planexporter->export($output); + $data->competencies = array(); + $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(false); + $data->contextid = $this->plan->get_context()->id; + + if ($data->plan->iscompleted) { + $ucproperty = 'usercompetencyplan'; + $ucexporter = 'core_competency\\external\\user_competency_plan_exporter'; + } else { + $ucproperty = 'usercompetency'; + $ucexporter = 'core_competency\\external\\user_competency_exporter'; + } + + $pclist = api::list_plan_competencies($this->plan); + $proficientcount = 0; + foreach ($pclist as $pc) { + $comp = $pc->competency; + $usercomp = $pc->$ucproperty; + + // Get the framework. + if (!isset($frameworks[$comp->get_competencyframeworkid()])) { + $frameworks[$comp->get_competencyframeworkid()] = $comp->get_framework(); + } + $framework = $frameworks[$comp->get_competencyframeworkid()]; + + // Get the scale. + $scaleid = $comp->get_scaleid(); + if ($scaleid === null) { + $scaleid = $framework->get_scaleid(); + } + if (!isset($scales[$framework->get_scaleid()])) { + $scales[$framework->get_scaleid()] = $framework->get_scale(); + } + $scale = $scales[$framework->get_scaleid()]; + + // Prepare the data. + $record = new stdClass(); + $exporter = new competency_exporter($comp, array('context' => $framework->get_context())); + $record->competency = $exporter->export($output); + + // Competency path. + $exporter = new competency_path_exporter([ + 'ancestors' => $comp->get_ancestors(), + 'framework' => $framework, + 'context' => $framework->get_context() + ]); + $record->comppath = $exporter->export($output); + + $exporter = new $ucexporter($usercomp, array('scale' => $scale)); + $record->$ucproperty = $exporter->export($output); + + $data->competencies[] = $record; + if ($usercomp->get_proficiency()) { + $proficientcount++; + } + } + $data->competencycount = count($data->competencies); + $data->proficientcompetencycount = $proficientcount; + if ($data->competencycount) { + $data->proficientcompetencypercentage = ((float) $proficientcount / (float) $data->competencycount) * 100.0; + } else { + $data->proficientcompetencypercentage = 0.0; + } + $data->proficientcompetencypercentageformatted = format_float($data->proficientcompetencypercentage); + return $data; + } +} diff --git a/admin/tool/lp/classes/output/plans_page.php b/admin/tool/lp/classes/output/plans_page.php new file mode 100644 index 00000000000..c0ac23f43a0 --- /dev/null +++ b/admin/tool/lp/classes/output/plans_page.php @@ -0,0 +1,108 @@ +. + +/** + * Class containing data for a user learning plans list page. + * + * @package tool_lp + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use renderer_base; +use stdClass; +use single_button; +use moodle_url; +use core_competency\api; +use core_competency\external\plan_exporter; +use core_competency\plan; +use core_competency\user_evidence; +use context_user; + +/** + * Class containing data for a user learning plans list page. + * + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plans_page implements renderable, templatable { + + /** @var array $navigation List of links to display on the page. Each link contains a url and a title. */ + protected $navigation = array(); + + /** @var array|\core_competency\plan[] $plans List of plans. */ + protected $plans = array(); + + /** @var context_user|null $context context. */ + protected $context = null; + + /** @var int|null $userid Userid. */ + protected $userid = null; + + /** + * Construct this renderable. + * + * @param int $userid + */ + public function __construct($userid) { + $this->userid = $userid; + $this->plans = api::list_user_plans($userid); + $this->context = context_user::instance($userid); + + if (plan::can_manage_user($userid) || plan::can_manage_user_draft($userid)) { + $addplan = new single_button( + new moodle_url('/admin/tool/lp/editplan.php', array('userid' => $userid)), + get_string('addnewplan', 'tool_lp'), 'get' + ); + $this->navigation[] = $addplan; + } + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $data->userid = $this->userid; + $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true); + $data->canreaduserevidence = user_evidence::can_read_user($this->userid); + $data->canmanageuserplans = plan::can_manage_user($this->userid); + + // Attach standard objects as mustache can not parse \core_competency\plan objects. + $data->plans = array(); + if ($this->plans) { + foreach ($this->plans as $plan) { + $exporter = new plan_exporter($plan, array('template' => $plan->get_template())); + $record = $exporter->export($output); + $data->plans[] = $record; + } + } + + $data->navigation = array(); + foreach ($this->navigation as $button) { + $data->navigation[] = $output->render($button); + } + + return $data; + } +} diff --git a/admin/tool/lp/classes/output/related_competencies.php b/admin/tool/lp/classes/output/related_competencies.php new file mode 100644 index 00000000000..cd753fc9b85 --- /dev/null +++ b/admin/tool/lp/classes/output/related_competencies.php @@ -0,0 +1,80 @@ +. + +/** + * Class containing data for a competency. + * + * @package tool_lp + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use renderer_base; +use stdClass; +use moodle_url; +use core_competency\api; +use core_competency\external\competency_exporter; + +/** + * Class containing data for related competencies. + * + * @package tool_lp + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class related_competencies implements renderable, templatable { + + /** @var array Related competencies. */ + protected $relatedcompetencies = null; + + /** + * Construct this renderable. + * + * @param int $competencyid + */ + public function __construct($competencyid) { + $this->competency = api::read_competency($competencyid); + $this->context = $this->competency->get_context(); + $this->relatedcompetencies = api::list_related_competencies($competencyid); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $data->relatedcompetencies = array(); + if ($this->relatedcompetencies) { + foreach ($this->relatedcompetencies as $competency) { + $exporter = new competency_exporter($competency, array('context' => $this->context)); + $record = $exporter->export($output); + $data->relatedcompetencies[] = $record; + } + } + + // We checked the user permissions in the constructor. + $data->showdeleterelatedaction = true; + + return $data; + } +} diff --git a/admin/tool/lp/classes/output/renderer.php b/admin/tool/lp/classes/output/renderer.php new file mode 100644 index 00000000000..f2c68c0aeaf --- /dev/null +++ b/admin/tool/lp/classes/output/renderer.php @@ -0,0 +1,255 @@ +. + +/** + * Renderer class for learning plans + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\output; + +defined('MOODLE_INTERNAL') || die(); + +use plugin_renderer_base; +use renderable; + +/** + * Renderer class for learning plans + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Defer to template. + * + * @param manage_competency_frameworks_page $page + * + * @return string html for the page + */ + public function render_manage_competency_frameworks_page(manage_competency_frameworks_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/manage_competency_frameworks_page', $data); + } + + /** + * Defer to template. + * + * @param manage_competencies_page $page + * + * @return string html for the page + */ + public function render_manage_competencies_page(manage_competencies_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/manage_competencies_page', $data); + } + + /** + * Defer to template. + * + * @param course_competencies_page $page + * + * @return string html for the page + */ + public function render_course_competencies_page(course_competencies_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/course_competencies_page', $data); + } + + /** + * Defer to template. + * + * @param template_competencies_page $page + * + * @return string html for the page + */ + public function render_template_competencies_page(template_competencies_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/template_competencies_page', $data); + } + + /** + * Defer to template. + * + * @param manage_templates_page $page + * + * @return string html for the page + */ + public function render_manage_templates_page(manage_templates_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/manage_templates_page', $data); + } + + /** + * Defer to template. + * + * @param plan_page $page + * @return bool|string + */ + public function render_plan_page(plan_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/plan_page', $data); + } + + /** + * Defer to template. + * + * @param plans_page $page + * @return bool|string + */ + public function render_plans_page(plans_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/plans_page', $data); + } + + /** + * Defer to template. + * + * @param renderable $page + * @return string + */ + public function render_related_competencies_section(renderable $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/related_competencies', $data); + } + + /** + * Defer to template. + * + * @param user_competency_summary_in_course $page + * @return string + */ + public function render_user_competency_summary_in_course(user_competency_summary_in_course $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/user_competency_summary_in_course', $data); + } + + /** + * Defer to template. + * + * @param user_competency_summary_in_plan $page + * @return string + */ + public function render_user_competency_summary_in_plan(user_competency_summary_in_plan $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/user_competency_summary_in_plan', $data); + } + + /** + * Render the template plans page. + * + * @param renderable $page + * @return string + */ + public function render_template_plans_page(renderable $page) { + return $page->table->out(50, true); + } + + /** + * Render the template cohorts page. + * + * @param renderable $page + * @return string + */ + public function render_template_cohorts_page(renderable $page) { + return $page->table->out(50, true); + } + + /** + * Defer to template. + * + * @param user_evidence_page $page + * @return string + */ + public function render_user_evidence_page(user_evidence_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/user_evidence_page', $data); + } + + /** + * Defer to template. + * + * @param user_evidence_list_page $page + * @return string + */ + public function render_user_evidence_list_page(user_evidence_list_page $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/user_evidence_list_page', $data); + } + + /** + * Defer to template. + * + * @param user_competency_course_navigation $nav + * @return string + */ + public function render_user_competency_course_navigation(user_competency_course_navigation $nav) { + $data = $nav->export_for_template($this); + return parent::render_from_template('tool_lp/user_competency_course_navigation', $data); + } + + /** + * Defer to template. + * + * @param user_competency_summary $page + * @return string + */ + public function render_user_competency_summary(user_competency_summary $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lp/user_competency_summary', $data); + } + + /** + * 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); + } +} diff --git a/admin/tool/lp/classes/output/template_cohorts_page.php b/admin/tool/lp/classes/output/template_cohorts_page.php new file mode 100644 index 00000000000..0964778ffbd --- /dev/null +++ b/admin/tool/lp/classes/output/template_cohorts_page.php @@ -0,0 +1,49 @@ +. + +/** + * Template cohorts page renderable. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +/** + * Template cohorts renderable. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_cohorts_page implements \renderable { + + /** + * Constructor. + * @param \core_competency\template $template + * @param \moodle_url $url + */ + public function __construct(\core_competency\template $template, \moodle_url $url) { + $this->template = $template; + $this->url = $url; + $this->table = new template_cohorts_table('tplcohorts', $template); + $this->table->define_baseurl($url); + } + +} diff --git a/admin/tool/lp/classes/output/template_cohorts_table.php b/admin/tool/lp/classes/output/template_cohorts_table.php new file mode 100644 index 00000000000..bdd68e3208d --- /dev/null +++ b/admin/tool/lp/classes/output/template_cohorts_table.php @@ -0,0 +1,185 @@ +. + +/** + * Template cohorts table. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/tablelib.php'); + +use html_writer; +use moodle_url; +use table_sql; +use core_competency\template; + +/** + * Template cohorts table class. + * + * Note that presently this table may display some rows although the current user + * does not have permission to view those cohorts. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_cohorts_table extends table_sql { + + /** @var context The context. */ + protected $context; + + /** @var \core_competency\template The template. */ + protected $template; + + /** + * Sets up the table. + * + * @param string $uniqueid Unique id of table. + * @param \core_competency\template $template The template. + */ + public function __construct($uniqueid, \core_competency\template $template) { + parent::__construct($uniqueid); + + // This object should not be used without the right permissions. + if (!$template->can_read()) { + throw new \required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + // Set protected properties. + $this->template = $template; + $this->context = $this->template->get_context(); + + // Define columns in the table. + $this->define_table_columns(); + + // Define configs. + $this->define_table_configs(); + } + + /** + * Column actions. + * + * @param object $row + * @return string + */ + protected function col_actions($row) { + global $OUTPUT; + + $action = new \confirm_action(get_string('areyousure')); + $url = new moodle_url($this->baseurl); + $url->params(array('removecohort' => $row->id, 'sesskey' => sesskey())); + $actionlink = $OUTPUT->action_link($url, '', $action, null, new \pix_icon('t/delete', + get_string('stopsyncingcohort', 'tool_lp'))); + + return $actionlink; + + } + + /** + * Setup the headers for the table. + */ + protected function define_table_columns() { + // Define headers and columns. + $cols = array( + 'name' => get_string('name', 'cohort'), + 'idnumber' => get_string('idnumber', 'cohort'), + ); + + if ($this->template->can_manage()) { + $cols['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, 'name', 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 = 'c.id, c.name, c.idnumber'; + + if ($count) { + $select = "COUNT(1)"; + } else { + $select = "$fields"; + } + + $sql = "SELECT $select + FROM {" . \core_competency\template_cohort::TABLE . "} tc + JOIN {cohort} c ON c.id = tc.cohortid + WHERE tc.templateid = :templateid"; + $params = array('templateid' => $this->template->get_id()); + + // 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); + } + } +} diff --git a/admin/tool/lp/classes/output/template_competencies_page.php b/admin/tool/lp/classes/output/template_competencies_page.php new file mode 100644 index 00000000000..e51b13d1dbb --- /dev/null +++ b/admin/tool/lp/classes/output/template_competencies_page.php @@ -0,0 +1,136 @@ +. + +/** + * Class containing data for learning plan template competencies page + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use renderer_base; +use stdClass; +use context; +use context_system; +use moodle_url; +use core_competency\external\template_exporter; +use core_competency\template; +use core_competency\api; +use tool_lp\external\competency_summary_exporter; +use tool_lp\external\template_statistics_exporter; +use tool_lp\template_statistics; + +/** + * Class containing data for learning plan template competencies page + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_competencies_page implements renderable, templatable { + + /** @var template $template Template for this page. */ + protected $template = null; + + /** @var \core_competency\competency[] $competencies List of competencies. */ + protected $competencies = array(); + + /** @var bool $canmanagecompetencyframeworks Can the current user manage competency frameworks. */ + protected $canmanagecompetencyframeworks = false; + + /** @var bool $canmanagecoursecompetencies Can the current user manage course competency frameworks.. */ + protected $canmanagecoursecompetencies = false; + + /** @var string $manageurl manage url. */ + protected $manageurl = null; + + /** @var context $pagecontext The page context. */ + protected $pagecontext = null; + + /** @var template_statistics $templatestatistics The generated summary statistics for this template. */ + protected $templatestatistics = null; + + /** + * Construct this renderable. + * + * @param template $template The learning plan template. + * @param context $pagecontext The page context. + */ + public function __construct(template $template, context $pagecontext) { + $this->pagecontext = $pagecontext; + $this->template = $template; + $this->templatestatistics = new template_statistics($template->get_id()); + $this->competencies = api::list_competencies_in_template($template); + $this->canmanagecompetencyframeworks = has_capability('moodle/competency:competencymanage', $this->pagecontext); + $this->canmanagetemplatecompetencies = has_capability('moodle/competency:templatemanage', $this->pagecontext); + $this->manageurl = new moodle_url('/admin/tool/lp/competencyframeworks.php', + array('pagecontextid' => $this->pagecontext->id)); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $data->template = (new template_exporter($this->template))->export($output); + $data->pagecontextid = $this->pagecontext->id; + $data->competencies = array(); + $contextcache = array(); + $frameworkcache = array(); + foreach ($this->competencies as $competency) { + if (!isset($contextcache[$competency->get_competencyframeworkid()])) { + $contextcache[$competency->get_competencyframeworkid()] = $competency->get_context(); + } + $context = $contextcache[$competency->get_competencyframeworkid()]; + if (!isset($frameworkcache[$competency->get_competencyframeworkid()])) { + $frameworkcache[$competency->get_competencyframeworkid()] = $competency->get_framework(); + } + $framework = $frameworkcache[$competency->get_competencyframeworkid()]; + + $courses = api::list_courses_using_competency($competency->get_id()); + $relatedcompetencies = api::list_related_competencies($competency->get_id()); + + $related = array( + 'competency' => $competency, + 'linkedcourses' => $courses, + 'context' => $context, + 'relatedcompetencies' => $relatedcompetencies, + 'framework' => $framework + ); + $exporter = new competency_summary_exporter(null, $related); + $record = $exporter->export($output); + + array_push($data->competencies, $record); + } + + $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(false); + $data->canmanagecompetencyframeworks = $this->canmanagecompetencyframeworks; + $data->canmanagetemplatecompetencies = $this->canmanagetemplatecompetencies; + $data->manageurl = $this->manageurl->out(true); + $exporter = new template_statistics_exporter($this->templatestatistics); + $data->statistics = $exporter->export($output); + $data->showcompetencylinks = true; + + return $data; + } +} diff --git a/admin/tool/lp/classes/output/template_plans_page.php b/admin/tool/lp/classes/output/template_plans_page.php new file mode 100644 index 00000000000..0042b7f0113 --- /dev/null +++ b/admin/tool/lp/classes/output/template_plans_page.php @@ -0,0 +1,49 @@ +. + +/** + * Template plans renderable. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +/** + * Template plans renderable. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_plans_page implements \renderable { + + /** + * Constructor. + * @param \core_competency\template $template + * @param \moodle_url $url + */ + public function __construct(\core_competency\template $template, \moodle_url $url) { + $this->template = $template; + $this->url = $url; + $this->table = new template_plans_table('tplplans', $template); + $this->table->define_baseurl($url); + } + +} diff --git a/admin/tool/lp/classes/output/template_plans_table.php b/admin/tool/lp/classes/output/template_plans_table.php new file mode 100644 index 00000000000..b495ded8884 --- /dev/null +++ b/admin/tool/lp/classes/output/template_plans_table.php @@ -0,0 +1,194 @@ +. + +/** + * Template plans table. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/tablelib.php'); + +use html_writer; +use moodle_url; +use table_sql; +use core_competency\template; + +/** + * Template plans table class. + * + * Note that presently this table may display some rows although the current user + * does not have permission to view those plans. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_plans_table extends table_sql { + + /** @var context The context. */ + protected $context; + + /** @var \core_competency\template The template. */ + protected $template; + + /** + * Sets up the table. + * + * @param string $uniqueid Unique id of table. + * @param \core_competency\template $template The template. + */ + public function __construct($uniqueid, \core_competency\template $template) { + parent::__construct($uniqueid); + + // This object should not be used without the right permissions. + if (!$template->can_read()) { + throw new \required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + // Set protected properties. + $this->template = $template; + $this->context = $this->template->get_context(); + $this->useridfield = 'userid'; + + // Define columns in the table. + $this->define_table_columns(); + + // Define configs. + $this->define_table_configs(); + } + + /** + * Format column name. + * + * @param stdClass $row + * @return string + */ + protected function col_name($row) { + return html_writer::link(new moodle_url('/admin/tool/lp/plan.php', array('id' => $row->id)), + format_string($row->name, true, array('context' => $this->context))); + } + + /** + * Setup the headers for the table. + */ + protected function define_table_columns() { + $extrafields = get_extra_user_fields($this->context); + + // Define headers and columns. + $cols = array( + 'name' => get_string('planname', 'tool_lp'), + '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()); + + $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); + } + + /** + * 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 = 'p.id, p.userid, p.name, '; + + // 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 {" . \core_competency\plan::TABLE . "} p + JOIN {user} u ON u.id = p.userid + WHERE p.templateid = :templateid"; + $params = array('templateid' => $this->template->get_id()); + + // 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); + } + } +} diff --git a/admin/tool/lp/classes/output/user_competency_course_navigation.php b/admin/tool/lp/classes/output/user_competency_course_navigation.php new file mode 100644 index 00000000000..ef40ba48e1e --- /dev/null +++ b/admin/tool/lp/classes/output/user_competency_course_navigation.php @@ -0,0 +1,138 @@ +. + +/** + * User competency page class. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; + +use renderable; +use renderer_base; +use templatable; +use context_course; +use \core_competency\external\competency_exporter; +use \core_competency\external\user_summary_exporter; +use stdClass; + +/** + * User competency course navigation class. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency_course_navigation implements renderable, templatable { + + /** @var userid */ + protected $userid; + + /** @var competencyid */ + protected $competencyid; + + /** @var courseid */ + protected $courseid; + + /** @var baseurl */ + protected $baseurl; + + /** + * Construct. + * + * @param int $userid + * @param int $competencyid + * @param int $courseid + * @param string $baseurl + */ + public function __construct($userid, $competencyid, $courseid, $baseurl) { + $this->userid = $userid; + $this->competencyid = $competencyid; + $this->courseid = $courseid; + $this->baseurl = $baseurl; + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + global $CFG, $DB, $PAGE; + + $context = context_course::instance($this->courseid); + + $data = new stdClass(); + $data->userid = $this->userid; + $data->competencyid = $this->competencyid; + $data->courseid = $this->courseid; + $data->baseurl = $this->baseurl; + $data->groupselector = ''; + + if (has_capability('moodle/competency:coursecompetencymanage', $context)) { + $course = $DB->get_record('course', array('id' => $this->courseid)); + $currentgroup = groups_get_course_group($course, true); + if ($currentgroup !== false) { + $select = groups_allgroups_course_menu($course, $PAGE->url, true, $currentgroup); + $data->groupselector = $select; + } + // Fetch showactive. + $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol); + $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol); + $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $context); + + $users = get_enrolled_users($context, 'moodle/competency:coursecompetencygradable', $currentgroup, + 'u.*', null, 0, 0, $showonlyactiveenrol); + + $data->users = array(); + foreach ($users as $user) { + $exporter = new user_summary_exporter($user); + $user = $exporter->export($output); + if ($user->id == $this->userid) { + $user->selected = true; + } + $data->users[] = $user; + } + $data->hasusers = true; + } else { + $data->users = array(); + $data->hasusers = false; + } + + $coursecompetencies = \core_competency\api::list_course_competencies($this->courseid); + $data->competencies = array(); + $contextcache = array(); + foreach ($coursecompetencies as $coursecompetency) { + $frameworkid = $coursecompetency['competency']->get_competencyframeworkid(); + if (!isset($contextcache[$frameworkid])) { + $contextcache[$frameworkid] = $coursecompetency['competency']->get_context(); + } + $context = $contextcache[$frameworkid]; + $coursecompetencycontext = $context; + $exporter = new competency_exporter($coursecompetency['competency'], array('context' => $coursecompetencycontext)); + $competency = $exporter->export($output); + if ($competency->id == $this->competencyid) { + $competency->selected = true; + } + $data->competencies[] = $competency; + } + $data->hascompetencies = count($data->competencies); + return $data; + } +} diff --git a/admin/tool/lp/classes/output/user_competency_summary.php b/admin/tool/lp/classes/output/user_competency_summary.php new file mode 100644 index 00000000000..f40880b754e --- /dev/null +++ b/admin/tool/lp/classes/output/user_competency_summary.php @@ -0,0 +1,86 @@ +. + +/** + * User competency summary. + * + * @package tool_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use core_user; +use renderer_base; +use renderable; +use templatable; +use core_competency\api; +use core_competency\user_competency; +use tool_lp\external\user_competency_summary_exporter; + +/** + * User competency summary class. + * + * @package tool_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency_summary implements renderable, templatable { + + /** @var usercompetency */ + protected $usercompetency; + /** @var array */ + protected $related; + + /** + * Constructor. + * + * @param user_competency $usercompetency The user competency. + * @param array $related Related objects. + */ + public function __construct(user_competency $usercompetency, array $related = array()) { + $this->usercompetency = $usercompetency; + $this->related = $related; + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + if (!isset($related['user'])) { + $related['user'] = core_user::get_user($this->usercompetency->get_userid()); + } + if (!isset($related['competency'])) { + $related['competency'] = $this->usercompetency->get_competency(); + } + + $related += array( + 'usercompetency' => $this->usercompetency, + 'usercompetencyplan' => null, + 'usercompetencycourse' => null, + 'evidence' => api::list_evidence($this->usercompetency->get_userid(), $this->usercompetency->get_competencyid()), + 'relatedcompetencies' => api::list_related_competencies($this->usercompetency->get_competencyid()) + ); + $exporter = new user_competency_summary_exporter(null, $related); + $data = $exporter->export($output); + + return $data; + } +} diff --git a/admin/tool/lp/classes/output/user_competency_summary_in_course.php b/admin/tool/lp/classes/output/user_competency_summary_in_course.php new file mode 100644 index 00000000000..2abc6f9212c --- /dev/null +++ b/admin/tool/lp/classes/output/user_competency_summary_in_course.php @@ -0,0 +1,98 @@ +. + +/** + * User competency page class. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; + +use renderable; +use renderer_base; +use templatable; +use core_competency\api; +use core_competency\user_competency; +use tool_lp\external\user_competency_summary_in_course_exporter; + +/** + * User competency page class. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency_summary_in_course implements renderable, templatable { + + /** @var userid */ + protected $userid; + + /** @var competencyid */ + protected $competencyid; + + /** @var courseid */ + protected $courseid; + + /** + * Construct. + * + * @param int $userid + * @param int $competencyid + * @param int $courseid + */ + public function __construct($userid, $competencyid, $courseid) { + $this->userid = $userid; + $this->competencyid = $competencyid; + $this->courseid = $courseid; + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + global $DB; + + $usercompetencycourse = api::get_user_competency_in_course($this->courseid, $this->userid, $this->competencyid); + $competency = $usercompetencycourse->get_competency(); + if (empty($usercompetencycourse) || empty($competency)) { + throw new \invalid_parameter_exception('Invalid params. The competency does not belong to the course.'); + } + + $relatedcompetencies = api::list_related_competencies($competency->get_id()); + $user = $DB->get_record('user', array('id' => $this->userid)); + $evidence = api::list_evidence_in_course($this->userid, $this->courseid, $this->competencyid); + $course = $DB->get_record('course', array('id' => $this->courseid)); + + $params = array( + 'competency' => $competency, + 'usercompetencycourse' => $usercompetencycourse, + 'evidence' => $evidence, + 'user' => $user, + 'course' => $course, + 'scale' => $competency->get_scale(), + 'relatedcompetencies' => $relatedcompetencies + ); + $exporter = new user_competency_summary_in_course_exporter(null, $params); + $data = $exporter->export($output); + + return $data; + } +} diff --git a/admin/tool/lp/classes/output/user_competency_summary_in_plan.php b/admin/tool/lp/classes/output/user_competency_summary_in_plan.php new file mode 100644 index 00000000000..e650aa15ecd --- /dev/null +++ b/admin/tool/lp/classes/output/user_competency_summary_in_plan.php @@ -0,0 +1,96 @@ +. + +/** + * User competency page class. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use core_competency\api; +use tool_lp\external\user_competency_summary_in_plan_exporter; + +/** + * User competency page class. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency_summary_in_plan implements renderable, templatable { + + /** @var competencyid */ + protected $competencyid; + + /** @var planid */ + protected $planid; + + /** + * Construct. + * + * @param int $competencyid + * @param int $planid + */ + public function __construct($competencyid, $planid) { + $this->competencyid = $competencyid; + $this->planid = $planid; + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(\renderer_base $output) { + global $DB; + + $plan = api::read_plan($this->planid); + $pc = api::get_plan_competency($plan, $this->competencyid); + $competency = $pc->competency; + $usercompetency = $pc->usercompetency; + $usercompetencyplan = $pc->usercompetencyplan; + + if (empty($competency)) { + throw new \invalid_parameter_exception('Invalid params. The competency does not belong to the plan.'); + } + + $relatedcompetencies = api::list_related_competencies($competency->get_id()); + $userid = $plan->get_userid(); + $user = $DB->get_record('user', array('id' => $userid)); + $evidence = api::list_evidence($userid, $this->competencyid, $plan->get_id()); + + $params = array( + 'competency' => $competency, + 'usercompetency' => $usercompetency, + 'usercompetencyplan' => $usercompetencyplan, + 'evidence' => $evidence, + 'user' => $user, + 'plan' => $plan, + 'relatedcompetencies' => $relatedcompetencies + ); + $exporter = new user_competency_summary_in_plan_exporter(null, $params); + $data = $exporter->export($output); + + return $data; + } +} diff --git a/admin/tool/lp/classes/output/user_evidence_list_page.php b/admin/tool/lp/classes/output/user_evidence_list_page.php new file mode 100644 index 00000000000..9ea1db8c99d --- /dev/null +++ b/admin/tool/lp/classes/output/user_evidence_list_page.php @@ -0,0 +1,110 @@ +. + +/** + * Page listing the evidence of prior learning of a user. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; + +use renderable; +use templatable; +use renderer_base; +use stdClass; +use single_button; +use moodle_url; +use core_competency\api; +use tool_lp\external\user_evidence_summary_exporter; +use core_competency\user_evidence; +use context_user; + +/** + * Class for the page listing the evidence of prior learning of a user. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_evidence_list_page implements renderable, templatable { + + /** @var array $navigation List of links to display on the page. Each link contains a url and a title. */ + protected $navigation = array(); + + /** @var tool_lp\user_evidence[] $evidence List of user evidence. */ + protected $evidence = array(); + + /** @var context_user|null $context context. */ + protected $context = null; + + /** @var int|null $userid Userid. */ + protected $userid = null; + + /** @var bool Can the user manage the evidence. */ + protected $canmanage; + + /** + * Construct this renderable. + * + * @param int $userid + */ + public function __construct($userid) { + $this->userid = $userid; + $this->context = context_user::instance($userid); + $this->evidence = api::list_user_evidence($userid); + $this->canmanage = user_evidence::can_manage_user($this->userid); + + if ($this->canmanage) { + $addevidence = new single_button( + new moodle_url('/admin/tool/lp/user_evidence_edit.php', array('userid' => $userid)), + get_string('addnewuserevidence', 'tool_lp'), 'get' + ); + $this->navigation[] = $addevidence; + } + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $data->userid = $this->userid; + $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true); + $data->canmanage = $this->canmanage; + + $data->evidence = array(); + if ($this->evidence) { + foreach ($this->evidence as $evidence) { + $userevidencesummaryexporter = new user_evidence_summary_exporter($evidence, array( + 'context' => $this->context + )); + $data->evidence[] = $userevidencesummaryexporter->export($output); + } + } + + $data->navigation = array(); + foreach ($this->navigation as $button) { + $data->navigation[] = $output->render($button); + } + + return $data; + } +} diff --git a/admin/tool/lp/classes/output/user_evidence_page.php b/admin/tool/lp/classes/output/user_evidence_page.php new file mode 100644 index 00000000000..96f71655641 --- /dev/null +++ b/admin/tool/lp/classes/output/user_evidence_page.php @@ -0,0 +1,74 @@ +. + +/** + * User evidence page output. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lp\output; + +use moodle_url; +use renderable; +use templatable; +use stdClass; +use core_competency\api; +use tool_lp\external\user_evidence_summary_exporter; + +/** + * User evidence page class. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_evidence_page implements renderable, templatable { + + /** @var context The context. */ + protected $context; + + /** @var userevidence The user evidence. */ + protected $userevidence; + + /** + * Construct. + * + * @param user_evidence $userevidence + */ + public function __construct($userevidence) { + $this->userevidence = $userevidence; + $this->context = $this->userevidence->get_context(); + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(\renderer_base $output) { + $data = new stdClass(); + + $userevidencesummaryexporter = new user_evidence_summary_exporter($this->userevidence, array( + 'context' => $this->context)); + $data->userevidence = $userevidencesummaryexporter->export($output); + $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true); + + return $data; + } +} diff --git a/admin/tool/lp/classes/page_helper.php b/admin/tool/lp/classes/page_helper.php new file mode 100644 index 00000000000..476339a7519 --- /dev/null +++ b/admin/tool/lp/classes/page_helper.php @@ -0,0 +1,461 @@ +. + +/** + * Page helper. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use context; +use moodle_exception; +use moodle_url; +use core_user; +use context_user; +use context_course; +use stdClass; + +/** + * Page helper. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class page_helper { + + /** + * Set-up a course page. + * + * Example: + * list($title, $subtitle) = page_helper::setup_for_course($pagecontextid, $url, $course, $pagetitle); + * echo $OUTPUT->heading($title); + * echo $OUTPUT->heading($subtitle, 3); + * + * @param moodle_url $url The current page. + * @param stdClass $course The course. + * @param string $subtitle The title of the subpage, if any. + * @return array With the following: + * - Page title + * - Page sub title + * - Return URL (course competencies page) + */ + public static function setup_for_course(moodle_url $url, $course, $subtitle = '') { + global $PAGE; + + $context = context_course::instance($course->id); + + $PAGE->set_course($course); + + if (!empty($subtitle)) { + $title = $subtitle; + } else { + $title = get_string('coursecompetencies', 'tool_lp'); + } + + $returnurl = new moodle_url('/admin/tool/lp/coursecompetencies.php', array('courseid' => $course->id)); + + $heading = $context->get_context_name(); + $PAGE->set_pagelayout('incourse'); + $PAGE->set_url($url); + $PAGE->set_title($title); + $PAGE->set_heading($heading); + + if (!empty($subtitle)) { + $PAGE->navbar->add(get_string('coursecompetencies', 'tool_lp'), $returnurl); + // We're in a sub page without a specific template. + $PAGE->navbar->add($subtitle, $url); + } + + return array($title, $subtitle, $returnurl); + } + + /** + * Set-up a template page. + * + * Example: + * list($title, $subtitle) = page_helper::setup_for_template($pagecontextid, $url, $template, $pagetitle); + * echo $OUTPUT->heading($title); + * echo $OUTPUT->heading($subtitle, 3); + * + * @param int $pagecontextid The page context ID. + * @param moodle_url $url The current page. + * @param \core_competency\template $template The template, if any. + * @param string $subtitle The title of the subpage, if any. + * @param string $returntype The desired return page. + * @return array With the following: + * - Page title + * - Page sub title + * - Return URL + */ + public static function setup_for_template($pagecontextid, moodle_url $url, $template = null, $subtitle = '', + $returntype = null) { + global $PAGE, $SITE; + + $pagecontext = context::instance_by_id($pagecontextid); + $context = $pagecontext; + if (!empty($template)) { + $context = $template->get_context(); + } + + $templatesurl = new moodle_url('/admin/tool/lp/learningplans.php', array('pagecontextid' => $pagecontextid)); + $templateurl = null; + if ($template) { + $templateurl = new moodle_url('/admin/tool/lp/templatecompetencies.php', [ + 'templateid' => $template->get_id(), + 'pagecontextid' => $pagecontextid + ]); + } + + $returnurl = $templatesurl; + if ($returntype != 'templates' && $templateurl) { + $returnurl = $templateurl; + } + + $PAGE->navigation->override_active_url($templatesurl); + $PAGE->set_context($pagecontext); + + if (!empty($template)) { + $title = format_string($template->get_shortname(), true, array('context' => $context)); + } else { + $title = get_string('templates', 'tool_lp'); + } + + if ($pagecontext->contextlevel == CONTEXT_SYSTEM) { + $heading = $SITE->fullname; + } else if ($pagecontext->contextlevel == CONTEXT_COURSECAT) { + $heading = $pagecontext->get_context_name(); + } else { + throw new coding_exception('Unexpected context!'); + } + + $PAGE->set_pagelayout('admin'); + $PAGE->set_url($url); + $PAGE->set_title($title); + $PAGE->set_heading($heading); + + if (!empty($template)) { + $PAGE->navbar->add($title, $templateurl); + if (!empty($subtitle)) { + $PAGE->navbar->add($subtitle, $url); + } + + } else if (!empty($subtitle)) { + // We're in a sub page without a specific template. + $PAGE->navbar->add($subtitle, $url); + } + + return array($title, $subtitle, $returnurl); + } + + /** + * Set-up a plan page. + * + * Example: + * list($title, $subtitle) = page_helper::setup_for_plan($url, $template, $pagetitle); + * echo $OUTPUT->heading($title); + * echo $OUTPUT->heading($subtitle, 3); + * + * @param int $userid The user ID. + * @param moodle_url $url The current page. + * @param \core_competency\plan $plan The plan, if any. + * @param string $subtitle The title of the subpage, if any. + * @param string $returntype The desired return page. + * @return array With the following: + * - Page title + * - Page sub title + * - Return URL (main plan page) + */ + public static function setup_for_plan($userid, moodle_url $url, $plan = null, $subtitle = '', $returntype = null) { + global $PAGE, $USER; + + // Check that the user is a valid user. + $user = core_user::get_user($userid); + if (!$user || !core_user::is_real_user($userid)) { + throw new \moodle_exception('invaliduser', 'error'); + } + + $context = context_user::instance($user->id); + + $plansurl = new moodle_url('/admin/tool/lp/plans.php', array('userid' => $userid)); + $planurl = null; + if ($plan) { + $planurl = new moodle_url('/admin/tool/lp/plan.php', array('id' => $plan->get_id())); + } + + $returnurl = $plansurl; + if ($returntype != 'plans' && $planurl) { + $returnurl = $planurl; + } + + $PAGE->navigation->override_active_url($plansurl); + $PAGE->set_context($context); + + // If not his own plan, we want to extend the navigation for the user. + $iscurrentuser = ($USER->id == $user->id); + if (!$iscurrentuser) { + $PAGE->navigation->extend_for_user($user); + $PAGE->navigation->set_userid_for_parent_checks($user->id); + } + + if (!empty($plan)) { + $title = format_string($plan->get_name(), true, array('context' => $context)); + } else { + $title = get_string('learningplans', 'tool_lp'); + } + + $PAGE->set_pagelayout('standard'); + $PAGE->set_url($url); + $PAGE->set_title($title); + $PAGE->set_heading($title); + + if (!empty($plan)) { + $PAGE->navbar->add($title, $planurl); + if (!empty($subtitle)) { + $PAGE->navbar->add($subtitle, $url); + } + } else if (!empty($subtitle)) { + // We're in a sub page without a specific plan. + $PAGE->navbar->add($subtitle, $url); + } + + return array($title, $subtitle, $returnurl); + } + + /** + * Set-up a user evidence page. + * + * Example: + * list($title, $subtitle) = page_helper::setup_for_user_evidence($url, $template, $pagetitle); + * echo $OUTPUT->heading($title); + * echo $OUTPUT->heading($subtitle, 3); + * + * @param int $userid The user ID. + * @param moodle_url $url The current page. + * @param \core_competency\user_evidence $evidence The user evidence, if any. + * @param string $subtitle The title of the subpage, if any. + * @param string $returntype The desired return page. + * @return array With the following: + * - Page title + * - Page sub title + * - Return URL (main plan page) + */ + public static function setup_for_user_evidence($userid, moodle_url $url, $evidence = null, $subtitle = '', $returntype = null) { + global $PAGE, $USER; + + // Check that the user is a valid user. + $user = core_user::get_user($userid); + if (!$user || !core_user::is_real_user($userid)) { + throw new \moodle_exception('invaliduser', 'error'); + } + + $context = context_user::instance($user->id); + + $evidencelisturl = new moodle_url('/admin/tool/lp/user_evidence_list.php', array('userid' => $userid)); + $evidenceurl = null; + if ($evidence) { + $evidenceurl = new moodle_url('/admin/tool/lp/user_evidence.php', array('id' => $evidence->get_id())); + } + + $returnurl = $evidencelisturl; + if ($returntype != 'list' && $evidenceurl) { + $returnurl = $evidenceurl; + } + + $PAGE->navigation->override_active_url($evidencelisturl); + $PAGE->set_context($context); + + // If not his own evidence, we want to extend the navigation for the user. + $iscurrentuser = ($USER->id == $user->id); + if (!$iscurrentuser) { + $PAGE->navigation->extend_for_user($user); + $PAGE->navigation->set_userid_for_parent_checks($user->id); + } + + if (!empty($evidence)) { + $title = format_string($evidence->get_name(), true, array('context' => $context)); + } else { + $title = get_string('userevidence', 'tool_lp'); + } + + $PAGE->set_pagelayout('standard'); + $PAGE->set_url($url); + $PAGE->set_title($title); + $PAGE->set_heading($title); + + if (!empty($evidence)) { + $PAGE->navbar->add($title, $evidenceurl); + if (!empty($subtitle)) { + $PAGE->navbar->add($subtitle, $url); + } + } else if (!empty($subtitle)) { + // We're in a sub page without a specific evidence. + $PAGE->navbar->add($subtitle, $url); + } + + return array($title, $subtitle, $returnurl); + } + + /** + * Set-up a framework page. + * + * Example: + * list($pagetitle, $pagesubtitle, $url, $frameworksurl) = page_helper::setup_for_framework($id, $pagecontextid); + * echo $OUTPUT->heading($pagetitle); + * echo $OUTPUT->heading($pagesubtitle, 3); + * + * @param int $id The framework ID. + * @param int $pagecontextid The page context ID. + * @param \core_competency\competency_framework $framework The framework. + * @param string $returntype The desired return page. + * @return array With the following: + * - Page title + * - Page sub title + * - Page URL + * - Page framework URL + */ + public static function setup_for_framework($id, $pagecontextid, $framework = null, $returntype = null) { + global $PAGE; + + // We keep the original context in the URLs, so that we remain in the same context. + $url = new moodle_url("/admin/tool/lp/editcompetencyframework.php", array('id' => $id, 'pagecontextid' => $pagecontextid)); + if ($returntype) { + $url->param('return', $returntype); + } + $frameworksurl = new moodle_url('/admin/tool/lp/competencyframeworks.php', array('pagecontextid' => $pagecontextid)); + + $PAGE->navigation->override_active_url($frameworksurl); + $title = get_string('competencies', 'core_competency'); + if (empty($id)) { + $pagetitle = get_string('competencyframeworks', 'tool_lp'); + $pagesubtitle = get_string('addnewcompetencyframework', 'tool_lp'); + + $url->remove_params(array('id')); + $PAGE->navbar->add($pagesubtitle, $url); + } else { + $pagetitle = $framework->get_shortname(); + $pagesubtitle = get_string('editcompetencyframework', 'tool_lp'); + if ($returntype == 'competencies') { + $frameworksurl = new moodle_url('/admin/tool/lp/competencies.php', array( + 'pagecontextid' => $pagecontextid, + 'competencyframeworkid' => $id + )); + } else { + $frameworksurl->param('competencyframeworkid', $id); + } + + $PAGE->navbar->add($pagetitle, $frameworksurl); + $PAGE->navbar->add($pagesubtitle, $url); + } + + $PAGE->set_context(context::instance_by_id($pagecontextid)); + $PAGE->set_pagelayout('admin'); + $PAGE->set_url($url); + $PAGE->set_title($title); + $PAGE->set_heading($title); + return array($pagetitle, $pagesubtitle, $url, $frameworksurl); + } + + /** + * Set-up a competency page. + * + * Example: + * list($title, $subtitle) = page_helper::setup_for_competency($pagecontextid, $url, $competency, $pagetitle); + * echo $OUTPUT->heading($title); + * echo $OUTPUT->heading($subtitle, 3); + * + * @param int $pagecontextid The page context ID. + * @param moodle_url $url The current page. + * @param \core_competency\competency_framework $framework The competency framework. + * @param \core_competency\competency $competency The competency, if any. + * @param \core_competency\competency $parent The parent competency, if any. + * @return array With the following: + * - Page title + * - Page sub title + * - Return URL (main competencies page) + * @throws coding_exception + */ + public static function setup_for_competency($pagecontextid, moodle_url $url, $framework, $competency = null, $parent = null) { + global $PAGE, $SITE; + + // Set page context. + $pagecontext = context::instance_by_id($pagecontextid); + $PAGE->set_context($pagecontext); + + // Set page heading. + if ($pagecontext->contextlevel == CONTEXT_SYSTEM) { + $heading = $SITE->fullname; + } else if ($pagecontext->contextlevel == CONTEXT_COURSECAT) { + $heading = $pagecontext->get_context_name(); + } else { + throw new coding_exception('Unexpected context!'); + } + $PAGE->set_heading($heading); + + // Set override active url. + $frameworksurl = new moodle_url('/admin/tool/lp/competencyframeworks.php', ['pagecontextid' => $pagecontextid]); + $PAGE->navigation->override_active_url($frameworksurl); + + // Set return url. + $returnurloptions = [ + 'competencyframeworkid' => $framework->get_id(), + 'pagecontextid' => $pagecontextid + ]; + $returnurl = new moodle_url('/admin/tool/lp/competencies.php', $returnurloptions); + $PAGE->navbar->add($framework->get_shortname(), $returnurl); + + // Set page layout. + $PAGE->set_pagelayout('admin'); + + if (empty($competency)) { + // Add mode. + $title = format_string($framework->get_shortname(), true, ['context' => $pagecontext]); + + // Set the sub-title for add mode. + $level = $parent ? $parent->get_level() + 1 : 1; + $subtitle = get_string('taxonomy_add_' . $framework->get_taxonomy($level), 'tool_lp'); + + } else { + // Edit mode. + $title = format_string($competency->get_shortname(), true, ['context' => $competency->get_context()]); + + // Add competency name to breadcrumbs, if available. + $PAGE->navbar->add($title); + + // Set the sub-title for edit mode. + $subtitle = get_string('taxonomy_edit_' . $framework->get_taxonomy($competency->get_level()), 'tool_lp'); + } + + // Set page title. + $PAGE->set_title($title); + + // Set page url. + $PAGE->set_url($url); + + // Add editing mode link to breadcrumbs, if available. + if (!empty($subtitle)) { + $PAGE->navbar->add($subtitle, $url); + } + + return [$title, $subtitle, $returnurl]; + } +} diff --git a/admin/tool/lp/classes/template_statistics.php b/admin/tool/lp/classes/template_statistics.php new file mode 100644 index 00000000000..3877fa32ca7 --- /dev/null +++ b/admin/tool/lp/classes/template_statistics.php @@ -0,0 +1,80 @@ +. + +/** + * Template statistics class + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp; +defined('MOODLE_INTERNAL') || die(); + +use core_competency\api; +use core_competency\plan; +use core_competency\template; + +/** + * Template statistics class. + * + * @package tool_lp + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_statistics { + + /** @var $competencycount The number of competencies in the template */ + public $competencycount = 0; + + /** @var $unlinkedcompetencycount The number of unlinked competencies in the template */ + public $unlinkedcompetencycount = 0; + + /** @var $plancount The number of plans for the template */ + public $plancount = 0; + + /** @var $completedplancount The number of completed plans for the template */ + public $completedplancount = 0; + + /** @var $usercompetencyplancount The number of competencies in completed plans for the template */ + public $usercompetencyplancount = 0; + + /** @var $proficientusercompetencyplancount The number of proficient competencies in completed plans for the template */ + public $proficientusercompetencyplancount = 0; + + /** @var $leastproficientcompetencies The competencies in this template that were proficient the least times */ + public $leastproficientcompetencies = array(); + + /** + * Return the custom definition of the properties of this model. + * + * @param int $templateid The template we want to generate statistics for. + */ + public function __construct($templateid) { + $template = new template($templateid); + $this->competencycount = api::count_competencies_in_template($template); + $this->unlinkedcompetencycount = api::count_competencies_in_template_with_no_courses($template); + + $this->plancount = api::count_plans_for_template($template, 0); + $this->completedplancount = api::count_plans_for_template($template, plan::STATUS_COMPLETE); + + $this->usercompetencyplancount = api::count_user_competency_plans_for_template($template); + $this->proficientusercompetencyplancount = api::count_user_competency_plans_for_template($template, true); + + $this->leastproficientcompetencies = api::get_least_proficient_competencies_for_template($template, 0, 3); + } +} diff --git a/admin/tool/lp/classes/url_resolver.php b/admin/tool/lp/classes/url_resolver.php new file mode 100644 index 00000000000..89a117bec54 --- /dev/null +++ b/admin/tool/lp/classes/url_resolver.php @@ -0,0 +1,173 @@ +. + +/** + * URL resolver. + * + * @package tool_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lp; +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; + +/** + * URL resolver class. + * + * @package tool_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class url_resolver { + + /** + * The URL where the competency can be found. + * + * @param int $competencyid The competency ID. + * @param int $pagecontextid The ID of the context we are in. + * @return moodle_url + */ + public function competency($competencyid, $pagecontextid) { + return new moodle_url('/admin/tool/lp/editcompetency.php', array( + 'id' => $competencyid, + 'pagecontextid' => $pagecontextid + )); + } + + /** + * The URL where the framework can be found. + * + * @param int $frameworkid The framework ID. + * @param int $pagecontextid The ID of the context we are in. + * @return moodle_url + */ + public function framework($frameworkid, $pagecontextid) { + return new moodle_url('/admin/tool/lp/competencies.php', array( + 'competencyframeworkid' => $frameworkid, + 'pagecontextid' => $pagecontextid + )); + } + + /** + * The URL where the frameworks can be found. + * + * @param int $pagecontextid The ID of the context that we are browsing. + * @return moodle_url + */ + public function frameworks($pagecontextid) { + return new moodle_url('/admin/tool/lp/competencyframeworks.php', array('pagecontextid' => $pagecontextid)); + } + + /** + * The URL where the plan can be found. + * + * @param int $planid The plan ID. + * @return moodle_url + */ + public function plan($planid) { + return new moodle_url('/admin/tool/lp/plan.php', array('id' => $planid)); + } + + /** + * The URL where the plans of a user can be found. + * + * @param int $userid The user ID. + * @return moodle_url + */ + public function plans($userid) { + return new moodle_url('/admin/tool/lp/plans.php', array('userid' => $userid)); + } + + /** + * The URL where the template can be found. + * + * @param int $templateid The template ID. + * @param int $pagecontextid The ID of the context we are in. + * @return moodle_url + */ + public function template($templateid, $pagecontextid) { + return new moodle_url('/admin/tool/lp/templatecompetencies.php', array( + 'templateid' => $templateid, + 'pagecontextid' => $pagecontextid + )); + } + + /** + * The URL where the templates can be found. + * + * @param int $pagecontextid The ID of the context that we are browsing. + * @return moodle_url + */ + public function templates($pagecontextid) { + return new moodle_url('/admin/tool/lp/learningplans.php', array('pagecontextid' => $pagecontextid)); + } + + /** + * The URL where the user competency can be found. + * + * @param int $usercompetencyid The user competency ID + * @return moodle_url + */ + public function user_competency($usercompetencyid) { + return new moodle_url('/admin/tool/lp/user_competency.php', array('id' => $usercompetencyid)); + } + + /** + * The URL where the user competency can be found in the context of a course. + * + * @param int $userid The user ID + * @param int $competencyid The competency ID. + * @param int $courseid The course ID. + * @return moodle_url + */ + public function user_competency_in_course($userid, $competencyid, $courseid) { + return new moodle_url('/admin/tool/lp/user_competency_in_course.php', array( + 'userid' => $userid, + 'competencyid' => $competencyid, + 'courseid' => $courseid + )); + } + + /** + * The URL where the user competency can be found in the context of a plan. + * + * @param int $userid The user ID + * @param int $competencyid The competency ID. + * @param int $planid The plan ID. + * @return moodle_url + */ + public function user_competency_in_plan($userid, $competencyid, $planid) { + return new moodle_url('/admin/tool/lp/user_competency_in_plan.php', array( + 'userid' => $userid, + 'competencyid' => $competencyid, + 'planid' => $planid + )); + } + + /** + * The URL where the user evidence (of prior learning) can be found. + * + * @param int $userevidenceid The user evidence ID + * @return moodle_url + */ + public function user_evidence($userevidenceid) { + return new moodle_url('/admin/tool/lp/user_evidence.php', array('id' => $userevidenceid)); + } + +} diff --git a/admin/tool/lp/competencies.php b/admin/tool/lp/competencies.php new file mode 100644 index 00000000000..53c7713ea8a --- /dev/null +++ b/admin/tool/lp/competencies.php @@ -0,0 +1,67 @@ +. + +/** + * This page lets users to manage site wide competencies. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +$id = required_param('competencyframeworkid', PARAM_INT); +$pagecontextid = required_param('pagecontextid', PARAM_INT); // Reference to the context we came from. +$search = optional_param('search', '', PARAM_RAW); + +require_login(); +\core_competency\api::require_enabled(); + +$pagecontext = context::instance_by_id($pagecontextid); +$framework = \core_competency\api::read_framework($id); +$context = $framework->get_context(); + +if (!\core_competency\competency_framework::can_read_context($context)) { + throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', ''); +} + +$title = get_string('competencies', 'core_competency'); +$pagetitle = get_string('competenciesforframework', 'tool_lp', $framework->get_shortname()); + +// Set up the page. +$url = new moodle_url("/admin/tool/lp/competencies.php", array('competencyframeworkid' => $framework->get_id(), + 'pagecontextid' => $pagecontextid)); +$frameworksurl = new moodle_url('/admin/tool/lp/competencyframeworks.php', array('pagecontextid' => $pagecontextid)); + +$PAGE->navigation->override_active_url($frameworksurl); +$PAGE->set_context($pagecontext); +$PAGE->set_pagelayout('admin'); +$PAGE->set_url($url); +$PAGE->navbar->add($framework->get_shortname(), $url); +$PAGE->set_title($title); +$PAGE->set_heading($title); +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); + +$page = new \tool_lp\output\manage_competencies_page($framework, $search, $pagecontext); +echo $output->render($page); + +// Log the framework viewed event after rendering the page. +\core_competency\api::competency_framework_viewed($framework); + +echo $output->footer(); diff --git a/admin/tool/lp/competencyframeworks.php b/admin/tool/lp/competencyframeworks.php new file mode 100644 index 00000000000..5b76b5f0826 --- /dev/null +++ b/admin/tool/lp/competencyframeworks.php @@ -0,0 +1,59 @@ +. + +/** + * This page lets users to manage site wide competencies. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +$pagecontextid = required_param('pagecontextid', PARAM_INT); +$context = context::instance_by_id($pagecontextid); + +$url = new moodle_url("/admin/tool/lp/competencyframeworks.php"); +$url->param('pagecontextid', $pagecontextid); + +require_login(); +\core_competency\api::require_enabled(); + +if (!\core_competency\competency_framework::can_read_context($context)) { + throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', ''); +} + +$title = get_string('competencies', 'core_competency'); +$pagetitle = get_string('competencyframeworks', 'tool_lp'); +$pagesubtitle = get_string('listcompetencyframeworkscaption', 'tool_lp'); + +// Set up the page. +$PAGE->set_context($context); +$PAGE->set_pagelayout('admin'); +$PAGE->set_url($url); +$PAGE->set_title($title); +$PAGE->set_heading($title); +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); +echo $output->heading($pagetitle, 2); +echo $output->heading($pagesubtitle, 3); + +$page = new \tool_lp\output\manage_competency_frameworks_page($context); +echo $output->render($page); + +echo $output->footer(); diff --git a/admin/tool/lp/coursecompetencies.php b/admin/tool/lp/coursecompetencies.php new file mode 100644 index 00000000000..4beab6922e9 --- /dev/null +++ b/admin/tool/lp/coursecompetencies.php @@ -0,0 +1,50 @@ +. + +/** + * This page lets users to manage site wide competencies. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); + +$id = required_param('courseid', PARAM_INT); + +$params = array('id' => $id); +$course = $DB->get_record('course', $params, '*', MUST_EXIST); + +require_login($course); +\core_competency\api::require_enabled(); + +$context = context_course::instance($course->id); +$urlparams = array('courseid' => $id); + +$url = new moodle_url('/admin/tool/lp/coursecompetencies.php', $urlparams); + +list($title, $subtitle) = \tool_lp\page_helper::setup_for_course($url, $course); + +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); +echo $output->heading($title); + + +$page = new \tool_lp\output\course_competencies_page($course->id); +echo $output->render($page); + +echo $output->footer(); diff --git a/admin/tool/lp/db/services.php b/admin/tool/lp/db/services.php new file mode 100644 index 00000000000..209db55ccb1 --- /dev/null +++ b/admin/tool/lp/db/services.php @@ -0,0 +1,190 @@ +. + + +/** + * Learning plan webservice functions. + * + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = array( + + // Learning plan related functions. + 'tool_lp_data_for_competency_frameworks_manage_page' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_competency_frameworks_manage_page', + 'classpath' => '', + 'description' => 'Load the data for the competency frameworks manage page template', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'tool_lp_data_for_competency_summary' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_competency_summary', + 'classpath' => '', + 'description' => 'Load competency data for summary template.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'tool_lp_data_for_competencies_manage_page' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_competencies_manage_page', + 'classpath' => '', + 'description' => 'Load the data for the competencies manage page template', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'tool_lp_list_courses_using_competency' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'list_courses_using_competency', + 'classpath' => '', + 'description' => 'List the courses using a competency', + 'type' => 'read', + 'capabilities' => 'moodle/competency:coursecompetencyview', + 'ajax' => true, + ), + 'tool_lp_data_for_course_competencies_page' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_course_competencies_page', + 'classpath' => '', + 'description' => 'Load the data for the course competencies page template.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:coursecompetencyview', + 'ajax' => true, + ), + 'tool_lp_data_for_template_competencies_page' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_template_competencies_page', + 'classpath' => '', + 'description' => 'Load the data for the template competencies page template.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:templateview', + 'ajax' => true, + ), + 'tool_lp_data_for_templates_manage_page' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_templates_manage_page', + 'classpath' => '', + 'description' => 'Load the data for the learning plan templates manage page template', + 'type' => 'read', + 'capabilities' => 'moodle/competency:templateview', + 'ajax' => true, + ), + 'tool_lp_data_for_plans_page' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_plans_page', + 'classpath' => '', + 'description' => 'Load the data for the plans page template', + 'type' => 'read', + 'capabilities' => 'moodle/competency:planviewown', + 'ajax' => true, + ), + 'tool_lp_data_for_plan_page' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_plan_page', + 'classpath' => '', + 'description' => 'Load the data for the plan page template.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:planview', + 'ajax' => true, + ), + 'tool_lp_data_for_related_competencies_section' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_related_competencies_section', + 'classpath' => '', + 'description' => 'Load the data for the related competencies template.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'tool_lp_search_users' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'search_users', + 'classpath' => '', + 'description' => 'Search for users.', + 'type' => 'read', + 'capabilities' => '', + 'ajax' => true, + ), + 'tool_lp_search_cohorts' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'search_cohorts', + 'classpath' => '', + 'description' => 'Search for cohorts.', + 'type' => 'read', + 'capabilities' => 'moodle/cohort:view', + 'ajax' => true, + ), + + // User evidence. + 'tool_lp_data_for_user_evidence_list_page' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_user_evidence_list_page', + 'classpath' => '', + 'description' => 'Load the data for the user evidence list page template', + 'type' => 'read', + 'capabilities' => 'moodle/competency:userevidenceview', + 'ajax' => true, + ), + 'tool_lp_data_for_user_evidence_page' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_user_evidence_page', + 'classpath' => '', + 'description' => 'Load the data for the user evidence page template', + 'type' => 'read', + 'capabilities' => 'moodle/competency:userevidenceview', + 'ajax' => true, + ), + + // User competency. + 'tool_lp_data_for_user_competency_summary' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_user_competency_summary', + 'classpath' => '', + 'description' => 'Load a summary of a user competency.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:planview', + 'ajax' => true, + ), + 'tool_lp_data_for_user_competency_summary_in_plan' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_user_competency_summary_in_plan', + 'classpath' => '', + 'description' => 'Load a summary of a user competency.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:planview', + 'ajax' => true, + ), + 'tool_lp_data_for_user_competency_summary_in_course' => array( + 'classname' => 'tool_lp\external', + 'methodname' => 'data_for_user_competency_summary_in_course', + 'classpath' => '', + 'description' => 'Load a summary of a user competency.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:coursecompetencyview', + 'ajax' => true, + ), +); + diff --git a/admin/tool/lp/editcompetency.php b/admin/tool/lp/editcompetency.php new file mode 100644 index 00000000000..cbaa76e4f99 --- /dev/null +++ b/admin/tool/lp/editcompetency.php @@ -0,0 +1,111 @@ +. + +/** + * This page lets users to manage site wide competencies. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +$id = optional_param('id', 0, PARAM_INT); +$competencyframeworkid = optional_param('competencyframeworkid', 0, PARAM_INT); +$pagecontextid = required_param('pagecontextid', PARAM_INT); // Reference to the context we came from. +$parentid = optional_param('parentid', 0, PARAM_INT); + +require_login(); +\core_competency\api::require_enabled(); + +if (empty($competencyframeworkid) && empty($id)) { + throw new coding_exception('Competencyframeworkid param is required'); +} + +// Get competency framework. +$competencyframework = null; +if (!empty($competencyframeworkid)) { + $competencyframework = \core_competency\api::read_framework($competencyframeworkid); +} + +// Get competency. +$competency = null; +if (!empty($id)) { + $competency = \core_competency\api::read_competency($id); + if (empty($competencyframework)) { + $competencyframework = $competency->get_framework(); + } +} + +// Get parent competency, if any. +$parent = null; +if ($competency) { + $parent = $competency->get_parent(); +} else if ($parentid) { + $parent = \core_competency\api::read_competency($parentid); +} + +// Get page URL. +$urloptions = [ + 'id' => $id, + 'competencyframeworkid' => $competencyframework->get_id(), + 'parentid' => $parentid, + 'pagecontextid' => $pagecontextid +]; +$url = new moodle_url("/admin/tool/lp/editcompetency.php", $urloptions); + +// Set up the page. +list($title, $subtitle, $returnurl) = \tool_lp\page_helper::setup_for_competency($pagecontextid, $url, $competencyframework, + $competency, $parent); + +// Set up the form. +$formoptions = [ + 'competencyframework' => $competencyframework, + 'parent' => $parent, + 'persistent' => $competency, + 'pagecontextid' => $pagecontextid +]; +$form = new \tool_lp\form\competency($url->out(false), $formoptions); + +// Form cancelled. +if ($form->is_cancelled()) { + redirect($returnurl); +} + +// Get form data. +$data = $form->get_data(); +if ($data) { + if (empty($competency)) { + \core_competency\api::create_competency($data); + $returnmsg = get_string('competencycreated', 'tool_lp'); + } else { + \core_competency\api::update_competency($data); + $returnmsg = get_string('competencyupdated', 'tool_lp'); + } + redirect($returnurl, $returnmsg, null, \core\output\notification::NOTIFY_SUCCESS); +} + +// Render the page. +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); +echo $output->heading($title); +echo $output->heading($subtitle, 3); + +$form->display(); + +echo $output->footer(); diff --git a/admin/tool/lp/editcompetencyframework.php b/admin/tool/lp/editcompetencyframework.php new file mode 100644 index 00000000000..5ae6cd85af6 --- /dev/null +++ b/admin/tool/lp/editcompetencyframework.php @@ -0,0 +1,76 @@ +. + +/** + * This page lets users to manage site wide competencies. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +$id = optional_param('id', 0, PARAM_INT); +$returntype = optional_param('return', null, PARAM_TEXT); +$pagecontextid = required_param('pagecontextid', PARAM_INT); // Reference to where we can from. + +$framework = null; +if (!empty($id)) { + // Always use the context from the framework when it exists. + $framework = new \core_competency\competency_framework($id); + $context = $framework->get_context(); +} else { + $context = context::instance_by_id($pagecontextid); +} + +// We check that we have the permission to edit this framework, in its own context. +require_login(); +\core_competency\api::require_enabled(); +require_capability('moodle/competency:competencymanage', $context); + +// Set up the framework page. +list($pagetitle, $pagesubtitle, $url, $frameworksurl) = tool_lp\page_helper::setup_for_framework($id, + $pagecontextid, $framework, $returntype); +$output = $PAGE->get_renderer('tool_lp'); +$form = new \tool_lp\form\competency_framework($url->out(false), array('context' => $context, 'persistent' => $framework)); + +if ($form->is_cancelled()) { + redirect($frameworksurl); +} else if ($data = $form->get_data()) { + if (empty($data->id)) { + // Create new framework. + $data->contextid = $context->id; + $framework = \core_competency\api::create_framework($data); + $frameworkmanageurl = new moodle_url('/admin/tool/lp/competencies.php', array( + 'pagecontextid' => $pagecontextid, + 'competencyframeworkid' => $framework->get_id() + )); + $messagesuccess = get_string('competencyframeworkcreated', 'tool_lp'); + redirect($frameworkmanageurl, $messagesuccess, 0, \core\output\notification::NOTIFY_SUCCESS); + } else { + \core_competency\api::update_framework($data); + $messagesuccess = get_string('competencyframeworkupdated', 'tool_lp'); + redirect($frameworksurl, $messagesuccess, 0, \core\output\notification::NOTIFY_SUCCESS); + } +} + +echo $output->header(); +echo $output->heading($pagetitle, 2); +echo $output->heading($pagesubtitle, 3); +$form->display(); +echo $output->footer(); diff --git a/admin/tool/lp/editplan.php b/admin/tool/lp/editplan.php new file mode 100644 index 00000000000..7f8faa9412f --- /dev/null +++ b/admin/tool/lp/editplan.php @@ -0,0 +1,101 @@ +. + +/** + * Page to edit a plan. + * + * @package tool_lp + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +$userid = optional_param('userid', false, PARAM_INT); +$id = optional_param('id', false, PARAM_INT); +$returntype = optional_param('return', null, PARAM_ALPHA); + +require_login(0, false); +\core_competency\api::require_enabled(); + +$url = new moodle_url('/admin/tool/lp/editplan.php', array('id' => $id, 'userid' => $userid, 'return' => $returntype)); + +$plan = null; +if (empty($id)) { + $pagetitle = get_string('addnewplan', 'tool_lp'); + list($title, $subtitle, $returnurl) = \tool_lp\page_helper::setup_for_plan($userid, $url, null, $pagetitle, $returntype); +} else { + $plan = \core_competency\api::read_plan($id); + + // The userid parameter must be the same as the owner of the plan. + if ($userid != $plan->get_userid()) { + throw new coding_exception('Inconsistency between the userid parameter and the userid of the plan'); + } + + $pagetitle = get_string('editplan', 'tool_lp'); + list($title, $subtitle, $returnurl) = \tool_lp\page_helper::setup_for_plan($userid, $url, $plan, $pagetitle, $returntype); +} + +$output = $PAGE->get_renderer('tool_lp'); + +// Custom data to pass to the form. +$customdata = array('userid' => $userid, 'context' => $PAGE->context, 'persistent' => $plan); + +// User can create plan if he can_manage_user with active/complete status +// or if he can_manage_user_draft with draft status. +$cancreate = \core_competency\plan::can_manage_user_draft($userid) || \core_competency\plan::can_manage_user($userid); + +// If editing plan, check if user has permissions to edit it. +if ($plan != null) { + if (!$plan->can_manage()) { + throw new required_capability_exception($PAGE->context, 'moodle/competency:planmanage', 'nopermissions', ''); + } + if (!$plan->can_be_edited()) { + throw new coding_exception('Completed plan can not be edited'); + } +} else if (!$cancreate) { + throw new required_capability_exception($PAGE->context, 'moodle/competency:planmanage', 'nopermissions', ''); +} + +$form = new \tool_lp\form\plan($url->out(false), $customdata); +if ($form->is_cancelled()) { + redirect($returnurl); +} + +$data = $form->get_data(); + +if ($data) { + if (empty($data->id)) { + $plan = \core_competency\api::create_plan($data); + $returnurl = new moodle_url('/admin/tool/lp/plan.php', ['id' => $plan->get_id()]); + $returnmsg = get_string('plancreated', 'tool_lp'); + } else { + \core_competency\api::update_plan($data); + $returnmsg = get_string('planupdated', 'tool_lp'); + } + redirect($returnurl, $returnmsg, null, \core\output\notification::NOTIFY_SUCCESS); +} + +echo $output->header(); +echo $output->heading($title); +if (!empty($subtitle)) { + echo $output->heading($subtitle, 3); +} + +$form->display(); + +echo $output->footer(); diff --git a/admin/tool/lp/edittemplate.php b/admin/tool/lp/edittemplate.php new file mode 100644 index 00000000000..d596f303b4e --- /dev/null +++ b/admin/tool/lp/edittemplate.php @@ -0,0 +1,94 @@ +. + +/** + * This page lets users to manage site wide learning plan templates. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +$id = optional_param('id', 0, PARAM_INT); +$returntype = optional_param('return', null, PARAM_ALPHA); +$pagecontextid = required_param('pagecontextid', PARAM_INT); // Reference to where we can from. + +$template = null; +if (!empty($id)) { + // Always use the context from the framework when it exists. + $template = new \core_competency\template($id); + $context = $template->get_context(); +} else { + $context = context::instance_by_id($pagecontextid); +} + +// We check that we have the permission to edit this framework, in its own context. +require_login(0, false); +\core_competency\api::require_enabled(); +require_capability('moodle/competency:templatemanage', $context); + +// We keep the original context in the URLs, so that we remain in the same context. +$url = new moodle_url("/admin/tool/lp/edittemplate.php", [ + 'id' => $id, + 'pagecontextid' => $pagecontextid, + 'return' => $returntype +]); + +if (empty($id)) { + $pagetitle = get_string('addnewtemplate', 'tool_lp'); + list($title, $subtitle, $returnurl) = \tool_lp\page_helper::setup_for_template($pagecontextid, $url, null, $pagetitle, + $returntype); +} else { + $template = \core_competency\api::read_template($id); + $pagetitle = get_string('edittemplate', 'tool_lp'); + list($title, $subtitle, $returnurl) = \tool_lp\page_helper::setup_for_template($pagecontextid, $url, $template, + $pagetitle, $returntype); +} + +$form = new \tool_lp\form\template($url->out(false), array('persistent' => $template, 'context' => $context)); +if ($form->is_cancelled()) { + redirect($returnurl); +} + +$data = $form->get_data(); +if ($data) { + if (empty($data->id)) { + $template = \core_competency\api::create_template($data); + $returnurl = new moodle_url('/admin/tool/lp/templatecompetencies.php', [ + 'templateid' => $template->get_id(), + 'pagecontextid' => $pagecontextid + ]); + $returnmsg = get_string('templatecreated', 'tool_lp'); + } else { + \core_competency\api::update_template($data); + $returnmsg = get_string('templateupdated', 'tool_lp'); + } + redirect($returnurl, $returnmsg, null, \core\output\notification::NOTIFY_SUCCESS); +} + +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); +echo $output->heading($title); +if (!empty($subtitle)) { + echo $output->heading($subtitle, 3); +} + +$form->display(); + +echo $output->footer(); diff --git a/admin/tool/lp/lang/en/tool_lp.php b/admin/tool/lp/lang/en/tool_lp.php new file mode 100644 index 00000000000..d4269a7b3f8 --- /dev/null +++ b/admin/tool/lp/lang/en/tool_lp.php @@ -0,0 +1,287 @@ +. + +/** + * Strings for component 'tool_lp', language 'en' + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['actions'] = 'Actions'; +$string['activities'] = 'Activities'; +$string['addcohorts'] = 'Add cohorts'; +$string['addcohortstosync'] = 'Add cohorts to sync'; +$string['addcompetency'] = 'Add competency'; +$string['addcoursecompetencies'] = 'Add competencies to course'; +$string['addcrossreferencedcompetency'] = 'Add cross-referenced competency'; +$string['addingcompetencywillresetparentrule'] = 'Adding a new competency will remove the rule set on \'{$a}\'. Do you want to continue?'; +$string['addnewcompetency'] = 'Add new competency'; +$string['addnewcompetencyframework'] = 'Add new competency framework'; +$string['addnewplan'] = 'Add new learning plan'; +$string['addnewtemplate'] = 'Add new learning plan template'; +$string['addnewuserevidence'] = 'Add new evidence'; +$string['addtemplatecompetencies'] = 'Add competencies to learning plan template'; +$string['aisrequired'] = '\'{$a}\' is required'; +$string['aplanswerecreated'] = '{$a} plans were created.'; +$string['aplanswerecreatedmoremayrequiresync'] = '{$a} plans were created, more will be created during the next synchronization.'; +$string['assigncohorts'] = 'Assign cohorts'; +$string['averageproficiencyrate'] = 'The average proficiency rate for completed plans based on this template is {$a} %'; +$string['cancelreviewrequest'] = 'Cancel review request'; +$string['cannotaddrules'] = 'This competency cannot be configured.'; +$string['cannotcreateuserplanswhentemplateduedateispassed'] = 'New user plans can not be created: this template\'s due date has, or is about to, expire.'; +$string['cannotcreateuserplanswhentemplatehidden'] = 'New user plans can not be created while this template is hidden.'; +$string['category'] = 'Category'; +$string['chooserating'] = 'Choose a rating...'; +$string['cohortssyncedtotemplate'] = 'Cohorts synced to this template'; +$string['competenciesforframework'] = 'Competencies for {$a}'; +$string['competenciesmostoftennotproficient'] = 'Competencies most often not proficient in completed plans'; +$string['competenciesmostoftennotproficientincourse'] = 'Competencies most often not proficient in this course'; +$string['competencycannotbedeleted'] = 'The competency \'{$a}\' can not be deleted'; +$string['competencycreated'] = 'Competency created'; +$string['competencycrossreferencedcompetencies'] = '{$a} cross-referenced competencies'; +$string['competencyframework'] = 'Competency framework'; +$string['competencyframeworkcreated'] = 'Competency framework created.'; +$string['competencyframeworkname'] = 'Name'; +$string['competencyframeworkroot'] = 'No parent (Top level competency)'; +$string['competencyframeworks'] = 'Competency Frameworks'; +$string['competencyframeworkupdated'] = 'Competency framework updated.'; +$string['competencyoutcome_complete'] = 'Mark as complete'; +$string['competencyoutcome_evidence'] = 'Attach an evidence'; +$string['competencyoutcome_none'] = 'None'; +$string['competencyoutcome_recommend'] = 'Recommend the competency'; +$string['competencypicker'] = 'Competency picker'; +$string['competencyrule'] = 'Competency rule'; +$string['competencyupdated'] = 'Competency updated'; +$string['completeplan'] = 'Complete this learning plan'; +$string['completeplanconfirm'] = 'Set the plan \'{$a}\' to completed? The current status of all the users competencies in this plan will be recorded, and the plan will become read only.'; +$string['configurecoursecompetencysettings'] = 'Configure course competencies'; +$string['configurescale'] = 'Configure scales'; +$string['coursecompetencies'] = 'Course competencies'; +$string['coursecompetencyratingsarenotpushedtouserplans'] = 'Competency ratings in this course do not affect individual learning plans.'; +$string['coursecompetencyratingsarepushedtouserplans'] = 'Competency ratings in this course are updated immediately in individual learning plans.'; +$string['coursecompetencyratingsquestion'] = 'When a course competency is rated, does the rating update the competency in the users learning plans, or is it only applied to the course?'; +$string['coursesusingthiscompetency'] = 'Courses linked to this competency'; +$string['coveragesummary'] = '{$a->competenciescoveredcount} of {$a->competenciescount} competencies are covered ( {$a->coveragepercentage} % )'; +$string['createplans'] = 'Create plans'; +$string['createlearningplans'] = 'Create learning plans'; +$string['crossreferencedcompetencies'] = 'Cross-referenced competencies'; +$string['default'] = 'Default'; +$string['deletecompetency'] = 'Delete competency \'{$a}\'?'; +$string['deletecompetencyframework'] = 'Delete competency framework \'{$a}\'?'; +$string['deletecompetencyparenthasrule'] = 'Delete competency \'{$a}\'? This will also remove the rule set on its parent.'; +$string['deleteplan'] = 'Delete plan \'{$a}\'?'; +$string['deleteplans'] = 'Delete the plans'; +$string['deletetemplate'] = 'Delete learning plan template \'{$a}\'?'; +$string['deletetemplatewithplans'] = 'This learning plan template has user plans associated. You have to indicate how to process those plans.'; +$string['deletethisplan'] = 'Delete this learning plan'; +$string['deletethisuserevidence'] = 'Delete this evidence'; +$string['deleteuserevidence'] = 'Delete the evidence of prior learning \'{$a}\'?'; +$string['description'] = 'Description'; +$string['duedate'] = 'Due date'; +$string['duedate_help'] = 'The date that a learning plan should be completed by.'; +$string['editcompetency'] = 'Edit competency'; +$string['editcompetencyframework'] = 'Edit competency framework'; +$string['editplan'] = 'Edit learning plan'; +$string['editrating'] = 'Edit rating'; +$string['edittemplate'] = 'Edit learning plan template'; +$string['editthisplan'] = 'Edit this learning plan'; +$string['editthisuserevidence'] = 'Edit this evidence'; +$string['edituserevidence'] = 'Edit evidence'; +$string['evidence'] = 'Evidence'; +$string['findcourses'] = 'Find courses'; +$string['frameworkcannotbedeleted'] = 'The competency framework \'{$a}\' can not be deleted'; +$string['hidden'] = 'Hidden'; +$string['hiddenhint'] = '(hidden)'; +$string['idnumber'] = 'Id number'; +$string['inheritfromframework'] = 'Inherit from framework (default)'; +$string['itemstoadd'] = 'Items to add'; +$string['jumptocompetency'] = 'Jump to competency'; +$string['jumptouser'] = 'Jump to user'; +$string['learningplancompetencies'] = 'Learning plan competencies'; +$string['learningplans'] = 'Learning plans'; +$string['levela'] = 'Level {$a}'; +$string['linkcompetencies'] = 'Link competencies'; +$string['linkcompetency'] = 'Link competency'; +$string['linkedcompetencies'] = 'Linked competencies'; +$string['linkedcourses'] = 'Linked courses'; +$string['linkedcourseslist'] = 'Linked courses:'; +$string['listcompetencyframeworkscaption'] = 'List of competency frameworks'; +$string['listofevidence'] = 'List of evidence'; +$string['listplanscaption'] = 'List of learning plans'; +$string['listtemplatescaption'] = 'List of learning plan templates'; +$string['loading'] = 'Loading...'; +$string['locatecompetency'] = 'Locate competency'; +$string['managecompetenciesandframeworks'] = 'Manage competencies and frameworks'; +$string['modcompetencies'] = 'Course competencies'; +$string['modcompetencies_help'] = 'Course competencies linked to this activity.'; +$string['move'] = 'Move'; +$string['movecompetency'] = 'Move competency'; +$string['movecompetencyafter'] = 'Move competency after \'{$a}\''; +$string['movecompetencyframework'] = 'Move competency framework'; +$string['movecompetencytochildofselfwillresetrules'] = 'Moving the competency will remove its own rule, and the rules set on its parent and destination. Do you want to continune?'; +$string['movecompetencywillresetrules'] = 'Moving the competency will remove the rules set on its parent and destination. Do you want to continune?'; +$string['moveframeworkafter'] = 'Move competency framework after \'{$a}\''; +$string['movetonewparent'] = 'Relocate'; +$string['myplans'] = 'My plans'; +$string['nfiles'] = '{$a} file(s)'; +$string['noactivities'] = 'No activities'; +$string['nocompetencies'] = 'No competencies have been created in this framework.'; +$string['nocompetenciesincourse'] = 'No competencies have been linked to this course.'; +$string['nocompetenciesintemplate'] = 'No competencies have been linked to this template.'; +$string['nocompetencyframeworks'] = 'No competency frameworks have been created yet.'; +$string['nocompetencyselected'] = 'No competency selected'; +$string['nocrossreferencedcompetencies'] = 'No other competencies have been cross-referenced to this competency.'; +$string['noevidence'] = 'No evidence'; +$string['nofiles'] = 'No files'; +$string['nolinkedcourses'] = 'No courses are linked to this competency'; +$string['noparticipants'] = 'No participants found.'; +$string['noplanswerecreated'] = 'No plans were created.'; +$string['notemplates'] = 'No learning plan templates have been created yet.'; +$string['nourl'] = 'No URL'; +$string['nouserevidence'] = 'No evidence of prior learning have been added yet.'; +$string['nouserplans'] = 'No learning plans have been created yet.'; +$string['oneplanwascreated'] = 'A plan was created'; +$string['outcome'] = 'Outcome'; +$string['path'] = 'Path:'; +$string['parentcompetency'] = 'Parent'; +$string['parentcompetency_edit'] = 'Edit parent'; +$string['parentcompetency_help'] = 'Define the parent under which the competency will be added. It can either be another competency within the same framework, or the root of the competency framework for a top level competency.'; +$string['planapprove'] = 'Make active'; +$string['plancompleted'] = 'Plan completed'; +$string['plancreated'] = 'Learning plan created'; +$string['plandescription'] = 'Description'; +$string['planname'] = 'Name'; +$string['plantemplate'] = 'Select template'; +$string['plantemplate_help'] = 'A learning plan created from a template will contain a list of competencies that match the template. Updates to the template will be reflected in any plan created from that template.'; +$string['planunapprove'] = 'Send back to draft'; +$string['planupdated'] = 'Learning plan updated'; +$string['pluginname'] = 'Learning Plans'; +$string['points'] = 'Points'; +$string['pointsgivenfor'] = 'Points given for \'{$a}\''; +$string['proficient'] = 'Proficient'; +$string['progress'] = 'Progress'; +$string['rate'] = 'Rate'; +$string['ratecomment'] = 'Evidence notes'; +$string['rating'] = 'Rating'; +$string['ratingaffectsonlycourse'] = 'Rating a competency only updates the competency in this course'; +$string['ratingaffectsuserplans'] = 'Rating a competency also updates the competency in all of the users learning plans'; +$string['reopenplan'] = 'Reopen this learning plan'; +$string['reopenplanconfirm'] = 'Reopen the plan \'{$a}\'? The status of the users competencies that was recorded at the time the plan was previously completed will be deleted, and the plan will become active again.'; +$string['requestreview'] = 'Request review'; +$string['reviewer'] = 'Reviewer'; +$string['reviewstatus'] = 'Review status'; +$string['savechanges'] = 'Save changes'; +$string['scale'] = 'Scale'; +$string['scale_help'] = 'A scale determines how proficiency is measured in a competency. After selecting a scale, you must configure it. The item selected for Default is the rating that will be assigned when a competency is set to be automatically completed. The items selected for Proficient indicate which values will mark the competencies as proficient when they are rated.'; +$string['scalevalue'] = 'Scale value'; +$string['search'] = 'Search...'; +$string['selectcohortstosync'] = 'Select cohorts to sync'; +$string['selectcompetencymovetarget'] = 'Select a location to move this competency to:'; +$string['selectedcompetency'] = 'Selected competency'; +$string['selectuserstocreateplansfor'] = 'Select users to create plans for'; +$string['sendallcompetenciestoreview'] = 'Send all competencies in review for evidence of prior learning \'{$a}\''; +$string['sendcompetenciestoreview'] = 'Send competencies for review'; +$string['shortname'] = 'Name'; +$string['sitedefault'] = ' (Site default) '; +$string['startreview'] = 'Start review'; +$string['state'] = 'State'; +$string['status'] = 'Status'; +$string['stopreview'] = 'Finish review'; +$string['stopsyncingcohort'] = 'Stop syncing cohort'; +$string['taxonomies'] = 'Taxonomies'; +$string['taxonomy_add_behaviour'] = 'Add behaviour'; +$string['taxonomy_add_competency'] = 'Add competency'; +$string['taxonomy_add_concept'] = 'Add concept'; +$string['taxonomy_add_domain'] = 'Add domain'; +$string['taxonomy_add_indicator'] = 'Add indicator'; +$string['taxonomy_add_level'] = 'Add level'; +$string['taxonomy_add_outcome'] = 'Add outcome'; +$string['taxonomy_add_practice'] = 'Add practice'; +$string['taxonomy_add_proficiency'] = 'Add proficiency'; +$string['taxonomy_add_skill'] = 'Add skill'; +$string['taxonomy_add_value'] = 'Add value'; +$string['taxonomy_edit_behaviour'] = 'Edit behaviour'; +$string['taxonomy_edit_competency'] = 'Edit competency'; +$string['taxonomy_edit_concept'] = 'Edit concept'; +$string['taxonomy_edit_domain'] = 'Edit domain'; +$string['taxonomy_edit_indicator'] = 'Edit indicator'; +$string['taxonomy_edit_level'] = 'Edit level'; +$string['taxonomy_edit_outcome'] = 'Edit outcome'; +$string['taxonomy_edit_practice'] = 'Edit practice'; +$string['taxonomy_edit_proficiency'] = 'Edit proficiency'; +$string['taxonomy_edit_skill'] = 'Edit skill'; +$string['taxonomy_edit_value'] = 'Edit value'; +$string['taxonomy_parent_behaviour'] = 'Parent behaviour'; +$string['taxonomy_parent_competency'] = 'Parent competency'; +$string['taxonomy_parent_concept'] = 'Parent concept'; +$string['taxonomy_parent_domain'] = 'Parent domain'; +$string['taxonomy_parent_indicator'] = 'Parent indicator'; +$string['taxonomy_parent_level'] = 'Parent level'; +$string['taxonomy_parent_outcome'] = 'Parent outcome'; +$string['taxonomy_parent_practice'] = 'Parent practice'; +$string['taxonomy_parent_proficiency'] = 'Parent proficiency'; +$string['taxonomy_parent_skill'] = 'Parent skill'; +$string['taxonomy_parent_value'] = 'Parent value'; +$string['taxonomy_selected_behaviour'] = 'Selected behaviour'; +$string['taxonomy_selected_competency'] = 'Selected competency'; +$string['taxonomy_selected_concept'] = 'Selected concept'; +$string['taxonomy_selected_domain'] = 'Selected domain'; +$string['taxonomy_selected_indicator'] = 'Selected indicator'; +$string['taxonomy_selected_level'] = 'Selected level'; +$string['taxonomy_selected_outcome'] = 'Selected outcome'; +$string['taxonomy_selected_practice'] = 'Selected practice'; +$string['taxonomy_selected_proficiency'] = 'Selected proficiency'; +$string['taxonomy_selected_skill'] = 'Selected skill'; +$string['taxonomy_selected_value'] = 'Selected value'; +$string['template'] = 'Template'; +$string['templatebased'] = 'Template based'; +$string['templatecohortnotsyncedwhileduedateispassed'] = 'Cohorts will not be synchronized while the template\'s due date is passed.'; +$string['templatecohortnotsyncedwhilehidden'] = 'Cohorts will not be synchronized while this template is hidden.'; +$string['templatecompetencies'] = 'Template competencies'; +$string['templatecreated'] = 'Learning plan template created'; +$string['templatename'] = 'Name'; +$string['templates'] = 'Learning plan templates'; +$string['templateupdated'] = 'Learning plan template updated'; +$string['totalrequiredtocomplete'] = 'Total required to complete'; +$string['unlinkcompetencycourse'] = 'Unlink the competency \'{$a}\' from the course?'; +$string['unlinkcompetencyplan'] = 'Unlink the competency \'{$a}\' from the plan?'; +$string['unlinkcompetencytemplate'] = 'Unlink the competency \'{$a}\' from the template?'; +$string['unlinkplanstemplate'] = 'Unlink the plans from template'; +$string['unlinkplantemplate'] = 'Unlink from template'; +$string['unlinkplantemplateconfirm'] = 'Unlink the plan \'{$a}\' from its template? Any change made to the template will no longer be applied to the plan. This action can not be undone.'; +$string['uponcoursecompletion'] = 'Upon course completion:'; +$string['uponcoursemodulecompletion'] = 'Upon activity completion:'; +$string['usercompetencyfrozen'] = 'This record is now frozen. It reflects the state of the user\'s competency when their plan was marked as complete.'; +$string['userevidence'] = 'Evidence of prior learning'; +$string['userevidencecreated'] = 'Evidence of prior learning created'; +$string['userevidencedescription'] = 'Description'; +$string['userevidencefiles'] = 'Files'; +$string['userevidencename'] = 'Name'; +$string['userevidencesummary'] = 'Summary'; +$string['userevidenceupdated'] = 'Evidence of prior learning updated'; +$string['userevidenceurl'] = 'URL'; +$string['viewdetails'] = 'View details'; +$string['visible'] = 'Visible'; +$string['visible_help'] = 'A competency framework can be hidden from teachers. This could be useful if a framework is still in the process of being developed.'; +$string['when'] = 'When'; +$string['xcompetencieslinkedoutofy'] = '{$a->x} out of {$a->y} competencies linked to courses'; +$string['xcompetenciesproficientoutofy'] = '{$a->x} out of {$a->y} competencies are proficient'; +$string['xcompetenciesproficientoutofyincourse'] = 'You are proficient in {$a->x} out of {$a->y} competencies in this course.'; +$string['xplanscompletedoutofy'] = '{$a->x} out of {$a->y} plans completed for this template'; diff --git a/admin/tool/lp/learningplans.php b/admin/tool/lp/learningplans.php new file mode 100644 index 00000000000..cc16639f0b1 --- /dev/null +++ b/admin/tool/lp/learningplans.php @@ -0,0 +1,45 @@ +. + +/** + * This page lets users to manage site wide competencies. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +$pagecontextid = required_param('pagecontextid', PARAM_INT); +$context = context::instance_by_id($pagecontextid); + +require_login(0, false); +\core_competency\api::require_enabled(); +if (!\core_competency\template::can_read_context($context)) { + throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', ''); +} + +$url = new moodle_url('/admin/tool/lp/learningplans.php', array('pagecontextid' => $pagecontextid)); +list($title, $subtitle) = \tool_lp\page_helper::setup_for_template($pagecontextid, $url); + +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); +echo $output->heading($title); +$page = new \tool_lp\output\manage_templates_page($context); +echo $output->render($page); +echo $output->footer(); diff --git a/admin/tool/lp/lib.php b/admin/tool/lp/lib.php new file mode 100644 index 00000000000..04a5dfe9211 --- /dev/null +++ b/admin/tool/lp/lib.php @@ -0,0 +1,237 @@ +. + +/** + * This page contains navigation hooks for learning plans. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * This function extends the course navigation + * + * @param navigation_node $navigation The navigation node to extend + * @param stdClass $course The course to object for the tool + * @param context $coursecontext The context of the course + */ +function tool_lp_extend_navigation_course($navigation, $course, $coursecontext) { + if (!\core_competency\api::is_enabled()) { + return; + } + + // Just a link to course report. + $title = get_string('coursecompetencies', 'tool_lp'); + $path = new moodle_url("/admin/tool/lp/coursecompetencies.php", array('courseid' => $course->id)); + $settingsnode = navigation_node::create($title, + $path, + navigation_node::TYPE_SETTING, + null, + null, + new pix_icon('competency', '', 'tool_lp')); + if (isset($settingsnode)) { + $navigation->add_node($settingsnode); + } +} + + +/** + * This function extends the user navigation. + * + * @param navigation_node $navigation The navigation node to extend + * @param stdClass $user The user object + * @param context_user $usercontext The user context + * @param stdClass $course The course object + * @param context_course $coursecontext The context of the course + */ +function tool_lp_extend_navigation_user($navigation, $user, $usercontext, $course, $coursecontext) { + if (!\core_competency\api::is_enabled()) { + return; + } + + if (\core_competency\plan::can_read_user($user->id)) { + $node = $navigation->add(get_string('learningplans', 'tool_lp'), + new moodle_url('/admin/tool/lp/plans.php', array('userid' => $user->id))); + + if (\core_competency\user_evidence::can_read_user($user->id)) { + $node->add(get_string('userevidence', 'tool_lp'), + new moodle_url('/admin/tool/lp/user_evidence_list.php', array('userid' => $user->id))); + } + } + +} + +/** + * Add nodes to myprofile page. + * + * @param \core_user\output\myprofile\tree $tree Tree object + * @param stdClass $user user object + * @param bool $iscurrentuser + * @param stdClass $course Course object + * + * @return bool + */ +function tool_lp_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) { + if (!\core_competency\api::is_enabled()) { + return false; + } else if (!\core_competency\plan::can_read_user($user->id)) { + return false; + } + + $url = new moodle_url('/admin/tool/lp/plans.php', array('userid' => $user->id)); + $node = new core_user\output\myprofile\node('miscellaneous', 'learningplans', + get_string('learningplans', 'tool_lp'), null, $url); + $tree->add_node($node); + + return true; +} + +/** + * This function extends the category navigation to add learning plan links. + * + * @param navigation_node $navigation The navigation node to extend + * @param context $coursecategorycontext The context of the course category + */ +function tool_lp_extend_navigation_category_settings($navigation, $coursecategorycontext) { + if (!\core_competency\api::is_enabled()) { + return false; + } + + // We check permissions before renderring the links. + $templatereadcapability = \core_competency\template::can_read_context($coursecategorycontext); + $competencyreadcapability = \core_competency\competency_framework::can_read_context($coursecategorycontext); + if (!$templatereadcapability && !$competencyreadcapability) { + return false; + } + + // The link to the learning plan page. + if ($templatereadcapability) { + $title = get_string('templates', 'tool_lp'); + $path = new moodle_url("/admin/tool/lp/learningplans.php", array('pagecontextid' => $coursecategorycontext->id)); + $settingsnode = navigation_node::create($title, + $path, + navigation_node::TYPE_SETTING, + null, + null, + new pix_icon('competency', '', 'tool_lp')); + if (isset($settingsnode)) { + $navigation->add_node($settingsnode); + } + } + + // The link to the competency frameworks page. + if ($competencyreadcapability) { + $title = get_string('competencyframeworks', 'tool_lp'); + $path = new moodle_url("/admin/tool/lp/competencyframeworks.php", array('pagecontextid' => $coursecategorycontext->id)); + $settingsnode = navigation_node::create($title, + $path, + navigation_node::TYPE_SETTING, + null, + null, + new pix_icon('competency', '', 'tool_lp')); + if (isset($settingsnode)) { + $navigation->add_node($settingsnode); + } + } +} + +/** + * Inject the competencies elements into all moodle module settings forms. + * + * @param moodleform $formwrapper The moodle quickforms wrapper object. + * @param MoodleQuickForm $mform The actual form object (required to modify the form). + */ +function tool_lp_coursemodule_standard_elements($formwrapper, $mform) { + global $CFG, $COURSE; + + if (!\core_competency\api::is_enabled()) { + return; + } else if (!has_capability('moodle/competency:coursecompetencymanage', $formwrapper->get_context())) { + return; + } + + $mform->addElement('header', 'competenciessection', get_string('competencies', 'core_competency')); + + MoodleQuickForm::registerElementType('course_competencies', + "$CFG->dirroot/admin/tool/lp/classes/course_competencies_form_element.php", + 'tool_lp_course_competencies_form_element'); + $cmid = null; + if ($cm = $formwrapper->get_coursemodule()) { + $cmid = $cm->id; + } + $options = array( + 'courseid' => $COURSE->id, + 'cmid' => $cmid + ); + $mform->addElement('course_competencies', 'competencies', get_string('modcompetencies', 'tool_lp'), $options); + $mform->addHelpButton('competencies', 'modcompetencies', 'tool_lp'); + MoodleQuickForm::registerElementType('course_competency_rule', + "$CFG->dirroot/admin/tool/lp/classes/course_competency_rule_form_element.php", + 'tool_lp_course_competency_rule_form_element'); + // Reuse the same options. + $mform->addElement('course_competency_rule', 'competency_rule', get_string('uponcoursemodulecompletion', 'tool_lp'), $options); +} + +/** + * Hook the add/edit of the course module. + * + * @param stdClass $data Data from the form submission. + * @param stdClass $course The course. + */ +function tool_lp_coursemodule_edit_post_actions($data, $course) { + if (!\core_competency\api::is_enabled()) { + return $data; + } + + // It seems like the form did not contain any of the form fields, we can return. + if (!isset($data->competency_rule) && !isset($data->competencies)) { + return $data; + } + + // We bypass the API here and go direct to the persistent layer - because we don't want to do permission + // checks here - we need to load the real list of existing course module competencies. + $existing = \core_competency\course_module_competency::list_course_module_competencies($data->coursemodule); + + $existingids = array(); + foreach ($existing as $cmc) { + array_push($existingids, $cmc->get_competencyid()); + } + + $newids = isset($data->competencies) ? $data->competencies : array(); + + $removed = array_diff($existingids, $newids); + $added = array_diff($newids, $existingids); + + foreach ($removed as $removedid) { + \core_competency\api::remove_competency_from_course_module($data->coursemodule, $removedid); + } + foreach ($added as $addedid) { + \core_competency\api::add_competency_to_course_module($data->coursemodule, $addedid); + } + + if (isset($data->competency_rule)) { + // Now update the rules for each course_module_competency. + $current = \core_competency\api::list_course_module_competencies_in_course_module($data->coursemodule); + foreach ($current as $coursemodulecompetency) { + \core_competency\api::set_course_module_competency_ruleoutcome($coursemodulecompetency, $data->competency_rule); + } + } + + return $data; +} diff --git a/admin/tool/lp/pix/competency.png b/admin/tool/lp/pix/competency.png new file mode 100644 index 00000000000..52fe956c0ef Binary files /dev/null and b/admin/tool/lp/pix/competency.png differ diff --git a/admin/tool/lp/pix/competency.svg b/admin/tool/lp/pix/competency.svg new file mode 100644 index 00000000000..2f845cee491 --- /dev/null +++ b/admin/tool/lp/pix/competency.svg @@ -0,0 +1,76 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/admin/tool/lp/pix/url.png b/admin/tool/lp/pix/url.png new file mode 100644 index 00000000000..226105a5709 Binary files /dev/null and b/admin/tool/lp/pix/url.png differ diff --git a/admin/tool/lp/pix/url.svg b/admin/tool/lp/pix/url.svg new file mode 100644 index 00000000000..9528b2b0282 --- /dev/null +++ b/admin/tool/lp/pix/url.svg @@ -0,0 +1,21 @@ + + + +]> + + + + + diff --git a/admin/tool/lp/plan.php b/admin/tool/lp/plan.php new file mode 100644 index 00000000000..fded203dbe5 --- /dev/null +++ b/admin/tool/lp/plan.php @@ -0,0 +1,49 @@ +. + +/** + * Plan page. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__ . '/../../../config.php'); + +$id = required_param('id', PARAM_INT); + +require_login(null, false); +if (isguestuser()) { + throw new require_login_exception('Guests are not allowed here.'); +} +\core_competency\api::require_enabled(); + +$plan = \core_competency\api::read_plan($id); +$url = new moodle_url('/admin/tool/lp/plan.php', array('id' => $id)); + +list($title, $subtitle) = \tool_lp\page_helper::setup_for_plan($plan->get_userid(), $url, $plan); + +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); + +$page = new \tool_lp\output\plan_page($plan); +echo $output->render($page); + +// Trigger viewed event. +\core_competency\api::plan_viewed($plan); + +echo $output->footer(); diff --git a/admin/tool/lp/plans.php b/admin/tool/lp/plans.php new file mode 100644 index 00000000000..7ac14189cc6 --- /dev/null +++ b/admin/tool/lp/plans.php @@ -0,0 +1,45 @@ +. + +/** + * This page lets users see their learning plans. + * + * @package tool_lp + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); + +require_login(null, false); +if (isguestuser()) { + throw new require_login_exception('Guests are not allowed here.'); +} +\core_competency\api::require_enabled(); + +$userid = optional_param('userid', $USER->id, PARAM_INT); + +$url = new moodle_url('/admin/tool/lp/plans.php', array('userid' => $userid)); +list($title, $subtitle) = \tool_lp\page_helper::setup_for_plan($userid, $url); + +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); +echo $output->heading($title); + +$page = new \tool_lp\output\plans_page($userid); +echo $output->render($page); + +echo $output->footer(); diff --git a/admin/tool/lp/settings.php b/admin/tool/lp/settings.php new file mode 100644 index 00000000000..80758aedc94 --- /dev/null +++ b/admin/tool/lp/settings.php @@ -0,0 +1,50 @@ +. + +/** + * Links and settings. + * + * This file contains links and settings used by tool_lp. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +$parentname = 'competencies'; + +// If the plugin is enabled we add the pages. +if (\core_competency\api::is_enabled()) { + + // Manage competency frameworks page. + $temp = new admin_externalpage( + 'toollpcompetencies', + get_string('competencyframeworks', 'tool_lp'), + new moodle_url('/admin/tool/lp/competencyframeworks.php', array('pagecontextid' => context_system::instance()->id)), + array('moodle/competency:competencymanage') + ); + $ADMIN->add($parentname, $temp); + + // Manage learning plans page. + $temp = new admin_externalpage( + 'toollplearningplans', + get_string('templates', 'tool_lp'), + new moodle_url('/admin/tool/lp/learningplans.php', array('pagecontextid' => context_system::instance()->id)), + array('moodle/competency:templatemanage') + ); + $ADMIN->add($parentname, $temp); +} diff --git a/admin/tool/lp/styles.css b/admin/tool/lp/styles.css new file mode 100644 index 00000000000..c725a954e06 --- /dev/null +++ b/admin/tool/lp/styles.css @@ -0,0 +1,243 @@ +.path-admin-tool-lp [data-region="managecompetencies"] ul li, +.path-admin-tool-lp [data-region="plans"] ul li, +.path-admin-tool-lp [data-region="competencymovetree"] ul li, +.path-admin-tool-lp [data-region="competencylinktree"] ul li { + list-style-type: none; +} +.path-admin-tool-lp .progresstext { + display: inline-block; + height: 40px; + vertical-align: top; +} +.path-admin-tool-lp .progress { + width: 10em; + display: inline-block; + margin-left: 2em; + margin-right: 2em; +} +.dir-rtl.path-admin-tool-lp .progress .bar { + float: right; +} +.path-admin-tool-lp [data-region="managecompetencies"] ul[data-enhance="tree"], +.path-admin-tool-lp [data-region="plans"] ul[data-enhance="tree"], +.path-admin-tool-lp [data-region="competencylinktree"] ul[data-enhance="linktree"], +.path-admin-tool-lp [data-region="competencymovetree"] ul[data-enhance="movetree"] { + border: 1px solid #ccc; + box-shadow: inset 0 1px 1px rgba(0,0,0,0.075); + transition: border linear .2s,box-shadow linear .2s; + border-radius: 4px; + padding-left: 20px; + padding-right: 20px; + margin-left: 10px; + margin-right: 10px; +} +.path-admin-tool-lp [data-region="managecompetencies"] ul, +.path-admin-tool-lp [data-region="plans"] ul, +.path-admin-tool-lp [data-region="competencylinktree"] ul, +.path-admin-tool-lp [data-region="competencymovetree"] ul { + cursor: pointer; +} +.path-admin-tool-lp [data-region="competencylinktree"] ul li>span, +.path-admin-tool-lp [data-region="competencymovetree"] ul li>span, +.path-admin-tool-lp [data-region="plans"] ul li>span, +.path-admin-tool-lp [data-region="managecompetencies"] ul li>span { + padding-top: 2px; + padding-bottom: 2px; + padding-left: 4px; + padding-right: 4px; + border-radius: 4px; +} +.path-admin-tool-lp [data-region="competencylinktree"] ul [aria-selected="true"]>span, +.path-admin-tool-lp [data-region="competencymovetree"] ul [aria-selected="true"]>span, +.path-admin-tool-lp [data-region="plans"] ul [aria-selected="true"]>span, +.path-admin-tool-lp [data-region="managecompetencies"] ul [aria-selected="true"]>span { + background-color: #dfdfdf; +} +.path-admin-tool-lp [data-region="competencylinktree"] ul [tabindex="0"]>span, +.path-admin-tool-lp [data-region="competencymovetree"] ul [tabindex="0"]>span, +.path-admin-tool-lp [data-region="plans"] ul [tabindex="0"]>span, +.path-admin-tool-lp [data-region="managecompetencies"] ul [tabindex="0"]>span { + border: 2px solid #0070a8; +} +.path-admin-tool-lp [data-region="filtercompetencies"] input { + margin-left: 10px; +} + +.dir-rtl.path-admin-tool-lp [data-region="managecompetencies"] .row-fluid [class*="span"] { + float: right; +} + +.path-admin-tool-lp [data-region="link-buttons"], +.path-admin-tool-lp [data-region="move-buttons"] { + text-align: center; +} + +.path-admin-tool-lp [data-region="competencylinktree"]>ul { + overflow-y: auto; + height: 400px; +} + +.dir-rtl.path-admin-tool-lp [data-region="filtercompetencies"] input { + margin-right: 10px; +} + +.path-admin-tool-lp span.currentdragtarget { + border: 1px dashed; +} + +.path-admin-tool-lp .competencyactionsmenu { + display: inline-block; + vertical-align: text-top; +} + +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-base"] { + display: table; + width: 100%; +} +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"], +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] { + display: table-row; +} +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"] label, +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] label { + padding-right: 10px; +} +.dir-rtl.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"] label, +.dir-rtl.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] label { + padding-left: 10px; + padding-right: 0; +} +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"] label, +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"] select, +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] label, +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] select { + display: table-cell; +} +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"] select, +.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] select { + width: 100%; +} + +.path-admin-tool-lp .competency-rule-points { + margin-top: 10px; +} +.path-admin-tool-lp .competency-rule-points table input { + margin-bottom: 0; +} +.path-admin-tool-lp .competency-rule-points tr[data-competency] th { + font-weight: normal; +} +.path-admin-tool-lp .competency-rule-points input[type="number"] { + width: 50px; +} + +.competency-heading { + margin-bottom: 15px; +} +.competency-heading h4 { + margin: 0; +} + +/** Styles required for menubar js */ + +.tool-lp-menu { + margin: 0; +} + +.tool-lp-menu li { + float: left; + display: inline; + position: relative; + list-style-type: none; + white-space: nowrap; +} + +.tool-lp-sub-menu { + position: absolute; + list-style: none; + margin: 0; + top: -10px; +} + +.tool-lp-sub-menu li { + float: none; +} +.tool-lp-menu .tool-lp-sub-menu[aria-hidden=false] { + display: block; +} + +.tool-lp-menu ul[aria-hidden=true] { + display: none; +} + +.tool-lp-menu .caret { + margin: 8px; +} + +.tool-lp-menu.tool-lp-menu-open-left .tool-lp-sub-menu { + margin-left: -120px; +} + +.dir-rtl .tool-lp-menu.tool-lp-menu-open-left .tool-lp-sub-menu { + margin-left: 0px; + margin-right: -120px; +} + +/** This highlighting is copied from bootstrap - but can be overridden by a theme */ +.tool-lp-menu .tool-lp-sub-menu .menu-focus a { + color: #fff ; + text-decoration: none; + background-color: #00699e; + background-image: linear-gradient(to bottom,#0070a8,#005f8f); + background-repeat: repeat-x; +} + +/** check box and radio button on configure scale dialogue */ +input[type="radio"].tool_lp_scale_default, +input[type="checkbox"].tool_lp_scale_proficient { + margin-top: 0px; +} + +/** User evidence */ +.user-evidence-documents { + margin: 10px 20px; + list-style: none; +} +.user-evidence-competencies, +.user-evidence-documents li { + margin-bottom: 5px; + word-break: break-all; +} +[data-region="user-evidence-list"] .user-evidence-competencies, +[data-region="user-evidence-list"] .user-evidence-documents { + margin: 0; + list-style: none; +} + +.user-competency-course-navigation select { + display: none; +} +.user-competency-course-navigation { + width: 240px; +} + +/** Competency grader */ +.competency-grader textarea { + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +/** Broader RTL changes */ +.dir-rtl.path-admin-tool-lp .pull-left { + float: right; +} +.dir-rtl.path-admin-tool-lp .pull-right { + float: left; +} +.dir-rtl.path-admin-tool-lp dd { + margin-right: 10px; +} +.dir-rtl.path-admin-tool-lp ul.inline { + margin-right: 0; +} diff --git a/admin/tool/lp/template_cohorts.php b/admin/tool/lp/template_cohorts.php new file mode 100644 index 00000000000..b92dcd0b96d --- /dev/null +++ b/admin/tool/lp/template_cohorts.php @@ -0,0 +1,108 @@ +. + +/** + * List cohorts linked to a template. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__ . '/../../../config.php'); + +$id = required_param('id', PARAM_INT); +$pagecontextid = required_param('pagecontextid', PARAM_INT); // Reference to the context we came from. + +require_login(0, false); +\core_competency\api::require_enabled(); + +$template = \core_competency\api::read_template($id); +$context = $template->get_context(); +$canreadtemplate = $template->can_read(); +$canmanagetemplate = $template->can_manage(); +$duedatereached = $template->get_duedate() > 0 && $template->get_duedate() < time(); + +if (!$canreadtemplate) { + throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', ''); +} + +// Set up the page. +$url = new moodle_url('/admin/tool/lp/template_cohorts.php', array( + 'id' => $id, + 'pagecontextid' => $pagecontextid +)); +list($title, $subtitle) = \tool_lp\page_helper::setup_for_template($pagecontextid, $url, $template, + get_string('cohortssyncedtotemplate', 'tool_lp')); + +// Remove cohort. +if ($canmanagetemplate && ($removecohort = optional_param('removecohort', false, PARAM_INT)) !== false && confirm_sesskey()) { + \core_competency\api::delete_template_cohort($template, $removecohort); +} + +// Capture the form submission. +$form = new \tool_lp\form\template_cohorts($url->out(false), array('pagecontextid' => $pagecontextid)); +if ($canmanagetemplate && ($data = $form->get_data()) && !empty($data->cohorts)) { + $maxtocreate = 50; + $maxreached = false; + $i = 0; + foreach ($data->cohorts as $cohortid) { + + // Create the template/cohort relationship. + $relation = \core_competency\api::create_template_cohort($template, $cohortid); + + // Create a plan for each member if template visible, and the due date is not reached, and we didn't reach our limit yet. + if ($template->get_visible() && $i < $maxtocreate && !$duedatereached) { + + // Only create a few plans right now. + $tocreate = \core_competency\template_cohort::get_missing_plans($template->get_id(), $cohortid); + if ($i + count($tocreate) <= $maxtocreate) { + $i += \core_competency\api::create_plans_from_template_cohort($template, $cohortid); + } else { + $maxreached = true; + } + } + } + if ($i == 0) { + $notification = get_string('noplanswerecreated', 'tool_lp'); + } else if ($i == 1) { + $notification = get_string('oneplanwascreated', 'tool_lp'); + } else if ($maxreached) { + $notification = get_string('aplanswerecreatedmoremayrequiresync', 'tool_lp', $i); + } else { + $notification = get_string('aplanswerecreated', 'tool_lp', $i); + } + redirect($url, $notification); +} + +// Display the page. +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); +echo $output->heading($title); +echo $output->heading($subtitle, 3); +if ($canmanagetemplate) { + if ($template->get_visible() == false) { + // Display message to prevent that cohort will not be synchronzed if the template is hidden. + echo $output->notify_message(get_string('templatecohortnotsyncedwhilehidden', 'tool_lp')); + } else if ($duedatereached) { + echo $output->notify_message(get_string('templatecohortnotsyncedwhileduedateispassed', 'tool_lp')); + } + echo $form->display(); +} + +$page = new \tool_lp\output\template_cohorts_page($template, $url); +echo $output->render($page); +echo $output->footer(); diff --git a/admin/tool/lp/template_plans.php b/admin/tool/lp/template_plans.php new file mode 100644 index 00000000000..37586b5097b --- /dev/null +++ b/admin/tool/lp/template_plans.php @@ -0,0 +1,90 @@ +. + +/** + * List plans derived from the template. + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__ . '/../../../config.php'); + +$id = required_param('id', PARAM_INT); +$pagecontextid = required_param('pagecontextid', PARAM_INT); // Reference to the context we came from. + +require_login(0, false); +\core_competency\api::require_enabled(); + +$template = \core_competency\api::read_template($id); +$context = $template->get_context(); +$canreadtemplate = $template->can_read(); +$canmanagetemplate = $template->can_manage(); +if (!$canreadtemplate) { + throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', ''); +} + +// Set up the page. +$url = new moodle_url('/admin/tool/lp/template_plans.php', array( + 'id' => $id, + 'pagecontextid' => $pagecontextid +)); +list($title, $subtitle) = \tool_lp\page_helper::setup_for_template($pagecontextid, $url, $template, + get_string('userplans', 'core_competency')); + +// Capture the form submission. +$form = new \tool_lp\form\template_plans($url->out(false)); +if ($canmanagetemplate && ($data = $form->get_data()) && !empty($data->users)) { + $i = 0; + foreach ($data->users as $userid) { + $result = \core_competency\api::create_plan_from_template($template->get_id(), $userid); + if ($result) { + $i++; + } + } + if ($i == 0) { + $notification = get_string('noplanswerecreated', 'tool_lp'); + } else if ($i == 1) { + $notification = get_string('oneplanwascreated', 'tool_lp'); + } else { + $notification = get_string('aplanswerecreated', 'tool_lp', $i); + } + redirect($url, $notification); +} + +// Display the page. +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); +echo $output->heading($title); +echo $output->heading($subtitle, 3); + +// Do not display form when the template is hidden. +if ($canmanagetemplate) { + if (!$template->get_visible()) { + // Display message that plan can not be created if the template is hidden. + echo $output->notify_message(get_string('cannotcreateuserplanswhentemplatehidden', 'tool_lp')); + } else if ($template->get_duedate() > 0 && $template->get_duedate() < time() + 900) { + // Prevent the user from creating plans when the due date is passed, or in less than 15 minutes. + echo $output->notify_message(get_string('cannotcreateuserplanswhentemplateduedateispassed', 'tool_lp')); + } else { + echo $form->display(); + } +} + +$page = new \tool_lp\output\template_plans_page($template, $url); +echo $output->render($page); +echo $output->footer(); diff --git a/admin/tool/lp/templatecompetencies.php b/admin/tool/lp/templatecompetencies.php new file mode 100644 index 00000000000..a18f2169a0d --- /dev/null +++ b/admin/tool/lp/templatecompetencies.php @@ -0,0 +1,54 @@ +. + +/** + * This page lets users to manage template competencies. + * + * @package tool_lp + * @copyright 2015 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +$templateid = required_param('templateid', PARAM_INT); +$pagecontextid = required_param('pagecontextid', PARAM_INT); // Reference to the context we came from. + +require_login(0, false); +\core_competency\api::require_enabled(); + +$pagecontext = context::instance_by_id($pagecontextid); +$template = \core_competency\api::read_template($templateid); +$context = $template->get_context(); +if (!$template->can_read()) { + throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', ''); +} + +// Trigger a template viewed event. +\core_competency\api::template_viewed($template); + +// Set up the page. +$url = new moodle_url('/admin/tool/lp/templatecompetencies.php', array('templateid' => $template->get_id(), + 'pagecontextid' => $pagecontextid)); +list($title, $subtitle) = \tool_lp\page_helper::setup_for_template($pagecontextid, $url, $template); + +// Display the page. +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); +$page = new \tool_lp\output\template_competencies_page($template, $pagecontext); +echo $output->render($page); +echo $output->footer(); diff --git a/admin/tool/lp/templates/action_selector.mustache b/admin/tool/lp/templates/action_selector.mustache new file mode 100644 index 00000000000..1f307a7b138 --- /dev/null +++ b/admin/tool/lp/templates/action_selector.mustache @@ -0,0 +1,45 @@ +{{! + 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 . +}} +{{! + Select an action to execute. + + Classes required for JS: + * none + + Data attibutes required for JS: + * none + + Context variables required for this template: + * message - The message to explain why we ask user to select an action + * choices - List of possible actions + * confirm - Text for button confirms + * cancel - Text for button cancel + }} +
+
+ {{message}}
+

+
+ {{#choices}} +
+ {{/choices}} +

+
+ + +
+
diff --git a/admin/tool/lp/templates/comment_area.mustache b/admin/tool/lp/templates/comment_area.mustache new file mode 100644 index 00000000000..e2ac0646200 --- /dev/null +++ b/admin/tool/lp/templates/comment_area.mustache @@ -0,0 +1,113 @@ +{{! + 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 . +}} +{{! + @template tool_lp/comment_area + + IDs required for JS: + * all + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * autostart + * canpost + * cid + * collapsediconurl + * commentarea + * component + * contextid + * count + * courseid + * displaycancel + * fullwidth + * itemid + * linktext + * notoggle + * displaytotalcount + * template + * uniqid + + Example context (json): + { + } + +}} + +
+ {{^notoggle}} + + {{linktext}}{{linktext}} + {{#displaytotalcount}} + ({{count}}) + {{/displaytotalcount}} + + {{/notoggle}} + +
+
    +
  • +
+
+ + {{#canpost}} +
+
+ +
+
+ {{#str}}savecomment{{/str}} + {{#displaycancel}} + | {{#str}}cancel{{/str}} + {{/displaycancel}} +
+
+
+ {{/canpost}} + +
+
+{{#js}} +require(['core/str'], function(Str) { + // Preloading strings. + Str.get_strings([ + { key: 'addcomment', component: 'moodle' }, + { key: 'comments', component: 'moodle' }, + { key: 'commentscount', component: 'moodle' }, + { key: 'commentsrequirelogin', component: 'moodle' }, + { key: 'deletecomment', component: 'moodle' }, + ]).then(function() { + // Kick off when strings are loaded. + Y.use('core_comment', function(Y) { + M.core_comment.init(Y, { + client_id: '{{cid}}', + commentarea: '{{commentarea}}', + itemid: {{itemid}}, + page: 0, + courseid: {{courseid}}, + contextid: {{contextid}}, + component: '{{component}}', + notoggle: false, // {{notoggle}} True not supported just yet. + autostart: false // {{autostart}} True not supported just yet. + }); + }); + }); +}) +{{/js}} diff --git a/admin/tool/lp/templates/competencies_move_tree.mustache b/admin/tool/lp/templates/competencies_move_tree.mustache new file mode 100644 index 00000000000..0a51aef65f8 --- /dev/null +++ b/admin/tool/lp/templates/competencies_move_tree.mustache @@ -0,0 +1,47 @@ +{{! + 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 . +}} +{{! + Manage competencies template. + + Classes required for JS: + + Data attibutes required for JS: + * data-enhance=movetree + + Context variables required for this template: + * framework - + * competencies - array of objects containing id, shortname, idnumber, sortorder, parentid, competencyframeworkid, path +}} +
+ + +
    +
  • + {{framework.shortname}} +
      + {{#competencies}} + {{> tool_lp/competencies_tree }} + {{/competencies}} +
    +
  • +
+
+ +
+ + +
diff --git a/admin/tool/lp/templates/competencies_tree.mustache b/admin/tool/lp/templates/competencies_tree.mustache new file mode 100644 index 00000000000..5e30b8ad9d5 --- /dev/null +++ b/admin/tool/lp/templates/competencies_tree.mustache @@ -0,0 +1,19 @@ +
  • + {{#canmanage}} + + {{shortname}} + + {{/canmanage}} + {{^canmanage}} + + {{shortname}} + + {{/canmanage}} + {{#haschildren}} +
      + {{#children}} + {{> tool_lp/competencies_tree }} + {{/children}} +
    + {{/haschildren}} +
  • diff --git a/admin/tool/lp/templates/competencies_tree_root.mustache b/admin/tool/lp/templates/competencies_tree_root.mustache new file mode 100644 index 00000000000..c163870708a --- /dev/null +++ b/admin/tool/lp/templates/competencies_tree_root.mustache @@ -0,0 +1,9 @@ +
      +
    • {{shortname}} +
        + {{#competencies}} + {{> tool_lp/competencies_tree }} + {{/competencies}} +
      +
    • +
    diff --git a/admin/tool/lp/templates/competency_grader.mustache b/admin/tool/lp/templates/competency_grader.mustache new file mode 100644 index 00000000000..6b4704203c6 --- /dev/null +++ b/admin/tool/lp/templates/competency_grader.mustache @@ -0,0 +1,26 @@ +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    diff --git a/admin/tool/lp/templates/competency_path.mustache b/admin/tool/lp/templates/competency_path.mustache new file mode 100644 index 00000000000..8cb1a551008 --- /dev/null +++ b/admin/tool/lp/templates/competency_path.mustache @@ -0,0 +1,55 @@ +{{! + 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 . +}} +{{! + Competency path template. + + Classes required for JS: + * competencydialogue + + Context variables required for this template: + * framework - The competency framework + * id - competency id + * name - competency idnumber + * first - true if node is in first position + * last - true if node is in last position + * position - the position of the node in the list + + * ancestors - array of nodes + * id - competency id + * name - competency idnumber + * first - true if node is in first position + * last - true if node is in last position + * position - the position of the node in the list + + * pluginbaseurl - base url of plugin tool_lp +}} + +{{#js}} +require(['tool_lp/competencydialogue'], function(Compdialogue) { + var competencydialogue = new Compdialogue({includerelated : true}); + competencydialogue.watch('[id="competency-path-{{uniqid}}"]'); +}); +{{/js}} diff --git a/admin/tool/lp/templates/competency_picker.mustache b/admin/tool/lp/templates/competency_picker.mustache new file mode 100644 index 00000000000..fcb4464c0c2 --- /dev/null +++ b/admin/tool/lp/templates/competency_picker.mustache @@ -0,0 +1,30 @@ +
    +{{^singleFramework}} +

    {{#str}}competencyframeworks, tool_lp{{/str}}

    + +{{/singleFramework}} +

    {{#str}}locatecompetency, tool_lp{{/str}}

    + +
    + + + +
    +
      +
    • {{framework.shortname}} +
        + {{#competencies}} + {{> tool_lp/competencies_tree }} + {{/competencies}} +
      +
    • +
    +
    + + +
    +
    diff --git a/admin/tool/lp/templates/competency_picker_competencyform.mustache b/admin/tool/lp/templates/competency_picker_competencyform.mustache new file mode 100644 index 00000000000..f3f1ca04c46 --- /dev/null +++ b/admin/tool/lp/templates/competency_picker_competencyform.mustache @@ -0,0 +1,22 @@ +
    +

    {{#str}}locatecompetency, tool_lp{{/str}}

    + +
    + + + +
    +
      +
    • {{framework.shortname}} +
        + {{#competencies}} + {{> tool_lp/competencies_tree }} + {{/competencies}} +
      +
    • +
    +
    + + +
    +
    diff --git a/admin/tool/lp/templates/competency_picker_user_plans.mustache b/admin/tool/lp/templates/competency_picker_user_plans.mustache new file mode 100644 index 00000000000..1e052b94172 --- /dev/null +++ b/admin/tool/lp/templates/competency_picker_user_plans.mustache @@ -0,0 +1,34 @@ +
    +{{^singlePlan}} +

    {{#str}}learningplans, tool_lp{{/str}}

    + +{{/singlePlan}} + +

    {{#str}}locatecompetency, tool_lp{{/str}}

    + +
    + + + +
    + +
      +
    • {{plan.name}} +
        + {{#competencies}} + {{> tool_lp/competencies_tree }} + {{/competencies}} +
      +
    • +
    + +
    + + +
    + +
    diff --git a/admin/tool/lp/templates/competency_rule_config.mustache b/admin/tool/lp/templates/competency_rule_config.mustache new file mode 100644 index 00000000000..7c64ab6504e --- /dev/null +++ b/admin/tool/lp/templates/competency_rule_config.mustache @@ -0,0 +1,39 @@ +
    +
    + + {{^config}} +

    {{#str}}cannotaddrules, tool_lp{{/str}}

    + {{/config}} + + {{#config}} +
    +
    + + +
    +
    + + +
    +
    +
    +
    + {{/config}} +
    + +
    + {{#config}} + + {{/config}} + +
    +
    diff --git a/admin/tool/lp/templates/competency_rule_points.mustache b/admin/tool/lp/templates/competency_rule_points.mustache new file mode 100644 index 00000000000..ec8c60f628d --- /dev/null +++ b/admin/tool/lp/templates/competency_rule_points.mustache @@ -0,0 +1,27 @@ +
    + + + + + + + + + + {{#children}} + + + + + + {{/children}} + + + + + + + + +
    {{#str}}points, tool_lp{{/str}}{{#str}}required{{/str}}
    {{shortname}}
    {{#str}}totalrequiredtocomplete, tool_lp{{/str}}
    +
    diff --git a/admin/tool/lp/templates/competency_summary.mustache b/admin/tool/lp/templates/competency_summary.mustache new file mode 100644 index 00000000000..88a895e4536 --- /dev/null +++ b/admin/tool/lp/templates/competency_summary.mustache @@ -0,0 +1,31 @@ +
    +

    {{competency.shortname}} + {{competency.idnumber}} +

    + {{#framework}} +
    +

    {{framework.shortname}} - {{taxonomyterm}} +

    + {{/framework}} +
    + +{{#competency.description}} +

    {{{competency.description}}}

    +{{/competency.description}} +{{#comppath}} + {{#str}}path, tool_lp{{/str}} {{> tool_lp/competency_path }} +{{/comppath}} +{{#showrelatedcompetencies}} + {{> tool_lp/related_competencies }} +{{/showrelatedcompetencies}} + +{{#showrule}} +
    {{#str}}competencyrule, tool_lp{{/str}}
    +
    +
    {{#str}}outcome, tool_lp{{/str}}
    +
    {{rule.outcome}}
    +
    {{#str}}when, tool_lp{{/str}}
    +
    {{rule.type}}
    +
    +{{/showrule}} + diff --git a/admin/tool/lp/templates/course_competencies_page.mustache b/admin/tool/lp/templates/course_competencies_page.mustache new file mode 100644 index 00000000000..3782d4d12f2 --- /dev/null +++ b/admin/tool/lp/templates/course_competencies_page.mustache @@ -0,0 +1,131 @@ +{{! + 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 . +}} +{{! + Course competencies template. +}} +
    +
    +
    + {{#canmanagecoursecompetencies}} + + {{/canmanagecoursecompetencies}} +
    +
    +
    +{{#settings.pushratingstouserplans}} +

    + {{#str}}coursecompetencyratingsarepushedtouserplans, tool_lp{{/str}} +{{/settings.pushratingstouserplans}} +{{^settings.pushratingstouserplans}} +

    + {{#str}}coursecompetencyratingsarenotpushedtouserplans, tool_lp{{/str}} +{{/settings.pushratingstouserplans}} +{{#canconfigurecoursecompetencies}} + {{#pix}}t/edit, core, {{#str}}edit{{/str}}{{/pix}} +

    +{{/canconfigurecoursecompetencies}} +
    +{{#statistics}} +{{> tool_lp/course_competency_statistics }} +{{/statistics}} +
    + + +{{#competencies}} + + + +{{/competencies}} + +
    + {{#canmanagecoursecompetencies}} + + + {{/canmanagecoursecompetencies}} + {{#competency}} + +

    {{competency.shortname}} {{competency.idnumber}}

    +
    +

    {{{competency.description}}}

    + {{/competency}} + {{#comppath}} + {{#str}}path, tool_lp{{/str}} {{> tool_lp/competency_path }} + {{/comppath}} + {{#usercompetencycourse}} + {{#grade}} + {{gradename}} + {{/grade}} + {{/usercompetencycourse}} + {{#canmanagecoursecompetencies}} +
    + +
    + {{/canmanagecoursecompetencies}} +
    +

    +

      + {{#coursemodules}} +
    • {{name}}
    • + {{/coursemodules}} + {{^coursemodules}} +
    • {{#str}}noactivities, tool_lp{{/str}}
    • + {{/coursemodules}} +
    +

    +
    +
    +{{^competencies}} +

    + {{#str}}nocompetenciesincourse, tool_lp{{/str}} +

    +{{/competencies}} +
    +
    + {{#canmanagecompetencyframeworks}} +

    {{#str}}managecompetenciesandframeworks, tool_lp{{/str}}

    + {{/canmanagecompetencyframeworks}} +
    +
    +{{#js}} +require(['tool_lp/competencies'], function(mod) { + (new mod({{courseid}}, 'course', {{pagecontextid}})); +}); +{{/js}} +{{#canconfigurecoursecompetencies}} +{{#js}} +require(['tool_lp/course_competency_settings'], function(Mod) { + (new Mod('[data-action=configure-course-competency-settings]')); +}); +{{/js}} +{{/canconfigurecoursecompetencies}} diff --git a/admin/tool/lp/templates/course_competency_settings.mustache b/admin/tool/lp/templates/course_competency_settings.mustache new file mode 100644 index 00000000000..bc1e36105f2 --- /dev/null +++ b/admin/tool/lp/templates/course_competency_settings.mustache @@ -0,0 +1,21 @@ +
    + +
    +

    {{#str}}coursecompetencyratingsquestion, tool_lp{{/str}}

    + + + +
    +
    +
    + + +
    +
    +
    diff --git a/admin/tool/lp/templates/course_competency_statistics.mustache b/admin/tool/lp/templates/course_competency_statistics.mustache new file mode 100644 index 00000000000..cfb9df3d870 --- /dev/null +++ b/admin/tool/lp/templates/course_competency_statistics.mustache @@ -0,0 +1,76 @@ +{{! + 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 . +}} +{{! + @template tool_lp/course_competency_statistics + + IDs required for JS: + * none + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + + See competency/classes/external/course_competency_statistics_exporter + + Example context (json): + { + competencycount: 4, + proficientcompetencycount: 3, + proficientcompetencypercentage: 75, + proficientcompetencypercentageformatted: 75.0 + } + +}} +{{! + Template statistics template. +}} +{{#competencycount}} +
    + {{#canbegradedincourse}} +
    +
    + {{#str}}xcompetenciesproficientoutofyincourse, tool_lp, { "x": "{{proficientcompetencycount}}", "y": "{{competencycount}}" } {{/str}} +
    +
    +
    + {{proficientcompetencypercentageformatted}} % +
    +
    +
    + {{/canbegradedincourse}} + {{#canmanagecoursecompetencies}} + {{#leastproficientcount}} +
    +
    +

    {{#str}}competenciesmostoftennotproficientincourse, tool_lp{{/str}}

    +
    +
    + {{#leastproficient}} + +

    {{shortname}} {{idnumber}}

    +
    + {{/leastproficient}} +
    +
    + {{/leastproficientcount}} + {{/canmanagecoursecompetencies}} +
    +{{/competencycount}} diff --git a/admin/tool/lp/templates/evidence_summary.mustache b/admin/tool/lp/templates/evidence_summary.mustache new file mode 100644 index 00000000000..64f1bc613d4 --- /dev/null +++ b/admin/tool/lp/templates/evidence_summary.mustache @@ -0,0 +1,75 @@ +{{! + 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 . +}} +{{! + @template tool_lp/evidence_summary + + Moodle template for the the summary of a single piece of evidence. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * actionuser User record from a user_summary_exporter + * userdate User formatted date this evidence was created + * grade Raw grade value if set + * gradename Scale grade item + * description Description of the evidence + * url Optional url for the evidence + + Example context (json): + { "actionuser": { "id": 1, "fullname": "Legend", "profileimageurlsmall": "https://secure.gravatar.com/avatar/78f83716ad197a25e175fbb747cff1ff?s=35&d=mm" }, + "userdate": "Tuesday...", + "grade": 2, + "gradename": "Met", + "description": "The user competency was manually rated in the plan Personal plan A", + "url": "http://moodle.org/", + "candelete": true, + "id": 1 + } +}} +
    + {{#candelete}} + + {{/candelete}} +{{#actionuser}} +
    + {{> tool_lp/user_summary }} +
    +{{/actionuser}} + +{{#grade}} +

    {{gradename}}

    +{{/grade}} +

    {{description}}

    +{{#note}} +
    {{note}}
    +{{/note}} +{{#url}} +

    {{url}}

    +{{/url}} +
    + +{{#js}} +require(['tool_lp/evidence_delete'], function(EvidenceDelete) { + EvidenceDelete.register('[data-action="delete-evidence"]', '[data-region="evidence"]'); +}); +{{/js}} diff --git a/admin/tool/lp/templates/form-cohort-selector-suggestion.mustache b/admin/tool/lp/templates/form-cohort-selector-suggestion.mustache new file mode 100644 index 00000000000..fafe24f89c6 --- /dev/null +++ b/admin/tool/lp/templates/form-cohort-selector-suggestion.mustache @@ -0,0 +1,49 @@ +{{! + 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 . +}} +{{! + @template tool_lp/form-cohort-selector-suggestion + + Moodle template for the list of valid options in an autocomplate form element. + + 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" + } +}} + + {{name}} + {{#idnumber}} + {{idnumber}} + {{/idnumber}} + ( {{contextname}} ) + diff --git a/admin/tool/lp/templates/form-user-selector-suggestion.mustache b/admin/tool/lp/templates/form-user-selector-suggestion.mustache new file mode 100644 index 00000000000..0946f788069 --- /dev/null +++ b/admin/tool/lp/templates/form-user-selector-suggestion.mustache @@ -0,0 +1,57 @@ +{{! + 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 . +}} +{{! + @template tool_lp/form-user-selector-suggestion + + Moodle template for the list of valid options in an autocomplate form element. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * profileimageurlsmall Url to a small profile image. + * profileimageurl Url to a profile image. + * fullname Users full name + * hasidentity boolean to say if there are identity fields to show + * identity concatenated list of identity fields. + * id user id field + * email user email field + * idnumber user idnumber field + * phone1 user phone1 field + * phone2 user phone2 field + * department user department field + * institution user institution field + + Example context (json): + { "id": "1", + "fullname": "Admin", + "hasidentity": true, + "identity": "Admin User, 0144114141", + "profileimageurl": "invalid url", + "profileimageurlsmall": "invalid url" + } +}} + + + {{fullname}} + {{#hasidentity}} + {{identity}} + {{/hasidentity}} + diff --git a/admin/tool/lp/templates/linked_courses_summary.mustache b/admin/tool/lp/templates/linked_courses_summary.mustache new file mode 100644 index 00000000000..7208304e0dd --- /dev/null +++ b/admin/tool/lp/templates/linked_courses_summary.mustache @@ -0,0 +1,11 @@ +

    +{{#str}}coursesusingthiscompetency, tool_lp{{/str}} +

    +
      +{{#courses}} +
    1. {{{fullname}}} {{{shortname}}}
    2. +{{/courses}} +{{^courses}} + {{#str}}nolinkedcourses, tool_lp{{/str}} +{{/courses}} +
    diff --git a/admin/tool/lp/templates/loading.mustache b/admin/tool/lp/templates/loading.mustache new file mode 100644 index 00000000000..1770a02f6af --- /dev/null +++ b/admin/tool/lp/templates/loading.mustache @@ -0,0 +1,29 @@ +{{! + 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 . +}} +{{! + Manage learning plan templates template. + + Classes required for JS: + * None + + Data attibutes required for JS: + * None + + Context variables required for this template: + * None +}} +{{#pix}}y/loading, core, {{#str}}loading, tool_lp{{/str}}{{/pix}} diff --git a/admin/tool/lp/templates/manage_competencies_page.mustache b/admin/tool/lp/templates/manage_competencies_page.mustache new file mode 100644 index 00000000000..c57d62946a0 --- /dev/null +++ b/admin/tool/lp/templates/manage_competencies_page.mustache @@ -0,0 +1,133 @@ +{{! + 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 . +}} +{{! + Manage competencies template. + + Classes required for JS: + + Data attibutes required for JS: + * data-action=add + * data-enhance=tree + * data-region=term + * data-region=selected-competency + + Context variables required for this template: + * framework - + * competencies - array of objects containing id, shortname, idnumber, sortorder, parentid, competencyframeworkid, path + * canmanage - true if this user has permission to manage the competencies +}} +
    +

    + {{framework.shortname}} + {{#canmanage}} + {{#pix}}t/edit, core, {{#str}}editcompetencyframework, tool_lp{{/str}}{{/pix}} + {{/canmanage}} +

    +

    {{#str}}competencies, core_competency{{/str}}

    +
    +
    +

    +

    + + + +
    +

    +
      + {{> tool_lp/loading }} +
    +
    + + +{{/canmanage}} +
    + +{{#js}} +// Initialise the JS. +require(['tool_lp/tree', 'tool_lp/competencytree', 'tool_lp/competencyactions', 'jquery'], + function(ariatree, treeModel, actions, $) { + + treeModel.init({{framework.id}}, + '{{framework.shortname}}', + '{{search}}', + '[data-enhance=tree]', + {{canmanage}}); + + actions.init(treeModel, {{pagecontextid}}, '{{{framework.taxonomies}}}', {{{rulesmodules}}}); + +}); +{{/js}} + +
    diff --git a/admin/tool/lp/templates/manage_competency_frameworks_page.mustache b/admin/tool/lp/templates/manage_competency_frameworks_page.mustache new file mode 100644 index 00000000000..facc7c37d07 --- /dev/null +++ b/admin/tool/lp/templates/manage_competency_frameworks_page.mustache @@ -0,0 +1,109 @@ +{{! + 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 . +}} +{{! + Manage competency frameworks template. + + Classes required for JS: + * drag-parentnode + * drag-handlecontainer + * drag-samenode + * competencyframeworkactions + + Data attributes required for JS: + * data-region = managecompetencies + * data-frameworkid = id + * data-action = deletecompetencyframework + + Context variables required for this template: + * competencyframeworks - array of objects containing id, shortname, idnumber, visible + * canmanage - true if this user has permission to manage the frameworks + * navigation - array of strings containing buttons for navigation +}} +
    +
    + {{#navigation}} + {{{.}}} + {{/navigation}} +
    + + + + + + + + + + + {{#competencyframeworks}} + + + + + + + {{/competencyframeworks}} + +
    {{#str}}competencyframeworkname, tool_lp{{/str}}{{#str}}competencies, core_competency{{/str}}{{#str}}category, tool_lp{{/str}}{{#str}}actions, tool_lp{{/str}}
    {{shortname}} ({{idnumber}}) {{^visible}}{{#str}}hiddenhint, tool_lp{{/str}}{{/visible}}{{competenciescount}}{{contextnamenoprefix}} + {{#canmanage}} + + {{/canmanage}} +
    +{{^competencyframeworks}} +

    + {{#str}}nocompetencyframeworks, tool_lp{{/str}} +

    +{{/competencyframeworks}} + +{{#js}} +// Initialise the JS. +require(['tool_lp/frameworkactions', + 'tool_lp/menubar'], + function(actionsMod, menubar) { + + actionsMod.init({{pagecontextid}}); + + menubar.enhance('.competencyframeworkactions', { + "[data-action='deletecompetencyframework']": actionsMod.deleteHandler, + "[data-action='duplicatecompetencyframework']": actionsMod.duplicateHandler, + }); +}); + +{{/js}} + +
    diff --git a/admin/tool/lp/templates/manage_templates_page.mustache b/admin/tool/lp/templates/manage_templates_page.mustache new file mode 100644 index 00000000000..eb678c0f8f7 --- /dev/null +++ b/admin/tool/lp/templates/manage_templates_page.mustache @@ -0,0 +1,122 @@ +{{! + 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 . +}} +{{! + Manage learning plan templates template. + + Classes required for JS: + * drag-parentnode + * drag-samenode + * templateactions + + Data attibutes required for JS: + * data-region = managetemplates + * data-templateid = id + * data-action = deletetemplate + + Context variables required for this template: + * templates - array of objects containing id, shortname, visible, canmanage, contextname + * navigation - array of strings containing buttons for navigation +}} +
    +
    + {{#navigation}} + {{{.}}} + {{/navigation}} +
    + + + + + + + + + {{#canmanage}} + + {{/canmanage}} + + + + {{#templates}} + + + + + + + + {{/templates}} + +
    {{#str}}listtemplatescaption, tool_lp{{/str}}
    {{#str}}templatename, tool_lp{{/str}}{{#str}}category, tool_lp{{/str}}{{#str}}cohorts, core_cohort{{/str}}{{#str}}userplans, core_competency{{/str}}{{#str}}actions, tool_lp{{/str}}
    {{shortname}} {{^visible}}{{#str}}hiddenhint, tool_lp{{/str}}{{/visible}}{{contextnamenoprefix}}{{cohortscount}}{{planscount}} + {{#canmanage}} + + {{/canmanage}} +
    +{{^templates}} +

    + {{#str}}notemplates, tool_lp{{/str}} +

    +{{/templates}} + +{{#js}} +// Initialise the JS. +require(['tool_lp/templateactions', + 'tool_lp/menubar'], + function(actionsMod, menubar) { + + actionsMod.init({{pagecontextid}}); + + menubar.enhance('.templateactions', { + '[data-action="deletetemplate"]': actionsMod.deleteHandler, + '[data-action="duplicatetemplate"]': actionsMod.duplicateHandler + }); +}); + +{{/js}} + +
    diff --git a/admin/tool/lp/templates/no_frameworks_warning.mustache b/admin/tool/lp/templates/no_frameworks_warning.mustache new file mode 100644 index 00000000000..d93b418a970 --- /dev/null +++ b/admin/tool/lp/templates/no_frameworks_warning.mustache @@ -0,0 +1,3 @@ +

    + {{#str}}nocompetencyframeworks, tool_lp{{/str}} +

    diff --git a/admin/tool/lp/templates/plan_page.mustache b/admin/tool/lp/templates/plan_page.mustache new file mode 100644 index 00000000000..1b66ee3f022 --- /dev/null +++ b/admin/tool/lp/templates/plan_page.mustache @@ -0,0 +1,193 @@ +{{! + 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 . +}} +{{! + Plan page template. +}} +
    +

    + {{plan.name}} + {{#plan.canbeedited}} + {{#pix}}t/edit, core, {{#str}}editplan, tool_lp{{/str}}{{/pix}} + {{/plan.canbeedited}} +

    +
    +
    +
    {{#str}}status, tool_lp{{/str}}
    +
    + {{plan.statusname}} + + {{#plan.isinreview}} + - {{plan.reviewer.fullname}} + {{/plan.isinreview}} + + {{#plan.iscompleteallowed}} + ({{#str}}completeplan, tool_lp{{/str}}) + {{/plan.iscompleteallowed}} + {{#plan.isreopenallowed}} + ({{#str}}reopenplan, tool_lp{{/str}}) + {{/plan.isreopenallowed}} + {{#plan.isrequestreviewallowed}} + ({{#str}}requestreview, tool_lp{{/str}}) + {{/plan.isrequestreviewallowed}} + {{#plan.iscancelreviewrequestallowed}} + ({{#str}}cancelreviewrequest, tool_lp{{/str}}) + {{/plan.iscancelreviewrequestallowed}} + {{#plan.isstartreviewallowed}} + ({{#str}}startreview, tool_lp{{/str}}) + {{/plan.isstartreviewallowed}} + {{#plan.isstopreviewallowed}} + ({{#str}}stopreview, tool_lp{{/str}}) + {{/plan.isstopreviewallowed}} + {{#plan.isapproveallowed}} + ({{#str}}planapprove, tool_lp{{/str}}) + {{/plan.isapproveallowed}} + {{#plan.isunapproveallowed}} + ({{#str}}planunapprove, tool_lp{{/str}}) + {{/plan.isunapproveallowed}} + +
    + {{#plan.duedate}} +
    {{#str}}duedate, tool_lp{{/str}}
    +
    {{plan.duedateformatted}}
    + {{/plan.duedate}} + {{#plan.template}} +
    {{#str}}template, tool_lp{{/str}}
    +
    + {{#canread}} + + {{/canread}} + {{plan.template.shortname}}{{#canread}}{{/canread}} + {{#plan.isunlinkallowed}} + ({{#str}}unlinkplantemplate, tool_lp{{/str}}) + {{/plan.isunlinkallowed}} +
    + {{/plan.template}} + {{#description}} +
    {{#str}}description{{/str}}
    +
    {{{plan.description}}}
    + {{/description}} +
    {{#str}}progress, tool_lp{{/str}}
    +
    {{#str}}xcompetenciesproficientoutofy, tool_lp, { "x": "{{proficientcompetencycount}}", "y": "{{competencycount}}" }{{/str}} +
    +
    + {{proficientcompetencypercentageformatted}} % +
    +
    +
    +
    +
    + {{#plan.commentarea}} + {{#canpostorhascomments}} +
    + {{>tool_lp/comment_area}} +
    + {{/canpostorhascomments}} + {{/plan.commentarea}} +
    +
    +
    + + {{#plan.canbeedited}} + + {{/plan.canbeedited}} +
    +
    +

    {{#str}}learningplancompetencies, tool_lp{{/str}}

    + + + + + + + {{^plan.iscompleted}} + + + {{/plan.iscompleted}} + + + + {{#competencies}} + + + {{#plan.iscompleted}} + + + {{/plan.iscompleted}} + {{^plan.iscompleted}} + + + + {{/plan.iscompleted}} + {{^plan.iscompleted}} + + {{/plan.iscompleted}} + + {{/competencies}} + +
    {{#str}}shortname, tool_lp{{/str}}{{#str}}rating, tool_lp{{/str}}{{#str}}proficient, tool_lp{{/str}}{{#str}}status, tool_lp{{/str}}{{#str}}actions, tool_lp{{/str}}
    + {{#plan.canbeedited}} + + {{/plan.canbeedited}} + {{competency.shortname}} + {{competency.idnumber}} + {{#comppath}} +
    + {{#str}}path, tool_lp{{/str}} {{> tool_lp/competency_path }} + {{/comppath}} +
    {{usercompetencyplan.gradename}}{{usercompetencyplan.proficiencyname}}{{usercompetency.gradename}}{{usercompetency.proficiencyname}}{{usercompetency.statusname}} + +
    +
    +
    +{{#js}} +require(['tool_lp/competencies', 'tool_lp/planactions', 'tool_lp/user_competency_workflow'], function(mod, actionsMod, UserCompWorkflow) { + var planActions = new actionsMod('plan'); + + (new mod({{plan.id}}, 'plan', {{contextid}})); + planActions.registerEvents(); + + var ucw = new UserCompWorkflow(); + ucw.enhanceMenubar('.user-competency-actions'); + ucw.on('status-changed', planActions.refresh.bind(planActions, '[data-region="plan-summary"]')); + ucw.on('error-occured', planActions.refresh.bind(planActions, '[data-region="plan-summary"]')); +}); +{{/js}} + diff --git a/admin/tool/lp/templates/plans_page.mustache b/admin/tool/lp/templates/plans_page.mustache new file mode 100644 index 00000000000..6aee2d60a2c --- /dev/null +++ b/admin/tool/lp/templates/plans_page.mustache @@ -0,0 +1,174 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + User learning plans page. + + Classes required for JS: + * planactions + + Data attributes required for JS: + * data-region = plans + * data-planid = plans.id + * data-userid = userid + * data-action = deleteplan + + Context variables required for this template: + * userid - The plan user id + * pluginbaseurl - tool_lp base url + * plans - The list of learning plans + * navigation - array of strings containing buttons for navigation +}} + +
    +
    + {{#navigation}} + {{{.}}} + {{/navigation}} +
    + + + + + + + + + + + + {{#plans}} + + + + + + + {{/plans}} + +
    {{#str}}listplanscaption, tool_lp{{/str}}
    {{#str}}planname, tool_lp{{/str}}{{#str}}templatebased, tool_lp{{/str}}{{#str}}status, tool_lp{{/str}}{{#str}}actions, tool_lp{{/str}}
    + {{name}} + + {{#isbasedontemplate}} + {{#str}}yes{{/str}} + {{/isbasedontemplate}} + {{^isbasedontemplate}} + {{#str}}no{{/str}} + {{/isbasedontemplate}} + {{statusname}} + {{#canmanage}} +
    + +
    + {{/canmanage}} +
    + +{{^plans}} +

    + {{#str}}nouserplans, tool_lp{{/str}} +

    +{{/plans}} + +{{#canreaduserevidence}} + +{{/canreaduserevidence}} + +{{#js}} +// Initialise the JS. +require(['tool_lp/planactions'], + function(actionsMod) { + + var planActions = new actionsMod('plans'); + planActions.enhanceMenubar('.planactions'); +}); + +{{/js}} + +
    diff --git a/admin/tool/lp/templates/related_competencies.mustache b/admin/tool/lp/templates/related_competencies.mustache new file mode 100644 index 00000000000..897a26e5e5b --- /dev/null +++ b/admin/tool/lp/templates/related_competencies.mustache @@ -0,0 +1,30 @@ +
    +

    + {{#str}}crossreferencedcompetencies, tool_lp{{/str}}: +

    + +{{^relatedcompetencies}} + {{#str}}nocrossreferencedcompetencies, tool_lp{{/str}} +{{/relatedcompetencies}} +
    +{{#js}} +require(['tool_lp/competencydialogue'], function(Compdialogue) { + var competencydialogue = new Compdialogue({includerelated : true}); + competencydialogue.watch('[data-region="relatedcompetencies"]'); +}); +{{/js}} diff --git a/admin/tool/lp/templates/scale_configuration_page.mustache b/admin/tool/lp/templates/scale_configuration_page.mustache new file mode 100644 index 00000000000..6b9811948c2 --- /dev/null +++ b/admin/tool/lp/templates/scale_configuration_page.mustache @@ -0,0 +1,62 @@ +{{! + 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 . +}} +{{! + Set scale configuration for the competency framework. + + Classes required for JS: + * none + + Data attibutes required for JS: + * data-field + + Context variables required for this template: + * scales Array of id / name pairs. + + Example context (json): + { + "scales": [ + { "id": 1, "name": "Competent" }, + { "id": 2, "name": "Not competent"} + ] + } + }} +
    + + + + + + + + + + {{#scales}} + + + + + + {{/scales}} + +
    {{#str}}scalevalue, tool_lp{{/str}}{{#str}}default, tool_lp{{/str}}{{#pix }} req, core, {{#str}}requiredelement, form{{/str}} {{/pix }}{{#str}}proficient, tool_lp{{/str}}{{#pix }} req, core, {{#str}}requiredelement, form{{/str}}{{/pix }}
    {{name}}
    +
    + +
    + + +
    \ No newline at end of file diff --git a/admin/tool/lp/templates/template_competencies_page.mustache b/admin/tool/lp/templates/template_competencies_page.mustache new file mode 100644 index 00000000000..715bf601d0e --- /dev/null +++ b/admin/tool/lp/templates/template_competencies_page.mustache @@ -0,0 +1,88 @@ +{{! + 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 . +}} +{{! + Template competencies template. +}} +
    +

    + {{template.shortname}} + {{#template.canmanage}} + {{#pix}}t/edit, core, {{#str}}edittemplate, tool_lp{{/str}}{{/pix}} + {{/template.canmanage}} +

    +

    {{#str}}templatecompetencies, tool_lp{{/str}}

    + {{#statistics}} + {{> tool_lp/template_statistics }} + {{/statistics}} + {{#canmanagetemplatecompetencies}} +
    +
    + +
    +
    + {{/canmanagetemplatecompetencies}} +
    +
    +
    + {{#competencies}} +
    +
    + {{#canmanagetemplatecompetencies}} + + + {{/canmanagetemplatecompetencies}} + {{#competency}} + + {{> tool_lp/competency_summary }} + {{/competency}} + {{#str}}linkedcourseslist, tool_lp{{/str}} + {{#hascourses}} + + {{/hascourses}} + {{^hascourses}} + {{#str}}nolinkedcourses, tool_lp{{/str}} + {{/hascourses}} +
    +
    + {{/competencies}} +
    +
    + {{^competencies}} +

    + {{#str}}nocompetenciesintemplate, tool_lp{{/str}} +

    + {{/competencies}} +
    +
    + {{#canmanagecompetencyframeworks}} + + {{/canmanagecompetencyframeworks}} +
    +
    +{{#js}} +require(['tool_lp/competencies'], function(mod) { + (new mod({{template.id}}, 'template', {{pagecontextid}})); +}); +{{/js}} diff --git a/admin/tool/lp/templates/template_statistics.mustache b/admin/tool/lp/templates/template_statistics.mustache new file mode 100644 index 00000000000..c344ddedb67 --- /dev/null +++ b/admin/tool/lp/templates/template_statistics.mustache @@ -0,0 +1,102 @@ +{{! + 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 . +}} +{{! + @template tool_lp/template_statistics + + IDs required for JS: + * none + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + + See competency/classes/external/template_statistics_exporter + + Example context (json): + { + linkedcompetencycount: 4, + competencycount: 8, + linkedcompetencypercentage: 50, + plancount: 40, + completedplancount: 30, + completedplanpercentage: 75, + } + +}} +{{! + Template statistics template. +}} +{{#competencycount}} +
    +
    +
    + {{#str}}xcompetencieslinkedoutofy, tool_lp, { "x": "{{linkedcompetencycount}}", "y": "{{competencycount}}" } {{/str}} +
    +
    +
    + {{linkedcompetencypercentageformatted}} % +
    +
    +
    + {{#plancount}} +
    +
    + {{#str}}xplanscompletedoutofy, tool_lp, { "x": "{{completedplancount}}", "y": "{{plancount}}" } {{/str}} +
    +
    +
    + {{completedplanpercentageformatted}} % +
    +
    +
    + {{/plancount}} + {{#usercompetencyplancount}} +
    +
    + {{#str}}averageproficiencyrate, tool_lp, {{proficientusercompetencyplanpercentageformatted}} {{/str}} +
    +
    +
    + {{proficientusercompetencyplanpercentageformatted}} % +
    +
    +
    + {{/usercompetencyplancount}} + {{#leastproficientcount}} +
    +
    +

    {{#str}}competenciesmostoftennotproficient, tool_lp{{/str}}

    +
    +
    + {{#leastproficient}} + {{#showcompetencylinks}} + + {{/showcompetencylinks}} +

    {{shortname}} {{idnumber}}

    + {{#showcompetencylinks}} +
    + {{/showcompetencylinks}} + {{/leastproficient}} +
    +
    + {{/leastproficientcount}} +
    +{{/competencycount}} diff --git a/admin/tool/lp/templates/user_competency_course_navigation.mustache b/admin/tool/lp/templates/user_competency_course_navigation.mustache new file mode 100644 index 00000000000..4567a4711c3 --- /dev/null +++ b/admin/tool/lp/templates/user_competency_course_navigation.mustache @@ -0,0 +1,38 @@ +
    +

    {{{groupselector}}}

    +
    +{{#hasusers}} + + + + +{{/hasusers}} +
    +{{#hascompetencies}} + + + + +{{/hascompetencies}} +
    +
    +{{#js}} +require(['core/form-autocomplete', 'tool_lp/user_competency_course_navigation'], function(autocomplete, nav) { + (new nav('#user-nav-{{uniqid}}', '#competency-nav-{{uniqid}}', '{{baseurl}}', {{userid}}, {{competencyid}}, {{courseid}})); +{{#hasusers}} + autocomplete.enhance('#user-nav-{{uniqid}}', false, false, '{{#str}}jumptouser, tool_lp{{/str}}'); +{{/hasusers}} +{{#hascompetencies}} + autocomplete.enhance('#competency-nav-{{uniqid}}', false, false, '{{#str}}jumptocompetency, tool_lp{{/str}}'); +{{/hascompetencies}} + +}); +{{/js}} diff --git a/admin/tool/lp/templates/user_competency_summary.mustache b/admin/tool/lp/templates/user_competency_summary.mustache new file mode 100644 index 00000000000..4be2d6f9150 --- /dev/null +++ b/admin/tool/lp/templates/user_competency_summary.mustache @@ -0,0 +1,103 @@ +{{! + 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 . +}} +{{! + @template tool_lp/user_competency_page + + Moodle template for the the summary of a user competency + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * usercompetencysummary - object containing: + * showrelatedcompetencies - boolean + * cangrade - boolean + * competency - competency summary record + * user - user record + * usercompetency - user competency record + * evidence - array of evidence + + This template does not have an example context because it includes ajax functionality. +}} +
    +
    + {{#competency}} + {{> tool_lp/competency_summary }} + {{/competency}} +
    + {{#usercompetency}} +
    {{#str}}reviewstatus, tool_lp{{/str}}
    +
    {{statusname}} + + {{#isstatusinreview}} + - {{reviewer.fullname}} + {{/isstatusinreview}} + + {{#isrequestreviewallowed}}{{/isrequestreviewallowed}} + {{#iscancelreviewrequestallowed}}{{/iscancelreviewrequestallowed}} + {{#isstartreviewallowed}}{{/isstartreviewallowed}} + {{#isstopreviewallowed}}{{/isstopreviewallowed}} +
    +
    {{#str}}proficient, tool_lp{{/str}}
    +
    + + {{proficiencyname}} + +
    +
    {{#str}}rating, tool_lp{{/str}}
    +
    {{gradename}} + {{#cangrade}} + {{#str}}rate, tool_lp{{/str}} + {{/cangrade}} +
    + {{#js}} + require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info', 'tool_lp/user_competency_workflow'], function($, mod, info, UserCompWorkflow) { + + var competencyElement = $('[data-region-id="{{uniqid}}"]'); + var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}'); + + var ucw = new UserCompWorkflow(); + ucw.registerEvents('[data-region="user-competency-status"]'); + ucw.on('status-changed', infoReloader.reload.bind(infoReloader)); + ucw.on('error-occured', infoReloader.reload.bind(infoReloader)); + + var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}'); + inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader)); + }); + {{/js}} + {{/usercompetency}} +
    + {{#commentarea}} + {{#canpostorhascomments}} + {{>tool_lp/comment_area}} + {{/canpostorhascomments}} + {{/commentarea}} +
    +
    {{#str}}evidence, tool_lp{{/str}}
    +
    + {{#evidence}} + {{> tool_lp/evidence_summary }} + {{/evidence}} + {{^evidence}} +

    {{#str}}noevidence, tool_lp{{/str}}

    + {{/evidence}} +
    +
    +
    diff --git a/admin/tool/lp/templates/user_competency_summary_in_course.mustache b/admin/tool/lp/templates/user_competency_summary_in_course.mustache new file mode 100644 index 00000000000..0d05efc34c8 --- /dev/null +++ b/admin/tool/lp/templates/user_competency_summary_in_course.mustache @@ -0,0 +1,113 @@ +{{! + 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 . +}} +{{! + @template tool_lp/user_competency_summary_in_course + + Moodle template for the the summary of a user competency + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * usercompetencysummary - object containing: + * showrelatedcompetencies - boolean + * cangrade - boolean + * competency - competency summary record + * user - user record + * usercompetency - user competency record + * evidence - array of evidence + * course - course summary record + + This template does not have an example context because it includes ajax functionality. +}} +{{#usercompetencysummary}} +
    +
    + {{#competency}} + {{> tool_lp/competency_summary }} + {{/competency}} +
    +
    {{#str}}activities, tool_lp{{/str}}
    +
    +

    +

      + {{#coursemodules}} +
    • {{name}}
    • + {{/coursemodules}} + {{^coursemodules}} +
    • {{#str}}noactivities, tool_lp{{/str}}
    • + {{/coursemodules}} +
    +

    +
    + {{#displayuser}} + {{#user}} +
    {{#str}}user{{/str}}
    +
    + {{>tool_lp/user_summary}} +
    + {{/user}} + {{/displayuser}} + {{#usercompetencycourse}} +
    {{#str}}proficient, tool_lp{{/str}}
    +
    + + {{proficiencyname}} + +
    +
    {{#str}}rating, tool_lp{{/str}}
    +
    {{gradename}} + {{#cangrade}} + {{#str}}rate, tool_lp{{/str}} + {{/cangrade}} +
    + {{/usercompetencycourse}} + {{#js}} + require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info'], function($, mod, info) { + + var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '', '{{course.id}}', '{{#str}}chooserating, tool_lp{{/str}}'); + var competencyElement = $('[data-region-id="{{uniqid}}"]'); + + var displayuser = ('{{displayuser}}' == 'true') ? true : false; + var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}', '', '{{course.id}}', displayuser); + + inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader)); + }); + + {{/js}} +
    + {{#commentarea}} + {{#canpostorhascomments}} + {{>tool_lp/comment_area}} + {{/canpostorhascomments}} + {{/commentarea}} +
    +
    {{#str}}evidence, tool_lp{{/str}}
    +
    + {{#evidence}} + {{> tool_lp/evidence_summary }} + {{/evidence}} + {{^evidence}} +

    {{#str}}noevidence, tool_lp{{/str}}

    + {{/evidence}} +
    +
    +
    +{{/usercompetencysummary}} diff --git a/admin/tool/lp/templates/user_competency_summary_in_plan.mustache b/admin/tool/lp/templates/user_competency_summary_in_plan.mustache new file mode 100644 index 00000000000..34ab5e2996f --- /dev/null +++ b/admin/tool/lp/templates/user_competency_summary_in_plan.mustache @@ -0,0 +1,128 @@ +{{! + 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 . +}} +{{! + @template tool_lp/user_competency_summary_in_plan + + Moodle template for the the summary of a user competency + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * usercompetencysummary - object containing: + * showrelatedcompetencies - boolean + * cangrade - boolean + * competency - competency summary record + * user - user record + * usercompetency - user competency record + * usercompetencyplan - user competency plan record + * evidence - array of evidence + * commentarea - comment block + * plan - plan summary record + * iscompleted - is completed + + This template does not have an example context because it includes ajax functionality. +}} +{{#usercompetencysummary}} +
    +{{#plan.iscompleted}} + +{{/plan.iscompleted}} +
    + {{#competency}} + {{> tool_lp/competency_summary }} + {{/competency}} +
    + {{#usercompetency}} +
    {{#str}}reviewstatus, tool_lp{{/str}}
    +
    {{statusname}} + + {{#isstatusinreview}} + - {{reviewer.fullname}} + {{/isstatusinreview}} + + {{#isrequestreviewallowed}}{{/isrequestreviewallowed}} + {{#iscancelreviewrequestallowed}}{{/iscancelreviewrequestallowed}} + {{#isstartreviewallowed}}{{/isstartreviewallowed}} + {{#isstopreviewallowed}}{{/isstopreviewallowed}} +
    +
    {{#str}}proficient, tool_lp{{/str}}
    +
    + + {{proficiencyname}} + +
    +
    {{#str}}rating, tool_lp{{/str}}
    +
    {{gradename}} + {{#cangrade}} + + {{/cangrade}} +
    + {{#js}} + require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info', 'tool_lp/user_competency_workflow'], function($, mod, info, UserCompWorkflow) { + + var competencyElement = $('[data-region-id="{{uniqid}}"]'); + var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}'); + + var ucw = new UserCompWorkflow(); + ucw.registerEvents('[data-region="user-competency-status"]'); + ucw.on('status-changed', infoReloader.reload.bind(infoReloader)); + ucw.on('error-occured', infoReloader.reload.bind(infoReloader)); + + {{#cangrade}} + var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}'); + inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader)); + {{/cangrade}} + }); + {{/js}} + {{/usercompetency}} + {{#usercompetencyplan}} +
    {{#str}}rating, tool_lp{{/str}}
    +
    {{gradename}} - {{#str}}plancompleted, tool_lp{{/str}}
    +
    {{#str}}proficient, tool_lp{{/str}}
    +
    + + {{proficiencyname}} + +
    + {{/usercompetencyplan}} +
    + {{#commentarea}} + {{#canpostorhascomments}} + {{>tool_lp/comment_area}} + {{/canpostorhascomments}} + {{/commentarea}} +
    +
    {{#str}}evidence, tool_lp{{/str}}
    +
    + {{#evidence}} + {{> tool_lp/evidence_summary }} + {{/evidence}} + {{^evidence}} +

    {{#str}}noevidence, tool_lp{{/str}}

    + {{/evidence}} +
    +
    +
    +
    +{{/usercompetencysummary}} + diff --git a/admin/tool/lp/templates/user_evidence_list_page.mustache b/admin/tool/lp/templates/user_evidence_list_page.mustache new file mode 100644 index 00000000000..6c095da8f8c --- /dev/null +++ b/admin/tool/lp/templates/user_evidence_list_page.mustache @@ -0,0 +1,134 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + User evidence list page. + + Data attributes required for JS: + * data-action = user-evidence-delete + * data-region = user-evidence-list + * data-region = user-evidence-node + * data-id = user evidence ID + * data-userid = user evidence user ID + + Context variables required for this template: + * userid - The ID of the user whose page we're viewing + * canmanage - Whether the current user can manage the evidence + * evidence - The list of evidence + * navigation - Buttons to add to the page + * pluginbaseurl - The plugin base URL +}} + +
    +
    + {{#navigation}} + {{{.}}} + {{/navigation}} +
    + + + + + + + + {{#canmanage}} + + {{/canmanage}} + + + + {{#evidence}} + + + + + {{#canmanage}} + + + {{/evidence}} + +
    {{#str}}listofevidence, tool_lp{{/str}}
    {{#str}}userevidencename, tool_lp{{/str}}{{#str}}userevidencesummary, tool_lp{{/str}}{{#str}}linkedcompetencies, tool_lp{{/str}} ({{#str}}status, tool_lp{{/str}} / {{#str}}reviewer, tool_lp{{/str}}){{#str}}actions, tool_lp{{/str}}
    {{name}} + {{^hasurlorfiles}} + - + {{/hasurlorfiles}} + {{#hasurlorfiles}} + + {{/hasurlorfiles}} + + {{^competencycount}} + - + {{/competencycount}} + {{#competencycount}} +
      + {{#usercompetencies}} +
    • + {{competency.shortname}} {{competency.idnumber}} ({{usercompetency.statusname}}{{#usercompetency.reviewer.fullname}} / {{usercompetency.reviewer.fullname}}{{/usercompetency.reviewer.fullname}}) +
    • + {{/usercompetencies}} +
    + {{/competencycount}} +
    + + {{/canmanage}} +
    + +{{^evidence}} +

    {{#str}}nouserevidence, tool_lp{{/str}}

    +{{/evidence}} + +
    + +{{#js}} +require(['tool_lp/user_evidence_actions'], function(UserEvidenceActions) { + var uea = new UserEvidenceActions('list'); + uea.enhanceMenubar('.user-evidence-actions') +}); +{{/js}} diff --git a/admin/tool/lp/templates/user_evidence_page.mustache b/admin/tool/lp/templates/user_evidence_page.mustache new file mode 100644 index 00000000000..7e873f33664 --- /dev/null +++ b/admin/tool/lp/templates/user_evidence_page.mustache @@ -0,0 +1,117 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + User evidence page template. + + Data attributes required for JS: + * data-action = link-competency + * data-action = delete-competency-link + * data-region = user-evidence-page + * data-id = user evidence ID, or competency ID + * data-userid = user evidence user ID + + Context variables required for this template: + * userevidence - The user evidence + * pluginbaseurl - The plugin base URL +}} +{{#userevidence}} +
    +

    + {{name}} + {{#canmanage}} + {{#pix}}t/edit, core, {{#str}}editthisuserevidence, tool_lp{{/str}}{{/pix}} + {{/canmanage}} +

    + +
    + {{#description}} +
    + {{{description}}} +
    + {{/description}} + +
    + +
    +

    {{#str}}linkedcompetencies, tool_lp{{/str}}

    + + + + + + {{#canmanage}} + + {{/canmanage}} + + + + {{#usercompetencies}} + + + + {{#userevidence.canmanage}} + + {{/userevidence.canmanage}} + + {{/usercompetencies}} + +
    {{#str}}shortname, tool_lp{{/str}}{{#str}}status, tool_lp{{/str}} / {{#str}}reviewer, tool_lp{{/str}}{{#str}}actions, tool_lp{{/str}}
    + {{competency.shortname}} + + {{usercompetency.statusname}} {{#usercompetency.reviewer.fullname}} / {{usercompetency.reviewer.fullname}}{{/usercompetency.reviewer.fullname}} + + +
    + +
    +
    + {{#canmanage}} + + {{/canmanage}} +
    +
    +
    +
    + +{{#canmanage}} +{{#js}} +require(['tool_lp/user_evidence_actions'], function(UserEvidenceActions) { + + var uea = new UserEvidenceActions('evidence'); + uea.registerEvents(); + +}); +{{/js}} +{{/canmanage}} + +{{#js}} +require(['tool_lp/competencydialogue'], function(Compdialogue) { + var competencydialogue = new Compdialogue({includerelated : true}); + competencydialogue.watch('[data-region="user-evidence-competencies"]'); +}); +{{/js}} + +{{/userevidence}} diff --git a/admin/tool/lp/templates/user_summary.mustache b/admin/tool/lp/templates/user_summary.mustache new file mode 100644 index 00000000000..4aa459978d3 --- /dev/null +++ b/admin/tool/lp/templates/user_summary.mustache @@ -0,0 +1,59 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_lp/user-summary + + Moodle template for the list of valid options in an autocomplate form element. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * profileimageurlsmall Url to a small profile image. + * profileimageurl Url to a profile image. + * fullname Users full name + * hasidentity boolean to say if there are identity fields to show + * identity concatenated list of identity fields. + * id user id field + * email user email field + * idnumber user idnumber field + * phone1 user phone1 field + * phone2 user phone2 field + * department user department field + * institution user institution field + + Example context (json): + { "id": "1", + "fullname": "Admin", + "hasidentity": true, + "identity": "Admin User, 0144114141", + "profileimageurl": "invalid url", + "profileimageurlsmall": "invalid url" + } +}} + + + + {{fullname}} + {{#hasidentity}} + {{identity}} + {{/hasidentity}} + + diff --git a/admin/tool/lp/tests/behat/behat_tool_lp.php b/admin/tool/lp/tests/behat/behat_tool_lp.php new file mode 100644 index 00000000000..6b0bf40f7d9 --- /dev/null +++ b/admin/tool/lp/tests/behat/behat_tool_lp.php @@ -0,0 +1,95 @@ +. + +/** + * Step definition to generate database fixtures for learning plan system. + * + * @package tool_lp + * @category test + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php'); + +use Behat\Behat\Context\Step\Given as Given; + +/** + * Step definition for learning plan system. + * + * @package tool_lp + * @category test + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_tool_lp extends behat_base { + + /** + * Click on an entry in the edit menu. + * + * @When /^I click on "([^"]*)" of edit menu in the "([^"]*)" row$/ + * + * @param string $nodetext + * @param string $rowname + * @return Given[] array of steps + */ + public function click_on_edit_menu_of_the_row($nodetext, $rowname) { + $steps = array(); + + $xpathtarget = "//ul//li//ul//li[@class='tool-lp-menu-item']//a[contains(.,'". $nodetext ."')]"; + + $steps[] = new Given('I click on "Edit" "link" in the "' . $this->escape($rowname) . '" "table_row"'); + $steps[] = new Given('I click on "'.$xpathtarget.'" "xpath_element" in the "' . $this->escape($rowname) . '" "table_row"'); + + return $steps; + } + + /** + * Click on competency in the tree. + * + * @Given /^I select "([^"]*)" of the competency tree$/ + * + * @param string $competencyname + * @return Given[] array of steps + */ + public function select_of_the_competency_tree($competencyname) { + $steps = array(); + + $xpathtarget = "//li[@role='tree-item']//span[contains(.,'". $competencyname ."')]"; + + $steps[] = new Given('I click on "' . $xpathtarget . '" "xpath_element"'); + + return $steps; + } + + /** + * Select item from autocomplete list. + * + * @Given /^I click on "([^"]*)" item in the autocomplete list$/ + * + * @param string $item + * @return Given[] array of steps + */ + public function i_click_on_item_in_the_autocomplete_list($item) { + $steps = array(); + + $xpathtarget = "//ul[@class='form-autocomplete-suggestions']//li//span//span[contains(.,'". $item ."')]"; + + $steps[] = new Given('I click on "' . $xpathtarget . '" "xpath_element"'); + + return $steps; + } +} diff --git a/admin/tool/lp/tests/behat/behat_tool_lp_data_generators.php b/admin/tool/lp/tests/behat/behat_tool_lp_data_generators.php new file mode 100644 index 00000000000..632864c97e0 --- /dev/null +++ b/admin/tool/lp/tests/behat/behat_tool_lp_data_generators.php @@ -0,0 +1,362 @@ +. + +/** + * Step definition to generate database fixtures for learning plan system. + * + * @package tool_lp + * @category test + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php'); + +use Behat\Gherkin\Node\TableNode as TableNode; +use Behat\Behat\Exception\PendingException as PendingException; +use core_competency\competency; +use core_competency\competency_framework; +use core_competency\plan; +use core_competency\user_evidence; + +/** + * Step definition to generate database fixtures for learning plan system. + * + * @package tool_lp + * @category test + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_tool_lp_data_generators extends behat_base { + + /** + * @var tool_lp data generator + */ + protected $datageneratorlp; + + /** + * Each element specifies: + * - The data generator sufix used. + * - The required fields. + * - The mapping between other elements references and database field names. + * @var array + */ + protected static $elements = array( + 'frameworks' => array( + 'datagenerator' => 'framework', + 'required' => array() + ), + 'templates' => array( + 'datagenerator' => 'template', + 'required' => array() + ), + 'plans' => array( + 'datagenerator' => 'plan', + 'required' => array('user') + ), + 'competencies' => array( + 'datagenerator' => 'competency', + 'required' => array('framework') + ), + 'userevidence' => array( + 'datagenerator' => 'user_evidence', + 'required' => array('user') + ), + 'plancompetencies' => array( + 'datagenerator' => 'plan_competency', + 'required' => array('plan', 'competency') + ), + 'userevidencecompetencies' => array( + 'datagenerator' => 'user_evidence_competency', + 'required' => array('userevidence', 'competency') + ), + 'usercompetencies' => array( + 'datagenerator' => 'user_competency', + 'required' => array('user', 'competency') + ), + 'usercompetencyplans' => array( + 'datagenerator' => 'user_competency_plan', + 'required' => array('user', 'competency', 'plan') + ) + ); + + /** + * Creates the specified element. More info about available elements in http://docs.moodle.org/dev/Acceptance_testing#Fixtures. + * + * @Given /^the following lp "(?P(?:[^"]|\\")*)" exist:$/ + * + * @throws Exception + * @throws PendingException + * @param string $elementname The name of the entity to add + * @param TableNode $data + */ + public function the_following_lp_exist($elementname, TableNode $data) { + + // Now that we need them require the data generators. + require_once(__DIR__.'/../../../../../lib/phpunit/classes/util.php'); + + if (empty(self::$elements[$elementname])) { + throw new PendingException($elementname . ' data generator is not implemented'); + } + + $datagenerator = testing_util::get_data_generator(); + $this->datageneratorlp = $datagenerator->get_plugin_generator('core_competency'); + + $elementdatagenerator = self::$elements[$elementname]['datagenerator']; + $requiredfields = self::$elements[$elementname]['required']; + if (!empty(self::$elements[$elementname]['switchids'])) { + $switchids = self::$elements[$elementname]['switchids']; + } + + foreach ($data->getHash() as $elementdata) { + + // Check if all the required fields are there. + foreach ($requiredfields as $requiredfield) { + if (!isset($elementdata[$requiredfield])) { + throw new Exception($elementname . ' requires the field ' . $requiredfield . ' to be specified'); + } + } + + // Switch from human-friendly references to ids. + if (isset($switchids)) { + foreach ($switchids as $element => $field) { + $methodname = 'get_' . $element . '_id'; + + // Not all the switch fields are required, default vars will be assigned by data generators. + if (isset($elementdata[$element])) { + // Temp $id var to avoid problems when $element == $field. + $id = $this->{$methodname}($elementdata[$element]); + unset($elementdata[$element]); + $elementdata[$field] = $id; + } + } + } + + // Preprocess the entities that requires a special treatment. + if (method_exists($this, 'preprocess_' . $elementdatagenerator)) { + $elementdata = $this->{'preprocess_' . $elementdatagenerator}($elementdata); + } + + // Creates element. + $methodname = 'create_' . $elementdatagenerator; + if (method_exists($this->datageneratorlp, $methodname)) { + // Using data generators directly. + $this->datageneratorlp->{$methodname}($elementdata); + + } else if (method_exists($this, 'process_' . $elementdatagenerator)) { + // Using an alternative to the direct data generator call. + $this->{'process_' . $elementdatagenerator}($elementdata); + } else { + throw new PendingException($elementname . ' data generator is not implemented'); + } + } + } + + /** + * Adapt creating competency from framework idnumber or frameworkid. + * + * @param array $data + * @return array + */ + protected function preprocess_competency($data) { + if (isset($data['framework'])) { + $framework = competency_framework::get_record(array('idnumber' => $data['framework'])); + if ($framework) { + $data['competencyframeworkid'] = $framework->get_id(); + } else { + $framework = competency_framework::get_record(array('id' => $data['framework'])); + if ($framework) { + $data['competencyframeworkid'] = $framework->get_id(); + } else { + throw new Exception('Could not resolve framework with idnumber or id : "' . $data['category'] . '"'); + } + } + } + unset($data['framework']); + return $data; + } + + /** + * Adapt creating plan from user username. + * + * @param array $data + * @return array + */ + protected function preprocess_plan($data) { + global $DB; + + if (isset($data['user'])) { + $user = $DB->get_record('user', array('username' => $data['user']), '*', MUST_EXIST); + $data['userid'] = $user->id; + } + unset($data['user']); + + if (isset($data['reviewer'])) { + if (is_number($data['reviewer'])) { + $data['reviewerid'] = $data['reviewer']; + } else { + $user = $DB->get_record('user', array('username' => $data['reviewer']), '*', MUST_EXIST); + $data['reviewerid'] = $user->id; + } + unset($data['reviewer']); + } + + if (isset($data['status'])) { + switch ($data['status']) { + case 'draft': + $status = plan::STATUS_DRAFT; + break; + case 'in review': + $status = plan::STATUS_IN_REVIEW; + break; + case 'waiting for review': + $status = plan::STATUS_WAITING_FOR_REVIEW; + break; + case 'active': + $status = plan::STATUS_ACTIVE; + break; + case 'complete': + $status = plan::STATUS_COMPLETE; + break; + default: + throw new Exception('Could not resolve plan status with: "' . $data['status'] . '"'); + break; + } + + $data['status'] = $status; + } + + return $data; + } + + /** + * Adapt creating user_evidence from user username. + * + * @param array $data + * @return array + */ + protected function preprocess_user_evidence($data) { + global $DB; + + if (isset($data['user'])) { + $user = $DB->get_record('user', array('username' => $data['user']), '*', MUST_EXIST); + $data['userid'] = $user->id; + } + unset($data['user']); + return $data; + } + + /** + * Adapt creating plan_competency from plan name and competency shortname. + * + * @param array $data + * @return array + */ + protected function preprocess_plan_competency($data) { + global $DB; + + if (isset($data['plan'])) { + $plan = $DB->get_record(plan::TABLE, array('name' => $data['plan']), '*', MUST_EXIST); + $data['planid'] = $plan->id; + } + unset($data['plan']); + + if (isset($data['competency'])) { + $competency = $DB->get_record(competency::TABLE, array('shortname' => $data['competency']), '*', MUST_EXIST); + $data['competencyid'] = $competency->id; + } + unset($data['competency']); + return $data; + } + + /** + * Adapt creating plan_competency from user evidence name and competency shortname. + * + * @param array $data + * @return array + */ + protected function preprocess_user_evidence_competency($data) { + global $DB; + + if (isset($data['userevidence'])) { + $userevidence = $DB->get_record(user_evidence::TABLE, array('name' => $data['userevidence']), '*', MUST_EXIST); + $data['userevidenceid'] = $userevidence->id; + } + unset($data['userevidence']); + + if (isset($data['competency'])) { + $competency = $DB->get_record(competency::TABLE, array('shortname' => $data['competency']), '*', MUST_EXIST); + $data['competencyid'] = $competency->id; + } + unset($data['competency']); + return $data; + } + + /** + * Adapt creating user_competency from user name and competency shortname. + * + * @param array $data + * @return array + */ + protected function preprocess_user_competency($data) { + global $DB; + + if (isset($data['user'])) { + $user = $DB->get_record('user', array('username' => $data['user']), '*', MUST_EXIST); + $data['userid'] = $user->id; + } + unset($data['user']); + + if (isset($data['competency'])) { + $competency = $DB->get_record(competency::TABLE, array('shortname' => $data['competency']), '*', MUST_EXIST); + $data['competencyid'] = $competency->id; + } + unset($data['competency']); + + return $data; + } + + /** + * Adapt creating user_competency_plan from user name, competency shortname and plan name. + * + * @param array $data + * @return array + */ + protected function preprocess_user_competency_plan($data) { + global $DB; + + if (isset($data['user'])) { + $user = $DB->get_record('user', array('username' => $data['user']), '*', MUST_EXIST); + $data['userid'] = $user->id; + } + unset($data['user']); + + if (isset($data['competency'])) { + $competency = $DB->get_record(competency::TABLE, array('shortname' => $data['competency']), '*', MUST_EXIST); + $data['competencyid'] = $competency->id; + } + unset($data['competency']); + + if (isset($data['plan'])) { + $plan = $DB->get_record(plan::TABLE, array('name' => $data['plan']), '*', MUST_EXIST); + $data['planid'] = $plan->id; + } + unset($data['plan']); + + return $data; + } + +} diff --git a/admin/tool/lp/tests/behat/framework_crud.feature b/admin/tool/lp/tests/behat/framework_crud.feature new file mode 100644 index 00000000000..a77dcd02ff8 --- /dev/null +++ b/admin/tool/lp/tests/behat/framework_crud.feature @@ -0,0 +1,132 @@ +@tool @javascript @tool_lp @tool_lp_framework +Feature: Manage competency frameworks + As a competency framework admin + In order to perform CRUD operations on competency framework + I need to create, update and delete competency framework + + Background: + Given I log in as "admin" + And I am on site homepage + And I expand "Site administration" node + When I expand "Competencies" node + Then I should see "Competency Frameworks" + + Scenario: Create a new framework + Given I follow "Competency Frameworks" + And I should see "List of competency frameworks" + And I click on "Add new competency framework" "button" + And I should see "General" + And I should see "Taxonomies" + And I set the field "Name" to "Science Year-1" + And I set the field "Id number" to "Comp-frm-1" + And I press "Save changes" + And I should see "You must configure the scale by selecting default and proficient values" + And "Configure scales" "button" should be visible + And I press "Configure scales" + And I click on "//input[@data-field='tool_lp_scale_default_1']" "xpath_element" + And I click on "//input[@data-field='tool_lp_scale_proficient_1']" "xpath_element" + And I click on "//input[@value='Close']" "xpath_element" + When I press "Save changes" + Then I should see "Competency framework created" + And I should see "Science Year-1" + + Scenario: Read a framework + Given the following lp "frameworks" exist: + | shortname | idnumber | + | Science Year-2 | sc-y-2 | + And I follow "Competency Frameworks" + And I should see "Science Year-2" + When I click on "Science Year-2" "link" + Then I should see "Science Year-2" + + Scenario: Edit a framework + Given the following lp "frameworks" exist: + | shortname | idnumber | + | Science Year-3 | sc-y-3 | + And I follow "Competency Frameworks" + And I should see "Science Year-3" + And I click on "Edit" of edit menu in the "Science Year-3" row + And the field "Name" matches value "Science Year-3 " + And I set the field "Name" to "Science Year-3 Edited" + When I press "Save changes" + Then I should see "Competency framework updated" + And I should see "Science Year-3 Edited" + And I should see "sc-y-3" + + Scenario: Delete a framework + Given the following lp "frameworks" exist: + | shortname | idnumber | + | Science Year-4 | sc-y-4 | + And I follow "Competency Frameworks" + And I should see "Science Year-4" + And I should see "sc-y-4" + And I click on "Delete" of edit menu in the "Science Year-4" row + And "Confirm" "dialogue" should be visible + And "Delete" "button" should exist in the "Confirm" "dialogue" + And "Cancel" "button" should exist in the "Confirm" "dialogue" + And I click on "Cancel" "button" + And I click on "Delete" of edit menu in the "Science Year-4" row + And "Confirm" "dialogue" should be visible + When I click on "Delete" "button" + Then I should not see "Science Year-4" + And I should not see "sc-y-4" + +Scenario: Edit a framework with competencies in user competency + Given the following lp "frameworks" exist: + | shortname | idnumber | + | Science Year-5 | sc-y-5 | + And the following lp "competencies" exist: + | shortname | framework | + | Comp1 | sc-y-5 | + | Comp2 | sc-y-5 | + And the following lp "plans" exist: + | name | user | description | + | Plan Science-5 | admin | Plan description | + And the following lp "plancompetencies" exist: + | plan | competency | + | Plan Science-5 | Comp1 | + | Plan Science-5 | Comp2 | + And the following lp "usercompetencies" exist: + | user | competency | + | admin | Comp1 | + | admin | Comp2 | + And I follow "Competency Frameworks" + And I should see "Science Year-5" + And I click on "Edit" of edit menu in the "Science Year-5" row + And the field "Name" matches value "Science Year-5 " + And I set the field "Name" to "Science Year-5 Edited" + And the "scaleid" "select" should be readonly + When I press "Save changes" + Then I should see "Competency framework updated" + And I should see "Science Year-5 Edited" + And I should see "sc-y-5" + +Scenario: Edit a framework with competencies in user competency plan + Given the following lp "frameworks" exist: + | shortname | idnumber | + | Science Year-6 | sc-y-6 | + And the following lp "competencies" exist: + | shortname | framework | + | Comp1 | sc-y-6 | + | Comp2 | sc-y-6 | + And the following lp "plans" exist: + | name | user | description | + | Plan Science-6 | admin | Plan description | + And the following lp "plancompetencies" exist: + | plan | competency | + | Plan Science-6 | Comp1 | + | Plan Science-6 | Comp2 | + And the following lp "usercompetencyplans" exist: + | user | competency | plan | + | admin | Comp1 | Plan Science-6 | + | admin | Comp2 | Plan Science-6 | + And I follow "Competency Frameworks" + And I should see "Science Year-6" + And I click on "Edit" of edit menu in the "Science Year-6" row + And the field "Name" matches value "Science Year-6 " + And I set the field "Name" to "Science Year-6 Edited" + And the "scaleid" "select" should be readonly + When I press "Save changes" + Then I should see "Competency framework updated" + And I should see "Science Year-6 Edited" + And I should see "sc-y-6" diff --git a/admin/tool/lp/tests/behat/plan_crud.feature b/admin/tool/lp/tests/behat/plan_crud.feature new file mode 100644 index 00000000000..82c6bd65318 --- /dev/null +++ b/admin/tool/lp/tests/behat/plan_crud.feature @@ -0,0 +1,132 @@ +@tool @javascript @tool_lp @tool_lp_plan +Feature: Manage plearning plan + As a learning plan admin + In order to perform CRUD operations on learning plan + I need to create, update and delete learning plan + + Background: + Given I log in as "admin" + And I am on site homepage + When I follow "Profile" in the user menu + Then I should see "Learning plans" + + Scenario: Create a new learning plan + Given I follow "Learning plans" + And I should see "List of learning plan" + And I click on "Add new learning plan" "button" + And I should see "Add new learning plan" + And I set the field "Name" to "Science plan" + And I set the field "Description" to "Here description of learning plan" + When I press "Save changes" + Then I should see "Learning plan created" + And I should see "Science plan" + + Scenario: Create a learning plan based on template + Given the following lp "templates" exist: + | shortname | description | + | Science template | science template description | + And I follow "Home" + And I expand "Site administration" node + And I expand "Competencies" node + And I follow "Learning plan templates" + And I click on ".template-userplans" "css_element" in the "Science template" "table_row" + And I click on ".form-autocomplete-downarrow" "css_element" + And I click on "Admin" item in the autocomplete list + When I click on "Create plans" "button" + Then I should see "A plan was created" + And I should see "Admin User" in the "Science template" "table_row" + + Scenario: Create a learning plan from template cohort + Given the following lp "templates" exist: + | shortname | description | + | Science template cohort | science template description | + And the following "users" exist: + | username | firstname | lastname | email | + | student-plan1 | Student | 1 | studentplan1@example.com | + | student-plan2 | Student | 2 | studentplan2@example.com | + And the following "cohorts" exist: + | name | idnumber | + | cohort plan | COHORTPLAN | + And the following "cohort members" exist: + | user | cohort | + | student-plan1 | COHORTPLAN | + | student-plan2 | COHORTPLAN | + And I follow "Home" + And I expand "Site administration" node + And I expand "Competencies" node + And I follow "Learning plan templates" + And I click on ".template-cohorts" "css_element" in the "Science template cohort" "table_row" + And I click on ".form-autocomplete-downarrow" "css_element" + And I click on "cohort plan" item in the autocomplete list + When I click on "Add cohorts" "button" + Then I should see "2 plans were created." + And I follow "Learning plan templates" + And I click on ".template-userplans" "css_element" in the "Science template cohort" "table_row" + And I should see "Student 1" + And I should see "Student 2" + + Scenario: Read a learning plan + Given the following lp "plans" exist: + | name | user | description | + | Science plan Year-1 | admin | science plan description | + And I follow "Learning plans" + And I should see "Science plan Year-1" + When I click on "Science plan Year-1" "link" + Then I should see "Science plan Year-1" + And I should see "Learning plan competencies" + + Scenario: Manage a learning plan competencies + Given the following lp "plans" exist: + | name | user | description | + | Science plan Year-manage | admin | science plan description | + And the following lp "frameworks" exist: + | shortname | idnumber | + | Framework 1 | sc-y-2 | + And the following lp "competencies" exist: + | shortname | framework | + | comp1 | sc-y-2 | + | comp2 | sc-y-2 | + And I follow "Learning plans" + And I should see "Science plan Year-manage" + And I follow "Science plan Year-manage" + And I should see "Add competency" + And I press "Add competency" + And "Competency picker" "dialogue" should be visible + And I select "comp1" of the competency tree + When I click on "Add" "button" in the "Competency picker" "dialogue" + Then "comp1" "table_row" should exist + And I click on "Delete" of edit menu in the "comp1" row + And "Confirm" "dialogue" should be visible + And I click on "Confirm" "button" + And "comp1" "table_row" should not exist + + Scenario: Edit a learning plan + Given the following lp "plans" exist: + | name | user | description | + | Science plan Year-2 | admin | science plan description | + | Science plan Year-3 | admin | science plan description | + And I follow "Learning plans" + And I should see "Science plan Year-2" + And I should see "Science plan Year-3" + And I click on "Edit" of edit menu in the "Science plan Year-3" row + And the field "Name" matches value "Science plan Year-3" + And I set the field "Name" to "Science plan Year-3 Edited" + When I press "Save changes" + Then I should see "Learning plan updated" + And I should see "Science plan Year-3 Edited" + + Scenario: Delete a learning plan + Given the following lp "plans" exist: + | name | user | description | + | Science plan Year-4 | admin | science plan description | + And I follow "Learning plans" + And I should see "Science plan Year-4" + And I click on "Delete" of edit menu in the "Science plan Year-4" row + And "Confirm" "dialogue" should be visible + And "Delete" "button" should exist in the "Confirm" "dialogue" + And "Cancel" "button" should exist in the "Confirm" "dialogue" + And I click on "Cancel" "button" + And I click on "Delete" of edit menu in the "Science plan Year-4" row + And "Confirm" "dialogue" should be visible + When I click on "Delete" "button" + Then I should not see "Science plan Year-4" diff --git a/admin/tool/lp/tests/behat/plan_workflow.feature b/admin/tool/lp/tests/behat/plan_workflow.feature new file mode 100644 index 00000000000..8a3a4386262 --- /dev/null +++ b/admin/tool/lp/tests/behat/plan_workflow.feature @@ -0,0 +1,210 @@ +@tool @javascript @tool_lp @tool_lp_plan_workflow +Feature: Manage plan workflow + As a user + In order to change the status of plan + I need to be able to change the status of a plan + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | user1 | User | 1 | user1@example.com | + | user2 | User | 2 | user2@example.com | + | manager1 | Manager | 1 | manager@example.com | + And the following "roles" exist: + | shortname | name | archetype | + | usermanageowndraftplan | User manage own draft plan role | user | + | usermanageownplan | User manage own plan role | user | + | manageplan | Manager all plans role | manager | + And the following "role assigns" exist: + | user | role | contextlevel | reference | + | user1 | usermanageowndraftplan | System | | + | user2 | usermanageownplan | System | | + | manager1 | manageplan | System | | + And the following lp "frameworks" exist: + | shortname | idnumber | + | Test-Framework | ID-FW1 | + And the following lp "competencies" exist: + | shortname | framework | + | Test-Comp1 | ID-FW1 | + | Test-Comp2 | ID-FW1 | + And the following lp "plans" exist: + | name | user | description | + | Test-Plan1 | user1 | Description of plan for user 1 | + | Test-Plan2 | user2 | Description of plan for user 2 | + And the following lp "plancompetencies" exist: + | plan | competency | + | Test-Plan1 | Test-Comp1 | + | Test-Plan1 | Test-Comp2 | + | Test-Plan2 | Test-Comp1 | + | Test-Plan2 | Test-Comp2 | + And I log in as "admin" + And I set the following system permissions of "User manage own draft plan role" role: + | capability | permission | + | moodle/competency:planmanageowndraft | Allow | + And I set the following system permissions of "User manage own plan role" role: + | capability | permission | + | moodle/competency:planmanageowndraft | Allow | + | moodle/competency:planmanageown | Allow | + And I set the following system permissions of "Manager all plans role" role: + | capability | permission | + | moodle/competency:planmanage | Allow | + | moodle/competency:planmanagedraft | Allow | + | moodle/competency:planmanageowndraft | Allow | + | moodle/competency:planview | Allow | + | moodle/competency:planreview | Allow | + | moodle/competency:planrequestreview | Allow | + And I log out + + Scenario: User can manages his own plan draft + Given I log in as "user1" + And I follow "Profile" in the user menu + When I follow "Learning plans" + Then I should see "List of learning plans" + And I should see "Test-Plan1" + And I should not see "Test-Plan2" + And I click on "Request review" of edit menu in the "Test-Plan1" row + And I should see "Waiting for review" + And I click on "Cancel review" of edit menu in the "Test-Plan1" row + And I should see "Draft" + And I log out + + Scenario: User can manages his own plan + Given I log in as "user2" + And I follow "Profile" in the user menu + When I follow "Learning plans" + Then I should see "List of learning plans" + And I should see "Test-Plan2" + And I should not see "Test-Plan1" + And I click on "Request review" of edit menu in the "Test-Plan2" row + And I should see "Waiting for review" + And I click on "Start review" of edit menu in the "Test-Plan2" row + And I should see "In review" + And I click on "Finish review" of edit menu in the "Test-Plan2" row + And I should see "Draft" + And I click on "Make active" of edit menu in the "Test-Plan2" row + And I should see "Active" + And I click on "Complete this learning plan" of edit menu in the "Test-Plan2" row + And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue" + And I should see "Complete" + And I click on "Reopen this learning plan" of edit menu in the "Test-Plan2" row + And I click on "Reopen this learning plan" "button" in the "Confirm" "dialogue" + And I should see "Active" + And I log out + + Scenario: Manager can see learning plan with status waiting for review + Given the following lp "plans" exist: + | name | user | description | status | + | Test-Plan3 | user2 | Description of plan 3 for user 1 | waiting for review | + | Test-Plan4 | user1 | Description of plan 3 for user 1 | draft | + When I log in as "manager1" + Then I should see "Test-Plan3" + And I should not see "Test-Plan4" + And I log out + + Scenario: Manager can start review of learning plan with status waiting for review + Given the following lp "plans" exist: + | name | user | description | status | + | Test-Plan3 | user1 | Description of plan 3 for user 1 | waiting for review | + And I log in as "manager1" + And I follow "Test-Plan3" + And I should see "User 1" + And I should see "Test-Plan3" + When I follow "Start review" + Then I should see "In review" + And I log out + + Scenario: Manager can reject a learning plan with status in review + Given the following lp "plans" exist: + | name | user | description | status | reviewer | + | Test-Plan3 | user1 | Description of plan 3 for user 1 | in review | manager1 | + And I log in as "manager1" + And I follow "Test-Plan3" + And I should see "User 1" + And I should see "Test-Plan3" + And I should see "In review" + When I follow "Finish review" + Then I should see "Draft" + And I log out + + Scenario: Manager can accept a learning plan with status in review + Given the following lp "plans" exist: + | name | user | description | status | reviewer | + | Test-Plan3 | user1 | Description of plan 3 for user 1 | in review | manager1 | + And I log in as "manager1" + And I follow "Test-Plan3" + And I should see "User 1" + And I should see "Test-Plan3" + And I should see "In review" + When I follow "Make active" + Then I should see "Active" + And I log out + + Scenario: Manager send back to draft an active learning plan + Given the following lp "plans" exist: + | name | user | description | status | reviewer | + | Test-Plan3 | user1 | Description of plan 3 for user 1 | active | manager1 | + | Test-Plan4 | user1 | Description of plan 4 for user 1 | active | manager1 | + And I log in as "manager1" + And I expand "Site administration" node + And I expand "Users" node + And I expand "Accounts" node + And I follow "Browse list of users" + And I follow "User 1" + And I follow "Learning plans" + And I should see "List of learning plans" + And I follow "Learning plans" + When I click on "Send back to draft" of edit menu in the "Test-Plan3" row + And I follow "Test-Plan4" + And I follow "Send back to draft" + And I follow "Learning plans" + Then I should see "Draft" + And I should not see "Active" + And I log out + + Scenario: Manager change an active learning plan to completed + Given the following lp "plans" exist: + | name | user | description | status | reviewer | + | Test-Plan3 | user1 | Description of plan 3 for user 1 | active | manager1 | + | Test-Plan4 | user1 | Description of plan 4 for user 1 | active | manager1 | + And I log in as "manager1" + And I expand "Site administration" node + And I expand "Users" node + And I expand "Accounts" node + And I follow "Browse list of users" + And I follow "User 1" + And I follow "Learning plans" + And I should see "List of learning plans" + And I follow "Learning plans" + When I click on "Complete this learning plan" of edit menu in the "Test-Plan3" row + And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue" + And I follow "Test-Plan4" + And I follow "Complete this learning plan" + And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue" + And I follow "Learning plans" + Then I should see "Complete" + And I should not see "Active" + And I log out + +Scenario: Manager reopen a complete learning plan + Given the following lp "plans" exist: + | name | user | description | status | reviewer | + | Test-Plan3 | user1 | Description of plan 3 for user 1 | complete | manager1 | + | Test-Plan4 | user1 | Description of plan 4 for user 1 | complete | manager1 | + And I log in as "manager1" + And I expand "Site administration" node + And I expand "Users" node + And I expand "Accounts" node + And I follow "Browse list of users" + And I follow "User 1" + And I follow "Learning plans" + And I should see "List of learning plans" + And I follow "Learning plans" + When I click on "Reopen this learning plan" of edit menu in the "Test-Plan3" row + And I click on "Reopen this learning plan" "button" in the "Confirm" "dialogue" + And I follow "Test-Plan4" + And I follow "Reopen this learning plan" + And I click on "Reopen this learning plan" "button" in the "Confirm" "dialogue" + And I follow "Learning plans" + Then I should see "Active" + And I should not see "Complete" + And I log out diff --git a/admin/tool/lp/tests/behat/template_crud.feature b/admin/tool/lp/tests/behat/template_crud.feature new file mode 100644 index 00000000000..a6a3f5dfe4d --- /dev/null +++ b/admin/tool/lp/tests/behat/template_crud.feature @@ -0,0 +1,62 @@ +@tool @javascript @tool_lp @tool_lp_template +Feature: Manage plearning plan templates + As a learning plan admin + In order to perform CRUD operations on learning plan template + I need to create, update and delete learning plan temlate + + Background: + Given I log in as "admin" + And I am on site homepage + And I expand "Site administration" node + When I expand "Competencies" node + Then I should see "Learning plan templates" + + Scenario: Create a new learning plan template + Given I follow "Learning plan templates" + And I should see "List of learning plan templates" + And I click on "Add new learning plan template" "button" + And I should see "Add new learning plan template" + And I set the field "Name" to "Science template" + And I set the field "Description" to "Here description of learning plan template" + When I press "Save changes" + Then I should see "Learning plan template created" + And I should see "Science template" + + Scenario: Read a learning plan template + Given the following lp "templates" exist: + | shortname | description | + | Science template Year-2 | science template description | + And I follow "Learning plan templates" + And I should see "Science template Year-2" + When I click on "Science template Year-2" "link" + Then I should see "Science template Year-2" + And I should see "Template competencies" + + Scenario: Edit a learning plan template + Given the following lp "templates" exist: + | shortname | description | + | Science template Year-3 | science template description | + And I follow "Learning plan templates" + And I should see "Science template Year-3" + And I click on "Edit" of edit menu in the "Science template Year-3" row + And the field "Name" matches value "Science template Year-3" + And I set the field "Name" to "Science template Year-3 Edited" + When I press "Save changes" + Then I should see "Learning plan template updated" + And I should see "Science template Year-3 Edited" + + Scenario: Delete a learning plan template + Given the following lp "templates" exist: + | shortname | description | + | Science template Year-4 | science template description | + And I follow "Learning plan templates" + And I should see "Science template Year-4" + And I click on "Delete" of edit menu in the "Science template Year-4" row + And "Confirm" "dialogue" should be visible + And "Delete" "button" should exist in the "Confirm" "dialogue" + And "Cancel" "button" should exist in the "Confirm" "dialogue" + And I click on "Cancel" "button" + And I click on "Delete" of edit menu in the "Science template Year-4" row + And "Confirm" "dialogue" should be visible + When I click on "Delete" "button" + Then I should not see "Science template Year-4" diff --git a/admin/tool/lp/tests/behat/user_evidence_comp_link.feature b/admin/tool/lp/tests/behat/user_evidence_comp_link.feature new file mode 100644 index 00000000000..4ebd5c920ab --- /dev/null +++ b/admin/tool/lp/tests/behat/user_evidence_comp_link.feature @@ -0,0 +1,70 @@ +@tool @javascript @tool_lp @tool_lp_user_evidence_comp_link +Feature: Manage competencies linked to evidence of prior learning + To link or unlink competency to evidence of prior learning + As learning plan admin + I need to link and unlink competencies from evidence of prior learning + + Background: + Given the following lp "frameworks" exist: + | shortname | idnumber | + | Test-Framework | ID-FW1 | + And the following lp "competencies" exist: + | shortname | framework | + | Test-Comp1 | ID-FW1 | + | Test-Comp2 | ID-FW1 | + And the following lp "plans" exist: + | name | user | description | + | Test-Plan | admin | Plan description | + And the following lp "plancompetencies" exist: + | plan | competency | + | Test-Plan | Test-Comp1 | + | Test-Plan | Test-Comp2 | + And the following lp "userevidence" exist: + | name | description | user | + | Test-Evidence | Description evidence | admin | + When I log in as "admin" + And I follow "Profile" in the user menu + And I follow "Learning plans" + Then I should see "Evidence of prior learning" + + Scenario: Link competency to evidence of prior learning from page + Given I follow "Evidence of prior learning" + And I should see "List of evidence" + And I should see "Test-Evidence" + And I click on "Test-Evidence" "link" + And I should see "Linked competencies" + And I press "Link competencies" + And "Competency picker" "dialogue" should be visible + And I select "Test-Comp1" of the competency tree + When I click on "Add" "button" in the "Competency picker" "dialogue" + Then "Test-Comp1" "table_row" should exist + + Scenario: Link competency to evidence of prior learning from list + Given I follow "Evidence of prior learning" + And I should see "List of evidence" + And I should see "Test-Evidence" + And I click on "Link" of edit menu in the "Test-Evidence" row + And "Competency picker" "dialogue" should be visible + And I select "Test-Comp2" of the competency tree + When I click on "Add" "button" in the "Competency picker" "dialogue" + Then "Test-Comp2" "table_row" should exist + + Scenario: Unlink competency from evidence of prior learning + Given the following lp "userevidencecompetencies" exist: + | userevidence | competency | + | Test-Evidence | Test-Comp1 | + | Test-Evidence | Test-Comp2 | + Given the following lp "usercompetencies" exist: + | user | competency | + | admin | Test-Comp1 | + | admin | Test-Comp2 | + And I follow "Evidence of prior learning" + And I should see "List of evidence" + And I should see "Test-Evidence" + And I click on "Test-Evidence" "link" + And I should see "Linked competencies" + And I should see "Test-Comp1" + And I should see "Test-Comp2" + When I click on "Delete" "link" in the "Test-Comp1" "table_row" + Then I should not see "Test-Comp1" + And I should see "Test-Comp2" diff --git a/admin/tool/lp/tests/behat/user_evidence_crud.feature b/admin/tool/lp/tests/behat/user_evidence_crud.feature new file mode 100644 index 00000000000..6a46e199afa --- /dev/null +++ b/admin/tool/lp/tests/behat/user_evidence_crud.feature @@ -0,0 +1,81 @@ +@tool @javascript @tool_lp @tool_lp_user_evidence +Feature: Manage evidence of prior learning + In order to perform CRUD operations on evidence of prior learning + As a user + I need to create, update and delete evidence of prior learning + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + |user1 | User | 1 | user1@example.com | + When I log in as "user1" + And I follow "Profile" in the user menu + And I follow "Learning plans" + Then I should see "Evidence of prior learning" + + Scenario: Create a new evidence of prior learning + Given I follow "Evidence of prior learning" + And I should see "List of evidence" + When I click on "Add new evidence" "button" + And I set the field "Name" to "Evidence-1" + And I set the field "Description" to "Evidence-1 description" + And I press "Save changes" + Then I should see "Evidence of prior learning created" + And I should see "Evidence-1" + + Scenario: Read an evidence of prior learning + Given the following lp "userevidence" exist: + | name | description | user | + | Evidence-2 | Evidence-2 description | user1 | + And I follow "Evidence of prior learning" + And I should see "List of evidence" + And I should see "Evidence-2" + When I click on "Evidence-2" "link" + Then I should see "Evidence-2" + + Scenario: Edit an evidence of prior learning + Given the following lp "userevidence" exist: + | name | description | user | + | Evidence-3 | Evidence-3 description | user1 | + And I follow "Evidence of prior learning" + And I should see "List of evidence" + And I click on "Edit" of edit menu in the "Evidence-3" row + And the field "Name" matches value "Evidence-3" + When I set the field "Name" to "Evidence-3 Edited" + And I press "Save changes" + Then I should see "Evidence of prior learning updated" + And I should see "Evidence-3 Edited" + + Scenario: Delete an evidence of prior learning + Given the following lp "userevidence" exist: + | name | description | user | + | Evidence-4 | Evidence-4 description | user1 | + And I follow "Evidence of prior learning" + And I should see "List of evidence" + And I click on "Delete" of edit menu in the "Evidence-4" row + And I click on "Cancel" "button" + And I click on "Delete" of edit menu in the "Evidence-4" row + And "Confirm" "dialogue" should be visible + When I click on "Delete" "button" + Then I should not see "Evidence-4" + + Scenario: List evidences of prior learning + Given the following "users" exist: + | username | firstname | lastname | email | + |user2 | User | 2 | user2@example.com | + And the following lp "userevidence" exist: + | name | description | user | + | Evidence-5 | Evidence-5 description | user1 | + | Evidence-6 | Evidence-6 description | user2 | + And I follow "Evidence of prior learning" + And I should see "List of evidence" + And I should see "Evidence-5" + When I log out + And I log in as "user2" + And I follow "Profile" in the user menu + And I follow "Learning plans" + And I follow "Evidence of prior learning" + Then I should see "List of evidence" + And I should see "Evidence-6" + And I should not see "Evidence-5" + diff --git a/admin/tool/lp/tests/externallib_test.php b/admin/tool/lp/tests/externallib_test.php new file mode 100644 index 00000000000..1c85942ea5a --- /dev/null +++ b/admin/tool/lp/tests/externallib_test.php @@ -0,0 +1,518 @@ +. +/** + * External learning plans webservice API tests. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +use core_competency\api; +use tool_lp\external; +use core_competency\invalid_persistent_exception; +use core_competency\plan; +use core_competency\related_competency; +use core_competency\user_competency; +use core_competency\user_competency_plan; +use core_competency\plan_competency; +use core_competency\template; +use core_competency\template_competency; +use core_competency\course_competency_settings; + +/** + * External learning plans webservice API tests. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_lp_external_testcase extends externallib_advanced_testcase { + + /** @var stdClass $creator User with enough permissions to create insystem context. */ + protected $creator = null; + + /** @var stdClass $catcreator User with enough permissions to create incategory context. */ + protected $catcreator = null; + + /** @var stdClass $category Category */ + protected $category = null; + + /** @var stdClass $category Category */ + protected $othercategory = null; + + /** @var stdClass $user User with enough permissions to view insystem context */ + protected $user = null; + + /** @var stdClass $catuser User with enough permissions to view incategory context */ + protected $catuser = null; + + /** @var int Creator role id */ + protected $creatorrole = null; + + /** @var int User role id */ + protected $userrole = 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. + $creator = $this->getDataGenerator()->create_user(); + $user = $this->getDataGenerator()->create_user(); + $catuser = $this->getDataGenerator()->create_user(); + $catcreator = $this->getDataGenerator()->create_user(); + $category = $this->getDataGenerator()->create_category(); + $othercategory = $this->getDataGenerator()->create_category(); + $syscontext = context_system::instance(); + $catcontext = context_coursecat::instance($category->id); + + // Fetching default authenticated user role. + $userroles = get_archetype_roles('user'); + $this->assertCount(1, $userroles); + $authrole = array_pop($userroles); + + // Reset all default authenticated users permissions. + unassign_capability('moodle/competency:competencygrade', $authrole->id); + unassign_capability('moodle/competency:competencymanage', $authrole->id); + unassign_capability('moodle/competency:competencyview', $authrole->id); + unassign_capability('moodle/competency:planmanage', $authrole->id); + unassign_capability('moodle/competency:planmanagedraft', $authrole->id); + unassign_capability('moodle/competency:planmanageown', $authrole->id); + unassign_capability('moodle/competency:planview', $authrole->id); + unassign_capability('moodle/competency:planviewdraft', $authrole->id); + unassign_capability('moodle/competency:planviewown', $authrole->id); + unassign_capability('moodle/competency:planviewowndraft', $authrole->id); + unassign_capability('moodle/competency:templatemanage', $authrole->id); + unassign_capability('moodle/competency:templateview', $authrole->id); + unassign_capability('moodle/cohort:manage', $authrole->id); + unassign_capability('moodle/competency:coursecompetencyconfigure', $authrole->id); + + // Creating specific roles. + $this->creatorrole = create_role('Creator role', 'lpcreatorrole', 'learning plan creator role description'); + $this->userrole = create_role('User role', 'lpuserrole', 'learning plan user role description'); + + assign_capability('moodle/competency:competencymanage', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:competencycompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:planmanage', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:planmanagedraft', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:planview', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:planviewdraft', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:templatemanage', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:competencygrade', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/cohort:manage', CAP_ALLOW, $this->creatorrole, $syscontext->id); + + assign_capability('moodle/competency:competencyview', CAP_ALLOW, $this->userrole, $syscontext->id); + assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $syscontext->id); + assign_capability('moodle/competency:planviewown', CAP_ALLOW, $this->userrole, $syscontext->id); + assign_capability('moodle/competency:planviewowndraft', CAP_ALLOW, $this->userrole, $syscontext->id); + + role_assign($this->creatorrole, $creator->id, $syscontext->id); + role_assign($this->creatorrole, $catcreator->id, $catcontext->id); + role_assign($this->userrole, $user->id, $syscontext->id); + role_assign($this->userrole, $catuser->id, $catcontext->id); + + $this->creator = $creator; + $this->catcreator = $catcreator; + $this->user = $user; + $this->catuser = $catuser; + $this->category = $category; + $this->othercategory = $othercategory; + + accesslib_clear_all_caches_for_unit_testing(); + } + + public function test_search_users_by_capability() { + global $CFG; + $this->resetAfterTest(true); + + $dg = $this->getDataGenerator(); + $ux = $dg->create_user(); + $u1 = $dg->create_user(array('idnumber' => 'Cats', 'firstname' => 'Bob', 'lastname' => 'Dyyylan', + 'email' => 'bobbyyy@dyyylan.com', 'phone1' => '123456', 'phone2' => '78910', 'department' => 'Marketing', + 'institution' => 'HQ')); + + // First we search with no capability assigned. + $this->setUser($ux); + $result = external::search_users('yyylan', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(0, $result['users']); + $this->assertEquals(0, $result['count']); + + // Now we assign a different capability. + $usercontext = context_user::instance($u1->id); + $systemcontext = context_system::instance(); + $customrole = $this->assignUserCapability('moodle/competency:planview', $usercontext->id); + + $result = external::search_users('yyylan', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(0, $result['users']); + $this->assertEquals(0, $result['count']); + + // Now we assign a matching capability in the same role. + $usercontext = context_user::instance($u1->id); + $this->assignUserCapability('moodle/competency:planmanage', $usercontext->id, $customrole); + + $result = external::search_users('yyylan', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(1, $result['users']); + $this->assertEquals(1, $result['count']); + + // Now assign another role with the same capability (test duplicates). + role_assign($this->creatorrole, $ux->id, $usercontext->id); + $result = external::search_users('yyylan', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(1, $result['users']); + $this->assertEquals(1, $result['count']); + + // Now lets try a different user with only the role at system level. + $ux2 = $dg->create_user(); + role_assign($this->creatorrole, $ux2->id, $systemcontext->id); + $this->setUser($ux2); + $result = external::search_users('yyylan', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(1, $result['users']); + $this->assertEquals(1, $result['count']); + + // Now lets try a different user with only the role at user level. + $ux3 = $dg->create_user(); + role_assign($this->creatorrole, $ux3->id, $usercontext->id); + $this->setUser($ux3); + $result = external::search_users('yyylan', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(1, $result['users']); + $this->assertEquals(1, $result['count']); + + // Switch back. + $this->setUser($ux); + + // Now add a prevent override (will change nothing because we still have an ALLOW). + assign_capability('moodle/competency:planmanage', CAP_PREVENT, $customrole, $usercontext->id); + $result = external::search_users('yyylan', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(1, $result['users']); + $this->assertEquals(1, $result['count']); + + // Now change to a prohibit override (should prevent access). + assign_capability('moodle/competency:planmanage', CAP_PROHIBIT, $customrole, $usercontext->id); + $result = external::search_users('yyylan', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(1, $result['users']); + $this->assertEquals(1, $result['count']); + + } + + /** + * Ensures that overrides, as well as system permissions, are respected. + */ + public function test_search_users_by_capability_the_comeback() { + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + + $master = $dg->create_user(); + $manager = $dg->create_user(); + $slave1 = $dg->create_user(array('lastname' => 'MOODLER')); + $slave2 = $dg->create_user(array('lastname' => 'MOODLER')); + $slave3 = $dg->create_user(array('lastname' => 'MOODLER')); + + $syscontext = context_system::instance(); + $slave1context = context_user::instance($slave1->id); + $slave2context = context_user::instance($slave2->id); + $slave3context = context_user::instance($slave3->id); + + // Creating a role giving the site config. + $roleid = $dg->create_role(); + assign_capability('moodle/site:config', CAP_ALLOW, $roleid, $syscontext->id, true); + + // Create a role override for slave 2. + assign_capability('moodle/site:config', CAP_PROHIBIT, $roleid, $slave2context->id, true); + + // Assigning the role. + // Master -> System context. + // Manager -> User context. + role_assign($roleid, $master->id, $syscontext); + role_assign($roleid, $manager->id, $slave1context); + + // Flush accesslib. + accesslib_clear_all_caches_for_unit_testing(); + + // Confirm. + // Master has system permissions. + $this->setUser($master); + $this->assertTrue(has_capability('moodle/site:config', $syscontext)); + $this->assertTrue(has_capability('moodle/site:config', $slave1context)); + $this->assertFalse(has_capability('moodle/site:config', $slave2context)); + $this->assertTrue(has_capability('moodle/site:config', $slave3context)); + + // Manager only has permissions in slave 1. + $this->setUser($manager); + $this->assertFalse(has_capability('moodle/site:config', $syscontext)); + $this->assertTrue(has_capability('moodle/site:config', $slave1context)); + $this->assertFalse(has_capability('moodle/site:config', $slave2context)); + $this->assertFalse(has_capability('moodle/site:config', $slave3context)); + + // Now do the test. + $this->setUser($master); + $result = external::search_users('MOODLER', 'moodle/site:config'); + $this->assertCount(2, $result['users']); + $this->assertEquals(2, $result['count']); + $this->assertArrayHasKey($slave1->id, $result['users']); + $this->assertArrayHasKey($slave3->id, $result['users']); + + $this->setUser($manager); + $result = external::search_users('MOODLER', 'moodle/site:config'); + $this->assertCount(1, $result['users']); + $this->assertEquals(1, $result['count']); + $this->assertArrayHasKey($slave1->id, $result['users']); + } + + public function test_search_users() { + global $CFG; + $this->resetAfterTest(true); + + $dg = $this->getDataGenerator(); + $ux = $dg->create_user(); + $u1 = $dg->create_user(array('idnumber' => 'Cats', 'firstname' => 'Bob', 'lastname' => 'Dyyylan', + 'email' => 'bobbyyy@dyyylan.com', 'phone1' => '123456', 'phone2' => '78910', 'department' => 'Marketing', + 'institution' => 'HQ')); + $u2 = $dg->create_user(array('idnumber' => 'Dogs', 'firstname' => 'Alice', 'lastname' => 'Dyyylan', + 'email' => 'alyyyson@dyyylan.com', 'phone1' => '33333', 'phone2' => '77777', 'department' => 'Development', + 'institution' => 'O2')); + $u3 = $dg->create_user(array('idnumber' => 'Fish', 'firstname' => 'Thomas', 'lastname' => 'Xow', + 'email' => 'fishyyy@moodle.com', 'phone1' => '77777', 'phone2' => '33333', 'department' => 'Research', + 'institution' => 'Bob')); + + // We need to give the user the capability we are searching for on each of the test users. + $this->setAdminUser(); + $usercontext = context_user::instance($u1->id); + $dummyrole = $this->assignUserCapability('moodle/competency:planmanage', $usercontext->id); + $usercontext = context_user::instance($u2->id); + $this->assignUserCapability('moodle/competency:planmanage', $usercontext->id, $dummyrole); + $usercontext = context_user::instance($u3->id); + $this->assignUserCapability('moodle/competency:planmanage', $usercontext->id, $dummyrole); + + $this->setUser($ux); + $usercontext = context_user::instance($u1->id); + $this->assignUserCapability('moodle/competency:planmanage', $usercontext->id, $dummyrole); + $usercontext = context_user::instance($u2->id); + $this->assignUserCapability('moodle/competency:planmanage', $usercontext->id, $dummyrole); + $usercontext = context_user::instance($u3->id); + $this->assignUserCapability('moodle/competency:planmanage', $usercontext->id, $dummyrole); + + $this->setAdminUser(); + + // No identity fields. + $CFG->showuseridentity = ''; + $result = external::search_users('cats', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(0, $result['users']); + $this->assertEquals(0, $result['count']); + + // Filter by name. + $CFG->showuseridentity = ''; + $result = external::search_users('dyyylan', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(2, $result['users']); + $this->assertEquals(2, $result['count']); + $this->assertEquals($u2->id, $result['users'][0]['id']); + $this->assertEquals($u1->id, $result['users'][1]['id']); + + // Filter by institution and name. + $CFG->showuseridentity = 'institution'; + $result = external::search_users('bob', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(2, $result['users']); + $this->assertEquals(2, $result['count']); + $this->assertEquals($u1->id, $result['users'][0]['id']); + $this->assertEquals($u3->id, $result['users'][1]['id']); + + // Filter by id number. + $CFG->showuseridentity = 'idnumber'; + $result = external::search_users('cats', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(1, $result['users']); + $this->assertEquals(1, $result['count']); + $this->assertEquals($u1->id, $result['users'][0]['id']); + $this->assertEquals($u1->idnumber, $result['users'][0]['idnumber']); + $this->assertEmpty($result['users'][0]['email']); + $this->assertEmpty($result['users'][0]['phone1']); + $this->assertEmpty($result['users'][0]['phone2']); + $this->assertEmpty($result['users'][0]['department']); + $this->assertEmpty($result['users'][0]['institution']); + + // Filter by email. + $CFG->showuseridentity = 'email'; + $result = external::search_users('yyy', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(3, $result['users']); + $this->assertEquals(3, $result['count']); + $this->assertEquals($u2->id, $result['users'][0]['id']); + $this->assertEquals($u2->email, $result['users'][0]['email']); + $this->assertEquals($u1->id, $result['users'][1]['id']); + $this->assertEquals($u1->email, $result['users'][1]['email']); + $this->assertEquals($u3->id, $result['users'][2]['id']); + $this->assertEquals($u3->email, $result['users'][2]['email']); + + // Filter by any. + $CFG->showuseridentity = 'idnumber,email,phone1,phone2,department,institution'; + $result = external::search_users('yyy', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(3, $result['users']); + $this->assertEquals(3, $result['count']); + $this->assertArrayHasKey('idnumber', $result['users'][0]); + $this->assertArrayHasKey('email', $result['users'][0]); + $this->assertArrayHasKey('phone1', $result['users'][0]); + $this->assertArrayHasKey('phone2', $result['users'][0]); + $this->assertArrayHasKey('department', $result['users'][0]); + $this->assertArrayHasKey('institution', $result['users'][0]); + + // Switch to a user that cannot view identity fields. + $this->setUser($ux); + $CFG->showuseridentity = 'idnumber,email,phone1,phone2,department,institution'; + + // Only names are included. + $result = external::search_users('fish'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(0, $result['users']); + $this->assertEquals(0, $result['count']); + + $result = external::search_users('bob', 'moodle/competency:planmanage'); + $result = external_api::clean_returnvalue(external::search_users_returns(), $result); + $this->assertCount(1, $result['users']); + $this->assertEquals(1, $result['count']); + $this->assertEquals($u1->id, $result['users'][0]['id']); + $this->assertEmpty($result['users'][0]['idnumber']); + $this->assertEmpty($result['users'][0]['email']); + $this->assertEmpty($result['users'][0]['phone1']); + $this->assertEmpty($result['users'][0]['phone2']); + $this->assertEmpty($result['users'][0]['department']); + $this->assertEmpty($result['users'][0]['institution']); + } + + public function test_data_for_user_competency_summary_in_plan() { + global $CFG; + + $this->setUser($this->creator); + + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $f1 = $lpg->create_framework(); + + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + + $tpl = $lpg->create_template(); + $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $c1->get_id())); + + $plan = $lpg->create_plan(array('userid' => $this->user->id, 'templateid' => $tpl->get_id(), 'name' => 'Evil')); + + $uc = $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c1->get_id())); + + $evidence = \core_competency\external::grade_competency_in_plan($plan->get_id(), $c1->get_id(), 1, true); + $evidence = \core_competency\external::grade_competency_in_plan($plan->get_id(), $c1->get_id(), 2, true); + + $summary = external::data_for_user_competency_summary_in_plan($c1->get_id(), $plan->get_id()); + $this->assertTrue($summary->usercompetencysummary->cangrade); + $this->assertEquals('Evil', $summary->plan->name); + $this->assertEquals('B', $summary->usercompetencysummary->usercompetency->gradename); + $this->assertEquals('B', $summary->usercompetencysummary->evidence[0]->gradename); + $this->assertEquals('A', $summary->usercompetencysummary->evidence[1]->gradename); + } + + /** + * Search cohorts. + */ + public function test_search_cohorts() { + $this->resetAfterTest(true); + + $syscontext = array('contextid' => context_system::instance()->id); + $catcontext = array('contextid' => context_coursecat::instance($this->category->id)->id); + $othercatcontext = array('contextid' => context_coursecat::instance($this->othercategory->id)->id); + + $cohort1 = $this->getDataGenerator()->create_cohort(array_merge($syscontext, array('name' => 'Cohortsearch 1'))); + $cohort2 = $this->getDataGenerator()->create_cohort(array_merge($catcontext, array('name' => 'Cohortsearch 2'))); + $cohort3 = $this->getDataGenerator()->create_cohort(array_merge($othercatcontext, array('name' => 'Cohortsearch 3'))); + + // Check for parameter $includes = 'parents'. + + // A user without permission in the system. + $this->setUser($this->user); + try { + $result = external::search_cohorts("Cohortsearch", $syscontext, 'parents'); + $this->fail('Invalid permissions in system'); + } catch (required_capability_exception $e) { + // All good. + } + + // A user without permission in a category. + $this->setUser($this->catuser); + try { + $result = external::search_cohorts("Cohortsearch", $catcontext, 'parents'); + $this->fail('Invalid permissions in category'); + } catch (required_capability_exception $e) { + // All good. + } + + // A user with permissions in the system. + $this->setUser($this->creator); + $result = external::search_cohorts("Cohortsearch", $syscontext, 'parents'); + $this->assertEquals(1, count($result['cohorts'])); + $this->assertEquals('Cohortsearch 1', $result['cohorts'][$cohort1->id]->name); + + // A user with permissions in the category. + $this->setUser($this->catcreator); + $result = external::search_cohorts("Cohortsearch", $catcontext, 'parents'); + $this->assertEquals(2, count($result['cohorts'])); + $cohorts = array(); + foreach ($result['cohorts'] as $cohort) { + $cohorts[] = $cohort->name; + } + $this->assertTrue(in_array('Cohortsearch 1', $cohorts)); + $this->assertTrue(in_array('Cohortsearch 2', $cohorts)); + + // Check for parameter $includes = 'self'. + $this->setUser($this->creator); + $result = external::search_cohorts("Cohortsearch", $othercatcontext, 'self'); + $this->assertEquals(1, count($result['cohorts'])); + $this->assertEquals('Cohortsearch 3', $result['cohorts'][$cohort3->id]->name); + + // Check for parameter $includes = 'all'. + $this->setUser($this->creator); + $result = external::search_cohorts("Cohortsearch", $syscontext, 'all'); + $this->assertEquals(3, count($result['cohorts'])); + + // Detect invalid parameter $includes. + $this->setUser($this->creator); + try { + $result = external::search_cohorts("Cohortsearch", $syscontext, 'invalid'); + $this->fail('Invalid parameter includes'); + } catch (coding_exception $e) { + // All good. + } + } + +} diff --git a/admin/tool/lp/user_competency.php b/admin/tool/lp/user_competency.php new file mode 100644 index 00000000000..51933c1565a --- /dev/null +++ b/admin/tool/lp/user_competency.php @@ -0,0 +1,68 @@ +. + +/** + * User competency page. + * + * @package tool_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__ . '/../../../config.php'); + +$id = required_param('id', PARAM_INT); + +require_login(null, false); +if (isguestuser()) { + throw new require_login_exception('Guests are not allowed here.'); +} +\core_competency\api::require_enabled(); + +$uc = \core_competency\api::get_user_competency_by_id($id); +$params = array('id' => $id); +$url = new moodle_url('/admin/tool/lp/user_competency.php', $params); + +$user = core_user::get_user($uc->get_userid()); +if (!$user || !core_user::is_real_user($user->id)) { + throw new moodle_exception('invaliduser', 'error'); +} +$iscurrentuser = ($USER->id == $user->id); + +$competency = $uc->get_competency(); +$compexporter = new \core_competency\external\competency_exporter($competency, array('context' => $competency->get_context())); + +$PAGE->set_pagelayout('standard'); +$PAGE->set_url($url); +$PAGE->navigation->override_active_url(new moodle_url('/admin/tool/lp/plans.php', array('userid' => $uc->get_userid()))); +$PAGE->set_context($uc->get_context()); +if (!$iscurrentuser) { + $PAGE->navigation->extend_for_user($user); + $PAGE->navigation->set_userid_for_parent_checks($user->id); +} +$output = $PAGE->get_renderer('tool_lp'); +$compdata = $compexporter->export($output); +$PAGE->navbar->add($compdata->shortname, $url); +$PAGE->set_title($compdata->shortname); +$PAGE->set_heading($compdata->shortname); + +echo $output->header(); +$page = new \tool_lp\output\user_competency_summary($uc); +echo $output->render($page); +// Trigger viewed event. +\core_competency\api::user_competency_viewed($uc); + +echo $output->footer(); diff --git a/admin/tool/lp/user_competency_in_course.php b/admin/tool/lp/user_competency_in_course.php new file mode 100644 index 00000000000..9a478f99462 --- /dev/null +++ b/admin/tool/lp/user_competency_in_course.php @@ -0,0 +1,101 @@ +. + +/** + * User competency page. Lists everything known about a user competency. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__ . '/../../../config.php'); + +$userid = optional_param('userid', 0, PARAM_INT); +$competencyid = required_param('competencyid', PARAM_INT); +$courseid = required_param('courseid', PARAM_INT); + +require_login(null, false); +if (isguestuser()) { + throw new require_login_exception('Guests are not allowed here.'); +} +\core_competency\api::require_enabled(); + +$course = $DB->get_record('course', array('id' => $courseid)); +$context = context_course::instance($courseid); +$currentgroup = groups_get_course_group($course, true); +if (empty($userid)) { + $gradable = get_enrolled_users($context, 'moodle/competency:coursecompetencygradable', $currentgroup, 'u.id', null, 0, 1); + if (empty($gradable)) { + $userid = 0; + } else { + $userid = array_pop($gradable)->id; + } +} else { + $gradable = get_enrolled_users($context, 'moodle/competency:coursecompetencygradable', $currentgroup, 'u.id'); + if (count($gradable) == 0) { + $userid = 0; + } else if (!in_array($userid, array_keys($gradable))) { + $userid = array_shift($gradable)->id; + } +} + +$params = array('userid' => $userid, 'competencyid' => $competencyid, 'courseid' => $courseid); +$url = new moodle_url('/admin/tool/lp/user_competency_in_course.php', $params); + +if ($userid > 0) { + $usercontext = context_user::instance($userid); + $user = $DB->get_record('user', array('id' => $userid)); +} +$competency = new \core_competency\competency($competencyid); + +// Does a permissions check for us. +if ($userid > 0) { + $usercompetencycourses = \core_competency\api::list_user_competencies_in_course($courseid, $userid); +} +$subtitle = $competency->get_shortname() . ' ' . $competency->get_idnumber() . ''; + +list($title, $subtitle) = \tool_lp\page_helper::setup_for_course($url, $course, $subtitle); + +$output = $PAGE->get_renderer('tool_lp'); +if ($userid > 0) { + $userheading = array( + 'heading' => fullname($user), + 'user' => $user, + 'usercontext' => $usercontext + ); +} +echo $output->header(); +if ($userid > 0) { + echo $OUTPUT->context_header($userheading, 3); +} + +$baseurl = new moodle_url('/admin/tool/lp/user_competency_in_course.php'); +$nav = new \tool_lp\output\user_competency_course_navigation($userid, $competencyid, $courseid, $baseurl); +echo $output->render($nav); +if ($userid > 0) { + $page = new \tool_lp\output\user_competency_summary_in_course($userid, $competencyid, $courseid); + echo $output->render($page); + + // Trigger the viewed event. + $uc = \core_competency\api::get_user_competency_in_course($courseid, $userid, $competencyid); + \core_competency\api::user_competency_viewed_in_course($uc, $courseid); +} else { + echo $output->container('', 'clearfix'); + echo $output->notify_problem(get_string('noparticipants', 'tool_lp')); +} + +echo $output->footer(); diff --git a/admin/tool/lp/user_competency_in_plan.php b/admin/tool/lp/user_competency_in_plan.php new file mode 100644 index 00000000000..1a1cd28de8a --- /dev/null +++ b/admin/tool/lp/user_competency_in_plan.php @@ -0,0 +1,62 @@ +. + +/** + * User competency page. Lists everything known about a user competency. + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__ . '/../../../config.php'); + +$userid = required_param('userid', PARAM_INT); +$competencyid = required_param('competencyid', PARAM_INT); +$planid = required_param('planid', PARAM_INT); + +require_login(null, false); +if (isguestuser()) { + throw new require_login_exception('Guests are not allowed here.'); +} +\core_competency\api::require_enabled(); + +$params = array('userid' => $userid, 'competencyid' => $competencyid); +$params['planid'] = $planid; +$plan = \core_competency\api::read_plan($planid); +$url = new moodle_url('/admin/tool/lp/user_competency_in_plan.php', $params); +$competency = new \core_competency\competency($competencyid); +$framework = $competency->get_framework(); + +list($title, $subtitle) = \tool_lp\page_helper::setup_for_plan($userid, $url, $plan); + +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); +echo $output->heading($title); + +$page = new \tool_lp\output\user_competency_summary_in_plan($competencyid, $planid); +echo $output->render($page); +// Trigger the viewed event. +$pc = \core_competency\api::get_plan_competency($plan, $competency->get_id()); +if ($plan->get_status() == \core_competency\plan::STATUS_COMPLETE) { + $usercompetencyplan = $pc->usercompetencyplan; + \core_competency\api::user_competency_plan_viewed($usercompetencyplan); +} else { + $usercompetency = $pc->usercompetency; + \core_competency\api::user_competency_viewed_in_plan($usercompetency, $plan->get_id()); +} + +echo $output->footer(); diff --git a/admin/tool/lp/user_evidence.php b/admin/tool/lp/user_evidence.php new file mode 100644 index 00000000000..dfb5dcb733d --- /dev/null +++ b/admin/tool/lp/user_evidence.php @@ -0,0 +1,45 @@ +. + +/** + * User evidence (evidence of prior learning). + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); + +require_login(null, false); +if (isguestuser()) { + throw new require_login_exception('Guests are not allowed here.'); +} +\core_competency\api::require_enabled(); + +$id = required_param('id', PARAM_INT); + +$userevidence = \core_competency\api::read_user_evidence($id); +$url = new moodle_url('/admin/tool/lp/user_evidence.php', array('id' => $id)); +list($title, $subtitle) = \tool_lp\page_helper::setup_for_user_evidence($userevidence->get_userid(), $url, $userevidence); + +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); + +$page = new \tool_lp\output\user_evidence_page($userevidence); +echo $output->render($page); + +echo $output->footer(); diff --git a/admin/tool/lp/user_evidence_edit.php b/admin/tool/lp/user_evidence_edit.php new file mode 100644 index 00000000000..e353514c4a7 --- /dev/null +++ b/admin/tool/lp/user_evidence_edit.php @@ -0,0 +1,122 @@ +. + +/** + * User evidence (evidence of prior learning). + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); + +require_login(null, false); +if (isguestuser()) { + throw new require_login_exception('Guests are not allowed here.'); +} +\core_competency\api::require_enabled(); + +$userid = optional_param('userid', $USER->id, PARAM_INT); +$id = optional_param('id', null, PARAM_INT); +$returntype = optional_param('return', null, PARAM_ALPHA); + +$url = new moodle_url('/admin/tool/lp/user_evidence_edit.php', array('id' => $id, 'userid' => $userid, 'return' => $returntype)); + +$userevidence = null; +if (empty($id)) { + $pagetitle = get_string('addnewuserevidence', 'tool_lp'); + list($title, $subtitle, $returnurl) = \tool_lp\page_helper::setup_for_user_evidence($userid, $url, null, + $pagetitle, $returntype); + +} else { + $userevidence = \core_competency\api::read_user_evidence($id); + + // The userid parameter must be the same as the owner of the evidence. + if ($userid != $userevidence->get_userid()) { + throw new coding_exception('Inconsistency between the userid parameter and the userid of the plan.'); + } + + $pagetitle = get_string('edituserevidence', 'tool_lp'); + list($title, $subtitle, $returnurl) = \tool_lp\page_helper::setup_for_user_evidence($userid, $url, $userevidence, + $pagetitle, $returntype); +} + +// The context has been set to the user context in the page_helper. +$context = $PAGE->context; + +$fileareaoptions = array('subdirs' => false); +$customdata = array( + 'fileareaoptions' => $fileareaoptions, + 'persistent' => $userevidence, + 'userid' => $userid, +); + +// Check if user has permissions to manage user evidence. +if ($userevidence != null) { + if (!$userevidence->can_manage()) { + throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); + } + $customdata['evidence'] = $userevidence; + +} else if (!\core_competency\user_evidence::can_manage_user($userid)) { + throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); +} + +$form = new \tool_lp\form\user_evidence($url->out(false), $customdata); +if ($form->is_cancelled()) { + redirect($returnurl); +} + +// Load existing user evidence. +$itemid = null; +if ($userevidence) { + $itemid = $userevidence->get_id(); +} + +// Massaging the file API. +$draftitemid = file_get_submitted_draft_itemid('files'); +file_prepare_draft_area($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions); +$form->set_data((object) array('files' => $draftitemid)); + +// Hurray, the user has submitted the form! Everyone loves forms :)! +if ($data = $form->get_data()) { + require_sesskey(); + $draftitemid = $data->files; + unset($data->files); + + if (empty($userevidence)) { + $userevidence = \core_competency\api::create_user_evidence($data, $draftitemid); + $returnurl = new moodle_url('/admin/tool/lp/user_evidence.php', ['id' => $userevidence->get_id()]); + $returnmsg = get_string('userevidencecreated', 'tool_lp'); + } else { + \core_competency\api::update_user_evidence($data, $draftitemid); + $returnmsg = get_string('userevidenceupdated', 'tool_lp'); + } + redirect($returnurl, $returnmsg, null, \core\output\notification::NOTIFY_SUCCESS); +} + +// We're getting there... +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); +echo $output->heading($title); +if (!empty($subtitle)) { + echo $output->heading($subtitle, 3); +} + +$form->display(); + +echo $output->footer(); diff --git a/admin/tool/lp/user_evidence_list.php b/admin/tool/lp/user_evidence_list.php new file mode 100644 index 00000000000..0299992ac57 --- /dev/null +++ b/admin/tool/lp/user_evidence_list.php @@ -0,0 +1,45 @@ +. + +/** + * User evidence (evidence of prior learning). + * + * @package tool_lp + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); + +require_login(null, false); +if (isguestuser()) { + throw new require_login_exception('Guests are not allowed here.'); +} +\core_competency\api::require_enabled(); + +$userid = optional_param('userid', $USER->id, PARAM_INT); + +$url = new moodle_url('/admin/tool/lp/user_evidence_list.php', array('userid' => $userid)); +list($title, $subtitle) = \tool_lp\page_helper::setup_for_user_evidence($userid, $url); + +$output = $PAGE->get_renderer('tool_lp'); +echo $output->header(); +echo $output->heading($title); + +$page = new \tool_lp\output\user_evidence_list_page($userid); +echo $output->render($page); + +echo $output->footer(); diff --git a/admin/tool/lp/version.php b/admin/tool/lp/version.php new file mode 100644 index 00000000000..b0351dcd71f --- /dev/null +++ b/admin/tool/lp/version.php @@ -0,0 +1,30 @@ +. + +/** + * Plugin version info + * + * @package tool_lp + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +$plugin->version = 2016020925; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2014110400; // Requires this Moodle version. +$plugin->component = 'tool_lp'; // Full name of the plugin (used for diagnostics). diff --git a/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-debug.js b/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-debug.js new file mode 100644 index 00000000000..caebb22ebe9 --- /dev/null +++ b/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-debug.js @@ -0,0 +1,72 @@ +YUI.add('moodle-tool_lp-dragdrop-reorder', function (Y, NAME) { + +/** + * Simple drag and drop. + * + * Used when we just want a list of things that can be re-ordered by dragging. + * + * @class M.tool_lp.dragdrop_reorder + * @constructor + * @extends M.core.dragdrop + */ +var DRAGREORDER = function() { + DRAGREORDER.superclass.constructor.apply(this, arguments); +}; + +var CSS = { + EDITINGMOVE: 'editing_move', + ICONCLASS: 'iconsmall' +}; +Y.extend(DRAGREORDER, M.core.dragdrop, { + initializer: function(args) { + if (Y.one('.' + args.parentNodeClass).all('.' + args.dragHandleInsertClass).size() <= 1) { + // We can't re-order when there is only one item. + return; + } + // Set group for parent class + this.groups = [args.group]; + this.samenodeclass = args.sameNodeClass; + this.parentnodeclass = args.parentNodeClass; + this.draghandleinsertclass = args.dragHandleInsertClass; + this.draghandle = this.get_drag_handle(args.dragHandleText, + CSS.EDITINGMOVE, CSS.ICONCLASS, true); + + this.samenodelabel = args.sameNodeLabel; + this.parentnodelabel = args.parentNodeLabel; + this.callback = args.callback; + + var delegate = new Y.DD.Delegate({ + container: '.' + args.parentNodeClass, + nodes: '.' + args.sameNodeClass, + target: true, + handles: ['.' + CSS.EDITINGMOVE], + dragConfig: {groups: this.groups} + }); + + delegate.dd.plug(Y.Plugin.DDProxy); + + Y.one('.' + args.parentNodeClass) + .all('.' + args.dragHandleInsertClass) + .each( + function (node) { + node.insert(this.draghandle.cloneNode(true)); + } , this); + }, + + drop_hit: function(e) { + this.callback(e); + } + +}, { + NAME: 'tool_lp-dragdrop-reorder', + ATTRS: { + } +}); + +M.tool_lp = M.tool_lp || {}; +M.tool_lp.dragdrop_reorder = function(params) { + new DRAGREORDER(params); +}; + + +}, '@VERSION@', {"requires": ["moodle-core-dragdrop"]}); diff --git a/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-min.js b/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-min.js new file mode 100644 index 00000000000..f91699e6e38 --- /dev/null +++ b/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder-min.js @@ -0,0 +1 @@ +YUI.add("moodle-tool_lp-dragdrop-reorder",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)},r={EDITINGMOVE:"editing_move",ICONCLASS:"iconsmall"};e.extend(n,M.core.dragdrop,{initializer:function(t){if(e.one("."+t.parentNodeClass).all("."+t.dragHandleInsertClass).size()<=1)return;this.groups=[t.group],this.samenodeclass=t.sameNodeClass,this.parentnodeclass=t.parentNodeClass,this.draghandleinsertclass=t.dragHandleInsertClass,this.draghandle=this.get_drag_handle(t.dragHandleText,r.EDITINGMOVE,r.ICONCLASS,!0),this.samenodelabel=t.sameNodeLabel,this.parentnodelabel=t.parentNodeLabel,this.callback=t.callback;var n=new e.DD.Delegate({container:"."+t.parentNodeClass,nodes:"."+t.sameNodeClass,target:!0,handles:["."+r.EDITINGMOVE],dragConfig:{groups:this.groups}});n.dd.plug(e.Plugin.DDProxy),e.one("."+t.parentNodeClass).all("."+t.dragHandleInsertClass).each(function(e){e.insert(this.draghandle.cloneNode(!0))},this)},drop_hit:function(e){this.callback(e)}},{NAME:"tool_lp-dragdrop-reorder",ATTRS:{}}),M.tool_lp=M.tool_lp||{},M.tool_lp.dragdrop_reorder=function(e){new n(e)}},"@VERSION@",{requires:["moodle-core-dragdrop"]}); diff --git a/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder.js b/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder.js new file mode 100644 index 00000000000..caebb22ebe9 --- /dev/null +++ b/admin/tool/lp/yui/build/moodle-tool_lp-dragdrop-reorder/moodle-tool_lp-dragdrop-reorder.js @@ -0,0 +1,72 @@ +YUI.add('moodle-tool_lp-dragdrop-reorder', function (Y, NAME) { + +/** + * Simple drag and drop. + * + * Used when we just want a list of things that can be re-ordered by dragging. + * + * @class M.tool_lp.dragdrop_reorder + * @constructor + * @extends M.core.dragdrop + */ +var DRAGREORDER = function() { + DRAGREORDER.superclass.constructor.apply(this, arguments); +}; + +var CSS = { + EDITINGMOVE: 'editing_move', + ICONCLASS: 'iconsmall' +}; +Y.extend(DRAGREORDER, M.core.dragdrop, { + initializer: function(args) { + if (Y.one('.' + args.parentNodeClass).all('.' + args.dragHandleInsertClass).size() <= 1) { + // We can't re-order when there is only one item. + return; + } + // Set group for parent class + this.groups = [args.group]; + this.samenodeclass = args.sameNodeClass; + this.parentnodeclass = args.parentNodeClass; + this.draghandleinsertclass = args.dragHandleInsertClass; + this.draghandle = this.get_drag_handle(args.dragHandleText, + CSS.EDITINGMOVE, CSS.ICONCLASS, true); + + this.samenodelabel = args.sameNodeLabel; + this.parentnodelabel = args.parentNodeLabel; + this.callback = args.callback; + + var delegate = new Y.DD.Delegate({ + container: '.' + args.parentNodeClass, + nodes: '.' + args.sameNodeClass, + target: true, + handles: ['.' + CSS.EDITINGMOVE], + dragConfig: {groups: this.groups} + }); + + delegate.dd.plug(Y.Plugin.DDProxy); + + Y.one('.' + args.parentNodeClass) + .all('.' + args.dragHandleInsertClass) + .each( + function (node) { + node.insert(this.draghandle.cloneNode(true)); + } , this); + }, + + drop_hit: function(e) { + this.callback(e); + } + +}, { + NAME: 'tool_lp-dragdrop-reorder', + ATTRS: { + } +}); + +M.tool_lp = M.tool_lp || {}; +M.tool_lp.dragdrop_reorder = function(params) { + new DRAGREORDER(params); +}; + + +}, '@VERSION@', {"requires": ["moodle-core-dragdrop"]}); diff --git a/admin/tool/lp/yui/src/dragdrop-reorder/build.json b/admin/tool/lp/yui/src/dragdrop-reorder/build.json new file mode 100644 index 00000000000..d8ef06d0dd1 --- /dev/null +++ b/admin/tool/lp/yui/src/dragdrop-reorder/build.json @@ -0,0 +1,10 @@ +{ + "name": "moodle-tool_lp-dragdrop-reorder", + "builds": { + "moodle-tool_lp-dragdrop-reorder": { + "jsfiles": [ + "dragdropreorder.js" + ] + } + } +} diff --git a/admin/tool/lp/yui/src/dragdrop-reorder/js/dragdropreorder.js b/admin/tool/lp/yui/src/dragdrop-reorder/js/dragdropreorder.js new file mode 100644 index 00000000000..c2feccb8b68 --- /dev/null +++ b/admin/tool/lp/yui/src/dragdrop-reorder/js/dragdropreorder.js @@ -0,0 +1,67 @@ +/** + * Simple drag and drop. + * + * Used when we just want a list of things that can be re-ordered by dragging. + * + * @class M.tool_lp.dragdrop_reorder + * @constructor + * @extends M.core.dragdrop + */ +var DRAGREORDER = function() { + DRAGREORDER.superclass.constructor.apply(this, arguments); +}; + +var CSS = { + EDITINGMOVE: 'editing_move', + ICONCLASS: 'iconsmall' +}; +Y.extend(DRAGREORDER, M.core.dragdrop, { + initializer: function(args) { + if (Y.one('.' + args.parentNodeClass).all('.' + args.dragHandleInsertClass).size() <= 1) { + // We can't re-order when there is only one item. + return; + } + // Set group for parent class + this.groups = [args.group]; + this.samenodeclass = args.sameNodeClass; + this.parentnodeclass = args.parentNodeClass; + this.draghandleinsertclass = args.dragHandleInsertClass; + this.draghandle = this.get_drag_handle(args.dragHandleText, + CSS.EDITINGMOVE, CSS.ICONCLASS, true); + + this.samenodelabel = args.sameNodeLabel; + this.parentnodelabel = args.parentNodeLabel; + this.callback = args.callback; + + var delegate = new Y.DD.Delegate({ + container: '.' + args.parentNodeClass, + nodes: '.' + args.sameNodeClass, + target: true, + handles: ['.' + CSS.EDITINGMOVE], + dragConfig: {groups: this.groups} + }); + + delegate.dd.plug(Y.Plugin.DDProxy); + + Y.one('.' + args.parentNodeClass) + .all('.' + args.dragHandleInsertClass) + .each( + function (node) { + node.insert(this.draghandle.cloneNode(true)); + } , this); + }, + + drop_hit: function(e) { + this.callback(e); + } + +}, { + NAME: 'tool_lp-dragdrop-reorder', + ATTRS: { + } +}); + +M.tool_lp = M.tool_lp || {}; +M.tool_lp.dragdrop_reorder = function(params) { + new DRAGREORDER(params); +}; diff --git a/admin/tool/lp/yui/src/dragdrop-reorder/meta/dragdrop-reorder.json b/admin/tool/lp/yui/src/dragdrop-reorder/meta/dragdrop-reorder.json new file mode 100644 index 00000000000..aea885e5e0f --- /dev/null +++ b/admin/tool/lp/yui/src/dragdrop-reorder/meta/dragdrop-reorder.json @@ -0,0 +1,7 @@ +{ + "moodle-tool_lp-dragdrop-reorder": { + "requires": [ + "moodle-core-dragdrop" + ] + } +} diff --git a/admin/tool/lpmigrate/classes/form/migrate_framework.php b/admin/tool/lpmigrate/classes/form/migrate_framework.php new file mode 100644 index 00000000000..db50360558e --- /dev/null +++ b/admin/tool/lpmigrate/classes/form/migrate_framework.php @@ -0,0 +1,104 @@ +. + +/** + * Form. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lpmigrate\form; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); +\MoodleQuickForm::registerElementType('framework_autocomplete', + $CFG->dirroot . '/admin/tool/lp/classes/form/framework_autocomplete.php', + '\\tool_lp\\form\\framework_autocomplete'); + +/** + * Form class. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class migrate_framework extends \moodleform { + + /** @var context The page context. */ + protected $pagecontext; + + /** + * Constructor. + * @param \context $context The page context. + */ + public function __construct(\context $context) { + $this->pagecontext = $context; + parent::__construct(); + } + + protected function definition() { + $mform = $this->_form; + + $mform->addElement('header', 'hdrcourses', get_string('frameworks', 'tool_lpmigrate')); + + $mform->addElement('framework_autocomplete', 'from', get_string('migratefrom', 'tool_lpmigrate'), array( + 'contextid' => $this->pagecontext->id, + 'onlyvisible' => '0', + ), 1, 2, 3); + $mform->addRule('from', get_string('required'), 'required', null); + $mform->addHelpButton('from', 'migratefrom', 'tool_lpmigrate'); + + $mform->addElement('framework_autocomplete', 'to', get_string('migrateto', 'tool_lpmigrate'), array( + 'contextid' => $this->pagecontext->id, + 'onlyvisible' => '1', // We cannot add competencies from hidden frameworks, so it must be visible. + ), 1, 2, 3); + $mform->addRule('to', get_string('required'), 'required', null); + $mform->addHelpButton('to', 'migrateto', 'tool_lpmigrate'); + + $mform->addElement('header', 'hdrcourses', get_string('courses')); + $mform->addElement('course', 'allowedcourses', get_string('limittothese', 'tool_lpmigrate'), + array('showhidden' => true, 'multiple' => true)); + $mform->addHelpButton('allowedcourses', 'allowedcourses', 'tool_lpmigrate'); + $mform->addElement('course', 'disallowedcourses', get_string('excludethese', 'tool_lpmigrate'), + array('showhidden' => true, 'multiple' => true)); + $mform->addHelpButton('disallowedcourses', 'disallowedcourses', 'tool_lpmigrate'); + $mform->addElement('date_time_selector', 'coursestartdate', get_string('startdatefrom', 'tool_lpmigrate'), + array('optional' => true)); + $mform->addHelpButton('coursestartdate', 'coursestartdate', 'tool_lpmigrate'); + + $this->add_action_buttons(true, get_string('performmigration', 'tool_lpmigrate')); + } + + public function validation($data, $files) { + $errors = array(); + + if ($data['from'] == $data['to']) { + $errors['to'] = get_string('errorcannotmigratetosameframework', 'tool_lpmigrate'); + + } else if (!empty($data['from']) && !empty($data['to'])) { + $mapper = new \tool_lpmigrate\framework_mapper($data['from'], $data['to']); + $mapper->automap(); + if (!$mapper->has_mappings()) { + $errors['to'] = 'Could not map to any competency in this framework.'; + } + } + + return $errors; + } + +} diff --git a/admin/tool/lpmigrate/classes/framework_mapper.php b/admin/tool/lpmigrate/classes/framework_mapper.php new file mode 100644 index 00000000000..83090fe67d1 --- /dev/null +++ b/admin/tool/lpmigrate/classes/framework_mapper.php @@ -0,0 +1,189 @@ +. + +/** + * Framework mapper. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lpmigrate; +defined('MOODLE_INTERNAL') || die(); + +use core_competency\api; + +/** + * Framework mapper class. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class framework_mapper { + + /** @var int The ID of the framework we're migrating from. */ + protected $from; + /** @var int The ID of the framework we're migrating to. */ + protected $to; + /** @var array The collection of objects at origin. */ + protected $collectionfrom; + /** @var array The collection of objects at destination. */ + protected $collectionto; + /** @var array Mappings. */ + protected $mappings = array(); + + /** + * Constructor. + * @param int $from Framework ID from. + * @param int $to Framework ID to. + */ + public function __construct($from, $to) { + $this->from = $from; + $this->to = $to; + } + + /** + * Add a mapping. + * @param int $idfrom From ID. + * @param int $idto To ID. + */ + public function add_mapping($idfrom, $idto) { + $this->mappings[$idfrom] = $idto; + } + + /** + * Auto map the frameworks. + * @return void + */ + public function automap() { + $map = array(); + + // Shallow copy. + $collectionfrom = $this->get_collection_from(); + $collectionto = $this->get_collection_to(); + + // Find mappings. + foreach ($collectionfrom as $keyfrom => $compfrom) { + foreach ($collectionto as $keyto => $compto) { + if ($compfrom->get_idnumber() == $compto->get_idnumber()) { + $map[$compfrom->get_id()] = $compto->get_id(); + unset($collectionfrom[$keyfrom]); + unset($collectionto[$keyto]); + break; + } + } + } + + $this->mappings = $map; + } + + /** + * Get all IDs at origin. + * @return array + */ + public function get_all_from() { + return array_keys($this->get_collection_from()); + } + + /** + * Get all IDs at destination. + * @return array + */ + public function get_all_to() { + return array_keys($this->get_collection_to()); + } + + /** + * Get the collection at origin. + * @return array + */ + protected function get_collection_from() { + if ($this->collectionfrom === null) { + $this->collectionfrom = api::search_competencies('', $this->from); + } + return $this->collectionfrom; + } + + /** + * Get the collection at destination. + * @return array + */ + protected function get_collection_to() { + if ($this->collectionto === null) { + $this->collectionto = api::search_competencies('', $this->to); + } + return $this->collectionto; + } + + /** + * Get the defined mappings. + * @return array + */ + public function get_mappings() { + return $this->mappings; + } + + /** + * Get the IDs of the objects at origin which do not have a mapping at destination. + * @return array + */ + public function get_unmapped_from() { + return array_keys(array_diff_key($this->get_collection_from(), $this->mappings)); + } + + /** + * Get the origin objects with missing mappings. + * @return array + */ + public function get_unmapped_objects_from() { + return array_diff_key($this->get_collection_from(), $this->mappings); + } + + /** + * Get the IDs of the objects at destination which do not have a mapping at origin. + * @return array + */ + public function get_unmapped_to() { + return array_keys(array_diff_key($this->get_collection_to(), array_flip($this->mappings))); + } + + /** + * Get the destination objects with missing mappings. + * @return array + */ + public function get_unmapped_objects_to() { + return array_diff_key($this->get_collection_to(), array_flip($this->mappings)); + } + + /** + * Whether some mappings were set. + * @return bool + */ + public function has_mappings() { + return !empty($this->mappings); + } + + /** + * Reset the mappings. + * @return void + */ + public function reset_mappings() { + $this->mappings = array(); + } + +} diff --git a/admin/tool/lpmigrate/classes/framework_processor.php b/admin/tool/lpmigrate/classes/framework_processor.php new file mode 100644 index 00000000000..1f06438eb7d --- /dev/null +++ b/admin/tool/lpmigrate/classes/framework_processor.php @@ -0,0 +1,631 @@ +. + +/** + * Processor. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lpmigrate; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use moodle_exception; +use core_competency\api; +use core_competency\competency; +use core_competency\course_competency; +use core_competency\course_module_competency; + +/** + * Processor class. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class framework_processor { + + /** @var array Indexed as courseid => competencyids */ + protected $coursescompetencies = array(); + /** @var array Indexed as courseid => competencyid => ruleoutcome*/ + protected $coursescompetenciesoutcomes = array(); + /** @var array Indexed as courseid => cmid => competencyids */ + protected $modulecompetencies = array(); + /** @var array Indexed as courseid => cmid => competencyid => ruleoutcome*/ + protected $modulecompetenciesoutcomes = array(); + /** @var array The IDs of the objects of origin. */ + protected $fromids = array(); + /** @var array The mapping originId => destinationId. */ + protected $mappings = array(); + + /** @var array Courses found. */ + protected $coursesfound = array(); + /** @var array Course modules found. */ + protected $cmsfound = array(); + /** @var integer Number of migrations expected in courses. */ + protected $coursecompetencyexpectedmigrations = 0; + /** @var integer Count of migrations in the course level. */ + protected $coursecompetencymigrations = 0; + /** @var integer Count of removals in the course level. */ + protected $coursecompetencyremovals = 0; + /** @var integer Number of migrations expected in CMs. */ + protected $modulecompetencyexpectedmigrations = 0; + /** @var integer Count of migrations in CMs. */ + protected $modulecompetencymigrations = 0; + /** @var integer Count of removals in CMs. */ + protected $modulecompetencyremovals = 0; + /** @var array IDs of objects missing a mapping in origin, originId => true. */ + protected $missingmappings = array(); + /** @var array List of errors. */ + protected $errors = array(); + /** @var array List of warnings. */ + protected $warnings = array(); + + /** @var array List of course IDs that can be migrated. */ + protected $allowedcourses = array(); + /** @var int Minimum start date of courses that can be migrated. */ + protected $coursestartdatefrom = 0; + /** @var array List of course IDs that cannot be migrated. */ + protected $disallowedcourses = array(); + /** @var bool Whether to remove the original competency when its destination was already there. */ + protected $removeoriginalwhenalreadypresent = false; + /** @var bool Whether to remove the competency from course, or cm, when a mapping is not found. */ + protected $removewhenmappingismissing = false; + + /** @var boolean Has this processor run? */ + protected $proceeded = false; + /** @var framework_mapper The mapper. */ + protected $mapper; + /** @var \core\progress\base The progress. */ + protected $progress; + + /** + * Constructor. + * + * @param framework_mapper $mapper The mapper. + * @param \core\progress\base $progress The progress object. + */ + public function __construct(framework_mapper $mapper, \core\progress\base $progress = null) { + $this->mapper = $mapper; + + if ($progress == null) { + $progress = new \core\progress\none(); + } + $this->progress = $progress; + } + + /** + * Process the mapping. + * @return void + */ + protected function process_mapping() { + $this->mappings = $this->mapper->get_mappings(); + $this->fromids = $this->mapper->get_all_from(); + } + + /** + * Identifies what courses and their competencies to work with. + * @return void + */ + protected function find_coursescompetencies() { + global $DB; + $this->progress->start_progress(get_string('findingcoursecompetencies', 'tool_lpmigrate'), 3); + $this->progress->increment_progress(); + + $joins = array(); + $conditions = array(); + $params = array(); + + // Limit to mapped objects. + list($insql, $inparams) = $DB->get_in_or_equal($this->fromids, SQL_PARAMS_NAMED); + $conditions[] = "c.id $insql"; + $params += $inparams; + + // Restriction on course IDs. + if (!empty($this->allowedcourses)) { + list($insql, $inparams) = $DB->get_in_or_equal($this->allowedcourses, SQL_PARAMS_NAMED); + $conditions[] = "cc.courseid $insql"; + $params += $inparams; + } + if (!empty($this->disallowedcourses)) { + list($insql, $inparams) = $DB->get_in_or_equal($this->disallowedcourses, SQL_PARAMS_NAMED, 'param', false); + $conditions[] = "cc.courseid $insql"; + $params += $inparams; + } + + // Restriction on start date. + if (!empty($this->coursestartdatefrom)) { + $joins[] = "JOIN {course} co + ON co.id = cc.courseid"; + $conditions[] = "co.startdate >= :startdate"; + $params += array('startdate' => $this->coursestartdatefrom); + } + + // Find the courses. + $ccs = array(); + $ccsoutcomes = array(); + $joins = implode(' ', $joins); + $conditions = implode(' AND ', $conditions); + $sql = "SELECT cc.id, cc.courseid, cc.competencyid, cc.ruleoutcome + FROM {" . course_competency::TABLE . "} cc + JOIN {" . competency::TABLE . "} c + ON c.id = cc.competencyid + $joins + WHERE $conditions + ORDER BY cc.sortorder, cc.id"; + + $records = $DB->get_recordset_sql($sql, $params); + $this->progress->increment_progress(); + + foreach ($records as $record) { + if (!isset($ccs[$record->courseid])) { + $ccs[$record->courseid] = array(); + $ccsoutcomes[$record->courseid] = array(); + } + $ccs[$record->courseid][] = $record->competencyid; + $ccsoutcomes[$record->courseid][$record->competencyid] = $record->ruleoutcome; + } + $records->close(); + + $this->coursescompetencies = $ccs; + $this->coursescompetenciesoutcomes = $ccsoutcomes; + $this->coursesfound = $ccs; + + $this->progress->increment_progress(); + $this->progress->end_progress(); + } + + /** + * Identifies what course modules and their competencies to work with. + * @return void + */ + protected function find_modulecompetencies() { + global $DB; + if (empty($this->coursescompetencies)) { + return; + } + + $this->progress->start_progress(get_string('findingmodulecompetencies', 'tool_lpmigrate'), 3); + $this->progress->increment_progress(); + + // Limit to mapped objects. + list($inidsql, $inidparams) = $DB->get_in_or_equal($this->fromids, SQL_PARAMS_NAMED); + + // Limit to known courses. + list($incoursesql, $incourseparams) = $DB->get_in_or_equal(array_keys($this->coursescompetencies), SQL_PARAMS_NAMED); + $sql = "SELECT mc.id, cm.course AS courseid, mc.cmid, mc.competencyid, mc.ruleoutcome + FROM {" . course_module_competency::TABLE . "} mc + JOIN {course_modules} cm + ON cm.id = mc.cmid + AND cm.course $incoursesql + JOIN {" . competency::TABLE . "} c + ON c.id = mc.competencyid + WHERE c.id $inidsql + ORDER BY mc.sortorder, mc.id"; + $params = $inidparams + $incourseparams; + + $records = $DB->get_recordset_sql($sql, $params); + $this->progress->increment_progress(); + $cmsfound = array(); + + $cmcs = array(); + $cmcsoutcomes = array(); + foreach ($records as $record) { + if (!isset($cmcs[$record->courseid])) { + $cmcs[$record->courseid] = array(); + $cmcsoutcomes[$record->courseid] = array(); + } + if (!isset($cmcs[$record->courseid][$record->cmid])) { + $cmcs[$record->courseid][$record->cmid] = array(); + $cmcsoutcomes[$record->courseid][$record->cmid] = array(); + } + $cmcs[$record->courseid][$record->cmid][] = $record->competencyid; + $cmcsoutcomes[$record->courseid][$record->cmid][$record->competencyid] = $record->ruleoutcome; + $cmsfound[$record->cmid] = true; + } + $records->close(); + + $this->modulecompetencies = $cmcs; + $this->modulecompetenciesoutcomes = $cmcsoutcomes; + $this->cmsfound = $cmsfound; + + $this->progress->increment_progress(); + $this->progress->end_progress(); + } + + /** + * Return a list of CMs found. + * @return int + */ + public function get_cms_found() { + return $this->cmsfound; + } + + /** + * Return the number of CMs found. + * @return int + */ + public function get_cms_found_count() { + return count($this->cmsfound); + } + + /** + * Return a list of courses found. + * @return int + */ + public function get_courses_found() { + return $this->coursesfound; + } + + /** + * Return the number of courses found. + * @return int + */ + public function get_courses_found_count() { + return count($this->coursesfound); + } + + /** + * Get the number of course migrations. + * @return int + */ + public function get_course_competency_migrations() { + return $this->coursecompetencymigrations; + } + + /** + * Get the number of removals. + * @return int + */ + public function get_course_competency_removals() { + return $this->coursecompetencyremovals; + } + + /** + * Get the number of expected course migrations. + * @return int + */ + public function get_expected_course_competency_migrations() { + return $this->coursecompetencyexpectedmigrations; + } + + /** + * Get the number of expected course module migrations. + * @return int + */ + public function get_expected_module_competency_migrations() { + return $this->modulecompetencyexpectedmigrations; + } + + /** + * Get the number of course module migrations. + * @return int + */ + public function get_module_competency_migrations() { + return $this->modulecompetencymigrations; + } + + /** + * Get the number of removals. + * @return int + */ + public function get_module_competency_removals() { + return $this->modulecompetencyremovals; + } + + /** + * Return a list of errors. + * @return array + */ + public function get_errors() { + return $this->errors; + } + + /** + * Get the missing mappings. + * @return array Where keys are origin IDs. + */ + public function get_missing_mappings() { + if (!$this->has_run()) { + throw new coding_exception('The processor has not run yet.'); + } + return $this->missingmappings; + } + + /** + * Return a list of warnings. + * @return array + */ + public function get_warnings() { + return $this->warnings; + } + + /** + * Whether the processor has run. + * @return boolean + */ + public function has_run() { + return $this->proceeded; + } + + /** + * Log an error. + * @param int $courseid The course ID. + * @param int $competencyid The competency ID. + * @param int $cmid The CM ID. + * @param string $message The error message. + * @return void + */ + protected function log_error($courseid, $competencyid, $cmid, $message) { + $this->errors[] = array( + 'courseid' => $courseid, + 'competencyid' => $competencyid, + 'cmid' => $cmid, + 'message' => $message + ); + } + + /** + * Log a warning. + * @param int $courseid The course ID. + * @param int $competencyid The competency ID. + * @param int $cmid The CM ID. + * @param string $message The warning message. + * @return void + */ + protected function log_warning($courseid, $competencyid, $cmid, $message) { + $this->warnings[] = array( + 'courseid' => $courseid, + 'competencyid' => $competencyid, + 'cmid' => $cmid, + 'message' => $message + ); + } + + /** + * Execute the whole task. + * @return void + */ + public function proceed() { + if ($this->has_run()) { + throw new coding_exception('The processor has already run.'); + } else if (!$this->mapper->has_mappings()) { + throw new coding_exception('Mapping was not set.'); + } + + $this->proceeded = true; + $this->process_mapping(); + $this->find_coursescompetencies(); + $this->find_modulecompetencies(); + $this->process_courses(); + } + + /** + * Process each course individually. + * @return void + */ + protected function process_courses() { + global $DB; + $this->progress->start_progress(get_string('migratingcourses', 'tool_lpmigrate'), count($this->coursescompetencies)); + + // Process each course. + foreach ($this->coursescompetencies as $courseid => $competencyids) { + $this->progress->increment_progress(); + + $competenciestoremovefromcourse = array(); + $skipcompetencies = array(); + + // First, add all the new competencies to the course. + foreach ($competencyids as $key => $competencyid) { + $this->coursecompetencyexpectedmigrations++; + $mapto = isset($this->mappings[$competencyid]) ? $this->mappings[$competencyid] : false; + + // Skip the competencies that are not mapped. + if ($mapto === false) { + $this->missingmappings[$competencyid] = true; + + if ($this->removewhenmappingismissing) { + $competenciestoremovefromcourse[$competencyid] = true; + } + + continue; + } + + $transaction = $DB->start_delegated_transaction(); + try { + // Add the new competency to the course. + if (api::add_competency_to_course($courseid, $mapto)) { + + // Find the added course competency. + $cc = course_competency::get_record(array('courseid' => $courseid, 'competencyid' => $mapto)); + + // Set the rule. + api::set_course_competency_ruleoutcome($cc, $this->coursescompetenciesoutcomes[$courseid][$competencyid]); + + // Adapt the sortorder. + api::reorder_course_competency($courseid, $mapto, $competencyid); + + $competenciestoremovefromcourse[$competencyid] = true; + $this->coursecompetencymigrations++; + + } else { + // The competency was already in the course... + if ($this->removeoriginalwhenalreadypresent) { + $competenciestoremovefromcourse[$competencyid] = true; + } else { + $this->log_warning($courseid, $competencyid, null, + get_string('warningdestinationcoursecompetencyalreadyexists', 'tool_lpmigrate')); + } + } + + } catch (moodle_exception $e) { + // There was a major problem with this competency, we will ignore it entirely for the course. + $skipcompetencies[$competencyid] = true; + + $this->log_error($courseid, $competencyid, null, + get_string('errorwhilemigratingcoursecompetencywithexception', 'tool_lpmigrate', $e->getMessage())); + + try { + $transaction->rollback($e); + } catch (moodle_exception $e) { + // Catch the re-thrown exception. + } + + continue; + } + $transaction->allow_commit(); + } + + // Then, convert the module competencies. + if (!empty($this->modulecompetencies[$courseid])) { + foreach ($this->modulecompetencies[$courseid] as $cmid => $competencyids) { + foreach ($competencyids as $competencyid) { + $this->modulecompetencyexpectedmigrations++; + + // This mapped competency was not added to the course. + if (!empty($skipcompetencies[$competencyid])) { + continue; + } + + $remove = true; + $mapto = isset($this->mappings[$competencyid]) ? $this->mappings[$competencyid] : false; + + // We don't have mapping. + if ($mapto === false) { + if (!$this->removewhenmappingismissing) { + $remove = false; + } + + } else { + // We have a mapping. + $transaction = $DB->start_delegated_transaction(); + try { + // The competency was added successfully. + if (api::add_competency_to_course_module($cmid, $mapto)) { + + // Find the added module competency. + $mc = course_module_competency::get_record(array('cmid' => $cmid, 'competencyid' => $mapto)); + + // Set the competency rule. + api::set_course_module_competency_ruleoutcome($mc, + $this->modulecompetenciesoutcomes[$courseid][$cmid][$competencyid]); + + // Adapt the sortorder. + api::reorder_course_module_competency($cmid, $mapto, $competencyid); + + $this->modulecompetencymigrations++; + + } else { + // The competency was already in the module. + if (!$this->removeoriginalwhenalreadypresent) { + $remove = false; + $competencieswithissues[$competencyid] = true; + $this->log_warning($courseid, $competencyid, $cmid, + get_string('warningdestinationmodulecompetencyalreadyexists', 'tool_lpmigrate')); + } + } + + } catch (moodle_exception $e) { + // There was a major problem with this competency in this module. + $message = get_string('errorwhilemigratingmodulecompetencywithexception', 'tool_lpmigrate', + $e->getMessage()); + $this->log_error($courseid, $competencyid, $cmid, $message); + + try { + $transaction->rollback($e); + } catch (moodle_exception $e) { + // Catch the re-thrown exception. + } + + continue; + } + $transaction->allow_commit(); + } + + try { + // Go away competency! + if ($remove && api::remove_competency_from_course_module($cmid, $competencyid)) { + $this->modulecompetencyremovals++; + } + } catch (moodle_exception $e) { + $this->log_warning($courseid, $competencyid, $cmid, + get_string('warningcouldnotremovemodulecompetency', 'tool_lpmigrate')); + } + } + } + } + + // Finally, we remove the course competencies, but only for the 100% successful ones. + foreach ($competenciestoremovefromcourse as $competencyid => $unused) { + + try { + // Process the course competency. + api::remove_competency_from_course($courseid, $competencyid); + $this->coursecompetencyremovals++; + } catch (moodle_exception $e) { + $this->log_warning($courseid, $competencyid, null, + get_string('warningcouldnotremovecoursecompetency', 'tool_lpmigrate')); + } + } + } + + $this->progress->end_progress(); + } + + /** + * Set the IDs of the courses that are allowed. + * @param array $courseids + */ + public function set_allowedcourses(array $courseids) { + $this->allowedcourses = $courseids; + } + + /** + * Set the minimum start date for courses to be migrated. + * @param int $value Timestamp, or 0. + */ + public function set_course_start_date_from($value) { + $this->coursestartdatefrom = intval($value); + } + + /** + * Set the IDs of the courses that are not allowed. + * @param array $courseids + */ + public function set_disallowedcourses(array $courseids) { + $this->disallowedcourses = $courseids; + } + + /** + * Set whether we should remove original competencies when the destination competency was already there. + * @param bool $value + */ + public function set_remove_original_when_destination_already_present($value) { + $this->removeoriginalwhenalreadypresent = $value; + } + + /** + * Set whether we should remove unmapped competencies. + * @param bool $value + */ + public function set_remove_when_mapping_is_missing($value) { + $this->removewhenmappingismissing = $value; + } + +} diff --git a/admin/tool/lpmigrate/classes/output/migrate_framework_results.php b/admin/tool/lpmigrate/classes/output/migrate_framework_results.php new file mode 100644 index 00000000000..5665bc6dca3 --- /dev/null +++ b/admin/tool/lpmigrate/classes/output/migrate_framework_results.php @@ -0,0 +1,155 @@ +. + +/** + * Migrate frameworks results. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_lpmigrate\output; +defined('MOODLE_INTERNAL') || die(); + +use context; +use context_course; +use context_module; +use moodle_url; +use renderable; +use templatable; +use stdClass; +use core_competency\competency; +use core_competency\competency_framework; +use core_competency\external\competency_exporter; +use core_competency\external\competency_framework_exporter; +use core_competency\url; +use tool_lpmigrate\framework_processor; + +/** + * Migrate frameworks results class. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class migrate_framework_results implements renderable, templatable { + + /** @var context The current page context. */ + protected $pagecontext; + /** @var framework_processor The processor. */ + protected $processor; + + /** + * Construct. + * + * @param context $pagecontext The current page context. + * @param framework_processor $processor The processor. + * @param competency_framework $frameworkfrom Framework from. + * @param competency_framework $frameworkto Framework to. + * @param array $unmappedfrom Competencies from unmapped. + * @param array $unmappedto Competencies to unmapped. + */ + public function __construct(context $pagecontext, framework_processor $processor, competency_framework $frameworkfrom, + competency_framework $frameworkto, array $unmappedfrom = array(), array $unmappedto = array()) { + if (!$processor->has_run()) { + throw new \coding_exception('The processor has not run.'); + } + $this->pagecontext = $pagecontext; + $this->processor = $processor; + $this->unmappedfrom = $unmappedfrom; + $this->unmappedto = $unmappedto; + $this->frameworkfrom = $frameworkfrom; + $this->frameworkto = $frameworkto; + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(\renderer_base $output) { + global $DB; + $data = new stdClass(); + + $missingmappings = $this->processor->get_missing_mappings(); + + $data->pagecontextid = $this->pagecontext->id; + $data->expectedccmigrations = $this->processor->get_expected_course_competency_migrations(); + $data->expectedmcmigrations = $this->processor->get_expected_module_competency_migrations(); + $data->ccmigrationscount = $this->processor->get_course_competency_migrations(); + $data->mcmigrationscount = $this->processor->get_module_competency_migrations(); + $data->ccremovalscount = $this->processor->get_course_competency_removals(); + $data->mcremovalscount = $this->processor->get_module_competency_removals(); + + $data->unmappedfrom = array(); + $data->unmappedto = array(); + + $exporter = new competency_framework_exporter($this->frameworkfrom); + $data->frameworkfrom = $exporter->export($output); + $exporter = new competency_framework_exporter($this->frameworkto); + $data->frameworkto = $exporter->export($output); + + $fromcontext = $this->frameworkfrom->get_context(); + $tocontext = $this->frameworkto->get_context(); + + $compcontext = null; + foreach ($this->unmappedfrom as $comp) { + $exporter = new competency_exporter($comp, array('context' => $fromcontext)); + $data->unmappedfrom[] = $exporter->export($output); + } + + foreach ($this->unmappedto as $comp) { + $exporter = new competency_exporter($comp, array('context' => $tocontext)); + $data->unmappedto[] = $exporter->export($output); + } + + $data->coursesfound = $this->processor->get_courses_found_count(); + $data->cmsfound = $this->processor->get_cms_found_count(); + $data->mappingsmissingcount = count($missingmappings); + $data->hasunmappedto = count($data->unmappedto) > 0; + $data->hasunmappedfrom = count($data->unmappedfrom) > 0; + $warnings = $this->processor->get_warnings(); + $data->warnings = array(); + $data->warningcount = count($warnings); + $errors = $this->processor->get_errors(); + $data->errors = array(); + $data->errorcount = count($errors); + + foreach ($warnings as $warning) { + $cmcontext = !empty($warning['cmid']) ? context_module::instance($warning['cmid']) : null; + $coursecontext = context_course::instance($warning['courseid']); + $warning['cm'] = $cmcontext ? $cmcontext->get_context_name() : null; + $warning['course'] = $coursecontext->get_context_name(); + $warning['competency'] = $DB->get_field(competency::TABLE, 'idnumber', array('id' => $warning['competencyid'])); + $data->warnings[] = $warning; + } + + foreach ($errors as $error) { + $cmcontext = !empty($error['cmid']) ? context_module::instance($error['cmid']) : null; + $coursecontext = context_course::instance($error['courseid']); + $error['cm'] = $cmcontext ? $cmcontext->get_context_name() : null; + $error['course'] = $coursecontext->get_context_name(); + $error['competency'] = $DB->get_field(competency::TABLE, 'idnumber', array('id' => $error['competencyid'])); + $data->errors[] = $error; + } + + $data->pluginbaseurl = (new moodle_url('/admin/tool/lpmigrate'))->out(false); + $data->frameworksurl = url::frameworks($this->pagecontext->id)->out(false); + + return $data; + } +} diff --git a/admin/tool/lpmigrate/classes/output/renderer.php b/admin/tool/lpmigrate/classes/output/renderer.php new file mode 100644 index 00000000000..1983c81f077 --- /dev/null +++ b/admin/tool/lpmigrate/classes/output/renderer.php @@ -0,0 +1,51 @@ +. + +/** + * Renderers. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_lpmigrate\output; + +defined('MOODLE_INTERNAL') || die(); + +use plugin_renderer_base; +use renderable; + +/** + * Renderer class. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Defer to template. + * + * @param migrate_framework_results $page + * @return string + */ + public function render_migrate_framework_results(migrate_framework_results $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('tool_lpmigrate/migrate_frameworks_results', $data); + } +} diff --git a/admin/tool/lpmigrate/db/access.php b/admin/tool/lpmigrate/db/access.php new file mode 100644 index 00000000000..f81d2249d8b --- /dev/null +++ b/admin/tool/lpmigrate/db/access.php @@ -0,0 +1,36 @@ +. + +/** + * Capabilities. + * + * @package tool_lpmigrate + * @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(); + +$capabilities = array( + 'tool/lpmigrate:frameworksmigrate' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), +); diff --git a/admin/tool/lpmigrate/frameworks.php b/admin/tool/lpmigrate/frameworks.php new file mode 100644 index 00000000000..a5edea4d52c --- /dev/null +++ b/admin/tool/lpmigrate/frameworks.php @@ -0,0 +1,79 @@ +. + +/** + * Page to migrate frameworks. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); + +$context = context_system::instance(); +require_login(null, false); +require_capability('tool/lpmigrate:frameworksmigrate', $context); + +$url = new moodle_url('/admin/tool/lpmigrate/frameworks.php'); +$title = get_string('migrateframeworks', 'tool_lpmigrate'); +$PAGE->set_context($context); +$PAGE->set_pagelayout('standard'); +$PAGE->set_url($url); +$PAGE->set_title($title); +$PAGE->set_heading(get_string('pluginname', 'tool_lpmigrate')); + +$output = $PAGE->get_renderer('tool_lpmigrate'); + +echo $output->header(); +echo $output->heading($title); + +$form = new \tool_lpmigrate\form\migrate_framework($context); +if ($form->is_cancelled()) { + redirect($url); + +} else if ($data = $form->get_data()) { + + // Map competencies from both framework. + $mapper = new \tool_lpmigrate\framework_mapper($data->from, $data->to); + $mapper->automap(); + + $progress = new \core\progress\display(); + $progress->set_display_names(true); + + $processor = new \tool_lpmigrate\framework_processor($mapper, $progress); + if (!empty($data->allowedcourses)) { + $processor->set_allowedcourses($data->allowedcourses); + } + if (!empty($data->disallowedcourses)) { + $processor->set_disallowedcourses($data->disallowedcourses); + } + $processor->set_course_start_date_from($data->coursestartdate); + $processor->proceed(); + + $unmappedfrom = $mapper->get_unmapped_objects_from(); + $unmappedto = $mapper->get_unmapped_objects_to(); + $renderable = new \tool_lpmigrate\output\migrate_framework_results($context, $processor, + \core_competency\api::read_framework($data->from), \core_competency\api::read_framework($data->to), + $unmappedfrom, $unmappedto); + echo $output->render($renderable); + +} else { + echo html_writer::tag('p', get_string('explanation', 'tool_lpmigrate')); + $form->display(); +} + +echo $output->footer(); diff --git a/admin/tool/lpmigrate/lang/en/tool_lpmigrate.php b/admin/tool/lpmigrate/lang/en/tool_lpmigrate.php new file mode 100644 index 00000000000..26c3b5dd202 --- /dev/null +++ b/admin/tool/lpmigrate/lang/en/tool_lpmigrate.php @@ -0,0 +1,67 @@ +. + +/** + * Language strings. + * + * @package tool_lpmigrate + * @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(); + +$string['allowedcourses'] = 'Courses allowed'; +$string['allowedcourses_help'] = 'When at least one course is specified, the migration will only occur on the courses listed here.'; +$string['continuetoframeworks'] = 'Continue to frameworks'; +$string['coursecompetencymigrations'] = 'Course competency migrations'; +$string['coursemodulecompetencymigrations'] = 'Course module competency migrations'; +$string['coursesfound'] = 'Courses found'; +$string['coursemodulesfound'] = 'Course modules found'; +$string['coursestartdate'] = 'Courses start date'; +$string['coursestartdate_help'] = 'When enabled, courses with a start date prior to the one specified here will not be migrated.'; +$string['disallowedcourses'] = 'Disallowed courses'; +$string['disallowedcourses_help'] = 'Any course specified here will not be migrated.'; +$string['errorcannotmigratetosameframework'] = 'Cannot migrate from and to the same framework.'; +$string['errorcouldnotmapcompetenciesinframework'] = 'Could not map to any competency in this framework.'; +$string['errors'] = 'Errors'; +$string['errorwhilemigratingcoursecompetencywithexception'] = 'Error while migrating the course competency: {$a}'; +$string['errorwhilemigratingmodulecompetencywithexception'] = 'Error while migrating the course module competency: {$a}'; +$string['excludethese'] = 'Exclude these'; +$string['explanation'] = 'When updating a competency framework to a newer version, this tool can be used to search for competencies in courses and course modules using the older framework, and update the links to point to the new framework. This works by finding competencies with matching ID numbers in the two frameworks, and then mapping all of the competency links in the specified list of courses to point to the new version of the competency in the new framework. It is not recommended to edit the old set of competencies directly as this would change all of the competencies that have already been awarded in users learning plans. Typically you would import the new version of a framework, hide the old framework, and use this tool to migrate new courses to the new framework.'; +$string['findingcoursecompetencies'] = 'Finding course competencies'; +$string['findingmodulecompetencies'] = 'Finding module competencies'; +$string['frameworks'] = 'Frameworks'; +$string['limittothese'] = 'Limit to these'; +$string['lpmigrate:frameworksmigrate'] = 'Migrate frameworks'; +$string['migrateframeworks'] = 'Migrate frameworks'; +$string['migratefrom'] = 'Migrate from'; +$string['migratefrom_help'] = 'Select the framework to migrate from.'; +$string['migratemore'] = 'Migrate more'; +$string['migrateto'] = 'Migrate to'; +$string['migrateto_help'] = 'Select the framework to migrate to. Note that it must be not be hidden.'; +$string['migratingcourses'] = 'Migrating courses'; +$string['missingmappings'] = 'Missing mappings'; +$string['performmigration'] = 'Perform migration'; +$string['pluginname'] = 'Competencies migration tool'; +$string['results'] = 'Results'; +$string['startdatefrom'] = 'Start date from'; +$string['unmappedin'] = 'Unmapped in {$a}'; +$string['warningcouldnotremovecoursecompetency'] = 'The course competency could not be removed.'; +$string['warningcouldnotremovemodulecompetency'] = 'The course module competency could not be removed.'; +$string['warningdestinationcoursecompetencyalreadyexists'] = 'The destination course competency already exists.'; +$string['warningdestinationmodulecompetencyalreadyexists'] = 'The destination course module competency already exists.'; +$string['warnings'] = 'Warnings'; diff --git a/admin/tool/lpmigrate/settings.php b/admin/tool/lpmigrate/settings.php new file mode 100644 index 00000000000..26b362295d6 --- /dev/null +++ b/admin/tool/lpmigrate/settings.php @@ -0,0 +1,39 @@ +. + +/** + * Links and settings. + * + * @package tool_lpmigrate + * @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(); + +if (\core_competency\api::is_enabled()) { + + $parentname = 'competencies'; + + // Manage competency frameworks page. + $temp = new admin_externalpage( + 'toollpmigrateframeworks', + get_string('migrateframeworks', 'tool_lpmigrate'), + new moodle_url('/admin/tool/lpmigrate/frameworks.php'), + array('tool/lpmigrate:frameworksmigrate') + ); + $ADMIN->add($parentname, $temp); + +} diff --git a/admin/tool/lpmigrate/templates/migrate_frameworks_results.mustache b/admin/tool/lpmigrate/templates/migrate_frameworks_results.mustache new file mode 100644 index 00000000000..fc6e074f06d --- /dev/null +++ b/admin/tool/lpmigrate/templates/migrate_frameworks_results.mustache @@ -0,0 +1,166 @@ +{{! + 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 . +}} +{{! + @template tool_lpmigrate/migrate_frameworks_results + + Migrate frameworks results. + + Classes required for JS: + * none + + Data attributes required for JS: + * data-action="competency-dialogue" + * data-id + * data-region="migration-results" + + Context variables required for this template: + * coursesfound + * cmsfound + * ccmigrationscount + * ccremovalscount + * mcmigrationscount + * mcremovalscount + * mappingsmissingcount + * warningcount + * errorcount + + Example context (json): + { + "ccmigrationscount": 2, + "ccremovalscount": 3, + "cmsfound": 4, + "coursesfound": 3, + "errorcount": 1, + "errors": [{ + "message": "This is a generic error", + "course": "The course A", + "cm": "The course module B", + "competency": "The competency C", + "competencyid": "1" + }], + "frameworkfrom": { + "id": 1, + "shortname": "Framework X" + }, + "frameworkto": { + "id": 2, + "shortname": "Framework Y" + }, + "hasunmappedfrom": 1, + "hasunmappedto": 1, + "mappingsmissingcount": 1, + "mcmigrationscount": 4, + "mcremovalscount": 5, + "unmappedfrom": [{ + "id": 2, + "shortname": "Competency U1", + "idnumber": "CU1" + }], + "unmappedto": [{ + "id": 3, + "shortname": "Competency Z2", + "idnumber": "CZ2" + }], + "warningcount": 1, + "warnings": [{ + "message": "This is a generic warning", + "course": "The course D", + "cm": "The course module E", + "competency": "The competency F", + "competencyid": "1" + }] + } + +}} + +
    +

    {{#str}}results, tool_lpmigrate{{/str}}

    +
    +
    {{#str}}coursesfound, tool_lpmigrate{{/str}}
    +
    {{coursesfound}}
    +
    {{#str}}coursemodulesfound, tool_lpmigrate{{/str}}
    +
    {{cmsfound}}
    +
    {{#str}}coursecompetencymigrations, tool_lpmigrate{{/str}}
    +
    +{{ccmigrationscount}} -{{ccremovalscount}}
    +
    {{#str}}coursemodulecompetencymigrations, tool_lpmigrate{{/str}}
    +
    +{{mcmigrationscount}} -{{mcremovalscount}}
    +
    {{#str}}missingmappings, tool_lpmigrate{{/str}}
    +
    {{mappingsmissingcount}}
    +
    {{#str}}warnings, tool_lpmigrate{{/str}}
    +
    {{warningcount}}
    +
    {{#str}}errors, tool_lpmigrate{{/str}}
    +
    {{errorcount}}
    +
    + + {{#hasunmappedfrom}} +

    {{#str}}unmappedin, tool_lpmigrate, {{frameworkfrom.shortname}}{{/str}}

    +
      + {{#unmappedfrom}} +
    • {{shortname}} {{idnumber}}
    • + {{/unmappedfrom}} +
    + {{/hasunmappedfrom}} + + {{#hasunmappedto}} +

    {{#str}}unmappedin, tool_lpmigrate, {{frameworkto.shortname}}{{/str}}

    +
      + {{#unmappedto}} +
    • {{shortname}} {{idnumber}}
    • + {{/unmappedto}} +
    + {{/hasunmappedto}} + + {{#warningcount}} +

    {{#str}}warnings, tool_lpmigrate{{/str}}

    +
      + {{#warnings}} +
    • +

      + {{message}}
      + {{course}} {{#cm}} - {{cm}} {{/cm}} - {{competency}} +

      +
    • + {{/warnings}} +
    + {{/warningcount}} + + {{#errorcount}} +

    {{#str}}errors, tool_lpmigrate{{/str}}

    +
      + {{#errors}} +
    • +

      + {{message}}
      + {{course}} {{#cm}} - {{cm}} {{/cm}} - {{competency}} +

      +
    • + {{/errors}} +
    + {{/errorcount}} +
    + +{{#js}} +require(['tool_lp/competencydialogue'], function(CompDialogue) { + var cd = new CompDialogue({includecourses: true, includerelated: true}); + cd.watch('[data-region="migration-results"]'); +}) +{{/js}} diff --git a/admin/tool/lpmigrate/tests/processor_test.php b/admin/tool/lpmigrate/tests/processor_test.php new file mode 100644 index 00000000000..441c4463066 --- /dev/null +++ b/admin/tool/lpmigrate/tests/processor_test.php @@ -0,0 +1,619 @@ +. + +/** + * Framework processor tests. + * + * @package tool_lpmigrate + * @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(); +global $CFG; + +use core_competency\course_competency; +use core_competency\course_module_competency; +use tool_lpmigrate\framework_mapper; +use tool_lpmigrate\framework_processor; + +/** + * Framework processor testcase. + * + * @package tool_lpmigrate + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_lpmigrate_framework_processor_testcase extends advanced_testcase { + + /** + * This sets up a few things, and assign class variables. + * + * We create 2 frameworks, each with 2 matching competencies and 1 foreign. + * Then we create 2 courses, and in each 1 CM. + * Then we attach some competencies from the first framework to courses and CM. + */ + public function setUp() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $f1 = $lpg->create_framework(array('idnumber' => 'BIO2015')); + $f2 = $lpg->create_framework(array('idnumber' => 'BIO2016')); + + $f1comps = array(); + $f1comps['A1'] = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'idnumber' => 'A1')); + $f1comps['A2'] = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'idnumber' => 'A2')); + $f1comps['A3'] = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'idnumber' => 'A3')); + $f1comps['X1'] = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'idnumber' => 'X1')); + + $f2comps = array(); + $f2comps['A1'] = $lpg->create_competency(array('competencyframeworkid' => $f2->get_id(), 'idnumber' => 'A1')); + $f2comps['A2'] = $lpg->create_competency(array('competencyframeworkid' => $f2->get_id(), 'idnumber' => 'A2')); + $f2comps['A3'] = $lpg->create_competency(array('competencyframeworkid' => $f2->get_id(), 'idnumber' => 'A3')); + $f2comps['Y1'] = $lpg->create_competency(array('competencyframeworkid' => $f2->get_id(), 'idnumber' => 'Y1')); + + $c1 = $dg->create_course(array('startdate' => time() - 72000)); + $c2 = $dg->create_course(array('startdate' => time() + 72000)); + $cms = array( + $c1->id => array( + 'F1' => $dg->create_module('forum', (object) array('course' => $c1->id)), + 'P1' => $dg->create_module('page', (object) array('course' => $c1->id)), + 'EmptyA' => $dg->create_module('page', (object) array('course' => $c1->id)), + ), + $c2->id => array( + 'F1' => $dg->create_module('forum', (object) array('course' => $c2->id)), + 'EmptyB' => $dg->create_module('page', (object) array('course' => $c2->id)), + ), + ); + + // Course CompetencieS. + $ccs = array( + $c1->id => array( + $f1comps['A1']->get_id() => $lpg->create_course_competency(array('courseid' => $c1->id, + 'competencyid' => $f1comps['A1']->get_id())), + $f1comps['A3']->get_id() => $lpg->create_course_competency(array('courseid' => $c1->id, + 'competencyid' => $f1comps['A3']->get_id())), + $f1comps['X1']->get_id() => $lpg->create_course_competency(array('courseid' => $c1->id, + 'competencyid' => $f1comps['X1']->get_id())), + ), + $c2->id => array( + $f1comps['A2']->get_id() => $lpg->create_course_competency(array('courseid' => $c2->id, + 'competencyid' => $f1comps['A2']->get_id())), + $f1comps['A3']->get_id() => $lpg->create_course_competency(array('courseid' => $c2->id, + 'competencyid' => $f1comps['A3']->get_id())), + ) + ); + + // Course Module CompetencieS. + $cmcs = array( + $cms[$c1->id]['F1']->cmid => array( + $f1comps['A1']->get_id() => $lpg->create_course_module_competency(array( + 'cmid' => $cms[$c1->id]['F1']->cmid, + 'competencyid' => $f1comps['A1']->get_id() + )), + $f1comps['X1']->get_id() => $lpg->create_course_module_competency(array( + 'cmid' => $cms[$c1->id]['F1']->cmid, + 'competencyid' => $f1comps['X1']->get_id() + )), + ), + $cms[$c1->id]['P1']->cmid => array( + $f1comps['A3']->get_id() => $lpg->create_course_module_competency(array( + 'cmid' => $cms[$c1->id]['P1']->cmid, + 'competencyid' => $f1comps['A3']->get_id() + )), + ), + $cms[$c2->id]['F1']->cmid => array( + $f1comps['A2']->get_id() => $lpg->create_course_module_competency(array( + 'cmid' => $cms[$c2->id]['F1']->cmid, + 'competencyid' => $f1comps['A2']->get_id() + )), + $f1comps['A3']->get_id() => $lpg->create_course_module_competency(array( + 'cmid' => $cms[$c2->id]['F1']->cmid, + 'competencyid' => $f1comps['A3']->get_id() + )), + ), + ); + + $this->assertCourseCompetencyExists($c1, $f1comps['A1']); + $this->assertCourseCompetencyExists($c1, $f1comps['A3']); + $this->assertCourseCompetencyExists($c1, $f1comps['X1']); + $this->assertCourseCompetencyExists($c2, $f1comps['A2']); + $this->assertCourseCompetencyExists($c2, $f1comps['A3']); + $this->assertModuleCompetencyExists($cms[$c1->id]['F1'], $f1comps['A1']); + $this->assertModuleCompetencyExists($cms[$c1->id]['P1'], $f1comps['A3']); + $this->assertModuleCompetencyExists($cms[$c1->id]['F1'], $f1comps['X1']); + $this->assertModuleCompetencyExists($cms[$c2->id]['F1'], $f1comps['A2']); + $this->assertModuleCompetencyExists($cms[$c2->id]['F1'], $f1comps['A3']); + + $this->f1 = $f1; + $this->f1comps = $f1comps; + $this->f2 = $f2; + $this->f2comps = $f2comps; + $this->c1 = $c1; + $this->c2 = $c2; + $this->cms = $cms; + $this->ccs = $ccs; + $this->cmcs = $cmcs; + } + + public function test_simple_migration() { + $this->setAdminUser(); + + $mapper = new framework_mapper($this->f1->get_id(), $this->f2->get_id()); + $mapper->automap(); + $processor = new framework_processor($mapper); + $processor->proceed(); + + $this->assertEquals(2, $processor->get_courses_found_count()); + $this->assertEquals(5, $processor->get_expected_course_competency_migrations()); + $this->assertEquals(4, $processor->get_course_competency_migrations()); + $this->assertEquals(4, $processor->get_course_competency_removals()); + + $this->assertEquals(3, $processor->get_cms_found_count()); + $this->assertEquals(5, $processor->get_expected_module_competency_migrations()); + $this->assertEquals(4, $processor->get_module_competency_migrations()); + $this->assertEquals(4, $processor->get_module_competency_removals()); + + $this->assertEquals(array(), $processor->get_warnings()); + $this->assertEquals(array(), $processor->get_errors()); + $this->assertEquals(array($this->f1comps['X1']->get_id() => true), $processor->get_missing_mappings()); + + $this->assertCourseCompetencyMigrated($this->c1, $this->f1comps['A1'], $this->f2comps['A1']); + $this->assertCourseCompetencyMigrated($this->c1, $this->f1comps['A3'], $this->f2comps['A3']); + $this->assertCourseCompetencyMigrated($this->c2, $this->f1comps['A2'], $this->f2comps['A2']); + $this->assertCourseCompetencyMigrated($this->c2, $this->f1comps['A3'], $this->f2comps['A3']); + + $this->assertModuleCompetencyMigrated($this->cms[$this->c1->id]['F1'], $this->f1comps['A1'], $this->f2comps['A1']); + $this->assertModuleCompetencyMigrated($this->cms[$this->c1->id]['P1'], $this->f1comps['A3'], $this->f2comps['A3']); + $this->assertModuleCompetencyMigrated($this->cms[$this->c2->id]['F1'], $this->f1comps['A2'], $this->f2comps['A2']); + $this->assertModuleCompetencyMigrated($this->cms[$this->c2->id]['F1'], $this->f1comps['A3'], $this->f2comps['A3']); + + $this->assertCourseCompetencyExists($this->c1, $this->f1comps['X1']); + $this->assertModuleCompetencyExists($this->cms[$this->c1->id]['F1'], $this->f1comps['X1']); + } + + public function test_remove_when_missing() { + $this->setAdminUser(); + + $mapper = new framework_mapper($this->f1->get_id(), $this->f2->get_id()); + $mapper->automap(); + $processor = new framework_processor($mapper); + $processor->set_remove_when_mapping_is_missing(true); + $processor->proceed(); + + $this->assertEquals(2, $processor->get_courses_found_count()); + $this->assertEquals(5, $processor->get_expected_course_competency_migrations()); + $this->assertEquals(4, $processor->get_course_competency_migrations()); + $this->assertEquals(5, $processor->get_course_competency_removals()); + + $this->assertEquals(3, $processor->get_cms_found_count()); + $this->assertEquals(5, $processor->get_expected_module_competency_migrations()); + $this->assertEquals(4, $processor->get_module_competency_migrations()); + $this->assertEquals(5, $processor->get_module_competency_removals()); + + $this->assertCount(0, $processor->get_errors()); + $this->assertCount(0, $processor->get_warnings()); + + $this->assertCourseCompetencyNotExists($this->c1, $this->f1comps['X1']); + $this->assertModuleCompetencyNotExists($this->cms[$this->c1->id]['F1'], $this->f1comps['X1']); + } + + public function test_allowed_courses() { + $this->setAdminUser(); + + $mapper = new framework_mapper($this->f1->get_id(), $this->f2->get_id()); + $mapper->automap(); + $processor = new framework_processor($mapper); + $processor->set_allowedcourses(array($this->c1->id)); + $processor->proceed(); + + $this->assertEquals(1, $processor->get_courses_found_count()); + $this->assertEquals(3, $processor->get_expected_course_competency_migrations()); + $this->assertEquals(2, $processor->get_course_competency_migrations()); + $this->assertEquals(2, $processor->get_course_competency_removals()); + + $this->assertEquals(2, $processor->get_cms_found_count()); + $this->assertEquals(3, $processor->get_expected_module_competency_migrations()); + $this->assertEquals(2, $processor->get_module_competency_migrations()); + $this->assertEquals(2, $processor->get_module_competency_removals()); + + $this->assertCount(0, $processor->get_errors()); + $this->assertCount(0, $processor->get_warnings()); + + $this->assertCourseCompetencyMigrated($this->c1, $this->f1comps['A1'], $this->f2comps['A1']); + $this->assertCourseCompetencyMigrated($this->c1, $this->f1comps['A3'], $this->f2comps['A3']); + $this->assertModuleCompetencyMigrated($this->cms[$this->c1->id]['F1'], $this->f1comps['A1'], $this->f2comps['A1']); + $this->assertModuleCompetencyMigrated($this->cms[$this->c1->id]['P1'], $this->f1comps['A3'], $this->f2comps['A3']); + + $this->assertCourseCompetencyNotMigrated($this->c2, $this->f1comps['A2'], $this->f2comps['A2']); + $this->assertCourseCompetencyNotMigrated($this->c2, $this->f1comps['A3'], $this->f2comps['A3']); + $this->assertModuleCompetencyNotMigrated($this->cms[$this->c2->id]['F1'], $this->f1comps['A2'], $this->f2comps['A2']); + $this->assertModuleCompetencyNotMigrated($this->cms[$this->c2->id]['F1'], $this->f1comps['A3'], $this->f2comps['A3']); + } + + public function test_disallowed_courses() { + $this->setAdminUser(); + + $mapper = new framework_mapper($this->f1->get_id(), $this->f2->get_id()); + $mapper->automap(); + $processor = new framework_processor($mapper); + $processor->set_disallowedcourses(array($this->c2->id)); + $processor->proceed(); + + $this->assertEquals(1, $processor->get_courses_found_count()); + $this->assertEquals(3, $processor->get_expected_course_competency_migrations()); + $this->assertEquals(2, $processor->get_course_competency_migrations()); + $this->assertEquals(2, $processor->get_course_competency_removals()); + + $this->assertEquals(2, $processor->get_cms_found_count()); + $this->assertEquals(3, $processor->get_expected_module_competency_migrations()); + $this->assertEquals(2, $processor->get_module_competency_migrations()); + $this->assertEquals(2, $processor->get_module_competency_removals()); + + $this->assertCount(0, $processor->get_errors()); + $this->assertCount(0, $processor->get_warnings()); + + $this->assertCourseCompetencyMigrated($this->c1, $this->f1comps['A1'], $this->f2comps['A1']); + $this->assertCourseCompetencyMigrated($this->c1, $this->f1comps['A3'], $this->f2comps['A3']); + $this->assertModuleCompetencyMigrated($this->cms[$this->c1->id]['F1'], $this->f1comps['A1'], $this->f2comps['A1']); + $this->assertModuleCompetencyMigrated($this->cms[$this->c1->id]['P1'], $this->f1comps['A3'], $this->f2comps['A3']); + + $this->assertCourseCompetencyNotMigrated($this->c2, $this->f1comps['A2'], $this->f2comps['A2']); + $this->assertCourseCompetencyNotMigrated($this->c2, $this->f1comps['A3'], $this->f2comps['A3']); + $this->assertModuleCompetencyNotMigrated($this->cms[$this->c2->id]['F1'], $this->f1comps['A2'], $this->f2comps['A2']); + $this->assertModuleCompetencyNotMigrated($this->cms[$this->c2->id]['F1'], $this->f1comps['A3'], $this->f2comps['A3']); + } + + public function test_course_start_date_from() { + $this->setAdminUser(); + + $mapper = new framework_mapper($this->f1->get_id(), $this->f2->get_id()); + $mapper->automap(); + $processor = new framework_processor($mapper); + $processor->set_course_start_date_from(time()); + $processor->proceed(); + + $this->assertEquals(1, $processor->get_courses_found_count()); + $this->assertEquals(2, $processor->get_expected_course_competency_migrations()); + $this->assertEquals(2, $processor->get_course_competency_migrations()); + $this->assertEquals(2, $processor->get_course_competency_removals()); + + $this->assertEquals(1, $processor->get_cms_found_count()); + $this->assertEquals(2, $processor->get_expected_module_competency_migrations()); + $this->assertEquals(2, $processor->get_module_competency_migrations()); + $this->assertEquals(2, $processor->get_module_competency_removals()); + + $this->assertCount(0, $processor->get_errors()); + $this->assertCount(0, $processor->get_warnings()); + + $this->assertCourseCompetencyNotMigrated($this->c1, $this->f1comps['A1'], $this->f2comps['A1']); + $this->assertCourseCompetencyNotMigrated($this->c1, $this->f1comps['A3'], $this->f2comps['A3']); + $this->assertModuleCompetencyNotMigrated($this->cms[$this->c1->id]['F1'], $this->f1comps['A1'], $this->f2comps['A1']); + $this->assertModuleCompetencyNotMigrated($this->cms[$this->c1->id]['P1'], $this->f1comps['A3'], $this->f2comps['A3']); + + $this->assertCourseCompetencyMigrated($this->c2, $this->f1comps['A2'], $this->f2comps['A2']); + $this->assertCourseCompetencyMigrated($this->c2, $this->f1comps['A3'], $this->f2comps['A3']); + $this->assertModuleCompetencyMigrated($this->cms[$this->c2->id]['F1'], $this->f1comps['A2'], $this->f2comps['A2']); + $this->assertModuleCompetencyMigrated($this->cms[$this->c2->id]['F1'], $this->f1comps['A3'], $this->f2comps['A3']); + } + + public function test_destination_competency_exists() { + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + // Pre-add the new competency to course 1. + $lpg->create_course_competency(array('courseid' => $this->c1->id, 'competencyid' => $this->f2comps['A1']->get_id())); + + // Pre-add the new competency to module in course 2. + $lpg->create_course_module_competency(array( + 'cmid' => $this->cms[$this->c2->id]['F1']->cmid, + 'competencyid' => $this->f2comps['A2']->get_id() + )); + + $mapper = new framework_mapper($this->f1->get_id(), $this->f2->get_id()); + $mapper->automap(); + $processor = new framework_processor($mapper); + $processor->proceed(); + + $this->assertEquals(2, $processor->get_courses_found_count()); + $this->assertEquals(5, $processor->get_expected_course_competency_migrations()); + $this->assertEquals(3, $processor->get_course_competency_migrations()); + $this->assertEquals(3, $processor->get_course_competency_removals()); + + $this->assertEquals(3, $processor->get_cms_found_count()); + $this->assertEquals(5, $processor->get_expected_module_competency_migrations()); + $this->assertEquals(3, $processor->get_module_competency_migrations()); + $this->assertEquals(3, $processor->get_module_competency_removals()); + + $this->assertEquals(array(), $processor->get_errors()); + $warnings = $processor->get_warnings(); + $this->assertCount(2, $warnings); + + $warning = array_shift($warnings); + $this->assertEquals($this->c1->id, $warning['courseid']); + $this->assertEquals($this->f1comps['A1']->get_id(), $warning['competencyid']); + $this->assertEquals(null, $warning['cmid']); + $this->assertRegexp('/competency already exists/', $warning['message']); + + $warning = array_shift($warnings); + $this->assertEquals($this->c2->id, $warning['courseid']); + $this->assertEquals($this->f1comps['A2']->get_id(), $warning['competencyid']); + $this->assertEquals($this->cms[$this->c2->id]['F1']->cmid, $warning['cmid']); + $this->assertRegexp('/competency already exists/', $warning['message']); + + $this->assertCourseCompetencyExists($this->c1, $this->f1comps['A1']); + $this->assertModuleCompetencyExists($this->cms[$this->c2->id]['F1'], $this->f1comps['A2']); + } + + public function test_destination_competency_exists_remove_original() { + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + // Pre-add the new competency to course 1. + $lpg->create_course_competency(array('courseid' => $this->c1->id, 'competencyid' => $this->f2comps['A1']->get_id())); + + // Pre-add the new competency to module in course 2. + $lpg->create_course_module_competency(array( + 'cmid' => $this->cms[$this->c2->id]['F1']->cmid, + 'competencyid' => $this->f2comps['A2']->get_id() + )); + + $mapper = new framework_mapper($this->f1->get_id(), $this->f2->get_id()); + $mapper->automap(); + $processor = new framework_processor($mapper); + $processor->set_remove_original_when_destination_already_present(true); + $processor->proceed(); + + $this->assertEquals(2, $processor->get_courses_found_count()); + $this->assertEquals(5, $processor->get_expected_course_competency_migrations()); + $this->assertEquals(3, $processor->get_course_competency_migrations()); + $this->assertEquals(4, $processor->get_course_competency_removals()); + + $this->assertEquals(3, $processor->get_cms_found_count()); + $this->assertEquals(5, $processor->get_expected_module_competency_migrations()); + $this->assertEquals(3, $processor->get_module_competency_migrations()); + $this->assertEquals(4, $processor->get_module_competency_removals()); + + $this->assertEquals(array(), $processor->get_errors()); + $this->assertEquals(array(), $processor->get_warnings()); + + $this->assertCourseCompetencyNotExists($this->c1, $this->f1comps['A1']); + $this->assertModuleCompetencyNotExists($this->cms[$this->c2->id]['F1'], $this->f1comps['A2']); + } + + public function test_permission_exception() { + global $DB; + if ($DB->get_dbfamily() === 'postgres' or $DB->get_dbfamily() === 'mssql') { + $this->markTestSkipped('The processor is having issues with the transaction initialised in ' + . 'advanced_testcase::runBare().'); + return; + } + + $dg = $this->getDataGenerator(); + $u = $dg->create_user(); + $role = $dg->create_role(); + $sysctx = context_system::instance(); + + $dg->enrol_user($u->id, $this->c1->id, 'editingteacher'); + $dg->enrol_user($u->id, $this->c2->id, 'editingteacher'); + assign_capability('moodle/competency:coursecompetencymanage', CAP_PROHIBIT, $role, $sysctx->id); + role_assign($role, $u->id, context_course::instance($this->c1->id)->id); + role_assign($role, $u->id, context_module::instance($this->cms[$this->c2->id]['F1']->cmid)->id); + + accesslib_clear_all_caches_for_unit_testing(); + $this->setUser($u); + + // Do C1 first. + $mapper = new framework_mapper($this->f1->get_id(), $this->f2->get_id()); + $mapper->automap(); + $processor = new framework_processor($mapper); + $processor->set_allowedcourses(array($this->c1->id)); + $processor->proceed(); + + $this->assertEquals(1, $processor->get_courses_found_count()); + $this->assertEquals(3, $processor->get_expected_course_competency_migrations()); + $this->assertEquals(0, $processor->get_course_competency_migrations()); + $this->assertEquals(0, $processor->get_course_competency_removals()); + + $this->assertEquals(2, $processor->get_cms_found_count()); + $this->assertEquals(3, $processor->get_expected_module_competency_migrations()); + $this->assertEquals(0, $processor->get_module_competency_migrations()); + $this->assertEquals(0, $processor->get_module_competency_removals()); + + $this->assertEquals(array(), $processor->get_warnings()); + $errors = $processor->get_errors(); + $this->assertCount(2, $errors); + $this->assertEquals($this->c1->id, $errors[0]['courseid']); + $this->assertEquals($this->f1comps['A1']->get_id(), $errors[0]['competencyid']); + $this->assertEquals(null, $errors[0]['cmid']); + $this->assertRegexp('/Sorry, but you do not currently have permissions to do that/', $errors[0]['message']); + $this->assertEquals($this->f1comps['A3']->get_id(), $errors[1]['competencyid']); + + $this->assertCourseCompetencyNotMigrated($this->c1, $this->f1comps['A1'], $this->f2comps['A1']); + $this->assertCourseCompetencyNotMigrated($this->c1, $this->f1comps['A3'], $this->f2comps['A3']); + $this->assertModuleCompetencyNotMigrated($this->cms[$this->c1->id]['F1'], $this->f1comps['A1'], $this->f2comps['A1']); + $this->assertModuleCompetencyNotMigrated($this->cms[$this->c1->id]['P1'], $this->f1comps['A3'], $this->f2comps['A3']); + + // Do C2 now. + $processor = new framework_processor($mapper); + $processor->set_allowedcourses(array($this->c2->id)); + $processor->proceed(); + + $this->assertEquals(1, $processor->get_courses_found_count()); + $this->assertEquals(2, $processor->get_expected_course_competency_migrations()); + $this->assertEquals(2, $processor->get_course_competency_migrations()); + $this->assertEquals(2, $processor->get_course_competency_removals()); + + $this->assertEquals(1, $processor->get_cms_found_count()); + $this->assertEquals(2, $processor->get_expected_module_competency_migrations()); + $this->assertEquals(0, $processor->get_module_competency_migrations()); + $this->assertEquals(0, $processor->get_module_competency_removals()); + + $this->assertEquals(array(), $processor->get_warnings()); + $errors = $processor->get_errors(); + $this->assertCount(2, $errors); + $this->assertEquals($this->c2->id, $errors[0]['courseid']); + $this->assertEquals($this->f1comps['A2']->get_id(), $errors[0]['competencyid']); + $this->assertEquals($this->cms[$this->c2->id]['F1']->cmid, $errors[0]['cmid']); + $this->assertRegexp('/Sorry, but you do not currently have permissions to do that/', $errors[0]['message']); + $this->assertEquals($this->f1comps['A3']->get_id(), $errors[1]['competencyid']); + + $this->assertCourseCompetencyMigrated($this->c2, $this->f1comps['A2'], $this->f2comps['A2']); + $this->assertCourseCompetencyMigrated($this->c2, $this->f1comps['A3'], $this->f2comps['A3']); + $this->assertModuleCompetencyNotMigrated($this->cms[$this->c2->id]['F1'], $this->f1comps['A2'], $this->f2comps['A2']); + $this->assertModuleCompetencyNotMigrated($this->cms[$this->c2->id]['F1'], $this->f1comps['A3'], $this->f2comps['A2']); + } + + /** + * Assert that the course competency exists. + * + * @param stdClass $course The course. + * @param competency $competency The competency. + */ + protected function assertCourseCompetencyExists($course, $competency) { + $this->assertTrue(course_competency::record_exists_select("courseid = :courseid AND competencyid = :competencyid", + array('courseid' => $course->id, 'competencyid' => $competency->get_id()))); + } + + /** + * Assert that the course competency does not exist. + * + * @param stdClass $course The course. + * @param competency $competency The competency. + */ + protected function assertCourseCompetencyNotExists($course, $competency) { + $this->assertFalse(course_competency::record_exists_select("courseid = :courseid AND competencyid = :competencyid", + array('courseid' => $course->id, 'competencyid' => $competency->get_id()))); + } + + /** + * Assert that the course competency was migrated. + * + * @param stdClass $course The course. + * @param competency $compfrom The competency from. + * @param competency $compto The competency to. + */ + protected function assertCourseCompetencyMigrated($course, $compfrom, $compto) { + $ccs = $this->ccs[$course->id]; + + $this->assertCourseCompetencyNotExists($course, $compfrom); + $this->assertCourseCompetencyExists($course, $compto); + + $before = $ccs[$compfrom->get_id()]; + $after = course_competency::get_record(array( + 'courseid' => $course->id, + 'competencyid' => $compto->get_id() + )); + + $this->assertNotEquals($before->get_id(), $after->get_id()); + $this->assertEquals($before->get_courseid(), $after->get_courseid()); + $this->assertEquals($before->get_sortorder(), $after->get_sortorder()); + $this->assertEquals($before->get_ruleoutcome(), $after->get_ruleoutcome()); + } + + /** + * Assert that the course competency was not migrated. + * + * @param stdClass $course The course. + * @param competency $compfrom The competency from. + * @param competency $compto The competency to. + */ + protected function assertCourseCompetencyNotMigrated($course, $compfrom, $compto) { + $ccs = $this->ccs[$course->id]; + + $this->assertCourseCompetencyExists($course, $compfrom); + $this->assertCourseCompetencyNotExists($course, $compto); + + $before = $ccs[$compfrom->get_id()]; + $after = $ccs[$compfrom->get_id()]; + + $this->assertEquals($before->get_id(), $after->get_id()); + $this->assertEquals($before->get_courseid(), $after->get_courseid()); + $this->assertEquals($before->get_sortorder(), $after->get_sortorder()); + $this->assertEquals($before->get_ruleoutcome(), $after->get_ruleoutcome()); + } + + /** + * Assert that the course module competency exists. + * + * @param stdClass $cm The CM. + * @param competency $competency The competency. + */ + protected function assertModuleCompetencyExists($cm, $competency) { + $this->assertTrue(course_module_competency::record_exists_select("cmid = :cmid AND competencyid = :competencyid", + array('cmid' => $cm->cmid, 'competencyid' => $competency->get_id()))); + } + + /** + * Assert that the course module competency does not exist. + * + * @param stdClass $cm The CM. + * @param competency $competency The competency. + */ + protected function assertModuleCompetencyNotExists($cm, $competency) { + $this->assertFalse(course_module_competency::record_exists_select("cmid = :cmid AND competencyid = :competencyid", + array('cmid' => $cm->cmid, 'competencyid' => $competency->get_id()))); + } + + /** + * Assert that the course module competency was migrated. + * + * @param stdClass $cm The CM. + * @param competency $compfrom The competency from. + * @param competency $compto The competency to. + */ + protected function assertModuleCompetencyMigrated($cm, $compfrom, $compto) { + $cmcs = $this->cmcs[$cm->cmid]; + + $this->assertModuleCompetencyNotExists($cm, $compfrom); + $this->assertModuleCompetencyExists($cm, $compto); + + $before = $cmcs[$compfrom->get_id()]; + $after = course_module_competency::get_record(array( + 'cmid' => $cm->cmid, + 'competencyid' => $compto->get_id() + )); + + $this->assertNotEquals($before->get_id(), $after->get_id()); + $this->assertEquals($before->get_cmid(), $after->get_cmid()); + $this->assertEquals($before->get_sortorder(), $after->get_sortorder()); + $this->assertEquals($before->get_ruleoutcome(), $after->get_ruleoutcome()); + } + + /** + * Assert that the course module competency was not migrated. + * + * @param stdClass $cm The CM. + * @param competency $compfrom The competency from. + * @param competency $compto The competency to. + */ + protected function assertModuleCompetencyNotMigrated($cm, $compfrom, $compto) { + $cmcs = $this->cmcs[$cm->cmid]; + + $this->assertModuleCompetencyExists($cm, $compfrom); + $this->assertModuleCompetencyNotExists($cm, $compto); + + $before = $cmcs[$compfrom->get_id()]; + $after = $cmcs[$compfrom->get_id()]; + + $this->assertEquals($before->get_id(), $after->get_id()); + $this->assertEquals($before->get_cmid(), $after->get_cmid()); + $this->assertEquals($before->get_sortorder(), $after->get_sortorder()); + $this->assertEquals($before->get_ruleoutcome(), $after->get_ruleoutcome()); + } + +} diff --git a/admin/tool/lpmigrate/version.php b/admin/tool/lpmigrate/version.php new file mode 100644 index 00000000000..8c3976b7636 --- /dev/null +++ b/admin/tool/lpmigrate/version.php @@ -0,0 +1,32 @@ +. + +/** + * Plugin version info. + * + * @package tool_lpmigrate + * @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(); + +$plugin->version = 2016022401; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2014110400; // Requires this Moodle version. +$plugin->component = 'tool_lpmigrate'; // Full name of the plugin (used for diagnostics). +$plugin->dependencies = array( + 'tool_lp' => ANY_VERSION +); diff --git a/backup/moodle2/backup_activity_task.class.php b/backup/moodle2/backup_activity_task.class.php index 5c47f36e4dd..f47eda68c39 100644 --- a/backup/moodle2/backup_activity_task.class.php +++ b/backup/moodle2/backup_activity_task.class.php @@ -189,6 +189,9 @@ abstract class backup_activity_task extends backup_task { // Generate the grade history file. The setting 'grade_histories' is handled in the step. $this->add_step(new backup_activity_grade_history_structure_step('activity_grade_history', 'grade_history.xml')); + // Generate the competency file. + $this->add_step(new backup_activity_competencies_structure_step('activity_competencies', 'competencies.xml')); + // Annotate the scales used in already annotated outcomes $this->add_step(new backup_annotate_scales_from_outcomes('annotate_scales')); diff --git a/backup/moodle2/backup_course_task.class.php b/backup/moodle2/backup_course_task.class.php index 7ac637cbebe..7cb258bf7c5 100644 --- a/backup/moodle2/backup_course_task.class.php +++ b/backup/moodle2/backup_course_task.class.php @@ -127,6 +127,9 @@ class backup_course_task extends backup_task { $this->add_step(new backup_course_logstores_structure_step('course_logstores', 'logstores.xml')); } + // Generate the course competencies. + $this->add_step(new backup_course_competencies_structure_step('course_competencies', 'competencies.xml')); + // Generate the inforef file (must be after ALL steps gathering annotations of ANY type) $this->add_step(new backup_inforef_structure_step('course', 'inforef.xml')); diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index b088b1a5a13..9367cf36f42 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -282,6 +282,9 @@ class backup_module_structure_step extends backup_structure_step { // attach local plugin structure to $module, multiple allowed $this->add_plugin_structure('local', $module, true); + // Attach admin tools plugin structure to $module. + $this->add_plugin_structure('tool', $module, true); + $module->add_child($tags); $tags->add_child($tag); @@ -411,6 +414,10 @@ class backup_course_structure_step extends backup_structure_step { // can save course data if required $this->add_plugin_structure('local', $course, true); + // Attach admin tools plugin structure to $course element; multiple plugins + // can save course data if required. + $this->add_plugin_structure('tool', $course, true); + // Build the tree $course->add_child($category); @@ -1528,6 +1535,68 @@ class backup_course_logstores_structure_step extends backup_structure_step { class backup_activity_logstores_structure_step extends backup_course_logstores_structure_step { } +/** + * Course competencies backup structure step. + */ +class backup_course_competencies_structure_step extends backup_structure_step { + + protected function define_structure() { + $wrapper = new backup_nested_element('course_competencies'); + + $settings = new backup_nested_element('settings', array('id'), array('pushratingstouserplans')); + $wrapper->add_child($settings); + + $sql = 'SELECT s.pushratingstouserplans + FROM {' . \core_competency\course_competency_settings::TABLE . '} s + WHERE s.courseid = :courseid'; + $settings->set_source_sql($sql, array('courseid' => backup::VAR_COURSEID)); + + $competencies = new backup_nested_element('competencies'); + $wrapper->add_child($competencies); + + $competency = new backup_nested_element('competency', null, array('idnumber', 'ruleoutcome', + 'sortorder', 'frameworkidnumber')); + $competencies->add_child($competency); + + $sql = 'SELECT c.idnumber, cc.ruleoutcome, cc.sortorder, f.idnumber AS frameworkidnumber + FROM {' . \core_competency\course_competency::TABLE . '} cc + JOIN {' . \core_competency\competency::TABLE . '} c ON c.id = cc.competencyid + JOIN {' . \core_competency\competency_framework::TABLE . '} f ON f.id = c.competencyframeworkid + WHERE cc.courseid = :courseid + ORDER BY cc.sortorder'; + $competency->set_source_sql($sql, array('courseid' => backup::VAR_COURSEID)); + + return $wrapper; + } +} + +/** + * Activity competencies backup structure step. + */ +class backup_activity_competencies_structure_step extends backup_structure_step { + + protected function define_structure() { + $wrapper = new backup_nested_element('course_module_competencies'); + + $competencies = new backup_nested_element('competencies'); + $wrapper->add_child($competencies); + + $competency = new backup_nested_element('competency', null, array('idnumber', 'ruleoutcome', + 'sortorder', 'frameworkidnumber')); + $competencies->add_child($competency); + + $sql = 'SELECT c.idnumber, cmc.ruleoutcome, cmc.sortorder, f.idnumber AS frameworkidnumber + FROM {' . \core_competency\course_module_competency::TABLE . '} cmc + JOIN {' . \core_competency\competency::TABLE . '} c ON c.id = cmc.competencyid + JOIN {' . \core_competency\competency_framework::TABLE . '} f ON f.id = c.competencyframeworkid + WHERE cmc.cmid = :coursemoduleid + ORDER BY cmc.sortorder'; + $competency->set_source_sql($sql, array('coursemoduleid' => backup::VAR_MODID)); + + return $wrapper; + } +} + /** * structure in charge of constructing the inforef.xml file for all the items we want * to have referenced there (users, roles, files...) diff --git a/backup/moodle2/backup_tool_plugin.class.php b/backup/moodle2/backup_tool_plugin.class.php new file mode 100644 index 00000000000..561ab0aba75 --- /dev/null +++ b/backup/moodle2/backup_tool_plugin.class.php @@ -0,0 +1,38 @@ +. + +/** + * Admin tool backup plugin base. + * + * @package core_backup + * @subpackage moodle2 + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Admin tool backup plugin base class. + * + * @package core_backup + * @subpackage moodle2 + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class backup_tool_plugin extends backup_plugin { + // Use default parent behaviour. +} diff --git a/backup/moodle2/restore_activity_task.class.php b/backup/moodle2/restore_activity_task.class.php index 116c8e2e432..69221c82071 100644 --- a/backup/moodle2/restore_activity_task.class.php +++ b/backup/moodle2/restore_activity_task.class.php @@ -179,6 +179,9 @@ abstract class restore_activity_task extends restore_task { $this->add_step(new restore_activity_logstores_structure_step('activity_logstores', 'logstores.xml')); } + // Activity competencies. + $this->add_step(new restore_activity_competencies_structure_step('activity_competencies', 'competencies.xml')); + // At the end, mark it as built $this->built = true; } diff --git a/backup/moodle2/restore_course_task.class.php b/backup/moodle2/restore_course_task.class.php index d32e1e27c58..733ab1f95a9 100644 --- a/backup/moodle2/restore_course_task.class.php +++ b/backup/moodle2/restore_course_task.class.php @@ -112,6 +112,9 @@ class restore_course_task extends restore_task { $this->add_step(new restore_calendarevents_structure_step('course_calendar', 'calendar.xml')); } + // Course competencies. + $this->add_step(new restore_course_competencies_structure_step('course_competencies', 'competencies.xml')); + // At the end, mark it as built $this->built = true; } diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index ec316b6412e..2de616595c2 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -1689,6 +1689,9 @@ class restore_course_structure_step extends restore_structure_step { // Apply for local plugins optional paths at course level $this->add_plugin_structure('local', $course); + // Apply for admin tool plugins optional paths at course level. + $this->add_plugin_structure('tool', $course); + return array($course, $category, $tag, $allowed_module); } @@ -3052,6 +3055,171 @@ class restore_course_logstores_structure_step extends restore_structure_step { class restore_activity_logstores_structure_step extends restore_course_logstores_structure_step { } +/** + * Restore course competencies structure step. + */ +class restore_course_competencies_structure_step extends restore_structure_step { + + /** + * Returns the structure. + * + * @return array + */ + protected function define_structure() { + $paths = array( + new restore_path_element('course_competency', '/course_competencies/competencies/competency'), + new restore_path_element('course_competency_settings', '/course_competencies/settings') + ); + return $paths; + } + + /** + * Process a course competency settings. + * + * @param array $data The data. + */ + public function process_course_competency_settings($data) { + global $DB; + + $data = (object) $data; + $courseid = $this->task->get_courseid(); + $exists = \core_competency\course_competency_settings::get_record(array('courseid' => $courseid)); + + // Now update or insert. + if ($exists) { + $settings = $exists; + $settings->set_pushratingstouserplans($data->pushratingstouserplans); + return $settings->update(); + } else { + $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $data->pushratingstouserplans); + $settings = new \core_competency\course_competency_settings(0, $data); + $result = $settings->create(); + return !empty($result); + } + } + + /** + * Process a course competency. + * + * @param array $data The data. + */ + public function process_course_competency($data) { + $data = (object) $data; + + // Mapping the competency by ID numbers. + $framework = \core_competency\competency_framework::get_record(array('idnumber' => $data->frameworkidnumber)); + if (!$framework) { + return; + } + $competency = \core_competency\competency::get_record(array('idnumber' => $data->idnumber, + 'competencyframeworkid' => $framework->get_id())); + if (!$competency) { + return; + } + + $params = array( + 'competencyid' => $competency->get_id(), + 'courseid' => $this->task->get_courseid() + ); + $query = 'competencyid = :competencyid AND courseid = :courseid'; + $existing = \core_competency\course_competency::record_exists_select($query, $params); + + if (!$existing) { + // Sortorder is ignored by precaution, anyway we should walk through the records in the right order. + $record = (object) $params; + $record->ruleoutcome = $data->ruleoutcome; + $coursecompetency = new \core_competency\course_competency(0, $record); + $coursecompetency->create(); + } + } + + /** + * Execute conditions. + * + * @return bool + */ + protected function execute_condition() { + + // Do not execute if the competencies XML file is not found. + $fullpath = $this->task->get_taskbasepath(); + $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; + if (!file_exists($fullpath)) { + return false; + } + + return true; + } +} + +/** + * Restore activity competencies structure step. + */ +class restore_activity_competencies_structure_step extends restore_structure_step { + + /** + * Defines the structure. + * + * @return array + */ + protected function define_structure() { + $paths = array( + new restore_path_element('course_module_competency', '/course_module_competencies/competencies/competency') + ); + return $paths; + } + + /** + * Process a course module competency. + * + * @param array $data The data. + */ + public function process_course_module_competency($data) { + $data = (object) $data; + + // Mapping the competency by ID numbers. + $framework = \core_competency\competency_framework::get_record(array('idnumber' => $data->frameworkidnumber)); + if (!$framework) { + return; + } + $competency = \core_competency\competency::get_record(array('idnumber' => $data->idnumber, + 'competencyframeworkid' => $framework->get_id())); + if (!$competency) { + return; + } + + $params = array( + 'competencyid' => $competency->get_id(), + 'cmid' => $this->task->get_moduleid() + ); + $query = 'competencyid = :competencyid AND cmid = :cmid'; + $existing = \core_competency\course_module_competency::record_exists_select($query, $params); + + if (!$existing) { + // Sortorder is ignored by precaution, anyway we should walk through the records in the right order. + $record = (object) $params; + $record->ruleoutcome = $data->ruleoutcome; + $coursemodulecompetency = new \core_competency\course_module_competency(0, $record); + $coursemodulecompetency->create(); + } + } + + /** + * Execute conditions. + * + * @return bool + */ + protected function execute_condition() { + + // Do not execute if the competencies XML file is not found. + $fullpath = $this->task->get_taskbasepath(); + $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; + if (!file_exists($fullpath)) { + return false; + } + + return true; + } +} /** * Defines the restore step for advanced grading methods attached to the activity module @@ -3595,6 +3763,9 @@ class restore_module_structure_step extends restore_structure_step { // Apply for 'local' plugins optional paths at module level $this->add_plugin_structure('local', $module); + // Apply for 'admin tool' plugins optional paths at module level. + $this->add_plugin_structure('tool', $module); + return $paths; } diff --git a/backup/moodle2/restore_tool_plugin.class.php b/backup/moodle2/restore_tool_plugin.class.php new file mode 100644 index 00000000000..e964ff72a55 --- /dev/null +++ b/backup/moodle2/restore_tool_plugin.class.php @@ -0,0 +1,38 @@ +. + +/** + * Admin tool restore plugin base. + * + * @package core_backup + * @subpackage moodle2 + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Admin tool restore plugin base class. + * + * @package core_backup + * @subpackage moodle2 + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class restore_tool_plugin extends restore_plugin { + // Use default parent behaviour. +} diff --git a/blocks/lp/block_lp.php b/blocks/lp/block_lp.php new file mode 100644 index 00000000000..84b02a0d655 --- /dev/null +++ b/blocks/lp/block_lp.php @@ -0,0 +1,81 @@ +. + +/** + * Block LP main file. + * + * @package block_lp + * @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(); + +/** + * Block LP class. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_lp extends block_base { + + /** + * Applicable formats. + * + * @return array + */ + public function applicable_formats() { + return array('site' => true, 'course' => true, 'my' => true); + } + + /** + * Init. + * + * @return void + */ + public function init() { + $this->title = get_string('pluginname', 'block_lp'); + } + + /** + * Get content. + * + * @return stdClass + */ + public function get_content() { + if (isset($this->content)) { + return $this->content; + } + $this->content = new stdClass(); + + if (!\core_competency\api::is_enabled()) { + return $this->content; + } + + $summary = new \block_lp\output\summary(); + if (!$summary->has_content()) { + return $this->content; + } + + $renderer = $this->page->get_renderer('block_lp'); + $this->content->text = $renderer->render($summary); + $this->content->footer = ''; + + return $this->content; + } + +} diff --git a/blocks/lp/classes/output/competencies_to_review_page.php b/blocks/lp/classes/output/competencies_to_review_page.php new file mode 100644 index 00000000000..86099304efd --- /dev/null +++ b/blocks/lp/classes/output/competencies_to_review_page.php @@ -0,0 +1,87 @@ +. + +/** + * Competencies to review renderable. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use renderer_base; +use stdClass; +use moodle_url; +use core_competency\api; +use core_competency\external\competency_exporter; +use core_competency\external\user_competency_exporter; +use core_competency\external\user_summary_exporter; + +/** + * Competencies to review renderable class. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competencies_to_review_page implements renderable, templatable { + + /** @var array Competencies to review. */ + protected $compstoreview; + + /** + * Constructor. + */ + public function __construct() { + $this->compstoreview = api::list_user_competencies_to_review(0, 1000); + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + + $compstoreview = array(); + foreach ($this->compstoreview['competencies'] as $compdata) { + $ucexporter = new user_competency_exporter($compdata->usercompetency, + array('scale' => $compdata->competency->get_scale())); + $compexporter = new competency_exporter($compdata->competency, + array('context' => $compdata->competency->get_context())); + $userexporter = new user_summary_exporter($compdata->user); + $compstoreview[] = array( + 'usercompetency' => $ucexporter->export($output), + 'competency' => $compexporter->export($output), + 'user' => $userexporter->export($output), + ); + } + + $data = array( + 'competencies' => $compstoreview, + 'pluginbaseurl' => (new moodle_url('/blocks/lp'))->out(false), + ); + + return $data; + } + +} diff --git a/blocks/lp/classes/output/plans_to_review_page.php b/blocks/lp/classes/output/plans_to_review_page.php new file mode 100644 index 00000000000..2cd63090859 --- /dev/null +++ b/blocks/lp/classes/output/plans_to_review_page.php @@ -0,0 +1,82 @@ +. + +/** + * Plans to review renderable. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use templatable; +use renderer_base; +use stdClass; +use moodle_url; +use core_competency\api; +use core_competency\external\plan_exporter; +use core_competency\external\user_summary_exporter; + +/** + * Plans to review renderable class. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plans_to_review_page implements renderable, templatable { + + /** @var array Plans to review. */ + protected $planstoreview; + + /** + * Constructor. + */ + public function __construct() { + $this->planstoreview = api::list_plans_to_review(0, 1000); + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + + $planstoreview = array(); + foreach ($this->planstoreview['plans'] as $plandata) { + $planexporter = new plan_exporter($plandata->plan, array('template' => $plandata->template)); + $userexporter = new user_summary_exporter($plandata->owner); + $planstoreview[] = array( + 'plan' => $planexporter->export($output), + 'user' => $userexporter->export($output), + ); + } + + $data = array( + 'plans' => $planstoreview, + 'pluginbaseurl' => (new moodle_url('/blocks/lp'))->out(false), + ); + + return $data; + } + +} diff --git a/blocks/lp/classes/output/renderer.php b/blocks/lp/classes/output/renderer.php new file mode 100644 index 00000000000..14e778371bf --- /dev/null +++ b/blocks/lp/classes/output/renderer.php @@ -0,0 +1,70 @@ +. + +/** + * Block LP renderer. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use plugin_renderer_base; +use renderable; + +/** + * Block LP renderer class. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Defer to template. + * @param renderable $page + * @return string + */ + public function render_competencies_to_review_page(renderable $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('block_lp/competencies_to_review_page', $data); + } + + /** + * Defer to template. + * @param renderable $page + * @return string + */ + public function render_plans_to_review_page(renderable $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('block_lp/plans_to_review_page', $data); + } + + /** + * Defer to template. + * @param renderable $summary + * @return string + */ + public function render_summary(renderable $summary) { + $data = $summary->export_for_template($this); + return parent::render_from_template('block_lp/summary', $data); + } + +} diff --git a/blocks/lp/classes/output/summary.php b/blocks/lp/classes/output/summary.php new file mode 100644 index 00000000000..22c81298608 --- /dev/null +++ b/blocks/lp/classes/output/summary.php @@ -0,0 +1,151 @@ +. + +/** + * Summary renderable. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_lp\output; +defined('MOODLE_INTERNAL') || die(); + +use core_competency\api; +use core_competency\external\competency_exporter; +use core_competency\external\plan_exporter; +use core_competency\external\user_competency_exporter; +use core_competency\external\user_summary_exporter; +use core_competency\plan; +use core_competency\url; +use renderable; +use renderer_base; +use templatable; + +/** + * Summary renderable class. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class summary implements renderable, templatable { + + /** @var array Active plans. */ + protected $activeplans = array(); + /** @var array Competencies to review. */ + protected $compstoreview = array(); + /** @var array Plans to review. */ + protected $planstoreview = array(); + /** @var array Plans. */ + protected $plans = array(); + /** @var stdClass The user. */ + protected $user; + + /** + * Constructor. + * @param stdClass $user The user. + */ + public function __construct($user = null) { + global $USER; + if (!$user) { + $user = $USER; + } + $this->user = $user; + + // Get the plans. + $this->plans = api::list_user_plans($this->user->id); + + // Get the competencies to review. + $this->compstoreview = api::list_user_competencies_to_review(0, 3); + + // Get the plans to review. + $this->planstoreview = api::list_plans_to_review(0, 3); + } + + public function export_for_template(renderer_base $output) { + $plans = array(); + foreach ($this->plans as $plan) { + if (count($plans) >= 3) { + break; + } + if ($plan->get_status() == plan::STATUS_ACTIVE) { + $plans[] = $plan; + } + } + $activeplans = array(); + foreach ($plans as $plan) { + $planexporter = new plan_exporter($plan, array('template' => $plan->get_template())); + $activeplans[] = $planexporter->export($output); + } + + $compstoreview = array(); + foreach ($this->compstoreview['competencies'] as $compdata) { + $ucexporter = new user_competency_exporter($compdata->usercompetency, + array('scale' => $compdata->competency->get_scale())); + $compexporter = new competency_exporter($compdata->competency, + array('context' => $compdata->competency->get_context())); + $userexporter = new user_summary_exporter($compdata->user); + $compstoreview[] = array( + 'usercompetency' => $ucexporter->export($output), + 'competency' => $compexporter->export($output), + 'user' => $userexporter->export($output), + ); + } + + $planstoreview = array(); + foreach ($this->planstoreview['plans'] as $plandata) { + $planexporter = new plan_exporter($plandata->plan, array('template' => $plandata->template)); + $userexporter = new user_summary_exporter($plandata->owner); + $planstoreview[] = array( + 'plan' => $planexporter->export($output), + 'user' => $userexporter->export($output), + ); + } + + $data = array( + 'hasplans' => !empty($this->plans), + 'hasactiveplans' => !empty($activeplans), + 'hasmoreplans' => count($this->plans) > count($activeplans), + 'activeplans' => $activeplans, + + 'compstoreview' => $compstoreview, + 'hascompstoreview' => $this->compstoreview['count'] > 0, + 'hasmorecompstoreview' => $this->compstoreview['count'] > 3, + + 'planstoreview' => $planstoreview, + 'hasplanstoreview' => $this->planstoreview['count'] > 0, + 'hasmoreplanstoreview' => $this->planstoreview['count'] > 3, + + 'plansurl' => url::plans($this->user->id)->out(false), + 'pluginbaseurl' => (new \moodle_url('/blocks/lp'))->out(false), + 'userid' => $this->user->id, + ); + + return $data; + } + + /** + * Returns whether there is content in the summary. + * + * @return boolean + */ + public function has_content() { + return !empty($this->plans) || $this->planstoreview['count'] > 0 || $this->compstoreview['count'] > 0; + } + +} diff --git a/blocks/lp/competencies_to_review.php b/blocks/lp/competencies_to_review.php new file mode 100644 index 00000000000..ec91f282214 --- /dev/null +++ b/blocks/lp/competencies_to_review.php @@ -0,0 +1,47 @@ +. + +/** + * Competencies to review page. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../config.php'); + +require_login(null, false); +if (isguestuser()) { + throw new require_login_exception('Guests are not allowed here.'); +} + +$toreviewstr = get_string('competenciestoreview', 'block_lp'); + +$url = new moodle_url('/blocks/lp/competencies_to_review.php'); +$PAGE->set_context(context_user::instance($USER->id)); +$PAGE->set_url($url); +$PAGE->set_title($toreviewstr); +$PAGE->navbar->add($toreviewstr, $url); + +$output = $PAGE->get_renderer('block_lp'); +echo $output->header(); +echo $output->heading($toreviewstr); + +$page = new \block_lp\output\competencies_to_review_page(); +echo $output->render($page); + +echo $output->footer(); diff --git a/blocks/lp/db/access.php b/blocks/lp/db/access.php new file mode 100644 index 00000000000..73da9c99f6c --- /dev/null +++ b/blocks/lp/db/access.php @@ -0,0 +1,57 @@ +. + +/** + * Block LP capabilities. + * + * @package block_lp + * @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(); + +$capabilities = array( + + // Whether or not the user can add the block. + 'block/lp:addinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/site:manageblocks' + ), + + // Whether or not the user can add the block on their dashboard. + 'block/lp:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ) + ), + + // Whether or not a user can see the block. + 'block/lp:view' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + ), + +); diff --git a/blocks/lp/lang/en/block_lp.php b/blocks/lp/lang/en/block_lp.php new file mode 100644 index 00000000000..80b88ca315e --- /dev/null +++ b/blocks/lp/lang/en/block_lp.php @@ -0,0 +1,34 @@ +. + +/** + * Block LP language strings. + * + * @package block_lp + * @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(); + +$string['competenciestoreview'] = 'Competencies to review'; +$string['lp:addinstance'] = 'Adding a new block'; +$string['lp:myaddinstance'] = 'Adding the block on the dashboard'; +$string['lp:view'] = 'Viewing the block'; +$string['planstoreview'] = 'Plans to review'; +$string['pluginname'] = 'Learning plans'; +$string['viewmore'] = 'View more...'; +$string['viewotherplans'] = 'View other plans...'; diff --git a/blocks/lp/plans_to_review.php b/blocks/lp/plans_to_review.php new file mode 100644 index 00000000000..973c1ae50dc --- /dev/null +++ b/blocks/lp/plans_to_review.php @@ -0,0 +1,47 @@ +. + +/** + * Competencies to review page. + * + * @package block_lp + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../config.php'); + +require_login(null, false); +if (isguestuser()) { + throw new require_login_exception('Guests are not allowed here.'); +} + +$toreviewstr = get_string('planstoreview', 'block_lp'); + +$url = new moodle_url('/blocks/lp/plans_to_review.php'); +$PAGE->set_context(context_user::instance($USER->id)); +$PAGE->set_url($url); +$PAGE->set_title($toreviewstr); +$PAGE->navbar->add($toreviewstr, $url); + +$output = $PAGE->get_renderer('block_lp'); +echo $output->header(); +echo $output->heading($toreviewstr); + +$page = new \block_lp\output\plans_to_review_page(); +echo $output->render($page); + +echo $output->footer(); diff --git a/blocks/lp/styles.css b/blocks/lp/styles.css new file mode 100644 index 00000000000..b17f1cbcebe --- /dev/null +++ b/blocks/lp/styles.css @@ -0,0 +1,14 @@ +.block_lp.block .content h3 { + padding: 0; + text-transform: none; +} +.block_lp .sub-content { + padding: 0 15px; +} +.block_lp ul { + list-style: none; + margin: 0; +} +.block_lp ul .more { + padding-top: 10px; +} diff --git a/blocks/lp/templates/competencies_to_review_page.mustache b/blocks/lp/templates/competencies_to_review_page.mustache new file mode 100644 index 00000000000..43e83784a8a --- /dev/null +++ b/blocks/lp/templates/competencies_to_review_page.mustache @@ -0,0 +1,49 @@ +{{! + 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 . +}} +{{! + Competencies to review. + + Classes required for JS: + * - + + Data attributes required for JS: + * - + + Context variables required for this template: + * competencies +}} + +
    + + + + + + + + + + {{#competencies}} + + + + + + {{/competencies}} + +
    {{#str}}shortname, tool_lp{{/str}}{{#str}}user{{/str}}{{#str}}reviewstatus, tool_lp{{/str}}
    {{competency.shortname}}{{user.fullname}}{{usercompetency.statusname}}
    +
    diff --git a/blocks/lp/templates/plans_to_review_page.mustache b/blocks/lp/templates/plans_to_review_page.mustache new file mode 100644 index 00000000000..9267a353155 --- /dev/null +++ b/blocks/lp/templates/plans_to_review_page.mustache @@ -0,0 +1,49 @@ +{{! + 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 . +}} +{{! + Plans to review. + + Classes required for JS: + * - + + Data attributes required for JS: + * - + + Context variables required for this template: + * plans +}} + +
    + + + + + + + + + + {{#plans}} + + + + + + {{/plans}} + +
    {{#str}}planname, tool_lp{{/str}}{{#str}}user{{/str}}{{#str}}reviewstatus, tool_lp{{/str}}
    {{plan.name}}{{user.fullname}}{{plan.statusname}}
    +
    diff --git a/blocks/lp/templates/summary.mustache b/blocks/lp/templates/summary.mustache new file mode 100644 index 00000000000..cc2514389f0 --- /dev/null +++ b/blocks/lp/templates/summary.mustache @@ -0,0 +1,87 @@ +{{! + 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 . +}} +{{! + Summary. + + Classes required for JS: + * None + + Data attibutes required for JS: + * None + + Context variables required for this template: + * hasplans + * hasactiveplans + * activeplans + * hasmoreplans + * hascompstoreview + * compstoreview + * hasmorecompstoreview + * hasplanstoreview + * planstoreview + * hasmoreplanstoreview +}} +
    + {{#hasplans}} +

    My plans

    +
    + {{#hasactiveplans}} + + {{/hasactiveplans}} + {{^hasactiveplans}} +

    No active plans at the moment. {{#str}}viewotherplans, block_lp{{/str}}

    + {{/hasactiveplans}} +
    + {{/hasplans}} + {{#hascompstoreview}} +

    {{#str}}competenciestoreview, block_lp{{/str}}

    +
    + +
    + {{/hascompstoreview}} + {{#hasplanstoreview}} +

    Plans to review

    +
    + +
    + {{/hasplanstoreview}} +
    diff --git a/blocks/lp/version.php b/blocks/lp/version.php new file mode 100644 index 00000000000..31fe05c6ed0 --- /dev/null +++ b/blocks/lp/version.php @@ -0,0 +1,32 @@ +. + +/** + * Block LP version file. + * + * @package block_lp + * @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(); + +$plugin->version = 2016011100; +$plugin->requires = 2014110400; +$plugin->component = 'block_lp'; +$plugin->dependencies = array( + 'tool_lp' => ANY_VERSION +); diff --git a/comment/lib.php b/comment/lib.php index d17d5bac5d4..7df025caad4 100644 --- a/comment/lib.php +++ b/comment/lib.php @@ -1039,6 +1039,87 @@ class comment { public function set_fullwidth($fullwidth = true) { $this->fullwidth = (bool)$fullwidth; } + + /** + * Return the template. + * + * @since 3.1 + * @return string + */ + public function get_template() { + return $this->template; + } + + /** + * Return the cid. + * + * @since 3.1 + * @return string + */ + public function get_cid() { + return $this->cid; + } + + /** + * Return the link text. + * + * @since 3.1 + * @return string + */ + public function get_linktext() { + return $this->linktext; + } + + /** + * Return no toggle. + * + * @since 3.1 + * @return bool + */ + public function get_notoggle() { + return $this->notoggle; + } + + /** + * Return display total count. + * + * @since 3.1 + * @return bool + */ + public function get_displaytotalcount() { + return $this->displaytotalcount; + } + + /** + * Return display cancel. + * + * @since 3.1 + * @return bool + */ + public function get_displaycancel() { + return $this->displaycancel; + } + + /** + * Return fullwidth. + * + * @since 3.1 + * @return bool + */ + public function get_fullwidth() { + return $this->fullwidth; + } + + /** + * Return autostart. + * + * @since 3.1 + * @return bool + */ + public function get_autostart() { + return $this->autostart; + } + } /** diff --git a/competency/classes/api.php b/competency/classes/api.php new file mode 100644 index 00000000000..9999c4002b0 --- /dev/null +++ b/competency/classes/api.php @@ -0,0 +1,5231 @@ +. + +/** + * Class for loading/storing competency frameworks from the DB. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use stdClass; +use cm_info; +use context; +use context_helper; +use context_system; +use context_course; +use context_module; +use context_user; +use coding_exception; +use require_login_exception; +use moodle_exception; +use moodle_url; +use required_capability_exception; + +/** + * Class for doing things with competency frameworks. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class api { + + /** + * Returns whether competencies are enabled. + * + * @return boolean True when enabled. + */ + public static function is_enabled() { + return get_config('core_competency', 'enabled'); + } + + /** + * Throws an exception if competencies are not enabled. + * + * @return void + * @throws moodle_exception + */ + public static function require_enabled() { + if (!static::is_enabled()) { + throw new moodle_exception('competenciesarenotenabled', 'core_competency'); + } + } + + /** + * Checks whether a scale is used anywhere in the plugin. + * + * This public API has two exceptions: + * - It MUST NOT perform any capability checks. + * - It MUST ignore whether competencies are enabled or not ({@link self::is_enabled()}). + * + * @param int $scaleid The scale ID. + * @return bool + */ + public static function is_scale_used_anywhere($scaleid) { + global $DB; + $sql = "SELECT s.id + FROM {scale} s + LEFT JOIN {" . competency_framework::TABLE ."} f + ON f.scaleid = :scaleid1 + LEFT JOIN {" . competency::TABLE ."} c + ON c.scaleid = :scaleid2 + WHERE f.id IS NOT NULL + OR c.id IS NOT NULL"; + return $DB->record_exists_sql($sql, ['scaleid1' => $scaleid, 'scaleid2' => $scaleid]); + } + + /** + * Validate if current user have acces to the course_module if hidden. + * + * @param mixed $cmmixed The cm_info class, course module record or its ID. + * @param bool $throwexception Throw an exception or not. + * @return bool + */ + protected static function validate_course_module($cmmixed, $throwexception = true) { + $cm = $cmmixed; + if (!is_object($cm)) { + $cmrecord = get_coursemodule_from_id(null, $cmmixed); + $modinfo = get_fast_modinfo($cmrecord->course); + $cm = $modinfo->get_cm($cmmixed); + } else if (!$cm instanceof cm_info) { + // Assume we got a course module record. + $modinfo = get_fast_modinfo($cm->course); + $cm = $modinfo->get_cm($cm->id); + } + + if (!$cm->uservisible) { + if ($throwexception) { + throw new require_login_exception('Course module is hidden'); + } else { + return false; + } + } + + return true; + } + + /** + * Validate if current user have acces to the course if hidden. + * + * @param mixed $courseorid The course or it ID. + * @param bool $throwexception Throw an exception or not. + * @return bool + */ + protected static function validate_course($courseorid, $throwexception = true) { + $course = $courseorid; + if (!is_object($course)) { + $course = get_course($course); + } + + $coursecontext = context_course::instance($course->id); + if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { + if ($throwexception) { + throw new require_login_exception('Course is hidden'); + } else { + return false; + } + } + + return true; + } + + /** + * Create a competency from a record containing all the data for the class. + * + * Requires moodle/competency:competencymanage 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_competency(stdClass $record) { + static::require_enabled(); + $competency = new competency(0, $record); + + // First we do a permissions check. + require_capability('moodle/competency:competencymanage', $competency->get_context()); + + // Reset the sortorder, use reorder instead. + $competency->set_sortorder(null); + $competency->create(); + + \core\event\competency_created::create_from_competency($competency)->trigger(); + + // Reset the rule of the parent. + $parent = $competency->get_parent(); + if ($parent) { + $parent->reset_rule(); + $parent->update(); + } + + return $competency; + } + + /** + * Delete a competency by id. + * + * Requires moodle/competency:competencymanage capability at the system context. + * + * @param int $id The record to delete. This will delete alot of related data - you better be sure. + * @return boolean + */ + public static function delete_competency($id) { + global $DB; + static::require_enabled(); + $competency = new competency($id); + + // First we do a permissions check. + require_capability('moodle/competency:competencymanage', $competency->get_context()); + + $events = array(); + $competencyids = array(intval($competency->get_id())); + $contextid = $competency->get_context()->id; + $competencyids = array_merge(competency::get_descendants_ids($competency), $competencyids); + if (!competency::can_all_be_deleted($competencyids)) { + return false; + } + $transaction = $DB->start_delegated_transaction(); + + try { + + // Reset the rule of the parent. + $parent = $competency->get_parent(); + if ($parent) { + $parent->reset_rule(); + $parent->update(); + } + + // Delete the competency separately so the after_delete event can be triggered. + $competency->delete(); + + // Delete the competencies. + competency::delete_multiple($competencyids); + + // Delete the competencies relation. + related_competency::delete_multiple_relations($competencyids); + + // Delete competency evidences. + user_evidence_competency::delete_by_competencyids($competencyids); + + // Register the competencies deleted events. + $events = \core\event\competency_deleted::create_multiple_from_competencyids($competencyids, $contextid); + + } catch (\Exception $e) { + $transaction->rollback($e); + } + + $transaction->allow_commit(); + // Trigger events. + foreach ($events as $event) { + $event->trigger(); + } + + return true; + } + + /** + * Reorder this competency. + * + * Requires moodle/competency:competencymanage capability at the system context. + * + * @param int $id The id of the competency to move. + * @return boolean + */ + public static function move_down_competency($id) { + static::require_enabled(); + $current = new competency($id); + + // First we do a permissions check. + require_capability('moodle/competency:competencymanage', $current->get_context()); + + $max = self::count_competencies(array('parentid' => $current->get_parentid(), + 'competencyframeworkid' => $current->get_competencyframeworkid())); + if ($max > 0) { + $max--; + } + + $sortorder = $current->get_sortorder(); + if ($sortorder >= $max) { + return false; + } + $sortorder = $sortorder + 1; + $current->set_sortorder($sortorder); + + $filters = array('parentid' => $current->get_parentid(), + 'competencyframeworkid' => $current->get_competencyframeworkid(), + 'sortorder' => $sortorder); + $children = self::list_competencies($filters, 'id'); + foreach ($children as $needtoswap) { + $needtoswap->set_sortorder($sortorder - 1); + $needtoswap->update(); + } + + // OK - all set. + $result = $current->update(); + + return $result; + } + + /** + * Reorder this competency. + * + * Requires moodle/competency:competencymanage capability at the system context. + * + * @param int $id The id of the competency to move. + * @return boolean + */ + public static function move_up_competency($id) { + static::require_enabled(); + $current = new competency($id); + + // First we do a permissions check. + require_capability('moodle/competency:competencymanage', $current->get_context()); + + $sortorder = $current->get_sortorder(); + if ($sortorder == 0) { + return false; + } + + $sortorder = $sortorder - 1; + $current->set_sortorder($sortorder); + + $filters = array('parentid' => $current->get_parentid(), + 'competencyframeworkid' => $current->get_competencyframeworkid(), + 'sortorder' => $sortorder); + $children = self::list_competencies($filters, 'id'); + foreach ($children as $needtoswap) { + $needtoswap->set_sortorder($sortorder + 1); + $needtoswap->update(); + } + + // OK - all set. + $result = $current->update(); + + return $result; + } + + /** + * Move this competency so it sits in a new parent. + * + * Requires moodle/competency:competencymanage capability at the system context. + * + * @param int $id The id of the competency to move. + * @param int $newparentid The new parent id for the competency. + * @return boolean + */ + public static function set_parent_competency($id, $newparentid) { + global $DB; + static::require_enabled(); + $current = new competency($id); + + // First we do a permissions check. + require_capability('moodle/competency:competencymanage', $current->get_context()); + if ($id == $newparentid) { + throw new coding_exception('Can not set a competency as a parent of itself.'); + } if ($newparentid == $current->get_parentid()) { + throw new coding_exception('Can not move a competency to the same location.'); + } + + // Some great variable assignment right here. + $currentparent = $current->get_parent(); + $parent = !empty($newparentid) ? new competency($newparentid) : null; + $parentpath = !empty($parent) ? $parent->get_path() : '/0/'; + + // We're going to change quite a few things. + $transaction = $DB->start_delegated_transaction(); + + // If we are moving a node to a child of itself: + // - promote all the child nodes by one level. + // - remove the rule on self. + // - re-read the parent. + $newparents = explode('/', $parentpath); + if (in_array($current->get_id(), $newparents)) { + $children = competency::get_records(array('parentid' => $current->get_id()), 'id'); + foreach ($children as $child) { + $child->set_parentid($current->get_parentid()); + $child->update(); + } + + // Reset the rule on self as our children have changed. + $current->reset_rule(); + + // The destination parent is one of our descendants, we need to re-fetch its values (path, parentid). + $parent->read(); + } + + // Reset the rules of initial parent and destination. + if (!empty($currentparent)) { + $currentparent->reset_rule(); + $currentparent->update(); + } + if (!empty($parent)) { + $parent->reset_rule(); + $parent->update(); + } + + // Do the actual move. + $current->set_parentid($newparentid); + $result = $current->update(); + + // All right, let's commit this. + $transaction->allow_commit(); + + return $result; + } + + /** + * Update the details for a competency. + * + * Requires moodle/competency:competencymanage capability at the system context. + * + * @param stdClass $record The new details for the competency. + * Note - must contain an id that points to the competency to update. + * + * @return boolean + */ + public static function update_competency($record) { + static::require_enabled(); + $competency = new competency($record->id); + + // First we do a permissions check. + require_capability('moodle/competency:competencymanage', $competency->get_context()); + + // Some things should not be changed in an update - they should use a more specific method. + $record->sortorder = $competency->get_sortorder(); + $record->parentid = $competency->get_parentid(); + $record->competencyframeworkid = $competency->get_competencyframeworkid(); + + $competency->from_record($record); + require_capability('moodle/competency:competencymanage', $competency->get_context()); + + // OK - all set. + $result = $competency->update(); + + // Trigger the update event. + \core\event\competency_updated::create_from_competency($competency)->trigger(); + + return $result; + } + + /** + * Read a the details for a single competency and return a record. + * + * Requires moodle/competency:competencyview capability at the system context. + * + * @param int $id The id of the competency to read. + * @param bool $includerelated Include related tags or not. + * @return stdClass + */ + public static function read_competency($id, $includerelated = false) { + static::require_enabled(); + $competency = new competency($id); + + // First we do a permissions check. + $context = $competency->get_context(); + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) { + throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', ''); + } + + // OK - all set. + if ($includerelated) { + $relatedcompetency = new related_competency(); + if ($related = $relatedcompetency->list_relations($id)) { + $competency->relatedcompetencies = $related; + } + } + + return $competency; + } + + /** + * Perform a text search based and return all results and their parents. + * + * Requires moodle/competency:competencyview capability at the framework context. + * + * @param string $textsearch A string to search for. + * @param int $competencyframeworkid The id of the framework to limit the search. + * @return array of competencies + */ + public static function search_competencies($textsearch, $competencyframeworkid) { + static::require_enabled(); + $framework = new competency_framework($competencyframeworkid); + + // First we do a permissions check. + $context = $framework->get_context(); + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) { + throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', ''); + } + + // OK - all set. + $competencies = competency::search($textsearch, $competencyframeworkid); + return $competencies; + } + + /** + * Perform a search based on the provided filters and return a paginated list of records. + * + * Requires moodle/competency:competencyview capability at some context. + * + * @param array $filters A list of filters to apply to the list. + * @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 competencies + */ + public static function list_competencies($filters, $sort = '', $order = 'ASC', $skip = 0, $limit = 0) { + static::require_enabled(); + if (!isset($filters['competencyframeworkid'])) { + $context = context_system::instance(); + } else { + $framework = new competency_framework($filters['competencyframeworkid']); + $context = $framework->get_context(); + } + + // First we do a permissions check. + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) { + throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', ''); + } + + // OK - all set. + return competency::get_records($filters, $sort, $order, $skip, $limit); + } + + /** + * Perform a search based on the provided filters and return a paginated list of records. + * + * Requires moodle/competency:competencyview capability at some context. + * + * @param array $filters A list of filters to apply to the list. + * @return int + */ + public static function count_competencies($filters) { + static::require_enabled(); + if (!isset($filters['competencyframeworkid'])) { + $context = context_system::instance(); + } else { + $framework = new competency_framework($filters['competencyframeworkid']); + $context = $framework->get_context(); + } + + // First we do a permissions check. + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) { + throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', ''); + } + + // OK - all set. + return competency::count_records($filters); + } + + /** + * Create a competency framework from a record containing all the data for the class. + * + * Requires moodle/competency:competencymanage capability at the system context. + * + * @param stdClass $record Record containing all the data for an instance of the class. + * @return competency_framework + */ + public static function create_framework(stdClass $record) { + static::require_enabled(); + $framework = new competency_framework(0, $record); + require_capability('moodle/competency:competencymanage', $framework->get_context()); + + // Account for different formats of taxonomies. + if (isset($record->taxonomies)) { + $framework->set_taxonomies($record->taxonomies); + } + + $framework = $framework->create(); + + // Trigger a competency framework created event. + \core\event\competency_framework_created::create_from_framework($framework)->trigger(); + + return $framework; + } + + /** + * Duplicate a competency framework by id. + * + * Requires moodle/competency:competencymanage capability at the system context. + * + * @param int $id The record to duplicate. All competencies associated and related will be duplicated. + * @return competency_framework the framework duplicated + */ + public static function duplicate_framework($id) { + global $DB; + static::require_enabled(); + + $framework = new competency_framework($id); + require_capability('moodle/competency:competencymanage', $framework->get_context()); + // Starting transaction. + $transaction = $DB->start_delegated_transaction(); + + try { + // Get a uniq idnumber based on the origin framework. + $idnumber = competency_framework::get_unused_idnumber($framework->get_idnumber()); + $framework->set_idnumber($idnumber); + // Adding the suffix copy to the shortname. + $framework->set_shortname(get_string('duplicateditemname', 'core_competency', $framework->get_shortname())); + $framework->set_id(0); + $framework = $framework->create(); + + // Array that match the old competencies ids with the new one to use when copying related competencies. + $frameworkcompetency = competency::get_framework_tree($id); + $matchids = self::duplicate_competency_tree($framework->get_id(), $frameworkcompetency, 0, 0); + + // Copy the related competencies. + $relcomps = related_competency::get_multiple_relations(array_keys($matchids)); + + foreach ($relcomps as $relcomp) { + $compid = $relcomp->get_competencyid(); + $relcompid = $relcomp->get_relatedcompetencyid(); + if (isset($matchids[$compid]) && isset($matchids[$relcompid])) { + $newcompid = $matchids[$compid]->get_id(); + $newrelcompid = $matchids[$relcompid]->get_id(); + if ($newcompid < $newrelcompid) { + $relcomp->set_competencyid($newcompid); + $relcomp->set_relatedcompetencyid($newrelcompid); + } else { + $relcomp->set_competencyid($newrelcompid); + $relcomp->set_relatedcompetencyid($newcompid); + } + $relcomp->set_id(0); + $relcomp->create(); + } else { + // Debugging message when there is no match found. + debugging('related competency id not found'); + } + } + + // Setting rules on duplicated competencies. + self::migrate_competency_tree_rules($frameworkcompetency, $matchids); + + $transaction->allow_commit(); + + } catch (\Exception $e) { + $transaction->rollback($e); + } + + // Trigger a competency framework created event. + \core\event\competency_framework_created::create_from_framework($framework)->trigger(); + + return $framework; + } + + /** + * Delete a competency framework by id. + * + * Requires moodle/competency:competencymanage capability at the system context. + * + * @param int $id The record to delete. This will delete alot of related data - you better be sure. + * @return boolean + */ + public static function delete_framework($id) { + global $DB; + static::require_enabled(); + $framework = new competency_framework($id); + require_capability('moodle/competency:competencymanage', $framework->get_context()); + + $events = array(); + $competenciesid = competency::get_ids_by_frameworkid($id); + $contextid = $framework->get_contextid(); + if (!competency::can_all_be_deleted($competenciesid)) { + return false; + } + $transaction = $DB->start_delegated_transaction(); + try { + if (!empty($competenciesid)) { + // Delete competencies. + competency::delete_by_frameworkid($id); + + // Delete the related competencies. + related_competency::delete_multiple_relations($competenciesid); + + // Delete the evidences for competencies. + user_evidence_competency::delete_by_competencyids($competenciesid); + } + + // Create a competency framework deleted event. + $event = \core\event\competency_framework_deleted::create_from_framework($framework); + $result = $framework->delete(); + + // Register the deleted events competencies. + $events = \core\event\competency_deleted::create_multiple_from_competencyids($competenciesid, $contextid); + + } catch (\Exception $e) { + $transaction->rollback($e); + } + + // Commit the transaction. + $transaction->allow_commit(); + + // If all operations are successfull then trigger the delete event. + $event->trigger(); + + // Trigger deleted event competencies. + foreach ($events as $event) { + $event->trigger(); + } + + return $result; + } + + /** + * Update the details for a competency framework. + * + * Requires moodle/competency:competencymanage capability at the system context. + * + * @param stdClass $record The new details for the framework. Note - must contain an id that points to the framework to update. + * @return boolean + */ + public static function update_framework($record) { + static::require_enabled(); + $framework = new competency_framework($record->id); + + // Check the permissions before update. + require_capability('moodle/competency:competencymanage', $framework->get_context()); + + // Account for different formats of taxonomies. + $framework->from_record($record); + if (isset($record->taxonomies)) { + $framework->set_taxonomies($record->taxonomies); + } + + // Trigger a competency framework updated event. + \core\event\competency_framework_updated::create_from_framework($framework)->trigger(); + + return $framework->update(); + } + + /** + * Read a the details for a single competency framework and return a record. + * + * Requires moodle/competency:competencyview capability at the system context. + * + * @param int $id The id of the framework to read. + * @return competency_framework + */ + public static function read_framework($id) { + static::require_enabled(); + $framework = new competency_framework($id); + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), + $framework->get_context())) { + throw new required_capability_exception($framework->get_context(), 'moodle/competency:competencyview', + 'nopermissions', ''); + } + return $framework; + } + + /** + * Logg the competency framework viewed event. + * + * @param competency_framework|int $frameworkorid The competency_framework object or competency framework id + * @return bool + */ + public static function competency_framework_viewed($frameworkorid) { + static::require_enabled(); + $framework = $frameworkorid; + if (!is_object($framework)) { + $framework = new competency_framework($framework); + } + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), + $framework->get_context())) { + throw new required_capability_exception($framework->get_context(), 'moodle/competency:competencyview', + 'nopermissions', ''); + } + \core\event\competency_framework_viewed::create_from_framework($framework)->trigger(); + return true; + } + + /** + * Logg the competency viewed event. + * + * @param competency|int $competencyorid The competency object or competency id + * @return bool + */ + public static function competency_viewed($competencyorid) { + static::require_enabled(); + $competency = $competencyorid; + if (!is_object($competency)) { + $competency = new competency($competency); + } + + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), + $competency->get_context())) { + throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview', + 'nopermissions', ''); + } + + \core\event\competency_viewed::create_from_competency($competency)->trigger(); + return true; + } + + /** + * Perform a search based on the provided filters and return a paginated list of records. + * + * Requires moodle/competency:competencyview 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) + * @param context $context The parent context of the frameworks. + * @param string $includes Defines what other contexts to fetch frameworks from. + * Accepted values are: + * - children: All descendants + * - parents: All parents, grand parents, etc... + * - self: Context passed only. + * @param bool $onlyvisible If true return only visible frameworks + * @param string $query A string to use to filter down the frameworks. + * @return array of competency_framework + */ + public static function list_frameworks($sort, $order, $skip, $limit, $context, $includes = 'children', + $onlyvisible = false, $query = '') { + global $DB; + static::require_enabled(); + + // Get all the relevant contexts. + $contexts = self::get_related_contexts($context, $includes, + array('moodle/competency:competencyview', 'moodle/competency:competencymanage')); + + if (empty($contexts)) { + throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', ''); + } + + // OK - all set. + list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED); + $select = "contextid $insql"; + if ($onlyvisible) { + $select .= " AND visible = :visible"; + $inparams['visible'] = 1; + } + + if (!empty($query) || is_numeric($query)) { + $sqlnamelike = $DB->sql_like('shortname', ':namelike', false); + $sqlidnlike = $DB->sql_like('idnumber', ':idnlike', false); + + $select .= " AND ($sqlnamelike OR $sqlidnlike) "; + $inparams['namelike'] = '%' . $DB->sql_like_escape($query) . '%'; + $inparams['idnlike'] = '%' . $DB->sql_like_escape($query) . '%'; + } + + return competency_framework::get_records_select($select, $inparams, $sort . ' ' . $order, '*', $skip, $limit); + } + + /** + * Perform a search based on the provided filters and return a paginated list of records. + * + * Requires moodle/competency:competencyview capability at the system context. + * + * @param context $context The parent context of the frameworks. + * @param string $includes Defines what other contexts to fetch frameworks from. + * Accepted values are: + * - children: All descendants + * - parents: All parents, grand parents, etc... + * - self: Context passed only. + * @return int + */ + public static function count_frameworks($context, $includes) { + global $DB; + static::require_enabled(); + + // Get all the relevant contexts. + $contexts = self::get_related_contexts($context, $includes, + array('moodle/competency:competencyview', 'moodle/competency:competencymanage')); + + if (empty($contexts)) { + throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', ''); + } + + // OK - all set. + list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED); + return competency_framework::count_records_select("contextid $insql", $inparams); + } + + /** + * Fetches all the relevant contexts. + * + * Note: This currently only supports system, category and user contexts. However user contexts + * behave a bit differently and will fallback on the system context. This is what makes the most + * sense because a user context does not have descendants, and only has system as a parent. + * + * @param context $context The context to start from. + * @param string $includes Defines what other contexts to find. + * Accepted values are: + * - children: All descendants + * - parents: All parents, grand parents, etc... + * - self: Context passed only. + * @param array $hasanycapability Array of capabilities passed to {@link has_any_capability()} in each context. + * @return context[] An array of contexts where keys are context IDs. + */ + public static function get_related_contexts($context, $includes, array $hasanycapability = null) { + global $DB; + static::require_enabled(); + + if (!in_array($includes, array('children', 'parents', 'self'))) { + throw new coding_exception('Invalid parameter value for \'includes\'.'); + } + + // If context user swap it for the context_system. + if ($context->contextlevel == CONTEXT_USER) { + $context = context_system::instance(); + } + + $contexts = array($context->id => $context); + + if ($includes == 'children') { + $params = array('coursecatlevel' => CONTEXT_COURSECAT, 'path' => $context->path . '/%'); + $pathlike = $DB->sql_like('path', ':path'); + $sql = "contextlevel = :coursecatlevel AND $pathlike"; + $rs = $DB->get_recordset_select('context', $sql, $params); + foreach ($rs as $record) { + $ctxid = $record->id; + context_helper::preload_from_record($record); + $contexts[$ctxid] = context::instance_by_id($ctxid); + } + $rs->close(); + + } else if ($includes == 'parents') { + $children = $context->get_parent_contexts(); + foreach ($children as $ctx) { + $contexts[$ctx->id] = $ctx; + } + } + + // Filter according to the capabilities required. + if (!empty($hasanycapability)) { + foreach ($contexts as $key => $ctx) { + if (!has_any_capability($hasanycapability, $ctx)) { + unset($contexts[$key]); + } + } + } + + return $contexts; + } + + /** + * Count all the courses using a competency. + * + * @param int $competencyid The id of the competency to check. + * @return int + */ + public static function count_courses_using_competency($competencyid) { + static::require_enabled(); + + // OK - all set. + $courses = course_competency::list_courses_min($competencyid); + $count = 0; + + // Now check permissions on each course. + foreach ($courses as $course) { + if (!self::validate_course($course, false)) { + continue; + } + + $context = context_course::instance($course->id); + $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); + if (!has_any_capability($capabilities, $context)) { + continue; + } + + $count++; + } + + return $count; + } + + /** + * List all the courses modules using a competency in a course. + * + * @param int $competencyid The id of the competency to check. + * @param int $courseid The id of the course to check. + * @return array[int] Array of course modules ids. + */ + public static function list_course_modules_using_competency($competencyid, $courseid) { + static::require_enabled(); + + $result = array(); + self::validate_course($courseid); + + $coursecontext = context_course::instance($courseid); + + // We will not check each module - course permissions should be enough. + $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); + if (!has_any_capability($capabilities, $coursecontext)) { + throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', 'nopermissions', ''); + } + + $cmlist = course_module_competency::list_course_modules($competencyid, $courseid); + foreach ($cmlist as $cmid) { + if (self::validate_course_module($cmid, false)) { + array_push($result, $cmid); + } + } + + return $result; + } + + /** + * List all the competencies linked to a course module. + * + * @param mixed $cmorid The course module, or its ID. + * @return array[competency] Array of competency records. + */ + public static function list_course_module_competencies_in_course_module($cmorid) { + static::require_enabled(); + $cm = $cmorid; + if (!is_object($cmorid)) { + $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST); + } + + // Check the user have access to the course module. + self::validate_course_module($cm); + $context = context_module::instance($cm->id); + + $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); + if (!has_any_capability($capabilities, $context)) { + throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', ''); + } + + $result = array(); + + $cmclist = course_module_competency::list_course_module_competencies($cm->id); + foreach ($cmclist as $id => $cmc) { + array_push($result, $cmc); + } + + return $result; + } + + /** + * List all the courses using a competency. + * + * @param int $competencyid The id of the competency to check. + * @return array[stdClass] Array of stdClass containing id and shortname. + */ + public static function list_courses_using_competency($competencyid) { + static::require_enabled(); + + // OK - all set. + $courses = course_competency::list_courses($competencyid); + $result = array(); + + // Now check permissions on each course. + foreach ($courses as $id => $course) { + $context = context_course::instance($course->id); + $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); + if (!has_any_capability($capabilities, $context)) { + unset($courses[$id]); + continue; + } + if (!self::validate_course($course, false)) { + unset($courses[$id]); + continue; + } + array_push($result, $course); + } + + return $result; + } + + /** + * Count the proficient competencies in a course for one user. + * + * @param int $courseid The id of the course to check. + * @param int $userid The id of the user to check. + * @return int + */ + public static function count_proficient_competencies_in_course_for_user($courseid, $userid) { + static::require_enabled(); + // Check the user have access to the course. + self::validate_course($courseid); + + // First we do a permissions check. + $context = context_course::instance($courseid); + + $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); + if (!has_any_capability($capabilities, $context)) { + throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', ''); + } + + // OK - all set. + return user_competency_course::count_proficient_competencies($courseid, $userid); + } + + /** + * Count all the competencies in a course. + * + * @param int $courseid The id of the course to check. + * @return int + */ + public static function count_competencies_in_course($courseid) { + static::require_enabled(); + // Check the user have access to the course. + self::validate_course($courseid); + + // First we do a permissions check. + $context = context_course::instance($courseid); + + $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); + if (!has_any_capability($capabilities, $context)) { + throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', ''); + } + + // OK - all set. + return course_competency::count_competencies($courseid); + } + + /** + * List the competencies associated to a course. + * + * @param mixed $courseorid The course, or its ID. + * @return array( array( + * 'competency' => \core_competency\competency, + * 'coursecompetency' => \core_competency\course_competency + * )) + */ + public static function list_course_competencies($courseorid) { + static::require_enabled(); + $course = $courseorid; + if (!is_object($courseorid)) { + $course = get_course($courseorid); + } + + // Check the user have access to the course. + self::validate_course($course); + $context = context_course::instance($course->id); + + $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); + if (!has_any_capability($capabilities, $context)) { + throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', ''); + } + + $result = array(); + + // TODO We could improve the performance of this into one single query. + $coursecompetencies = course_competency::list_course_competencies($course->id); + $competencies = course_competency::list_competencies($course->id); + + // Build the return values. + foreach ($coursecompetencies as $key => $coursecompetency) { + $result[] = array( + 'competency' => $competencies[$coursecompetency->get_competencyid()], + 'coursecompetency' => $coursecompetency + ); + } + + return $result; + } + + /** + * Get a user competency. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @return user_competency + */ + public static function get_user_competency($userid, $competencyid) { + static::require_enabled(); + $existing = user_competency::get_multiple($userid, array($competencyid)); + $uc = array_pop($existing); + + if (!$uc) { + $uc = user_competency::create_relation($userid, $competencyid); + $uc->create(); + } + + if (!$uc->can_read()) { + throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview', + 'nopermissions', ''); + } + return $uc; + } + + /** + * Get a user competency by ID. + * + * @param int $usercompetencyid The user competency ID. + * @return user_competency + */ + public static function get_user_competency_by_id($usercompetencyid) { + static::require_enabled(); + $uc = new user_competency($usercompetencyid); + if (!$uc->can_read()) { + throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview', + 'nopermissions', ''); + } + return $uc; + } + + /** + * List the competencies associated to a course module. + * + * @param mixed $cmorid The course module, or its ID. + * @return array( array( + * 'competency' => \core_competency\competency, + * 'coursemodulecompetency' => \core_competency\course_module_competency + * )) + */ + public static function list_course_module_competencies($cmorid) { + static::require_enabled(); + $cm = $cmorid; + if (!is_object($cmorid)) { + $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST); + } + + // Check the user have access to the course module. + self::validate_course_module($cm); + $context = context_module::instance($cm->id); + + $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); + if (!has_any_capability($capabilities, $context)) { + throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', ''); + } + + $result = array(); + + // TODO We could improve the performance of this into one single query. + $coursemodulecompetencies = course_competency::list_course_module_competencies($cm->id); + $competencies = course_module_competency::list_competencies($cm->id); + + // Build the return values. + foreach ($coursemodulecompetencies as $key => $coursemodulecompetency) { + $result[] = array( + 'competency' => $competencies[$coursemodulecompetency->get_competencyid()], + 'coursemodulecompetency' => $coursemodulecompetency + ); + } + + return $result; + } + + /** + * Get a user competency in a course. + * + * @param int $courseid The id of the course to check. + * @param int $userid The id of the course to check. + * @param int $competencyid The id of the competency. + * @return user_competency_course + */ + public static function get_user_competency_in_course($courseid, $userid, $competencyid) { + static::require_enabled(); + // First we do a permissions check. + $context = context_course::instance($courseid); + + $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); + if (!has_any_capability($capabilities, $context)) { + throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', ''); + } else if (!user_competency::can_read_user_in_course($userid, $courseid)) { + throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); + } + + // This will throw an exception if the competency does not belong to the course. + $competency = course_competency::get_competency($courseid, $competencyid); + + $params = array('courseid' => $courseid, 'userid' => $userid, 'competencyid' => $competencyid); + $exists = user_competency_course::get_record($params); + // Create missing. + if ($exists) { + $ucc = $exists; + } else { + $ucc = user_competency_course::create_relation($userid, $competency->get_id(), $courseid); + $ucc->create(); + } + + return $ucc; + } + + /** + * List all the user competencies in a course. + * + * @param int $courseid The id of the course to check. + * @param int $userid The id of the course to check. + * @return array of user_competency_course objects + */ + public static function list_user_competencies_in_course($courseid, $userid) { + static::require_enabled(); + // First we do a permissions check. + $context = context_course::instance($courseid); + $onlyvisible = 1; + + $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); + if (!has_any_capability($capabilities, $context)) { + throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', ''); + } else if (!user_competency::can_read_user_in_course($userid, $courseid)) { + throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); + } + + // OK - all set. + $competencylist = course_competency::list_competencies($courseid, false); + + $existing = user_competency_course::get_multiple($userid, $courseid, $competencylist); + // Create missing. + $orderedusercompetencycourses = array(); + + $somemissing = false; + foreach ($competencylist as $coursecompetency) { + $found = false; + foreach ($existing as $usercompetencycourse) { + if ($usercompetencycourse->get_competencyid() == $coursecompetency->get_id()) { + $found = true; + $orderedusercompetencycourses[$usercompetencycourse->get_id()] = $usercompetencycourse; + break; + } + } + if (!$found) { + $ucc = user_competency_course::create_relation($userid, $coursecompetency->get_id(), $courseid); + $ucc->create(); + $orderedusercompetencycourses[$ucc->get_id()] = $ucc; + } + } + + return $orderedusercompetencycourses; + } + + /** + * List the user competencies to review. + * + * The method returns values in this format: + * + * array( + * 'competencies' => array( + * (stdClass)( + * 'usercompetency' => (user_competency), + * 'competency' => (competency), + * 'user' => (user) + * ) + * ), + * 'count' => (int) + * ) + * + * @param int $skip The number of records to skip. + * @param int $limit The number of results to return. + * @param int $userid The user we're getting the competencies to review for. + * @return array Containing the keys 'count', and 'competencies'. The 'competencies' key contains an object + * which contains 'competency', 'usercompetency' and 'user'. + */ + public static function list_user_competencies_to_review($skip = 0, $limit = 50, $userid = null) { + global $DB, $USER; + static::require_enabled(); + if ($userid === null) { + $userid = $USER->id; + } + + $capability = 'moodle/competency:usercompetencyreview'; + $ucfields = user_competency::get_sql_fields('uc'); + $compfields = competency::get_sql_fields('c'); + $usercols = array('id') + get_user_fieldnames(); + $userfields = array(); + foreach ($usercols as $field) { + $userfields[] = "u." . $field . " AS usr_" . $field; + } + $userfields = implode(',', $userfields); + + $select = "SELECT $ucfields, $compfields, $userfields"; + $countselect = "SELECT COUNT('x')"; + $sql = " FROM {" . user_competency::TABLE . "} uc + JOIN {" . competency::TABLE . "} c + ON c.id = uc.competencyid + JOIN {user} u + ON u.id = uc.userid + WHERE (uc.status = :waitingforreview + OR (uc.status = :inreview AND uc.reviewerid = :reviewerid))"; + $ordersql = " ORDER BY c.shortname ASC"; + $params = array( + 'inreview' => user_competency::STATUS_IN_REVIEW, + 'reviewerid' => $userid, + 'waitingforreview' => user_competency::STATUS_WAITING_FOR_REVIEW, + ); + $countsql = $countselect . $sql; + + // Primary check to avoid the hard work of getting the users in which the user has permission. + $count = $DB->count_records_sql($countselect . $sql, $params); + if ($count < 1) { + return array('count' => 0, 'competencies' => array()); + } + + // TODO MDL-52243 Use core function. + list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql( + $capability, $userid, SQL_PARAMS_NAMED); + $params += $inparams; + $countsql = $countselect . $sql . " AND uc.userid $insql"; + $getsql = $select . $sql . " AND uc.userid $insql " . $ordersql; + + // Extracting the results. + $competencies = array(); + $records = $DB->get_recordset_sql($getsql, $params, $skip, $limit); + foreach ($records as $record) { + $objects = (object) array( + 'usercompetency' => new user_competency(0, user_competency::extract_record($record)), + 'competency' => new competency(0, competency::extract_record($record)), + 'user' => persistent::extract_record($record, 'usr_'), + ); + $competencies[] = $objects; + } + $records->close(); + + return array( + 'count' => $DB->count_records_sql($countsql, $params), + 'competencies' => $competencies + ); + } + + /** + * Add a competency to this course module. + * + * @param mixed $cmorid The course module, or id of the course module + * @param int $competencyid The id of the competency + * @return bool + */ + public static function add_competency_to_course_module($cmorid, $competencyid) { + static::require_enabled(); + $cm = $cmorid; + if (!is_object($cmorid)) { + $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST); + } + + // Check the user have access to the course module. + self::validate_course_module($cm); + + // First we do a permissions check. + $context = context_module::instance($cm->id); + + require_capability('moodle/competency:coursecompetencymanage', $context); + + // Check that the competency belongs to the course. + $exists = course_competency::get_records(array('courseid' => $cm->course, 'competencyid' => $competencyid)); + if (!$exists) { + throw new coding_exception('Cannot add a competency to a module if it does not belong to the course'); + } + + $record = new stdClass(); + $record->cmid = $cm->id; + $record->competencyid = $competencyid; + + $coursemodulecompetency = new course_module_competency(); + $exists = $coursemodulecompetency->get_records(array('cmid' => $cm->id, 'competencyid' => $competencyid)); + if (!$exists) { + $coursemodulecompetency->from_record($record); + if ($coursemodulecompetency->create()) { + return true; + } + } + return false; + } + + /** + * Remove a competency from this course module. + * + * @param mixed $cmorid The course module, or id of the course module + * @param int $competencyid The id of the competency + * @return bool + */ + public static function remove_competency_from_course_module($cmorid, $competencyid) { + static::require_enabled(); + $cm = $cmorid; + if (!is_object($cmorid)) { + $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST); + } + // Check the user have access to the course module. + self::validate_course_module($cm); + + // First we do a permissions check. + $context = context_module::instance($cm->id); + + require_capability('moodle/competency:coursecompetencymanage', $context); + + $record = new stdClass(); + $record->cmid = $cm->id; + $record->competencyid = $competencyid; + + $competency = new competency($competencyid); + $exists = course_module_competency::get_record(array('cmid' => $cm->id, 'competencyid' => $competencyid)); + if ($exists) { + return $exists->delete(); + } + return false; + } + + /** + * Move the course module competency up or down in the display list. + * + * Requires moodle/competency:coursecompetencymanage capability at the course module context. + * + * @param mixed $cmorid The course module, or id of the course module + * @param int $competencyidfrom The id of the competency we are moving. + * @param int $competencyidto The id of the competency we are moving to. + * @return boolean + */ + public static function reorder_course_module_competency($cmorid, $competencyidfrom, $competencyidto) { + static::require_enabled(); + $cm = $cmorid; + if (!is_object($cmorid)) { + $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST); + } + // Check the user have access to the course module. + self::validate_course_module($cm); + + // First we do a permissions check. + $context = context_module::instance($cm->id); + + require_capability('moodle/competency:coursecompetencymanage', $context); + + $down = true; + $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidfrom)); + if (count($matches) == 0) { + throw new coding_exception('The link does not exist'); + } + + $competencyfrom = array_pop($matches); + $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidto)); + if (count($matches) == 0) { + throw new coding_exception('The link does not exist'); + } + + $competencyto = array_pop($matches); + + $all = course_module_competency::get_records(array('cmid' => $cm->id), 'sortorder', 'ASC', 0, 0); + + if ($competencyfrom->get_sortorder() > $competencyto->get_sortorder()) { + // We are moving up, so put it before the "to" item. + $down = false; + } + + foreach ($all as $id => $coursemodulecompetency) { + $sort = $coursemodulecompetency->get_sortorder(); + if ($down && $sort > $competencyfrom->get_sortorder() && $sort <= $competencyto->get_sortorder()) { + $coursemodulecompetency->set_sortorder($coursemodulecompetency->get_sortorder() - 1); + $coursemodulecompetency->update(); + } else if (!$down && $sort >= $competencyto->get_sortorder() && $sort < $competencyfrom->get_sortorder()) { + $coursemodulecompetency->set_sortorder($coursemodulecompetency->get_sortorder() + 1); + $coursemodulecompetency->update(); + } + } + $competencyfrom->set_sortorder($competencyto->get_sortorder()); + return $competencyfrom->update(); + } + + /** + * Update ruleoutcome value for a course module competency. + * + * @param int|course_module_competency $coursemodulecompetencyorid The course_module_competency, or its ID. + * @param int $ruleoutcome The value of ruleoutcome. + * @return bool True on success. + */ + public static function set_course_module_competency_ruleoutcome($coursemodulecompetencyorid, $ruleoutcome) { + static::require_enabled(); + $coursemodulecompetency = $coursemodulecompetencyorid; + if (!is_object($coursemodulecompetency)) { + $coursemodulecompetency = new course_module_competency($coursemodulecompetencyorid); + } + + $cm = get_coursemodule_from_id('', $coursemodulecompetency->get_cmid(), 0, true, MUST_EXIST); + + self::validate_course_module($cm); + $context = context_module::instance($cm->id); + + require_capability('moodle/competency:coursecompetencymanage', $context); + + $coursemodulecompetency->set_ruleoutcome($ruleoutcome); + return $coursemodulecompetency->update(); + } + + /** + * Add a competency to this course. + * + * @param int $courseid The id of the course + * @param int $competencyid The id of the competency + * @return bool + */ + public static function add_competency_to_course($courseid, $competencyid) { + static::require_enabled(); + // Check the user have access to the course. + self::validate_course($courseid); + + // First we do a permissions check. + $context = context_course::instance($courseid); + + require_capability('moodle/competency:coursecompetencymanage', $context); + + $record = new stdClass(); + $record->courseid = $courseid; + $record->competencyid = $competencyid; + + $competency = new competency($competencyid); + + // Can not add a competency that belong to a hidden framework. + if ($competency->get_framework()->get_visible() == false) { + throw new coding_exception('A competency belonging to hidden framework can not be linked to course'); + } + + $coursecompetency = new course_competency(); + $exists = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyid)); + if (!$exists) { + $coursecompetency->from_record($record); + if ($coursecompetency->create()) { + return true; + } + } + return false; + } + + /** + * Remove a competency from this course. + * + * @param int $courseid The id of the course + * @param int $competencyid The id of the competency + * @return bool + */ + public static function remove_competency_from_course($courseid, $competencyid) { + static::require_enabled(); + // Check the user have access to the course. + self::validate_course($courseid); + + // First we do a permissions check. + $context = context_course::instance($courseid); + + require_capability('moodle/competency:coursecompetencymanage', $context); + + $record = new stdClass(); + $record->courseid = $courseid; + $record->competencyid = $competencyid; + + $competency = new competency($competencyid); + $coursecompetency = new course_competency(); + $exists = $coursecompetency->get_record(array('courseid' => $courseid, 'competencyid' => $competencyid)); + if ($exists) { + // Delete all course_module_competencies for this competency in this course. + $cmcs = course_module_competency::list_course_module_competencies($competencyid, $courseid); + foreach ($cmcs as $cmc) { + $cmc->delete(); + } + return $exists->delete(); + } + return false; + } + + /** + * Move the course competency up or down in the display list. + * + * Requires moodle/competency:coursecompetencymanage capability at the course context. + * + * @param int $courseid The course + * @param int $competencyidfrom The id of the competency we are moving. + * @param int $competencyidto The id of the competency we are moving to. + * @return boolean + */ + public static function reorder_course_competency($courseid, $competencyidfrom, $competencyidto) { + static::require_enabled(); + // Check the user have access to the course. + self::validate_course($courseid); + + // First we do a permissions check. + $context = context_course::instance($courseid); + + require_capability('moodle/competency:coursecompetencymanage', $context); + + $down = true; + $coursecompetency = new course_competency(); + $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidfrom)); + if (count($matches) == 0) { + throw new coding_exception('The link does not exist'); + } + + $competencyfrom = array_pop($matches); + $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidto)); + if (count($matches) == 0) { + throw new coding_exception('The link does not exist'); + } + + $competencyto = array_pop($matches); + + $all = $coursecompetency->get_records(array('courseid' => $courseid), 'sortorder', 'ASC', 0, 0); + + if ($competencyfrom->get_sortorder() > $competencyto->get_sortorder()) { + // We are moving up, so put it before the "to" item. + $down = false; + } + + foreach ($all as $id => $coursecompetency) { + $sort = $coursecompetency->get_sortorder(); + if ($down && $sort > $competencyfrom->get_sortorder() && $sort <= $competencyto->get_sortorder()) { + $coursecompetency->set_sortorder($coursecompetency->get_sortorder() - 1); + $coursecompetency->update(); + } else if (!$down && $sort >= $competencyto->get_sortorder() && $sort < $competencyfrom->get_sortorder()) { + $coursecompetency->set_sortorder($coursecompetency->get_sortorder() + 1); + $coursecompetency->update(); + } + } + $competencyfrom->set_sortorder($competencyto->get_sortorder()); + return $competencyfrom->update(); + } + + /** + * Update ruleoutcome value for a course competency. + * + * @param int|course_competency $coursecompetencyorid The course_competency, or its ID. + * @param int $ruleoutcome The value of ruleoutcome. + * @return bool True on success. + */ + public static function set_course_competency_ruleoutcome($coursecompetencyorid, $ruleoutcome) { + static::require_enabled(); + $coursecompetency = $coursecompetencyorid; + if (!is_object($coursecompetency)) { + $coursecompetency = new course_competency($coursecompetencyorid); + } + + $courseid = $coursecompetency->get_courseid(); + self::validate_course($courseid); + $coursecontext = context_course::instance($courseid); + + require_capability('moodle/competency:coursecompetencymanage', $coursecontext); + + $coursecompetency->set_ruleoutcome($ruleoutcome); + return $coursecompetency->update(); + } + + /** + * Create a learning plan template from a record containing all the data for the class. + * + * Requires moodle/competency:templatemanage capability. + * + * @param stdClass $record Record containing all the data for an instance of the class. + * @return template + */ + public static function create_template(stdClass $record) { + static::require_enabled(); + $template = new template(0, $record); + + // First we do a permissions check. + if (!$template->can_manage()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', + 'nopermissions', ''); + } + + // OK - all set. + $template = $template->create(); + + // Trigger a template created event. + \core\event\competency_template_created::create_from_template($template)->trigger(); + + return $template; + } + + /** + * Duplicate a learning plan template. + * + * Requires moodle/competency:templatemanage capability at the template context. + * + * @param int $id the template id. + * @return template + */ + public static function duplicate_template($id) { + static::require_enabled(); + $template = new template($id); + + // First we do a permissions check. + if (!$template->can_manage()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', + 'nopermissions', ''); + } + + // OK - all set. + $competencies = template_competency::list_competencies($id, false); + + // Adding the suffix copy. + $template->set_shortname(get_string('duplicateditemname', 'core_competency', $template->get_shortname())); + $template->set_id(0); + + $duplicatedtemplate = $template->create(); + + // Associate each competency for the duplicated template. + foreach ($competencies as $competency) { + self::add_competency_to_template($duplicatedtemplate->get_id(), $competency->get_id()); + } + + // Trigger a template created event. + \core\event\competency_template_created::create_from_template($duplicatedtemplate)->trigger(); + + return $duplicatedtemplate; + } + + /** + * Delete a learning plan template by id. + * If the learning plan template has associated cohorts they will be deleted. + * + * Requires moodle/competency:templatemanage capability. + * + * @param int $id The record to delete. + * @param boolean $deleteplans True to delete plans associaated to template, false to unlink them. + * @return boolean + */ + public static function delete_template($id, $deleteplans = true) { + global $DB; + static::require_enabled(); + $template = new template($id); + + // First we do a permissions check. + if (!$template->can_manage()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', + 'nopermissions', ''); + } + + $transaction = $DB->start_delegated_transaction(); + $success = true; + + // Check if there are cohorts associated. + $templatecohorts = template_cohort::get_relations_by_templateid($template->get_id()); + foreach ($templatecohorts as $templatecohort) { + $success = $templatecohort->delete(); + if (!$success) { + break; + } + } + + // Still OK, delete or unlink the plans from the template. + if ($success) { + $plans = plan::get_records(array('templateid' => $template->get_id())); + foreach ($plans as $plan) { + $success = $deleteplans ? self::delete_plan($plan->get_id()) : self::unlink_plan_from_template($plan); + if (!$success) { + break; + } + } + } + + // Still OK, delete the template comptencies. + if ($success) { + $success = template_competency::delete_by_templateid($template->get_id()); + } + + // OK - all set. + if ($success) { + // Create a template deleted event. + $event = \core\event\competency_template_deleted::create_from_template($template); + + $success = $template->delete(); + } + + if ($success) { + // Trigger a template deleted event. + $event->trigger(); + + // Commit the transaction. + $transaction->allow_commit(); + } else { + $transaction->rollback(new moodle_exception('Error while deleting the template.')); + } + + return $success; + } + + /** + * Update the details for a learning plan template. + * + * Requires moodle/competency:templatemanage capability. + * + * @param stdClass $record The new details for the template. Note - must contain an id that points to the template to update. + * @return boolean + */ + public static function update_template($record) { + global $DB; + static::require_enabled(); + $template = new template($record->id); + + // First we do a permissions check. + if (!$template->can_manage()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', + 'nopermissions', ''); + + } else if (isset($record->contextid) && $record->contextid != $template->get_contextid()) { + // We can never change the context of a template. + throw new coding_exception('Changing the context of an existing tempalte is forbidden.'); + + } + + $updateplans = false; + $before = $template->to_record(); + + $template->from_record($record); + $after = $template->to_record(); + + // Should we update the related plans? + if ($before->duedate != $after->duedate || + $before->shortname != $after->shortname || + $before->description != $after->description || + $before->descriptionformat != $after->descriptionformat) { + $updateplans = true; + } + + $transaction = $DB->start_delegated_transaction(); + $success = $template->update(); + + if (!$success) { + $transaction->rollback(new moodle_exception('Error while updating the template.')); + return $success; + } + + // Trigger a template updated event. + \core\event\competency_template_updated::create_from_template($template)->trigger(); + + if ($updateplans) { + plan::update_multiple_from_template($template); + } + + $transaction->allow_commit(); + + return $success; + } + + /** + * Read a the details for a single learning plan template and return a record. + * + * Requires moodle/competency:templateview capability at the system context. + * + * @param int $id The id of the template to read. + * @return template + */ + public static function read_template($id) { + static::require_enabled(); + $template = new template($id); + $context = $template->get_context(); + + // First we do a permissions check. + if (!$template->can_read()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + // OK - all set. + return $template; + } + + /** + * Perform a search based on the provided filters and return a paginated list of records. + * + * Requires moodle/competency:templateview 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) + * @param context $context The parent context of the frameworks. + * @param string $includes Defines what other contexts to fetch frameworks from. + * Accepted values are: + * - children: All descendants + * - parents: All parents, grand parents, etc... + * - self: Context passed only. + * @param bool $onlyvisible If should list only visible templates + * @return array of competency_framework + */ + public static function list_templates($sort, $order, $skip, $limit, $context, $includes = 'children', $onlyvisible = false) { + global $DB; + static::require_enabled(); + + // Get all the relevant contexts. + $contexts = self::get_related_contexts($context, $includes, + array('moodle/competency:templateview', 'moodle/competency:templatemanage')); + + // First we do a permissions check. + if (empty($contexts)) { + throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', ''); + } + + // Make the order by. + $orderby = ''; + if (!empty($sort)) { + $orderby = $sort . ' ' . $order; + } + + // OK - all set. + $template = new template(); + list($insql, $params) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED); + $select = "contextid $insql"; + + if ($onlyvisible) { + $select .= " AND visible = :visible"; + $params['visible'] = 1; + } + return $template->get_records_select($select, $params, $orderby, '*', $skip, $limit); + } + + /** + * Perform a search based on the provided filters and return how many results there are. + * + * Requires moodle/competency:templateview capability at the system context. + * + * @param context $context The parent context of the frameworks. + * @param string $includes Defines what other contexts to fetch frameworks from. + * Accepted values are: + * - children: All descendants + * - parents: All parents, grand parents, etc... + * - self: Context passed only. + * @return int + */ + public static function count_templates($context, $includes) { + global $DB; + static::require_enabled(); + + // First we do a permissions check. + $contexts = self::get_related_contexts($context, $includes, + array('moodle/competency:templateview', 'moodle/competency:templatemanage')); + + if (empty($contexts)) { + throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', ''); + } + + // OK - all set. + $template = new template(); + list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED); + return $template->count_records_select("contextid $insql", $inparams); + } + + /** + * Count all the templates using a competency. + * + * @param int $competencyid The id of the competency to check. + * @return int + */ + public static function count_templates_using_competency($competencyid) { + static::require_enabled(); + // First we do a permissions check. + $context = context_system::instance(); + $onlyvisible = 1; + + $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage'); + if (!has_any_capability($capabilities, $context)) { + throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', ''); + } + + if (has_capability('moodle/competency:templatemanage', $context)) { + $onlyvisible = 0; + } + + // OK - all set. + return template_competency::count_templates($competencyid, $onlyvisible); + } + + /** + * List all the learning plan templatesd using a competency. + * + * @param int $competencyid The id of the competency to check. + * @return array[stdClass] Array of stdClass containing id and shortname. + */ + public static function list_templates_using_competency($competencyid) { + static::require_enabled(); + // First we do a permissions check. + $context = context_system::instance(); + $onlyvisible = 1; + + $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage'); + if (!has_any_capability($capabilities, $context)) { + throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', ''); + } + + if (has_capability('moodle/competency:templatemanage', $context)) { + $onlyvisible = 0; + } + + // OK - all set. + return template_competency::list_templates($competencyid, $onlyvisible); + + } + + /** + * Count all the competencies in a learning plan template. + * + * @param template|int $templateorid The template or its ID. + * @return int + */ + public static function count_competencies_in_template($templateorid) { + static::require_enabled(); + // First we do a permissions check. + $template = $templateorid; + if (!is_object($template)) { + $template = new template($template); + } + + if (!$template->can_read()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + // OK - all set. + return template_competency::count_competencies($template->get_id()); + } + + /** + * Count all the competencies in a learning plan template with no linked courses. + * + * @param template|int $templateorid The template or its ID. + * @return int + */ + public static function count_competencies_in_template_with_no_courses($templateorid) { + // First we do a permissions check. + $template = $templateorid; + if (!is_object($template)) { + $template = new template($template); + } + + if (!$template->can_read()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + // OK - all set. + return template_competency::count_competencies_with_no_courses($template->get_id()); + } + + /** + * List all the competencies in a template. + * + * @param template|int $templateorid The template or its ID. + * @return array of competencies + */ + public static function list_competencies_in_template($templateorid) { + static::require_enabled(); + // First we do a permissions check. + $template = $templateorid; + if (!is_object($template)) { + $template = new template($template); + } + + if (!$template->can_read()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + // OK - all set. + return template_competency::list_competencies($template->get_id()); + } + + /** + * Add a competency to this template. + * + * @param int $templateid The id of the template + * @param int $competencyid The id of the competency + * @return bool + */ + public static function add_competency_to_template($templateid, $competencyid) { + static::require_enabled(); + // First we do a permissions check. + $template = new template($templateid); + if (!$template->can_manage()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', + 'nopermissions', ''); + } + + $record = new stdClass(); + $record->templateid = $templateid; + $record->competencyid = $competencyid; + + $competency = new competency($competencyid); + + // Can not add a competency that belong to a hidden framework. + if ($competency->get_framework()->get_visible() == false) { + throw new coding_exception('A competency belonging to hidden framework can not be added'); + } + + $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid)); + if (!$exists) { + $templatecompetency = new template_competency(0, $record); + $templatecompetency->create(); + return true; + } + return false; + } + + /** + * Remove a competency from this template. + * + * @param int $templateid The id of the template + * @param int $competencyid The id of the competency + * @return bool + */ + public static function remove_competency_from_template($templateid, $competencyid) { + static::require_enabled(); + // First we do a permissions check. + $template = new template($templateid); + if (!$template->can_manage()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', + 'nopermissions', ''); + } + + $record = new stdClass(); + $record->templateid = $templateid; + $record->competencyid = $competencyid; + + $competency = new competency($competencyid); + + $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid)); + if ($exists) { + $link = array_pop($exists); + return $link->delete(); + } + return false; + } + + /** + * Move the template competency up or down in the display list. + * + * Requires moodle/competency:templatemanage capability at the system context. + * + * @param int $templateid The template id + * @param int $competencyidfrom The id of the competency we are moving. + * @param int $competencyidto The id of the competency we are moving to. + * @return boolean + */ + public static function reorder_template_competency($templateid, $competencyidfrom, $competencyidto) { + static::require_enabled(); + // First we do a permissions check. + $context = context_system::instance(); + + require_capability('moodle/competency:templatemanage', $context); + + $down = true; + $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidfrom)); + if (count($matches) == 0) { + throw new coding_exception('The link does not exist'); + } + + $competencyfrom = array_pop($matches); + $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidto)); + if (count($matches) == 0) { + throw new coding_exception('The link does not exist'); + } + + $competencyto = array_pop($matches); + + $all = template_competency::get_records(array('templateid' => $templateid), 'sortorder', 'ASC', 0, 0); + + if ($competencyfrom->get_sortorder() > $competencyto->get_sortorder()) { + // We are moving up, so put it before the "to" item. + $down = false; + } + + foreach ($all as $id => $templatecompetency) { + $sort = $templatecompetency->get_sortorder(); + if ($down && $sort > $competencyfrom->get_sortorder() && $sort <= $competencyto->get_sortorder()) { + $templatecompetency->set_sortorder($templatecompetency->get_sortorder() - 1); + $templatecompetency->update(); + } else if (!$down && $sort >= $competencyto->get_sortorder() && $sort < $competencyfrom->get_sortorder()) { + $templatecompetency->set_sortorder($templatecompetency->get_sortorder() + 1); + $templatecompetency->update(); + } + } + $competencyfrom->set_sortorder($competencyto->get_sortorder()); + return $competencyfrom->update(); + } + + /** + * Create a relation between a template and a cohort. + * + * This silently ignores when the relation already existed. + * + * @param template|int $templateorid The template or its ID. + * @param stdClass|int $cohortorid The cohort ot its ID. + * @return template_cohort + */ + public static function create_template_cohort($templateorid, $cohortorid) { + global $DB; + static::require_enabled(); + + $template = $templateorid; + if (!is_object($template)) { + $template = new template($template); + } + require_capability('moodle/competency:templatemanage', $template->get_context()); + + $cohort = $cohortorid; + if (!is_object($cohort)) { + $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST); + } + + // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context. + $cohortcontext = context::instance_by_id($cohort->contextid); + if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) { + throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', ''); + } + + $tplcohort = template_cohort::get_relation($template->get_id(), $cohort->id); + if (!$tplcohort->get_id()) { + $tplcohort->create(); + } + + return $tplcohort; + } + + /** + * Remove a relation between a template and a cohort. + * + * @param template|int $templateorid The template or its ID. + * @param stdClass|int $cohortorid The cohort ot its ID. + * @return boolean True on success or when the relation did not exist. + */ + public static function delete_template_cohort($templateorid, $cohortorid) { + global $DB; + static::require_enabled(); + + $template = $templateorid; + if (!is_object($template)) { + $template = new template($template); + } + require_capability('moodle/competency:templatemanage', $template->get_context()); + + $cohort = $cohortorid; + if (!is_object($cohort)) { + $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST); + } + + $tplcohort = template_cohort::get_relation($template->get_id(), $cohort->id); + if (!$tplcohort->get_id()) { + return true; + } + + return $tplcohort->delete(); + } + + /** + * Lists user plans. + * + * @param int $userid + * @return \core_competency\plan[] + */ + public static function list_user_plans($userid) { + global $DB, $USER; + static::require_enabled(); + $select = 'userid = :userid'; + $params = array('userid' => $userid); + $context = context_user::instance($userid); + + // Check that we can read something here. + if (!plan::can_read_user($userid) && !plan::can_read_user_draft($userid)) { + throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', ''); + } + + // The user cannot view the drafts. + if (!plan::can_read_user_draft($userid)) { + list($insql, $inparams) = $DB->get_in_or_equal(plan::get_draft_statuses(), SQL_PARAMS_NAMED, 'param', false); + $select .= " AND status $insql"; + $params += $inparams; + } + // The user cannot view the non-drafts. + if (!plan::can_read_user($userid)) { + list($insql, $inparams) = $DB->get_in_or_equal(array(plan::STATUS_ACTIVE, plan::STATUS_COMPLETE), + SQL_PARAMS_NAMED, 'param', false); + $select .= " AND status $insql"; + $params += $inparams; + } + + return plan::get_records_select($select, $params, 'name ASC'); + } + + /** + * List the plans to review. + * + * The method returns values in this format: + * + * array( + * 'plans' => array( + * (stdClass)( + * 'plan' => (plan), + * 'template' => (template), + * 'owner' => (stdClass) + * ) + * ), + * 'count' => (int) + * ) + * + * @param int $skip The number of records to skip. + * @param int $limit The number of results to return. + * @param int $userid The user we're getting the plans to review for. + * @return array Containing the keys 'count', and 'plans'. The 'plans' key contains an object + * which contains 'plan', 'template' and 'owner'. + */ + public static function list_plans_to_review($skip = 0, $limit = 100, $userid = null) { + global $DB, $USER; + static::require_enabled(); + + if ($userid === null) { + $userid = $USER->id; + } + + $planfields = plan::get_sql_fields('p'); + $tplfields = template::get_sql_fields('t'); + $usercols = array('id') + get_user_fieldnames(); + $userfields = array(); + foreach ($usercols as $field) { + $userfields[] = "u." . $field . " AS usr_" . $field; + } + $userfields = implode(',', $userfields); + + $select = "SELECT $planfields, $tplfields, $userfields"; + $countselect = "SELECT COUNT('x')"; + + $sql = " FROM {" . plan::TABLE . "} p + JOIN {user} u + ON u.id = p.userid + LEFT JOIN {" . template::TABLE . "} t + ON t.id = p.templateid + WHERE (p.status = :waitingforreview + OR (p.status = :inreview AND p.reviewerid = :reviewerid)) + AND p.userid != :userid"; + + $params = array( + 'waitingforreview' => plan::STATUS_WAITING_FOR_REVIEW, + 'inreview' => plan::STATUS_IN_REVIEW, + 'reviewerid' => $userid, + 'userid' => $userid + ); + + // Primary check to avoid the hard work of getting the users in which the user has permission. + $count = $DB->count_records_sql($countselect . $sql, $params); + if ($count < 1) { + return array('count' => 0, 'plans' => array()); + } + + // TODO MDL-52243 Use core function. + list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql('moodle/competency:planreview', + $userid, SQL_PARAMS_NAMED); + $sql .= " AND p.userid $insql"; + $params += $inparams; + + $plans = array(); + $records = $DB->get_recordset_sql($select . $sql, $params, $skip, $limit); + foreach ($records as $record) { + $plan = new plan(0, plan::extract_record($record)); + $template = null; + + if ($plan->is_based_on_template()) { + $template = new template(0, template::extract_record($record)); + } + + $plans[] = (object) array( + 'plan' => $plan, + 'template' => $template, + 'owner' => persistent::extract_record($record, 'usr_'), + ); + } + $records->close(); + + return array( + 'count' => $DB->count_records_sql($countselect . $sql, $params), + 'plans' => $plans + ); + } + + /** + * Creates a learning plan based on the provided data. + * + * @param stdClass $record + * @return \core_competency\plan + */ + public static function create_plan(stdClass $record) { + global $USER; + static::require_enabled(); + $plan = new plan(0, $record); + + if ($plan->is_based_on_template()) { + throw new coding_exception('To create a plan from a template use api::create_plan_from_template().'); + } else if ($plan->get_status() == plan::STATUS_COMPLETE) { + throw new coding_exception('A plan cannot be created as complete.'); + } + + if (!$plan->can_manage()) { + $context = context_user::instance($plan->get_userid()); + throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); + } + + $plan->create(); + + // Trigger created event. + \core\event\competency_plan_created::create_from_plan($plan)->trigger(); + return $plan; + } + + /** + * Create a learning plan from a template. + * + * @param mixed $templateorid The template object or ID. + * @param int $userid + * @return false|\core_competency\plan Returns false when the plan already exists. + */ + public static function create_plan_from_template($templateorid, $userid) { + static::require_enabled(); + $template = $templateorid; + if (!is_object($template)) { + $template = new template($template); + } + + // The user must be able to view the template to use it as a base for a plan. + if (!$template->can_read()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + // Can not create plan from a hidden template. + if ($template->get_visible() == false) { + throw new coding_exception('A plan can not be created from a hidden template'); + } + + // Convert the template to a plan. + $record = $template->to_record(); + $record->templateid = $record->id; + $record->userid = $userid; + $record->name = $record->shortname; + $record->status = plan::STATUS_ACTIVE; + + unset($record->id); + unset($record->timecreated); + unset($record->timemodified); + unset($record->usermodified); + + // Remove extra keys. + $properties = plan::properties_definition(); + foreach ($record as $key => $value) { + if (!array_key_exists($key, $properties)) { + unset($record->$key); + } + } + + $plan = new plan(0, $record); + if (!$plan->can_manage()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', + 'nopermissions', ''); + } + + // We first apply the permission checks as we wouldn't want to leak information by returning early that + // the plan already exists. + if (plan::record_exists_select('templateid = :templateid AND userid = :userid', array( + 'templateid' => $template->get_id(), 'userid' => $userid))) { + return false; + } + + $plan->create(); + + // Trigger created event. + \core\event\competency_plan_created::create_from_plan($plan)->trigger(); + return $plan; + } + + /** + * Create learning plans from a template and cohort. + * + * @param mixed $templateorid The template object or ID. + * @param int $cohortid The cohort ID. + * @param bool $recreateunlinked When true the plans that were unlinked from this template will be re-created. + * @return int The number of plans created. + */ + public static function create_plans_from_template_cohort($templateorid, $cohortid, $recreateunlinked = false) { + global $DB, $CFG; + static::require_enabled(); + require_once($CFG->dirroot . '/cohort/lib.php'); + + $template = $templateorid; + if (!is_object($template)) { + $template = new template($template); + } + + // The user must be able to view the template to use it as a base for a plan. + if (!$template->can_read()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + // Can not create plan from a hidden template. + if ($template->get_visible() == false) { + throw new coding_exception('A plan can not be created from a hidden template'); + } + + // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context. + $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST); + $cohortcontext = context::instance_by_id($cohort->contextid); + if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) { + throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', ''); + } + + // Convert the template to a plan. + $recordbase = $template->to_record(); + $recordbase->templateid = $recordbase->id; + $recordbase->name = $recordbase->shortname; + $recordbase->status = plan::STATUS_ACTIVE; + + unset($recordbase->id); + unset($recordbase->timecreated); + unset($recordbase->timemodified); + unset($recordbase->usermodified); + + // Remove extra keys. + $properties = plan::properties_definition(); + foreach ($recordbase as $key => $value) { + if (!array_key_exists($key, $properties)) { + unset($recordbase->$key); + } + } + + // Create the plans. + $created = 0; + $userids = template_cohort::get_missing_plans($template->get_id(), $cohortid, $recreateunlinked); + foreach ($userids as $userid) { + $record = (object) (array) $recordbase; + $record->userid = $userid; + + $plan = new plan(0, $record); + if (!$plan->can_manage()) { + // Silently skip members where permissions are lacking. + continue; + } + + $plan->create(); + // Trigger created event. + \core\event\competency_plan_created::create_from_plan($plan)->trigger(); + $created++; + } + + return $created; + } + + /** + * Unlink a plan from its template. + * + * @param \core_competency\plan|int $planorid The plan or its ID. + * @return bool + */ + public static function unlink_plan_from_template($planorid) { + global $DB; + static::require_enabled(); + + $plan = $planorid; + if (!is_object($planorid)) { + $plan = new plan($planorid); + } + + // The user must be allowed to manage the plans of the user, nothing about the template. + if (!$plan->can_manage()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); + } + + // Only plan with status DRAFT or ACTIVE can be unliked.. + if ($plan->get_status() == plan::STATUS_COMPLETE) { + throw new coding_exception('Only draft or active plan can be unliked from a template'); + } + + // Early exit, it's already done... + if (!$plan->is_based_on_template()) { + return true; + } + + // Fetch the template. + $template = new template($plan->get_templateid()); + + // Now, proceed by copying all competencies to the plan, then update the plan. + $transaction = $DB->start_delegated_transaction(); + $competencies = template_competency::list_competencies($template->get_id(), false); + $i = 0; + foreach ($competencies as $competency) { + $record = (object) array( + 'planid' => $plan->get_id(), + 'competencyid' => $competency->get_id(), + 'sortorder' => $i++ + ); + $pc = new plan_competency(null, $record); + $pc->create(); + } + $plan->set_origtemplateid($template->get_id()); + $plan->set_templateid(null); + $success = $plan->update(); + $transaction->allow_commit(); + + // Trigger unlinked event. + \core\event\competency_plan_unlinked::create_from_plan($plan)->trigger(); + + return $success; + } + + /** + * Updates a plan. + * + * @param stdClass $record + * @return \core_competency\plan + */ + public static function update_plan(stdClass $record) { + static::require_enabled(); + + $plan = new plan($record->id); + + // Validate that the plan as it is can be managed. + if (!$plan->can_manage()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); + + } else if ($plan->get_status() == plan::STATUS_COMPLETE) { + // A completed plan cannot be edited. + throw new coding_exception('Completed plan cannot be edited.'); + + } else if ($plan->is_based_on_template()) { + // Prevent a plan based on a template to be edited. + throw new coding_exception('Cannot update a plan that is based on a template.'); + + } else if (isset($record->templateid) && $plan->get_templateid() != $record->templateid) { + // Prevent a plan to be based on a template. + throw new coding_exception('Cannot base a plan on a template.'); + + } else if (isset($record->userid) && $plan->get_userid() != $record->userid) { + // Prevent change of ownership as the capabilities are checked against that. + throw new coding_exception('A plan cannot be transfered to another user'); + + } else if (isset($record->status) && $plan->get_status() != $record->status) { + // Prevent change of status. + throw new coding_exception('To change the status of a plan use the appropriate methods.'); + + } + + $plan->from_record($record); + $plan->update(); + + // Trigger updated event. + \core\event\competency_plan_updated::create_from_plan($plan)->trigger(); + + return $plan; + } + + /** + * Returns a plan data. + * + * @param int $id + * @return \core_competency\plan + */ + public static function read_plan($id) { + static::require_enabled(); + $plan = new plan($id); + + if (!$plan->can_read()) { + $context = context_user::instance($plan->get_userid()); + throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', ''); + } + + return $plan; + } + + /** + * Plan event viewed. + * + * @param mixed $planorid The id or the plan. + * @return boolean + */ + public static function plan_viewed($planorid) { + static::require_enabled(); + $plan = $planorid; + if (!is_object($plan)) { + $plan = new plan($plan); + } + + // First we do a permissions check. + if (!$plan->can_read()) { + $context = context_user::instance($plan->get_userid()); + throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', ''); + } + + // Trigger a template viewed event. + \core\event\competency_plan_viewed::create_from_plan($plan)->trigger(); + + return true; + } + + /** + * Deletes a plan. + * + * Plans based on a template can be removed just like any other one. + * + * @param int $id + * @return bool Success? + */ + public static function delete_plan($id) { + global $DB; + static::require_enabled(); + + $plan = new plan($id); + + if (!$plan->can_manage()) { + $context = context_user::instance($plan->get_userid()); + throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); + } + + // Wrap the suppression in a DB transaction. + $transaction = $DB->start_delegated_transaction(); + + // Delete plan competencies. + $plancomps = plan_competency::get_records(array('planid' => $plan->get_id())); + foreach ($plancomps as $plancomp) { + $plancomp->delete(); + } + + // Delete archive user competencies if the status of the plan is complete. + if ($plan->get_status() == plan::STATUS_COMPLETE) { + self::remove_archived_user_competencies_in_plan($plan); + } + $event = \core\event\competency_plan_deleted::create_from_plan($plan); + $success = $plan->delete(); + + $transaction->allow_commit(); + + // Trigger deleted event. + $event->trigger(); + + return $success; + } + + /** + * Cancel the review of a plan. + * + * @param int|plan $planorid The plan, or its ID. + * @return bool + */ + public static function plan_cancel_review_request($planorid) { + static::require_enabled(); + $plan = $planorid; + if (!is_object($plan)) { + $plan = new plan($plan); + } + + // We need to be able to view the plan at least. + if (!$plan->can_read()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', ''); + } + + if ($plan->is_based_on_template()) { + throw new coding_exception('Template plans cannot be reviewed.'); // This should never happen. + } else if ($plan->get_status() != plan::STATUS_WAITING_FOR_REVIEW) { + throw new coding_exception('The plan review cannot be cancelled at this stage.'); + } else if (!$plan->can_request_review()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); + } + + $plan->set_status(plan::STATUS_DRAFT); + $result = $plan->update(); + + // Trigger review request cancelled event. + \core\event\competency_plan_review_request_cancelled::create_from_plan($plan)->trigger(); + + return $result; + } + + /** + * Request the review of a plan. + * + * @param int|plan $planorid The plan, or its ID. + * @return bool + */ + public static function plan_request_review($planorid) { + static::require_enabled(); + $plan = $planorid; + if (!is_object($plan)) { + $plan = new plan($plan); + } + + // We need to be able to view the plan at least. + if (!$plan->can_read()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', ''); + } + + if ($plan->is_based_on_template()) { + throw new coding_exception('Template plans cannot be reviewed.'); // This should never happen. + } else if ($plan->get_status() != plan::STATUS_DRAFT) { + throw new coding_exception('The plan cannot be sent for review at this stage.'); + } else if (!$plan->can_request_review()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); + } + + $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + $result = $plan->update(); + + // Trigger review requested event. + \core\event\competency_plan_review_requested::create_from_plan($plan)->trigger(); + + return $result; + } + + /** + * Start the review of a plan. + * + * @param int|plan $planorid The plan, or its ID. + * @return bool + */ + public static function plan_start_review($planorid) { + global $USER; + static::require_enabled(); + $plan = $planorid; + if (!is_object($plan)) { + $plan = new plan($plan); + } + + // We need to be able to view the plan at least. + if (!$plan->can_read()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', ''); + } + + if ($plan->is_based_on_template()) { + throw new coding_exception('Template plans cannot be reviewed.'); // This should never happen. + } else if ($plan->get_status() != plan::STATUS_WAITING_FOR_REVIEW) { + throw new coding_exception('The plan review cannot be started at this stage.'); + } else if (!$plan->can_review()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); + } + + $plan->set_status(plan::STATUS_IN_REVIEW); + $plan->set_reviewerid($USER->id); + $result = $plan->update(); + + // Trigger review started event. + \core\event\competency_plan_review_started::create_from_plan($plan)->trigger(); + + return $result; + } + + /** + * Stop reviewing a plan. + * + * @param int|plan $planorid The plan, or its ID. + * @return bool + */ + public static function plan_stop_review($planorid) { + static::require_enabled(); + $plan = $planorid; + if (!is_object($plan)) { + $plan = new plan($plan); + } + + // We need to be able to view the plan at least. + if (!$plan->can_read()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', ''); + } + + if ($plan->is_based_on_template()) { + throw new coding_exception('Template plans cannot be reviewed.'); // This should never happen. + } else if ($plan->get_status() != plan::STATUS_IN_REVIEW) { + throw new coding_exception('The plan review cannot be stopped at this stage.'); + } else if (!$plan->can_review()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); + } + + $plan->set_status(plan::STATUS_DRAFT); + $plan->set_reviewerid(null); + $result = $plan->update(); + + // Trigger review stopped event. + \core\event\competency_plan_review_stopped::create_from_plan($plan)->trigger(); + + return $result; + } + + /** + * Approve a plan. + * + * This means making the plan active. + * + * @param int|plan $planorid The plan, or its ID. + * @return bool + */ + public static function approve_plan($planorid) { + static::require_enabled(); + $plan = $planorid; + if (!is_object($plan)) { + $plan = new plan($plan); + } + + // We need to be able to view the plan at least. + if (!$plan->can_read()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', ''); + } + + // We can approve a plan that is either a draft, in review, or waiting for review. + if ($plan->is_based_on_template()) { + throw new coding_exception('Template plans are already approved.'); // This should never happen. + } else if (!$plan->is_draft()) { + throw new coding_exception('The plan cannot be approved at this stage.'); + } else if (!$plan->can_review()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); + } + + $plan->set_status(plan::STATUS_ACTIVE); + $plan->set_reviewerid(null); + $result = $plan->update(); + + // Trigger approved event. + \core\event\competency_plan_approved::create_from_plan($plan)->trigger(); + + return $result; + } + + /** + * Unapprove a plan. + * + * This means making the plan draft. + * + * @param int|plan $planorid The plan, or its ID. + * @return bool + */ + public static function unapprove_plan($planorid) { + static::require_enabled(); + $plan = $planorid; + if (!is_object($plan)) { + $plan = new plan($plan); + } + + // We need to be able to view the plan at least. + if (!$plan->can_read()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', ''); + } + + if ($plan->is_based_on_template()) { + throw new coding_exception('Template plans are always approved.'); // This should never happen. + } else if ($plan->get_status() != plan::STATUS_ACTIVE) { + throw new coding_exception('The plan cannot be sent back to draft at this stage.'); + } else if (!$plan->can_review()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); + } + + $plan->set_status(plan::STATUS_DRAFT); + $result = $plan->update(); + + // Trigger unapproved event. + \core\event\competency_plan_unapproved::create_from_plan($plan)->trigger(); + + return $result; + } + + /** + * Complete a plan. + * + * @param int|plan $planorid The plan, or its ID. + * @return bool + */ + public static function complete_plan($planorid) { + global $DB; + static::require_enabled(); + + $plan = $planorid; + if (!is_object($planorid)) { + $plan = new plan($planorid); + } + + // Validate that the plan can be managed. + if (!$plan->can_manage()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); + } + + // Check if the plan was already completed. + if ($plan->get_status() == plan::STATUS_COMPLETE) { + throw new coding_exception('The plan is already completed.'); + } + + $originalstatus = $plan->get_status(); + $plan->set_status(plan::STATUS_COMPLETE); + + // The user should also be able to manage the plan when it's completed. + if (!$plan->can_manage()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); + } + + // Put back original status because archive needs it to extract competencies from the right table. + $plan->set_status($originalstatus); + + // Do the things. + $transaction = $DB->start_delegated_transaction(); + self::archive_user_competencies_in_plan($plan); + $plan->set_status(plan::STATUS_COMPLETE); + $success = $plan->update(); + + if (!$success) { + $transaction->rollback(new moodle_exception('The plan could not be updated.')); + return $success; + } + + $transaction->allow_commit(); + + // Trigger updated event. + \core\event\competency_plan_completed::create_from_plan($plan)->trigger(); + + return $success; + } + + /** + * Reopen a plan. + * + * @param int|plan $planorid The plan, or its ID. + * @return bool + */ + public static function reopen_plan($planorid) { + global $DB; + static::require_enabled(); + + $plan = $planorid; + if (!is_object($planorid)) { + $plan = new plan($planorid); + } + + // Validate that the plan as it is can be managed. + if (!$plan->can_manage()) { + $context = context_user::instance($plan->get_userid()); + throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); + } + + $beforestatus = $plan->get_status(); + $plan->set_status(plan::STATUS_ACTIVE); + + // Validate if status can be changed. + if (!$plan->can_manage()) { + $context = context_user::instance($plan->get_userid()); + throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); + } + + // Wrap the updates in a DB transaction. + $transaction = $DB->start_delegated_transaction(); + + // Delete archived user competencies if the status of the plan is changed from complete to another status. + $mustremovearchivedcompetencies = ($beforestatus == plan::STATUS_COMPLETE && $plan->get_status() != plan::STATUS_COMPLETE); + if ($mustremovearchivedcompetencies) { + self::remove_archived_user_competencies_in_plan($plan); + } + + // If duedate less than or equal to duedate_threshold unset it. + if ($plan->get_duedate() <= time() + plan::DUEDATE_THRESHOLD) { + $plan->set_duedate(0); + } + + $success = $plan->update(); + + if (!$success) { + $transaction->rollback(new moodle_exception('The plan could not be updated.')); + return $success; + } + + $transaction->allow_commit(); + + // Trigger reopened event. + \core\event\competency_plan_reopened::create_from_plan($plan)->trigger(); + + return $success; + } + + /** + * Get a single competency from the user plan. + * + * @param int $planorid The plan, or its ID. + * @param int $competencyid The competency id. + * @return (object) array( + * 'competency' => \core_competency\competency, + * 'usercompetency' => \core_competency\user_competency + * 'usercompetencyplan' => \core_competency\user_competency_plan + * ) + * The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time. + */ + public static function get_plan_competency($planorid, $competencyid) { + static::require_enabled(); + $plan = $planorid; + if (!is_object($planorid)) { + $plan = new plan($planorid); + } + + if (!user_competency::can_read_user($plan->get_userid())) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:usercompetencyview', + 'nopermissions', ''); + } + + $competency = $plan->get_competency($competencyid); + + // Get user competencies from user_competency_plan if the plan status is set to complete. + $iscompletedplan = $plan->get_status() == plan::STATUS_COMPLETE; + if ($iscompletedplan) { + $usercompetencies = user_competency_plan::get_multiple($plan->get_userid(), $plan->get_id(), array($competencyid)); + $ucresultkey = 'usercompetencyplan'; + } else { + $usercompetencies = user_competency::get_multiple($plan->get_userid(), array($competencyid)); + $ucresultkey = 'usercompetency'; + } + + $found = count($usercompetencies); + + if ($found) { + $uc = array_pop($usercompetencies); + } else { + if ($iscompletedplan) { + throw new coding_exception('A user competency plan is missing'); + } else { + $uc = user_competency::create_relation($plan->get_userid(), $competency->get_id()); + $uc->create(); + } + } + + $plancompetency = (object) array( + 'competency' => $competency, + 'usercompetency' => null, + 'usercompetencyplan' => null + ); + $plancompetency->$ucresultkey = $uc; + + return $plancompetency; + } + + /** + * List the competencies in a user plan. + * + * @param int $planorid The plan, or its ID. + * @return array((object) array( + * 'competency' => \core_competency\competency, + * 'usercompetency' => \core_competency\user_competency + * 'usercompetencyplan' => \core_competency\user_competency_plan + * )) + * The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time. + */ + public static function list_plan_competencies($planorid) { + static::require_enabled(); + $plan = $planorid; + if (!is_object($planorid)) { + $plan = new plan($planorid); + } + + if (!$plan->can_read()) { + $context = context_user::instance($plan->get_userid()); + throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', ''); + } + + $result = array(); + $competencies = $plan->get_competencies(); + + // Get user competencies from user_competency_plan if the plan status is set to complete. + $iscompletedplan = $plan->get_status() == plan::STATUS_COMPLETE; + if ($iscompletedplan) { + $usercompetencies = user_competency_plan::get_multiple($plan->get_userid(), $plan->get_id(), $competencies); + $ucresultkey = 'usercompetencyplan'; + } else { + $usercompetencies = user_competency::get_multiple($plan->get_userid(), $competencies); + $ucresultkey = 'usercompetency'; + } + + // Build the return values. + foreach ($competencies as $key => $competency) { + $found = false; + + foreach ($usercompetencies as $uckey => $uc) { + if ($uc->get_competencyid() == $competency->get_id()) { + $found = true; + unset($usercompetencies[$uckey]); + break; + } + } + + if (!$found) { + if ($iscompletedplan) { + throw new coding_exception('A user competency plan is missing'); + } else { + $uc = user_competency::create_relation($plan->get_userid(), $competency->get_id()); + } + } + + $plancompetency = (object) array( + 'competency' => $competency, + 'usercompetency' => null, + 'usercompetencyplan' => null + ); + $plancompetency->$ucresultkey = $uc; + $result[] = $plancompetency; + } + + return $result; + } + + /** + * Add a competency to a plan. + * + * @param int $planid The id of the plan + * @param int $competencyid The id of the competency + * @return bool + */ + public static function add_competency_to_plan($planid, $competencyid) { + static::require_enabled(); + $plan = new plan($planid); + + // First we do a permissions check. + if (!$plan->can_manage()) { + throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); + + } else if ($plan->is_based_on_template()) { + throw new coding_exception('A competency can not be added to a learning plan based on a template'); + } + + if (!$plan->can_be_edited()) { + throw new coding_exception('A competency can not be added to a learning plan completed'); + } + + $competency = new competency($competencyid); + + // Can not add a competency that belong to a hidden framework. + if ($competency->get_framework()->get_visible() == false) { + throw new coding_exception('A competency belonging to hidden framework can not be added'); + } + + $exists = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid)); + if (!$exists) { + $record = new stdClass(); + $record->planid = $planid; + $record->competencyid = $competencyid; + $plancompetency = new plan_competency(0, $record); + $plancompetency->create(); + } + + return true; + } + + /** + * Remove a competency from a plan. + * + * @param int $planid The plan id + * @param int $competencyid The id of the competency + * @return bool + */ + public static function remove_competency_from_plan($planid, $competencyid) { + static::require_enabled(); + $plan = new plan($planid); + + // First we do a permissions check. + if (!$plan->can_manage()) { + $context = context_user::instance($plan->get_userid()); + throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); + + } else if ($plan->is_based_on_template()) { + throw new coding_exception('A competency can not be removed from a learning plan based on a template'); + } + + if (!$plan->can_be_edited()) { + throw new coding_exception('A competency can not be removed from a learning plan completed'); + } + + $link = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid)); + if ($link) { + return $link->delete(); + } + return false; + } + + /** + * Move the plan competency up or down in the display list. + * + * Requires moodle/competency:planmanage capability at the system context. + * + * @param int $planid The plan id + * @param int $competencyidfrom The id of the competency we are moving. + * @param int $competencyidto The id of the competency we are moving to. + * @return boolean + */ + public static function reorder_plan_competency($planid, $competencyidfrom, $competencyidto) { + static::require_enabled(); + $plan = new plan($planid); + + // First we do a permissions check. + if (!$plan->can_manage()) { + $context = context_user::instance($plan->get_userid()); + throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); + + } else if ($plan->is_based_on_template()) { + throw new coding_exception('A competency can not be reordered in a learning plan based on a template'); + } + + if (!$plan->can_be_edited()) { + throw new coding_exception('A competency can not be reordered in a learning plan completed'); + } + + $down = true; + $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidfrom)); + if (count($matches) == 0) { + throw new coding_exception('The link does not exist'); + } + + $competencyfrom = array_pop($matches); + $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidto)); + if (count($matches) == 0) { + throw new coding_exception('The link does not exist'); + } + + $competencyto = array_pop($matches); + + $all = plan_competency::get_records(array('planid' => $planid), 'sortorder', 'ASC', 0, 0); + + if ($competencyfrom->get_sortorder() > $competencyto->get_sortorder()) { + // We are moving up, so put it before the "to" item. + $down = false; + } + + foreach ($all as $id => $plancompetency) { + $sort = $plancompetency->get_sortorder(); + if ($down && $sort > $competencyfrom->get_sortorder() && $sort <= $competencyto->get_sortorder()) { + $plancompetency->set_sortorder($plancompetency->get_sortorder() - 1); + $plancompetency->update(); + } else if (!$down && $sort >= $competencyto->get_sortorder() && $sort < $competencyfrom->get_sortorder()) { + $plancompetency->set_sortorder($plancompetency->get_sortorder() + 1); + $plancompetency->update(); + } + } + $competencyfrom->set_sortorder($competencyto->get_sortorder()); + return $competencyfrom->update(); + } + + /** + * Cancel a user competency review request. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @return bool + */ + public static function user_competency_cancel_review_request($userid, $competencyid) { + static::require_enabled(); + $context = context_user::instance($userid); + $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); + if (!$uc || !$uc->can_read()) { + throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); + } else if ($uc->get_status() != user_competency::STATUS_WAITING_FOR_REVIEW) { + throw new coding_exception('The competency can not be cancel review request at this stage.'); + } else if (!$uc->can_request_review()) { + throw new required_capability_exception($context, 'moodle/competency:usercompetencyrequestreview', 'nopermissions', ''); + } + + $uc->set_status(user_competency::STATUS_IDLE); + $result = $uc->update(); + if ($result) { + \core\event\competency_user_competency_review_request_cancelled::create_from_user_competency($uc)->trigger(); + } + return $result; + } + + /** + * Request a user competency review. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @return bool + */ + public static function user_competency_request_review($userid, $competencyid) { + static::require_enabled(); + $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); + if (!$uc) { + $uc = user_competency::create_relation($userid, $competencyid); + $uc->create(); + } + + if (!$uc->can_read()) { + throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview', + 'nopermissions', ''); + } else if ($uc->get_status() != user_competency::STATUS_IDLE) { + throw new coding_exception('The competency can not be sent for review at this stage.'); + } else if (!$uc->can_request_review()) { + throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyrequestreview', + 'nopermissions', ''); + } + + $uc->set_status(user_competency::STATUS_WAITING_FOR_REVIEW); + $result = $uc->update(); + if ($result) { + \core\event\competency_user_competency_review_requested::create_from_user_competency($uc)->trigger(); + } + return $result; + } + + /** + * Start a user competency review. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @return bool + */ + public static function user_competency_start_review($userid, $competencyid) { + global $USER; + static::require_enabled(); + + $context = context_user::instance($userid); + $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); + if (!$uc || !$uc->can_read()) { + throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); + } else if ($uc->get_status() != user_competency::STATUS_WAITING_FOR_REVIEW) { + throw new coding_exception('The competency review can not be started at this stage.'); + } else if (!$uc->can_review()) { + throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', ''); + } + + $uc->set_status(user_competency::STATUS_IN_REVIEW); + $uc->set_reviewerid($USER->id); + $result = $uc->update(); + if ($result) { + \core\event\competency_user_competency_review_started::create_from_user_competency($uc)->trigger(); + } + return $result; + } + + /** + * Stop a user competency review. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @return bool + */ + public static function user_competency_stop_review($userid, $competencyid) { + static::require_enabled(); + $context = context_user::instance($userid); + $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); + if (!$uc || !$uc->can_read()) { + throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); + } else if ($uc->get_status() != user_competency::STATUS_IN_REVIEW) { + throw new coding_exception('The competency review can not be stopped at this stage.'); + } else if (!$uc->can_review()) { + throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', ''); + } + + $uc->set_status(user_competency::STATUS_IDLE); + $result = $uc->update(); + if ($result) { + \core\event\competency_user_competency_review_stopped::create_from_user_competency($uc)->trigger(); + } + return $result; + } + + /** + * Log user competency viewed event. + * + * @param user_competency|int $usercompetencyorid The user competency object or user competency id + * @return bool + */ + public static function user_competency_viewed($usercompetencyorid) { + static::require_enabled(); + $uc = $usercompetencyorid; + if (!is_object($uc)) { + $uc = new user_competency($uc); + } + + if (!$uc || !$uc->can_read()) { + throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview', + 'nopermissions', ''); + } + + \core\event\competency_user_competency_viewed::create_from_user_competency_viewed($uc)->trigger(); + return true; + } + + /** + * Log user competency viewed in plan event. + * + * @param user_competency|int $usercompetencyorid The user competency object or user competency id + * @param int $planid The plan ID + * @return bool + */ + public static function user_competency_viewed_in_plan($usercompetencyorid, $planid) { + static::require_enabled(); + $uc = $usercompetencyorid; + if (!is_object($uc)) { + $uc = new user_competency($uc); + } + + if (!$uc || !$uc->can_read()) { + throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview', + 'nopermissions', ''); + } + $plan = new plan($planid); + if ($plan->get_status() == plan::STATUS_COMPLETE) { + throw new coding_exception('To log the user competency in completed plan use user_competency_plan_viewed method.'); + } + + \core\event\competency_user_competency_viewed_in_plan::create_from_user_competency_viewed_in_plan($uc, $planid)->trigger(); + return true; + } + + /** + * Log user competency viewed in course event. + * + * @param user_competency_course|int $usercoursecompetencyorid The user competency course object or its ID. + * @param int $courseid The course ID + * @return bool + */ + public static function user_competency_viewed_in_course($usercoursecompetencyorid) { + static::require_enabled(); + $ucc = $usercoursecompetencyorid; + if (!is_object($ucc)) { + $ucc = new user_competency_course($ucc); + } + + if (!$ucc || !user_competency::can_read_user_in_course($ucc->get_userid(), $ucc->get_courseid())) { + throw new required_capability_exception($ucc->get_context(), 'moodle/competency:usercompetencyview', + 'nopermissions', ''); + } + + // Validate the course, this will throw an exception if not valid. + self::validate_course($ucc->get_courseid()); + + \core\event\competency_user_competency_viewed_in_course::create_from_user_competency_viewed_in_course($ucc)->trigger(); + return true; + } + + /** + * Log user competency plan viewed event. + * + * @param user_competency_plan|int $usercompetencyplanorid The user competency plan object or user competency plan id + * @return bool + */ + public static function user_competency_plan_viewed($usercompetencyplanorid) { + static::require_enabled(); + $ucp = $usercompetencyplanorid; + if (!is_object($ucp)) { + $ucp = new user_competency_plan($ucp); + } + + if (!$ucp || !user_competency::can_read_user($ucp->get_userid())) { + throw new required_capability_exception($ucp->get_context(), 'moodle/competency:usercompetencyview', + 'nopermissions', ''); + } + $plan = new plan($ucp->get_planid()); + if ($plan->get_status() != plan::STATUS_COMPLETE) { + throw new coding_exception('To log the user competency in non-completed plan use ' + . 'user_competency_viewed_in_plan method.'); + } + + \core\event\competency_user_competency_plan_viewed::create_from_user_competency_plan($ucp)->trigger(); + return true; + } + + /** + * Check if template has related data. + * + * @param int $templateid The id of the template to check. + * @return boolean + */ + public static function template_has_related_data($templateid) { + static::require_enabled(); + // First we do a permissions check. + $template = new template($templateid); + + if (!$template->can_read()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + // OK - all set. + return $template->has_plans(); + } + + /** + * List all the related competencies. + * + * @param int $competencyid The id of the competency to check. + * @return competency[] + */ + public static function list_related_competencies($competencyid) { + static::require_enabled(); + $competency = new competency($competencyid); + + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), + $competency->get_context())) { + throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview', + 'nopermissions', ''); + } + + return $competency->get_related_competencies(); + } + + /** + * Add a related competency. + * + * @param int $competencyid The id of the competency + * @param int $relatedcompetencyid The id of the related competency. + * @return bool False when create failed, true on success, or if the relation already existed. + */ + public static function add_related_competency($competencyid, $relatedcompetencyid) { + static::require_enabled(); + $competency1 = new competency($competencyid); + $competency2 = new competency($relatedcompetencyid); + + require_capability('moodle/competency:competencymanage', $competency1->get_context()); + + $relatedcompetency = related_competency::get_relation($competency1->get_id(), $competency2->get_id()); + if (!$relatedcompetency->get_id()) { + $relatedcompetency->create(); + return true; + } + + return true; + } + + /** + * Remove a related competency. + * + * @param int $competencyid The id of the competency. + * @param int $relatedcompetencyid The id of the related competency. + * @return bool True when it was deleted, false when it wasn't or the relation doesn't exist. + */ + public static function remove_related_competency($competencyid, $relatedcompetencyid) { + static::require_enabled(); + $competency = new competency($competencyid); + + // This only check if we have the permission in either competency because both competencies + // should belong to the same framework. + require_capability('moodle/competency:competencymanage', $competency->get_context()); + + $relatedcompetency = related_competency::get_relation($competencyid, $relatedcompetencyid); + if ($relatedcompetency->get_id()) { + return $relatedcompetency->delete(); + } + + return false; + } + + /** + * Read a user evidence. + * + * @param int $id + * @return user_evidence + */ + public static function read_user_evidence($id) { + static::require_enabled(); + $userevidence = new user_evidence($id); + + if (!$userevidence->can_read()) { + $context = $userevidence->get_context(); + throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', ''); + } + + return $userevidence; + } + + /** + * Create a new user evidence. + * + * @param object $data The data. + * @param int $draftitemid The draft ID in which files have been saved. + * @return user_evidence + */ + public static function create_user_evidence($data, $draftitemid = null) { + static::require_enabled(); + $userevidence = new user_evidence(null, $data); + $context = $userevidence->get_context(); + + if (!$userevidence->can_manage()) { + throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); + } + + $userevidence->create(); + if (!empty($draftitemid)) { + $fileareaoptions = array('subdirs' => true); + $itemid = $userevidence->get_id(); + file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions); + } + + // Trigger an evidence of prior learning created event. + \core\event\competency_user_evidence_created::create_from_user_evidence($userevidence)->trigger(); + + return $userevidence; + } + + /** + * Create a new user evidence. + * + * @param object $data The data. + * @param int $draftitemid The draft ID in which files have been saved. + * @return user_evidence + */ + public static function update_user_evidence($data, $draftitemid = null) { + static::require_enabled(); + $userevidence = new user_evidence($data->id); + $context = $userevidence->get_context(); + + if (!$userevidence->can_manage()) { + throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); + + } else if (array_key_exists('userid', $data) && $data->userid != $userevidence->get_userid()) { + throw new coding_exception('Can not change the userid of a user evidence.'); + } + + $userevidence->from_record($data); + $userevidence->update(); + + if (!empty($draftitemid)) { + $fileareaoptions = array('subdirs' => true); + $itemid = $userevidence->get_id(); + file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions); + } + + // Trigger an evidence of prior learning updated event. + \core\event\competency_user_evidence_updated::create_from_user_evidence($userevidence)->trigger(); + + return $userevidence; + } + + /** + * Delete a user evidence. + * + * @param int $id The user evidence ID. + * @return bool + */ + public static function delete_user_evidence($id) { + static::require_enabled(); + $userevidence = new user_evidence($id); + $context = $userevidence->get_context(); + + if (!$userevidence->can_manage()) { + throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); + } + + // Delete the user evidence. + $userevidence->delete(); + + // Delete associated files. + $fs = get_file_storage(); + $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $id); + + // Delete relation between evidence and competencies. + $userevidence->set_id($id); // Restore the ID to fully mock the object. + $competencies = user_evidence_competency::get_competencies_by_userevidenceid($id); + foreach ($competencies as $competency) { + static::delete_user_evidence_competency($userevidence, $competency->get_id()); + } + + // Trigger an evidence of prior learning deleted event. + \core\event\competency_user_evidence_deleted::create_from_user_evidence($userevidence)->trigger(); + + $userevidence->set_id(0); // Restore the object. + + return true; + } + + /** + * List the user evidence of a user. + * + * @param int $userid The user ID. + * @return user_evidence[] + */ + public static function list_user_evidence($userid) { + static::require_enabled(); + if (!user_evidence::can_read_user($userid)) { + $context = context_user::instance($userid); + throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', ''); + } + + $evidence = user_evidence::get_records(array('userid' => $userid), 'name'); + return $evidence; + } + + /** + * Link a user evidence with a competency. + * + * @param user_evidence|int $userevidenceorid User evidence or its ID. + * @param int $competencyid Competency ID. + * @return user_evidence_competency + */ + public static function create_user_evidence_competency($userevidenceorid, $competencyid) { + global $USER; + static::require_enabled(); + + $userevidence = $userevidenceorid; + if (!is_object($userevidence)) { + $userevidence = self::read_user_evidence($userevidence); + } + + // Perform user evidence capability checks. + if (!$userevidence->can_manage()) { + $context = $userevidence->get_context(); + throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); + } + + // Perform competency capability checks. + $competency = self::read_competency($competencyid); + + // Get (and create) the relation. + $relation = user_evidence_competency::get_relation($userevidence->get_id(), $competency->get_id()); + if (!$relation->get_id()) { + $relation->create(); + + $link = url::user_evidence($userevidence->get_id()); + self::add_evidence( + $userevidence->get_userid(), + $competency, + $userevidence->get_context(), + evidence::ACTION_LOG, + 'evidence_evidenceofpriorlearninglinked', + 'core_competency', + $userevidence->get_name(), + false, + $link->out(false), + null, + $USER->id + ); + } + + return $relation; + } + + /** + * Delete a relationship between a user evidence and a competency. + * + * @param user_evidence|int $userevidenceorid User evidence or its ID. + * @param int $competencyid Competency ID. + * @return bool + */ + public static function delete_user_evidence_competency($userevidenceorid, $competencyid) { + global $USER; + static::require_enabled(); + + $userevidence = $userevidenceorid; + if (!is_object($userevidence)) { + $userevidence = self::read_user_evidence($userevidence); + } + + // Perform user evidence capability checks. + if (!$userevidence->can_manage()) { + $context = $userevidence->get_context(); + throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); + } + + // Get (and delete) the relation. + $relation = user_evidence_competency::get_relation($userevidence->get_id(), $competencyid); + if (!$relation->get_id()) { + return true; + } + + $success = $relation->delete(); + if ($success) { + self::add_evidence( + $userevidence->get_userid(), + $competencyid, + $userevidence->get_context(), + evidence::ACTION_LOG, + 'evidence_evidenceofpriorlearningunlinked', + 'core_competency', + $userevidence->get_name(), + false, + null, + null, + $USER->id + ); + } + + return $success; + } + + /** + * Send request review for user evidence competencies. + * + * @param int $id The user evidence ID. + * @return bool + */ + public static function request_review_of_user_evidence_linked_competencies($id) { + $userevidence = new user_evidence($id); + $context = $userevidence->get_context(); + $userid = $userevidence->get_userid(); + + if (!$userevidence->can_manage()) { + throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); + } + + $usercompetencies = user_evidence_competency::get_user_competencies_by_userevidenceid($id); + foreach ($usercompetencies as $usercompetency) { + if ($usercompetency->get_status() == user_competency::STATUS_IDLE) { + static::user_competency_request_review($userid, $usercompetency->get_competencyid()); + } + } + + return true; + } + + /** + * Recursively duplicate competencies from a tree, we start duplicating from parents to children to have a correct path. + * This method does not copy the related competencies. + * + * @param int $frameworkid - framework id + * @param competency[] $tree - array of competencies object + * @param int $oldparent - old parent id + * @param int $newparent - new parent id + * @return competency[] $matchids - List of old competencies ids matched with new competencies object. + */ + protected static function duplicate_competency_tree($frameworkid, $tree, $oldparent = 0, $newparent = 0) { + $matchids = array(); + foreach ($tree as $node) { + if ($node->competency->get_parentid() == $oldparent) { + $parentid = $node->competency->get_id(); + + // Create the competency. + $competency = new competency(0, $node->competency->to_record()); + $competency->set_competencyframeworkid($frameworkid); + $competency->set_parentid($newparent); + $competency->set_path(''); + $competency->set_id(0); + $competency->reset_rule(); + $competency->create(); + + // Trigger the created event competency. + \core\event\competency_created::create_from_competency($competency)->trigger(); + + // Match the old id with the new one. + $matchids[$parentid] = $competency; + + if (!empty($node->children)) { + // Duplicate children competency. + $childrenids = self::duplicate_competency_tree($frameworkid, $node->children, $parentid, $competency->get_id()); + // Array_merge does not keep keys when merging so we use the + operator. + $matchids = $matchids + $childrenids; + } + } + } + return $matchids; + } + + /** + * Recursively migrate competency rules. + * + * @param competency[] $tree - array of competencies object + * @param competency[] $matchids - List of old competencies ids matched with new competencies object + */ + protected static function migrate_competency_tree_rules($tree, $matchids) { + + foreach ($tree as $node) { + $oldcompid = $node->competency->get_id(); + if ($node->competency->get_ruletype() && array_key_exists($oldcompid, $matchids)) { + try { + // Get the new competency. + $competency = $matchids[$oldcompid]; + $class = $node->competency->get_ruletype(); + $newruleconfig = $class::migrate_config($node->competency->get_ruleconfig(), $matchids); + $competency->set_ruleconfig($newruleconfig); + $competency->set_ruletype($class); + $competency->set_ruleoutcome($node->competency->get_ruleoutcome()); + $competency->update(); + } catch (\Exception $e) { + debugging('Could not migrate competency rule from: ' . $oldcompid . ' to: ' . $competency->get_id() . '.' . + ' Exception: ' . $e->getMessage(), DEBUG_DEVELOPER); + $competency->reset_rule(); + } + } + + if (!empty($node->children)) { + self::migrate_competency_tree_rules($node->children, $matchids); + } + } + } + + /** + * Archive user competencies in a plan. + * + * @param int $plan The plan object. + * @return void + */ + protected static function archive_user_competencies_in_plan($plan) { + + // Check if the plan was already completed. + if ($plan->get_status() == plan::STATUS_COMPLETE) { + throw new coding_exception('The plan is already completed.'); + } + + $competencies = $plan->get_competencies(); + $usercompetencies = user_competency::get_multiple($plan->get_userid(), $competencies); + + $i = 0; + foreach ($competencies as $competency) { + $found = false; + + foreach ($usercompetencies as $uckey => $uc) { + if ($uc->get_competencyid() == $competency->get_id()) { + $found = true; + + $ucprecord = $uc->to_record(); + $ucprecord->planid = $plan->get_id(); + $ucprecord->sortorder = $i; + unset($ucprecord->id); + unset($ucprecord->status); + unset($ucprecord->reviewerid); + + $usercompetencyplan = new user_competency_plan(0, $ucprecord); + $usercompetencyplan->create(); + + unset($usercompetencies[$uckey]); + break; + } + } + + // If the user competency doesn't exist, we create a new relation in user_competency_plan. + if (!$found) { + $usercompetencyplan = user_competency_plan::create_relation($plan->get_userid(), $competency->get_id(), + $plan->get_id()); + $usercompetencyplan->set_sortorder($i); + $usercompetencyplan->create(); + } + $i++; + } + } + + /** + * Delete archived user competencies in a plan. + * + * @param int $plan The plan object. + * @return void + */ + protected static function remove_archived_user_competencies_in_plan($plan) { + $competencies = $plan->get_competencies(); + $usercompetenciesplan = user_competency_plan::get_multiple($plan->get_userid(), $plan->get_id(), $competencies); + + foreach ($usercompetenciesplan as $ucpkey => $ucp) { + $ucp->delete(); + } + } + + /** + * List all the evidence for a user competency. + * + * @param int $userid The user id - only used if usercompetencyid is 0. + * @param int $competencyid The competency id - only used it usercompetencyid is 0. + * @param int $planid The plan id - not used yet - but can be used to only list archived evidence if a plan is completed. + * @param string $sort The field to sort the evidence by. + * @param string $order The ordering of the sorting. + * @param int $skip Number of records to skip. + * @param int $limit Number of records to return. + * @return \core_competency\evidence[] + * @return array of \core_competency\evidence + */ + public static function list_evidence($userid = 0, $competencyid = 0, $planid = 0, $sort = 'timecreated', + $order = 'DESC', $skip = 0, $limit = 0) { + static::require_enabled(); + + if (!user_competency::can_read_user($userid)) { + $context = context_user::instance($userid); + throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); + } + + $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); + if (!$usercompetency) { + return array(); + } + + $plancompleted = false; + if ($planid != 0) { + $plan = new plan($planid); + if ($plan->get_status() == plan::STATUS_COMPLETE) { + $plancompleted = true; + } + } + + $select = 'usercompetencyid = :usercompetencyid'; + $params = array('usercompetencyid' => $usercompetency->get_id()); + if ($plancompleted) { + $select .= ' AND timecreated <= :timecompleted'; + $params['timecompleted'] = $plan->get_timemodified(); + } + + $orderby = $sort . ' ' . $order; + $orderby .= !empty($orderby) ? ', id DESC' : 'id DESC'; // Prevent random ordering. + + $evidence = evidence::get_records_select($select, $params, $orderby, '*', $skip, $limit); + return $evidence; + } + + /** + * List all the evidence for a user competency in a course. + * + * @param int $userid The user ID. + * @param int $courseid The course ID. + * @param int $competencyid The competency ID. + * @param string $sort The field to sort the evidence by. + * @param string $order The ordering of the sorting. + * @param int $skip Number of records to skip. + * @param int $limit Number of records to return. + * @return \core_competency\evidence[] + */ + public static function list_evidence_in_course($userid = 0, $courseid = 0, $competencyid = 0, $sort = 'timecreated', + $order = 'DESC', $skip = 0, $limit = 0) { + static::require_enabled(); + + if (!user_competency::can_read_user_in_course($userid, $courseid)) { + $context = context_user::instance($userid); + throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); + } + + $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); + if (!$usercompetency) { + return array(); + } + + $params = array( + 'usercompetencyid' => $usercompetency->get_id(), + 'contextid' => context_course::instance($courseid)->id + ); + return evidence::get_records($params, $sort, $order, $skip, $limit); + } + + /** + * Create an evidence from a list of parameters. + * + * Requires no capability because evidence can be added in many situations under any user. + * + * @param int $userid The user id for which evidence is added. + * @param competency|int $competencyorid The competency, or its id for which evidence is added. + * @param context|int $contextorid The context in which the evidence took place. + * @param int $action The type of action to take on the competency. \core_competency\evidence::ACTION_*. + * @param string $descidentifier The strings identifier. + * @param string $desccomponent The strings component. + * @param mixed $desca Any arguments the string requires. + * @param bool $recommend When true, the user competency will be sent for review. + * @param string $url The url the evidence may link to. + * @param int $grade The grade, or scale ID item. + * @param int $actionuserid The ID of the user who took the action of adding the evidence. Null when system. + * This should be used when the action was taken by a real person, this will allow + * to keep track of all the evidence given by a certain person. + * @param string $note A note to attach to the evidence. + * @return evidence + * @throws coding_exception + * @throws invalid_persistent_exception + * @throws moodle_exception + */ + public static function add_evidence($userid, $competencyorid, $contextorid, $action, $descidentifier, $desccomponent, + $desca = null, $recommend = false, $url = null, $grade = null, $actionuserid = null, + $note = null) { + global $DB; + static::require_enabled(); + + // Some clearly important variable assignments right there. + $competencyid = $competencyorid; + $competency = null; + if (is_object($competencyid)) { + $competency = $competencyid; + $competencyid = $competency->get_id(); + } + $contextid = $contextorid; + $context = $contextorid; + if (is_object($contextorid)) { + $contextid = $contextorid->id; + } else { + $context = context::instance_by_id($contextorid); + } + $setucgrade = false; + $ucgrade = null; + $ucproficiency = null; + $usercompetencycourse = null; + + // Fetch or create the user competency. + $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); + if (!$usercompetency) { + $usercompetency = user_competency::create_relation($userid, $competencyid); + $usercompetency->create(); + } + + // What should we be doing? + switch ($action) { + + // Completing a competency. + case evidence::ACTION_COMPLETE: + // The logic here goes like this: + // + // if rating outside a course + // - set the default grade and proficiency ONLY if there is no current grade + // else we are in a course + // - set the defautl grade and proficiency in the course ONLY if there is no current grade in the course + // - then check the course settings to see if we should push the rating outside the course + // - if we should push it + // --- push it only if the user_competency (outside the course) has no grade + // Done. + + if ($grade !== null) { + throw new coding_exception("The grade MUST NOT be set with a 'completing' evidence."); + } + + // Fetch the default grade to attach to the evidence. + if (empty($competency)) { + $competency = new competency($competencyid); + } + list($grade, $proficiency) = $competency->get_default_grade(); + + // Add user_competency_course record when in a course or module. + if (in_array($context->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE))) { + $coursecontext = $context->get_course_context(); + $courseid = $coursecontext->instanceid; + $filterparams = array( + 'userid' => $userid, + 'competencyid' => $competencyid, + 'courseid' => $courseid + ); + // Fetch or create user competency course. + $usercompetencycourse = user_competency_course::get_record($filterparams); + if (!$usercompetencycourse) { + $usercompetencycourse = user_competency_course::create_relation($userid, $competencyid, $courseid); + $usercompetencycourse->create(); + } + // Only update the grade and proficiency if there is not already a grade. + if ($usercompetencycourse->get_grade() === null) { + // Set grade. + $usercompetencycourse->set_grade($grade); + // Set proficiency. + $usercompetencycourse->set_proficiency($proficiency); + } + + // Check the course settings to see if we should push to user plans. + $coursesettings = course_competency_settings::get_by_courseid($courseid); + $setucgrade = $coursesettings->get_pushratingstouserplans(); + + if ($setucgrade) { + // Only push to user plans if there is not already a grade. + if ($usercompetency->get_grade() !== null) { + $setucgrade = false; + } else { + $ucgrade = $grade; + $ucproficiency = $proficiency; + } + } + } else { + + // When completing the competency we fetch the default grade from the competency. But we only mark + // the user competency when a grade has not been set yet. Complete is an action to use with automated systems. + if ($usercompetency->get_grade() === null) { + $setucgrade = true; + $ucgrade = $grade; + $ucproficiency = $proficiency; + } + } + + break; + + // We override the grade, even overriding back to not set. + case evidence::ACTION_OVERRIDE: + $setucgrade = true; + $ucgrade = $grade; + if (empty($competency)) { + $competency = new competency($competencyid); + } + if ($ucgrade !== null) { + $ucproficiency = $competency->get_proficiency_of_grade($ucgrade); + } + + // Add user_competency_course record when in a course or module. + if (in_array($context->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE))) { + $coursecontext = $context->get_course_context(); + $courseid = $coursecontext->instanceid; + $filterparams = array( + 'userid' => $userid, + 'competencyid' => $competencyid, + 'courseid' => $courseid + ); + // Fetch or create user competency course. + $usercompetencycourse = user_competency_course::get_record($filterparams); + if (!$usercompetencycourse) { + $usercompetencycourse = user_competency_course::create_relation($userid, $competencyid, $courseid); + $usercompetencycourse->create(); + } + // Get proficiency. + $proficiency = $ucproficiency; + if ($proficiency === null) { + if (empty($competency)) { + $competency = new competency($competencyid); + } + $proficiency = $competency->get_proficiency_of_grade($grade); + } + // Set grade. + $usercompetencycourse->set_grade($grade); + // Set proficiency. + $usercompetencycourse->set_proficiency($proficiency); + + $coursesettings = course_competency_settings::get_by_courseid($courseid); + if (!$coursesettings->get_pushratingstouserplans()) { + $setucgrade = false; + } + } + + break; + + // Simply logging an evidence. + case evidence::ACTION_LOG: + if ($grade !== null) { + throw new coding_exception("The grade MUST NOT be set when 'logging' an evidence."); + } + break; + + // Whoops, this is not expected. + default: + throw new coding_exception('Unexpected action parameter when registering an evidence.'); + break; + } + + // Should we recommend? + if ($recommend && $usercompetency->get_status() == user_competency::STATUS_IDLE) { + $usercompetency->set_status(user_competency::STATUS_WAITING_FOR_REVIEW); + } + + // Setting the grade and proficiency for the user competency. + $wascompleted = false; + if ($setucgrade == true) { + if (!$usercompetency->get_proficiency() && $ucproficiency) { + $wascompleted = true; + } + $usercompetency->set_grade($ucgrade); + $usercompetency->set_proficiency($ucproficiency); + } + + // Prepare the evidence. + $record = new stdClass(); + $record->usercompetencyid = $usercompetency->get_id(); + $record->contextid = $contextid; + $record->action = $action; + $record->descidentifier = $descidentifier; + $record->desccomponent = $desccomponent; + $record->grade = $grade; + $record->actionuserid = $actionuserid; + $record->note = $note; + $evidence = new evidence(0, $record); + $evidence->set_desca($desca); + $evidence->set_url($url); + + // Validate both models, we should not operate on one if the other will not save. + if (!$usercompetency->is_valid()) { + throw new invalid_persistent_exception($usercompetency->get_errors()); + } else if (!$evidence->is_valid()) { + throw new invalid_persistent_exception($evidence->get_errors()); + } + + // Save the user_competency_course record. + if ($usercompetencycourse !== null) { + // Validate and update. + if (!$usercompetencycourse->is_valid()) { + throw new invalid_persistent_exception($usercompetencycourse->get_errors()); + } + $usercompetencycourse->update(); + } + + // Finally save. Pheww! + $usercompetency->update(); + $evidence->create(); + + // Trigger the evidence_created event. + \core\event\competency_evidence_created::create_from_evidence($evidence, $usercompetency, $recommend)->trigger(); + + // The competency was marked as completed, apply the rules. + if ($wascompleted) { + self::apply_competency_rules_from_usercompetency($usercompetency, $competency); + } + + return $evidence; + } + + /** + * Read an evidence. + * @param int $evidenceid The evidence ID. + * @return evidence + */ + public static function read_evidence($evidenceid) { + static::require_enabled(); + + $evidence = new evidence($evidenceid); + $uc = new user_competency($evidence->get_usercompetencyid()); + if (!$uc->can_read()) { + throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview', + 'nopermissions', ''); + } + + return $evidence; + } + + /** + * Delete an evidence. + * + * @param evidence|int $evidenceorid The evidence, or its ID. + * @return bool + */ + public static function delete_evidence($evidenceorid) { + $evidence = $evidenceorid; + if (!is_object($evidence)) { + $evidence = new evidence($evidenceorid); + } + + $uc = new user_competency($evidence->get_usercompetencyid()); + if (!evidence::can_delete_user($uc->get_userid())) { + throw new required_capability_exception($uc->get_context(), 'moodle/competency:evidencedelete', 'nopermissions', ''); + } + + return $evidence->delete(); + } + + /** + * Apply the competency rules from a user competency. + * + * The user competency passed should be one that was recently marked as complete. + * A user competency is considered 'complete' when it's proficiency value is true. + * + * This method will check if the parent of this usercompetency's competency has any + * rules and if so will see if they match. When matched it will take the required + * step to add evidence and trigger completion, etc... + * + * @param user_competency $usercompetency The user competency recently completed. + * @param competency|null $competency The competency of the user competency, useful to avoid unnecessary read. + * @return void + */ + protected static function apply_competency_rules_from_usercompetency(user_competency $usercompetency, + competency $competency = null) { + + // Perform some basic checks. + if (!$usercompetency->get_proficiency()) { + throw new coding_exception('The user competency passed is not completed.'); + } + if ($competency === null) { + $competency = $usercompetency->get_competency(); + } + if ($competency->get_id() != $usercompetency->get_competencyid()) { + throw new coding_exception('Mismatch between user competency and competency.'); + } + + // Fetch the parent. + $parent = $competency->get_parent(); + if ($parent === null) { + return; + } + + // The parent should have a rule, and a meaningful outcome. + $ruleoutcome = $parent->get_ruleoutcome(); + if ($ruleoutcome == competency::OUTCOME_NONE) { + return; + } + $rule = $parent->get_rule_object(); + if ($rule === null) { + return; + } + + // Fetch or create the user competency for the parent. + $userid = $usercompetency->get_userid(); + $parentuc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $parent->get_id())); + if (!$parentuc) { + $parentuc = user_competency::create_relation($userid, $parent->get_id()); + $parentuc->create(); + } + + // Does the rule match? + if (!$rule->matches($parentuc)) { + return; + } + + // Figuring out what to do. + $recommend = false; + if ($ruleoutcome == competency::OUTCOME_EVIDENCE) { + $action = evidence::ACTION_LOG; + + } else if ($ruleoutcome == competency::OUTCOME_RECOMMEND) { + $action = evidence::ACTION_LOG; + $recommend = true; + + } else if ($ruleoutcome == competency::OUTCOME_COMPLETE) { + $action = evidence::ACTION_COMPLETE; + + } else { + throw new moodle_exception('Unexpected rule outcome: ' + $ruleoutcome); + } + + // Finally add an evidence. + static::add_evidence( + $userid, + $parent, + $parent->get_context()->id, + $action, + 'evidence_competencyrule', + 'core_competency', + null, + $recommend + ); + } + + /** + * Observe when a course module is marked as completed. + * + * Note that the user being logged in while this happens may be anyone. + * Do not rely on capability checks here! + * + * @param \core\event\course_module_completion_updated $event + * @return void + */ + public static function observe_course_module_completion_updated(\core\event\course_module_completion_updated $event) { + if (!static::is_enabled()) { + return; + } + + $eventdata = $event->get_record_snapshot('course_modules_completion', $event->objectid); + + if ($eventdata->completionstate == COMPLETION_COMPLETE + || $eventdata->completionstate == COMPLETION_COMPLETE_PASS) { + $coursemodulecompetencies = course_module_competency::list_course_module_competencies($eventdata->coursemoduleid); + + $cm = get_coursemodule_from_id(null, $eventdata->coursemoduleid); + $fastmodinfo = get_fast_modinfo($cm->course)->cms[$cm->id]; + + $cmname = $fastmodinfo->name; + $url = $fastmodinfo->url; + + foreach ($coursemodulecompetencies as $coursemodulecompetency) { + $outcome = $coursemodulecompetency->get_ruleoutcome(); + $action = null; + $recommend = false; + $strdesc = 'evidence_coursemodulecompleted'; + + if ($outcome == course_module_competency::OUTCOME_EVIDENCE) { + $action = evidence::ACTION_LOG; + + } else if ($outcome == course_module_competency::OUTCOME_RECOMMEND) { + $action = evidence::ACTION_LOG; + $recommend = true; + + } else if ($outcome == course_module_competency::OUTCOME_COMPLETE) { + $action = evidence::ACTION_COMPLETE; + + } else { + throw new moodle_exception('Unexpected rule outcome: ' + $outcome); + } + + static::add_evidence( + $event->relateduserid, + $coursemodulecompetency->get_competencyid(), + $event->contextid, + $action, + $strdesc, + 'core_competency', + $cmname, + $recommend, + $url + ); + } + } + } + + /** + * Observe when a course is marked as completed. + * + * Note that the user being logged in while this happens may be anyone. + * Do not rely on capability checks here! + * + * @param \core\event\course_completed $event + * @return void + */ + public static function observe_course_completed(\core\event\course_completed $event) { + if (!static::is_enabled()) { + return; + } + + $sql = 'courseid = :courseid AND ruleoutcome != :nooutcome'; + $params = array( + 'courseid' => $event->courseid, + 'nooutcome' => course_competency::OUTCOME_NONE + ); + $coursecompetencies = course_competency::get_records_select($sql, $params); + + $course = get_course($event->courseid); + $courseshortname = format_string($course->shortname, null, array('context' => $event->contextid)); + + foreach ($coursecompetencies as $coursecompetency) { + + $outcome = $coursecompetency->get_ruleoutcome(); + $action = null; + $recommend = false; + $strdesc = 'evidence_coursecompleted'; + + if ($outcome == course_competency::OUTCOME_EVIDENCE) { + $action = evidence::ACTION_LOG; + + } else if ($outcome == course_competency::OUTCOME_RECOMMEND) { + $action = evidence::ACTION_LOG; + $recommend = true; + + } else if ($outcome == course_competency::OUTCOME_COMPLETE) { + $action = evidence::ACTION_COMPLETE; + + } else { + throw new moodle_exception('Unexpected rule outcome: ' + $outcome); + } + + static::add_evidence( + $event->relateduserid, + $coursecompetency->get_competencyid(), + $event->contextid, + $action, + $strdesc, + 'core_competency', + $courseshortname, + $recommend, + $event->get_url() + ); + } + } + + /** + * Action to perform when a course module is deleted. + * + * Do not call this directly, this is reserved for core use. + * + * @param stdClass $cm The CM object. + * @return void + */ + public static function hook_course_module_deleted(stdClass $cm) { + global $DB; + $DB->delete_records(course_module_competency::TABLE, array('cmid' => $cm->id)); + } + + /** + * Action to perform when a course is deleted. + * + * Do not call this directly, this is reserved for core use. + * + * @param stdClass $course The course object. + * @return void + */ + public static function hook_course_deleted(stdClass $course) { + global $DB; + $DB->delete_records(course_competency::TABLE, array('courseid' => $course->id)); + $DB->delete_records(course_competency_settings::TABLE, array('courseid' => $course->id)); + $DB->delete_records(user_competency_course::TABLE, array('courseid' => $course->id)); + } + + /** + * Action to perform when a course is being reset. + * + * Do not call this directly, this is reserved for core use. + * + * @param int $courseid The course ID. + * @return void + */ + public static function hook_course_reset_competency_ratings($courseid) { + global $DB; + $DB->delete_records(user_competency_course::TABLE, array('courseid' => $courseid)); + } + + /** + * Manually grade a user competency. + * + * @param int $userid + * @param int $competencyid + * @param int $grade + * @param string $note A note to attach to the evidence + * @return array of \core_competency\user_competency + */ + public static function grade_competency($userid, $competencyid, $grade, $note = null) { + global $USER; + static::require_enabled(); + + $uc = static::get_user_competency($userid, $competencyid); + $context = $uc->get_context(); + if (!user_competency::can_grade_user($uc->get_userid())) { + throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', ''); + } + + // Throws exception if competency not in plan. + $competency = $uc->get_competency(); + $competencycontext = $competency->get_context(); + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), + $competencycontext)) { + throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', ''); + } + + $action = evidence::ACTION_OVERRIDE; + $desckey = 'evidence_manualoverride'; + + $result = self::add_evidence($uc->get_userid(), + $competency, + $context->id, + $action, + $desckey, + 'core_competency', + null, + false, + null, + $grade, + $USER->id, + $note); + if ($result) { + $uc->read(); + $event = \core\event\competency_user_competency_rated::create_from_user_competency($uc); + $event->trigger(); + } + return $result; + } + + /** + * Manually grade a user competency from the plans page. + * + * @param mixed $planorid + * @param int $competencyid + * @param int $grade + * @param string $note A note to attach to the evidence + * @return array of \core_competency\user_competency + */ + public static function grade_competency_in_plan($planorid, $competencyid, $grade, $note = null) { + global $USER; + static::require_enabled(); + + $plan = $planorid; + if (!is_object($planorid)) { + $plan = new plan($planorid); + } + + $context = $plan->get_context(); + if (!user_competency::can_grade_user($plan->get_userid())) { + throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', ''); + } + + // Throws exception if competency not in plan. + $competency = $plan->get_competency($competencyid); + $competencycontext = $competency->get_context(); + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), + $competencycontext)) { + throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', ''); + } + + $action = evidence::ACTION_OVERRIDE; + $desckey = 'evidence_manualoverrideinplan'; + + $result = self::add_evidence($plan->get_userid(), + $competency, + $context->id, + $action, + $desckey, + 'core_competency', + $plan->get_name(), + false, + null, + $grade, + $USER->id, + $note); + if ($result) { + $uc = static::get_user_competency($plan->get_userid(), $competency->get_id()); + $event = \core\event\competency_user_competency_rated_in_plan::create_from_user_competency($uc, $plan->get_id()); + $event->trigger(); + } + return $result; + } + + /** + * Manually grade a user course competency from the course page. + * + * This may push the rating to the user competency + * if the course is configured this way. + * + * @param mixed $courseorid + * @param int $userid + * @param int $competencyid + * @param int $grade + * @param string $note A note to attach to the evidence + * @return array of \core_competency\user_competency + */ + public static function grade_competency_in_course($courseorid, $userid, $competencyid, $grade, $note = null) { + global $USER, $DB; + static::require_enabled(); + + $course = $courseorid; + if (!is_object($courseorid)) { + $course = $DB->get_record('course', array('id' => $courseorid)); + } + $context = context_course::instance($course->id); + + // Check that we can view the user competency details in the course. + if (!user_competency::can_read_user_in_course($userid, $course->id)) { + throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); + } + + // Validate the permission to grade. + if (!user_competency::can_grade_user_in_course($userid, $course->id)) { + throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', ''); + } + + // Check that competency is in course and visible to the current user. + $competency = course_competency::get_competency($course->id, $competencyid); + $competencycontext = $competency->get_context(); + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), + $competencycontext)) { + throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', ''); + } + + // Check that the user is enrolled in the course, and is "gradable". + if (!is_enrolled($context, $userid, 'moodle/competency:coursecompetencygradable')) { + throw new coding_exception('The competency may not be rated at this time.'); + } + + $action = evidence::ACTION_OVERRIDE; + $desckey = 'evidence_manualoverrideincourse'; + + $result = self::add_evidence($userid, + $competency, + $context->id, + $action, + $desckey, + 'core_competency', + $context->get_context_name(), + false, + null, + $grade, + $USER->id, + $note); + if ($result) { + $all = user_competency_course::get_multiple($userid, $course->id, array($competency->get_id())); + $uc = reset($all); + $event = \core\event\competency_user_competency_rated_in_course::create_from_user_competency_course($uc); + $event->trigger(); + } + return $result; + } + + /** + * Count the plans in the template, filtered by status. + * + * Requires moodle/competency:templateview capability at the system context. + * + * @param mixed $templateorid The id or the template. + * @param int $status One of the plan status constants (or 0 for all plans). + * @return int + */ + public static function count_plans_for_template($templateorid, $status = 0) { + static::require_enabled(); + $template = $templateorid; + if (!is_object($template)) { + $template = new template($template); + } + + // First we do a permissions check. + if (!$template->can_read()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + return plan::count_records_for_template($template->get_id(), $status); + } + + /** + * Count the user-completency-plans in the template, optionally filtered by proficiency. + * + * Requires moodle/competency:templateview capability at the system context. + * + * @param mixed $templateorid The id or the template. + * @param mixed $proficiency If true, filter by proficiency, if false filter by not proficient, if null - no filter. + * @return int + */ + public static function count_user_competency_plans_for_template($templateorid, $proficiency = null) { + static::require_enabled(); + $template = $templateorid; + if (!is_object($template)) { + $template = new template($template); + } + + // First we do a permissions check. + if (!$template->can_read()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + return user_competency_plan::count_records_for_template($template->get_id(), $proficiency); + } + + /** + * List the plans in the template, filtered by status. + * + * Requires moodle/competency:templateview capability at the system context. + * + * @param mixed $templateorid The id or the template. + * @param int $status One of the plan status constants (or 0 for all plans). + * @param int $skip The number of records to skip + * @param int $limit The max number of records to return + * @return plan[] + */ + public static function list_plans_for_template($templateorid, $status = 0, $skip = 0, $limit = 100) { + $template = $templateorid; + if (!is_object($template)) { + $template = new template($template); + } + + // First we do a permissions check. + if (!$template->can_read()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + return plan::get_records_for_template($template->get_id(), $status, $skip, $limit); + } + + /** + * Get the most often not completed competency for this course. + * + * Requires moodle/competency:coursecompetencyview capability at the course context. + * + * @param int $courseid The course id + * @param int $skip The number of records to skip + * @param int $limit The max number of records to return + * @return competency[] + */ + public static function get_least_proficient_competencies_for_course($courseid, $skip = 0, $limit = 100) { + static::require_enabled(); + $coursecontext = context_course::instance($courseid); + + if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $coursecontext)) { + throw new required_capability_exception($coursecontext, 'moodle/competency:competencyview', 'nopermissions', ''); + } + + return user_competency_course::get_least_proficient_competencies_for_course($courseid, $skip, $limit); + } + + /** + * Get the most often not completed competency for this template. + * + * Requires moodle/competency:templateview capability at the system context. + * + * @param mixed $templateorid The id or the template. + * @param int $skip The number of records to skip + * @param int $limit The max number of records to return + * @return competency[] + */ + public static function get_least_proficient_competencies_for_template($templateorid, $skip = 0, $limit = 100) { + static::require_enabled(); + $template = $templateorid; + if (!is_object($template)) { + $template = new template($template); + } + + // First we do a permissions check. + if (!$template->can_read()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + return user_competency_plan::get_least_proficient_competencies_for_template($template->get_id(), $skip, $limit); + } + + /** + * Template event viewed. + * + * Requires moodle/competency:templateview capability at the system context. + * + * @param mixed $templateorid The id or the template. + * @return boolean + */ + public static function template_viewed($templateorid) { + static::require_enabled(); + $template = $templateorid; + if (!is_object($template)) { + $template = new template($template); + } + + // First we do a permissions check. + if (!$template->can_read()) { + throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', + 'nopermissions', ''); + } + + // Trigger a template viewed event. + \core\event\competency_template_viewed::create_from_template($template)->trigger(); + + return true; + } + + /** + * Get the competency settings for a course. + * + * Requires moodle/competency:coursecompetencyview capability at the course context. + * + * @param int $courseid The course id + * @return course_competency_settings + */ + public static function read_course_competency_settings($courseid) { + static::require_enabled(); + + // First we do a permissions check. + if (!course_competency_settings::can_read($courseid)) { + $context = context_course::instance($courseid); + throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', ''); + } + + return course_competency_settings::get_by_courseid($courseid); + } + + /** + * Update the competency settings for a course. + * + * Requires moodle/competency:coursecompetencyconfigure capability at the course context. + * + * @param int $courseid The course id + * @param stdClass $settings List of settings. The only valid setting ATM is pushratginstouserplans (boolean). + * @return bool + */ + public static function update_course_competency_settings($courseid, $settings) { + static::require_enabled(); + + $settings = (object) $settings; + + // Get all the valid settings. + $pushratingstouserplans = isset($settings->pushratingstouserplans) ? $settings->pushratingstouserplans : false; + + // First we do a permissions check. + if (!course_competency_settings::can_manage_course($courseid)) { + $context = context_course::instance($courseid); + throw new required_capability_exception($context, 'moodle/competency:coursecompetencyconfigure', 'nopermissions', ''); + } + + $exists = course_competency_settings::get_record(array('courseid' => $courseid)); + + // Now update or insert. + if ($exists) { + $settings = $exists; + $settings->set_pushratingstouserplans($pushratingstouserplans); + return $settings->update(); + } else { + $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $pushratingstouserplans); + $settings = new course_competency_settings(0, $data); + $result = $settings->create(); + return !empty($result); + } + } + + + /** + * Function used to return a list of users where the given user has a particular capability. + * + * This is used e.g. to find all the users where someone is able to manage their learning plans, + * it also would be useful for mentees etc. + * + * @param string $capability - The capability string we are filtering for. If '' is passed, + * an always matching filter is returned. + * @param int $userid - The user id we are using for the access checks. Defaults to current user. + * @param int $type - The type of named params to return (passed to $DB->get_in_or_equal). + * @param string $prefix - The type prefix for the db table (passed to $DB->get_in_or_equal). + * @return list($sql, $params) Same as $DB->get_in_or_equal(). + * @todo MDL-52243 Move this function to lib/accesslib.php + */ + public static function filter_users_with_capability_on_user_context_sql($capability, $userid = 0, $type = SQL_PARAMS_QM, + $prefix='param') { + + global $USER, $DB; + $allresultsfilter = array('> 0', array()); + $noresultsfilter = array('= -1', array()); + + if (empty($capability)) { + return $allresultsfilter; + } + + if (!$capinfo = get_capability_info($capability)) { + throw new coding_exception('Capability does not exist: ' . $capability); + } + + if (empty($userid)) { + $userid = $USER->id; + } + + // Make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are. + if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) { + if (isguestuser($userid) or $userid == 0) { + return $noresultsfilter; + } + } + + if (is_siteadmin($userid)) { + // No filtering for site admins. + return $allresultsfilter; + } + + // Check capability on system level. + $syscontext = context_system::instance(); + $hassystem = has_capability($capability, $syscontext, $userid); + + $access = get_user_access_sitewide($userid); + // Build up a list of level 2 contexts (candidates to be user context). + $filtercontexts = array(); + foreach ($access['ra'] as $path => $role) { + $parts = explode('/', $path); + if (count($parts) == 3) { + $filtercontexts[$parts[2]] = $parts[2]; + } else if (count($parts) > 3) { + // We know this is not a user context because there is another path with more than 2 levels. + unset($filtercontexts[$parts[2]]); + } + } + + // Add all contexts in which a role may be overidden. + foreach ($access['rdef'] as $pathandroleid => $def) { + $matches = array(); + if (!isset($def[$capability])) { + // The capability is not mentioned, we can ignore. + continue; + } + + list($contextpath, $roleid) = explode(':', $pathandroleid, 2); + $parts = explode('/', $contextpath); + if (count($parts) != 3) { + // Only get potential user contexts, they only ever have 2 slashes /parentId/Id. + continue; + } + + $filtercontexts[$parts[2]] = $parts[2]; + } + + // No interesting contexts - return all or no results. + if (empty($filtercontexts)) { + if ($hassystem) { + return $allresultsfilter; + } else { + return $noresultsfilter; + } + } + // Fetch all interesting contexts for further examination. + list($insql, $params) = $DB->get_in_or_equal($filtercontexts, SQL_PARAMS_NAMED); + $params['level'] = CONTEXT_USER; + $fields = context_helper::get_preload_record_columns_sql('ctx'); + $interestingcontexts = $DB->get_recordset_sql('SELECT ' . $fields . ' + FROM {context} ctx + WHERE ctx.contextlevel = :level + AND ctx.id ' . $insql . ' + ORDER BY ctx.id', $params); + if ($hassystem) { + // If allowed at system, search for exceptions prohibiting the capability at user context. + $excludeusers = array(); + foreach ($interestingcontexts as $contextrecord) { + $candidateuserid = $contextrecord->ctxinstance; + context_helper::preload_from_record($contextrecord); + $usercontext = context_user::instance($candidateuserid); + // Has capability should use the data already preloaded. + if (!has_capability($capability, $usercontext, $userid)) { + $excludeusers[$candidateuserid] = $candidateuserid; + } + } + + // Construct SQL excluding users with this role assigned for this user. + if (empty($excludeusers)) { + $interestingcontexts->close(); + return $allresultsfilter; + } + list($sql, $params) = $DB->get_in_or_equal($excludeusers, $type, $prefix, false); + } else { + // If not allowed at system, search for exceptions allowing the capability at user context. + $allowusers = array(); + foreach ($interestingcontexts as $contextrecord) { + $candidateuserid = $contextrecord->ctxinstance; + context_helper::preload_from_record($contextrecord); + $usercontext = context_user::instance($candidateuserid); + // Has capability should use the data already preloaded. + if (has_capability($capability, $usercontext, $userid)) { + $allowusers[$candidateuserid] = $candidateuserid; + } + } + + // Construct SQL excluding users with this role assigned for this user. + if (empty($allowusers)) { + $interestingcontexts->close(); + return $noresultsfilter; + } + list($sql, $params) = $DB->get_in_or_equal($allowusers, $type, $prefix); + } + $interestingcontexts->close(); + + // Return the goods!. + return array($sql, $params); + } + +} diff --git a/competency/classes/competency.php b/competency/classes/competency.php new file mode 100644 index 00000000000..101d29a09df --- /dev/null +++ b/competency/classes/competency.php @@ -0,0 +1,853 @@ +. + +/** + * Class for loading/storing competencies from the DB. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use context_system; +use lang_string; +use stdClass; + +require_once($CFG->libdir . '/grade/grade_scale.php'); + +/** + * Class for loading/storing competencies from the DB. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency extends persistent { + + const TABLE = 'competency'; + + /** Outcome none. */ + const OUTCOME_NONE = 0; + /** Outcome evidence. */ + const OUTCOME_EVIDENCE = 1; + /** Outcome complete. */ + const OUTCOME_COMPLETE = 2; + /** Outcome recommend. */ + const OUTCOME_RECOMMEND = 3; + + /** @var competency Object before update. */ + protected $beforeupdate = null; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'shortname' => array( + 'type' => PARAM_TEXT + ), + 'idnumber' => array( + 'type' => PARAM_TEXT + ), + 'description' => array( + 'default' => '', + 'type' => PARAM_RAW + ), + 'descriptionformat' => array( + 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), + 'type' => PARAM_INT, + 'default' => FORMAT_HTML + ), + 'sortorder' => array( + 'default' => null, + 'type' => PARAM_INT + ), + 'parentid' => array( + 'default' => 0, + 'type' => PARAM_INT + ), + 'path' => array( + 'default' => '/0/', + 'type' => PARAM_RAW + ), + 'ruleoutcome' => array( + 'choices' => array(self::OUTCOME_NONE, self::OUTCOME_EVIDENCE, self::OUTCOME_COMPLETE, self::OUTCOME_RECOMMEND), + 'default' => self::OUTCOME_NONE, + 'type' => PARAM_INT + ), + 'ruletype' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED + ), + 'ruleconfig' => array( + 'default' => null, + 'type' => PARAM_RAW, + 'null' => NULL_ALLOWED + ), + 'scaleid' => array( + 'default' => null, + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED + ), + 'scaleconfiguration' => array( + 'default' => null, + 'type' => PARAM_RAW, + 'null' => NULL_ALLOWED + ), + 'competencyframeworkid' => array( + 'default' => 0, + 'type' => PARAM_INT + ), + ); + } + + /** + * Hook to execute before validate. + * + * @return void + */ + protected function before_validate() { + $this->beforeupdate = null; + $this->newparent = null; + + // During update. + if ($this->get_id()) { + $this->beforeupdate = new competency($this->get_id()); + + // The parent ID has changed. + if ($this->beforeupdate->get_parentid() != $this->get_parentid()) { + $this->newparent = $this->get_parent(); + + // Update path and sortorder. + $this->set_new_path($this->newparent); + $this->set_new_sortorder(); + } + + } else { + // During create. + + $this->set_new_path(); + // Always generate new sortorder when we create new competency. + $this->set_new_sortorder(); + + } + } + + /** + * Hook to execute after an update. + * + * @param bool $result Whether or not the update was successful. + * @return void + */ + protected function after_update($result) { + global $DB; + + if (!$result) { + $this->beforeupdate = null; + return; + } + + // The parent ID has changed, we need to fix all the paths of the children. + if ($this->beforeupdate->get_parentid() != $this->get_parentid()) { + $beforepath = $this->beforeupdate->get_path() . $this->get_id() . '/'; + + $like = $DB->sql_like('path', '?'); + $likesearch = $DB->sql_like_escape($beforepath) . '%'; + + $table = '{' . self::TABLE . '}'; + $sql = "UPDATE $table SET path = REPLACE(path, ?, ?) WHERE " . $like; + $DB->execute($sql, array( + $beforepath, + $this->get_path() . $this->get_id() . '/', + $likesearch + )); + + // Resolving sortorder holes left after changing parent. + $table = '{' . self::TABLE . '}'; + $sql = "UPDATE $table SET sortorder = sortorder -1 " + . " WHERE competencyframeworkid = ? AND parentid = ? AND sortorder > ?"; + $DB->execute($sql, array($this->get_competencyframeworkid(), + $this->beforeupdate->get_parentid(), + $this->beforeupdate->get_sortorder() + )); + } + + $this->beforeupdate = null; + } + + + /** + * Hook to execute after a delete. + * + * @param bool $result Whether or not the delete was successful. + * @return void + */ + protected function after_delete($result) { + global $DB; + if (!$result) { + return; + } + + // Resolving sortorder holes left after delete. + $table = '{' . self::TABLE . '}'; + $sql = "UPDATE $table SET sortorder = sortorder -1 WHERE competencyframeworkid = ? AND parentid = ? AND sortorder > ?"; + $DB->execute($sql, array($this->get_competencyframeworkid(), $this->get_parentid(), $this->get_sortorder())); + } + + /** + * Extracts the default grade from the scale configuration. + * + * Returns an array where the first element is the grade, and the second + * is a boolean representing whether or not this grade is considered 'proficient'. + * + * @return array(int grade, bool proficient) + */ + public function get_default_grade() { + $scaleid = $this->get_scaleid(); + $scaleconfig = $this->get_scaleconfiguration(); + if ($scaleid === null) { + $scaleconfig = $this->get_framework()->get_scaleconfiguration(); + } + return competency_framework::get_default_grade_from_scale_configuration($scaleconfig); + } + + /** + * Get the competency framework. + * + * @return competency_framework + */ + public function get_framework() { + return new competency_framework($this->get_competencyframeworkid()); + } + + /** + * Get the competency level. + * + * @return int + */ + public function get_level() { + $path = $this->get_path(); + $path = trim($path, '/'); + return substr_count($path, '/') + 1; + } + + /** + * Return the parent competency. + * + * @return null|competency + */ + public function get_parent() { + $parentid = $this->get_parentid(); + if (!$parentid) { + return null; + } + return new competency($parentid); + } + + /** + * Extracts the proficiency of a grade from the scale configuration. + * + * @param int $grade The grade (scale item ID). + * @return array(int grade, bool proficient) + */ + public function get_proficiency_of_grade($grade) { + $scaleid = $this->get_scaleid(); + $scaleconfig = $this->get_scaleconfiguration(); + if ($scaleid === null) { + $scaleconfig = $this->get_framework()->get_scaleconfiguration(); + } + return competency_framework::get_proficiency_of_grade_from_scale_configuration($scaleconfig, $grade); + } + + /** + * Return the related competencies. + * + * @return competency[] + */ + public function get_related_competencies() { + return related_competency::get_related_competencies($this->get_id()); + } + + /** + * Get the rule object. + * + * @return null|competency_rule + */ + public function get_rule_object() { + $rule = $this->get_ruletype(); + + if (!$rule || !is_subclass_of($rule, 'core_competency\\competency_rule')) { + // Double check that the rule is extending the right class to avoid bad surprises. + return null; + } + + return new $rule($this); + } + + /** + * Return the scale. + * + * @return \grade_scale + */ + public function get_scale() { + $scaleid = $this->get_scaleid(); + if ($scaleid === null) { + return $this->get_framework()->get_scale(); + } + $scale = \grade_scale::fetch(array('id' => $scaleid)); + $scale->load_items(); + return $scale; + } + + /** + * Returns true when the competency has user competencies. + * + * This is useful to determine if the competency, or part of it, should be locked down. + * + * @return boolean + */ + public function has_user_competencies() { + return user_competency::has_records_for_competency($this->get_id()) || + user_competency_plan::has_records_for_competency($this->get_id()); + } + + /** + * Check if the competency is the parent of passed competencies. + * + * @param array $ids IDs of supposedly direct children. + * @return boolean + */ + public function is_parent_of(array $ids) { + global $DB; + + list($insql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); + $params['parentid'] = $this->get_id(); + + return $DB->count_records_select(self::TABLE, "id $insql AND parentid = :parentid", $params) == count($ids); + } + + /** + * Reset the rule. + * + * @return void + */ + public function reset_rule() { + $this->set_ruleoutcome(static::OUTCOME_NONE); + $this->set_ruletype(null); + $this->set_ruleconfig(null); + } + + /** + * Helper method to set the path. + * + * @param competency $parent The parent competency object. + * @return void + */ + protected function set_new_path(competency $parent = null) { + $path = '/0/'; + if ($this->get_parentid()) { + $parent = $parent !== null ? $parent : $this->get_parent(); + $path = $parent->get_path() . $this->get_parentid() . '/'; + } + $this->set('path', $path); + } + + /** + * Helper method to set the sortorder. + * + * @return void + */ + protected function set_new_sortorder() { + $search = array('parentid' => $this->get_parentid(), 'competencyframeworkid' => $this->get_competencyframeworkid()); + $this->set('sortorder', $this->count_records($search)); + } + + /** + * This does a specialised search that finds all nodes in the tree with matching text on any text like field, + * and returns this node and all its parents in a displayable sort order. + * + * @param string $searchtext The text to search for. + * @param int $competencyframeworkid The competency framework to limit the search. + * @return persistent[] + */ + public static function search($searchtext, $competencyframeworkid) { + global $DB; + + $like1 = $DB->sql_like('shortname', ':like1', false); + $like2 = $DB->sql_like('idnumber', ':like2', false); + $like3 = $DB->sql_like('description', ':like3', false); + + $params = array( + 'like1' => '%' . $DB->sql_like_escape($searchtext) . '%', + 'like2' => '%' . $DB->sql_like_escape($searchtext) . '%', + 'like3' => '%' . $DB->sql_like_escape($searchtext) . '%', + 'frameworkid' => $competencyframeworkid + ); + + $sql = 'competencyframeworkid = :frameworkid AND ((' . $like1 . ') OR (' . $like2 . ') OR (' . $like3 . '))'; + $records = $DB->get_records_select(self::TABLE, $sql, $params, 'path, sortorder ASC', '*'); + + // Now get all the parents. + $parents = array(); + foreach ($records as $record) { + $split = explode('/', trim($record->path, '/')); + foreach ($split as $parent) { + $parents[intval($parent)] = true; + } + } + $parents = array_keys($parents); + + // Skip ones we already fetched. + foreach ($parents as $idx => $parent) { + if ($parent == 0 || isset($records[$parent])) { + unset($parents[$idx]); + } + } + + if (count($parents)) { + list($parentsql, $parentparams) = $DB->get_in_or_equal($parents, SQL_PARAMS_NAMED); + + $parentrecords = $DB->get_records_select(self::TABLE, 'id ' . $parentsql, + $parentparams, 'path, sortorder ASC', '*'); + + foreach ($parentrecords as $id => $record) { + $records[$id] = $record; + } + } + + $instances = array(); + // Convert to instances of this class. + foreach ($records as $record) { + $newrecord = new static(0, $record); + $instances[$newrecord->get_id()] = $newrecord; + } + return $instances; + } + + /** + * Validate the competency framework ID. + * + * @param int $value The framework ID. + * @return true|lang_string + */ + protected function validate_competencyframeworkid($value) { + + // During update. + if ($this->get_id()) { + + // Ensure that we are not trying to move the competency across frameworks. + if ($this->beforeupdate->get_competencyframeworkid() != $value) { + return new lang_string('invaliddata', 'error'); + } + + } else { + // During create. + + // Check that the framework exists. + if (!competency_framework::record_exists($value)) { + return new lang_string('invaliddata', 'error'); + } + } + + return true; + } + + /** + * Validate the ID number. + * + * @param string $value The ID number. + * @return true|lang_string + */ + protected function validate_idnumber($value) { + global $DB; + $sql = 'idnumber = :idnumber AND competencyframeworkid = :competencyframeworkid AND id <> :id'; + $params = array( + 'id' => $this->get_id(), + 'idnumber' => $value, + 'competencyframeworkid' => $this->get_competencyframeworkid() + ); + if ($DB->record_exists_select(self::TABLE, $sql, $params)) { + return new lang_string('idnumbertaken', 'error'); + } + return true; + } + + /** + * Validate the path. + * + * @param string $value The path. + * @return true|lang_string + */ + protected function validate_path($value) { + + // The last item should be the parent ID. + $id = $this->get_parentid(); + if (substr($value, -(strlen($id) + 2)) != '/' . $id . '/') { + return new lang_string('invaliddata', 'error'); + + } else if (!preg_match('@/([0-9]+/)+@', $value)) { + // The format of the path is not correct. + return new lang_string('invaliddata', 'error'); + + } else if ((substr_count($value, '/') - 1) > competency_framework::get_taxonomies_max_level()) { + // Validate the depth of the path. + return new lang_string('invaliddata', 'error'); + } + + return true; + } + + /** + * Validate the parent ID. + * + * @param string $value The ID. + * @return true|lang_string + */ + protected function validate_parentid($value) { + + // Check that the parent exists. But only if we don't have it already, and we actually have a parent. + if (!empty($value) && !$this->newparent && !self::record_exists($value)) { + return new lang_string('invaliddata', 'error'); + } + + // During update. + if ($this->get_id()) { + + // If there is a new parent. + if ($this->beforeupdate->get_parentid() != $value && $this->newparent) { + + // Check that the new parent belongs to the same framework. + if ($this->newparent->get_competencyframeworkid() != $this->get_competencyframeworkid()) { + return new lang_string('invaliddata', 'error'); + } + } + } + + return true; + } + + /** + * Validate the rule. + * + * @param string $value The ID. + * @return true|lang_string + */ + protected function validate_ruletype($value) { + if ($value === null) { + return true; + } + + if (!class_exists($value) || !is_subclass_of($value, 'core_competency\\competency_rule')) { + return new lang_string('invaliddata', 'error'); + } + + return true; + } + + /** + * Validate the rule config. + * + * @param string $value The ID. + * @return true|lang_string + */ + protected function validate_ruleconfig($value) { + $rule = $this->get_rule_object(); + + // We don't have a rule. + if (empty($rule)) { + if ($value === null) { + // No config, perfect. + return true; + } + // Config but no rules, whoops! + return new lang_string('invaliddata', 'error'); + } + + $valid = $rule->validate_config($value); + if ($valid !== true) { + // Whoops! + return new lang_string('invaliddata', 'error'); + } + + return true; + } + + /** + * Validate the scale ID. + * + * Note that the value for a scale can never be 0, null has to be used when + * the framework's scale has to be used. + * + * @param int $value + * @return true|lang_string + */ + protected function validate_scaleid($value) { + global $DB; + + if ($value === null) { + return true; + } + + // Always validate that the scale exists. + if (!$DB->record_exists_select('scale', 'id = :id', array('id' => $value))) { + return new lang_string('invalidscaleid', 'error'); + } + + // During update. + if ($this->get_id()) { + + // Validate that we can only change the scale when it is not used yet. + if ($this->beforeupdate->get_scaleid() != $value) { + if ($this->has_user_competencies()) { + return new lang_string('errorscalealreadyused', 'core_competency'); + } + } + + } + + return true; + } + + /** + * Validate the scale configuration. + * + * This logic is adapted from {@link \core_competency\competency_framework::validate_scaleconfiguration()}. + * + * @param string $value The scale configuration. + * @return bool|lang_string + */ + protected function validate_scaleconfiguration($value) { + $scaleid = $this->get('scaleid'); + if ($scaleid === null && $value === null) { + return true; + } + + $scaledefaultselected = false; + $proficientselected = false; + $scaleconfigurations = json_decode($value); + + if (is_array($scaleconfigurations)) { + + // The first element of the array contains the scale ID. + $scaleinfo = array_shift($scaleconfigurations); + if (empty($scaleinfo) || !isset($scaleinfo->scaleid) || $scaleinfo->scaleid != $scaleid) { + // This should never happen. + return new lang_string('errorscaleconfiguration', 'core_competency'); + } + + // Walk through the array to find proficient and default values. + foreach ($scaleconfigurations as $scaleconfiguration) { + if (isset($scaleconfiguration->scaledefault) && $scaleconfiguration->scaledefault) { + $scaledefaultselected = true; + } + if (isset($scaleconfiguration->proficient) && $scaleconfiguration->proficient) { + $proficientselected = true; + } + } + } + + if (!$scaledefaultselected || !$proficientselected) { + return new lang_string('errorscaleconfiguration', 'core_competency'); + } + + return true; + } + + /** + * Return whether or not the competency IDs share the same framework. + * + * @param array $ids Competency IDs + * @return bool + */ + public static function share_same_framework(array $ids) { + global $DB; + list($insql, $params) = $DB->get_in_or_equal($ids); + return $DB->count_records_select(self::TABLE, "id $insql", $params, "COUNT(DISTINCT(competencyframeworkid))") == 1; + } + + /** + * Get the available rules. + * + * @return array Keys are the class names, values are the name of the rule. + */ + public static function get_available_rules() { + // Fully qualified class names without leading slashes because get_class() does not add them either. + $rules = array( + 'core_competency\\competency_rule_all' => competency_rule_all::get_name(), + 'core_competency\\competency_rule_points' => competency_rule_points::get_name(), + ); + return $rules; + } + + /** + * Build a framework tree with competency nodes. + * + * @param int $frameworkid the framework id + * @return node[] tree of framework competency nodes + */ + public static function get_framework_tree($frameworkid) { + $competencies = self::search('', $frameworkid); + return self::build_tree($competencies, 0); + } + + /** + * Get the context from the framework. + * + * @return context + */ + public function get_context() { + return $this->get_framework()->get_context(); + } + + /** + * Recursively build up the tree of nodes. + * + * @param array $all - List of all competency classes. + * @param int $parentid - The current parent ID. Pass 0 to build the tree from the top. + * @return node[] $tree tree of nodes + */ + protected static function build_tree($all, $parentid) { + $tree = array(); + foreach ($all as $one) { + if ($one->get_parentid() == $parentid) { + $node = new stdClass(); + $node->competency = $one; + $node->children = self::build_tree($all, $one->get_id()); + $tree[] = $node; + } + } + return $tree; + } + + /** + * Check if we can delete competencies safely. + * + * This moethod does not check any capablities. + * Check if competency is used in a plan and user competency. + * Check if competency is used in a template. + * Check if competency is linked to a course. + * + * @param array $ids Array of competencies ids. + * @return bool True if we can delete the competencies. + */ + public static function can_all_be_deleted($ids) { + if (empty($ids)) { + return true; + } + // Check if competency is used in template. + if (template_competency::has_records_for_competencies($ids)) { + return false; + } + // Check if competency is used in plan. + if (plan_competency::has_records_for_competencies($ids)) { + return false; + } + // Check if competency is used in course. + if (course_competency::has_records_for_competencies($ids)) { + return false; + } + // Check if competency is used in user_competency. + if (user_competency::has_records_for_competencies($ids)) { + return false; + } + // Check if competency is used in user_competency_plan. + if (user_competency_plan::has_records_for_competencies($ids)) { + return false; + } + return true; + } + + /** + * Delete the competencies. + * + * This method is reserved to core usage. + * This method does not trigger the after_delete event. + * This method does not delete related objects such as related competencies and evidences. + * + * @param array $ids The competencies ids. + * @return bool True if the competencies were deleted successfully. + */ + public static function delete_multiple($ids) { + global $DB; + list($insql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); + return $DB->delete_records_select(self::TABLE, "id $insql", $params); + } + + /** + * Get descendant ids. + * + * @param competency $competency The competency. + * @return array Array of competencies ids. + */ + public static function get_descendants_ids($competency) { + global $DB; + + $path = $DB->sql_like_escape($competency->get_path() . $competency->get_id() . '/') . '%'; + $like = $DB->sql_like('path', ':likepath'); + return $DB->get_fieldset_select(self::TABLE, 'id', $like, array('likepath' => $path)); + } + + /** + * Get competencyids by frameworkid. + * + * @param int $frameworkid The competency framework ID. + * @return array Array of competency ids. + */ + public static function get_ids_by_frameworkid($frameworkid) { + global $DB; + + return $DB->get_fieldset_select(self::TABLE, 'id', 'competencyframeworkid = :frmid', array('frmid' => $frameworkid)); + } + + /** + * Delete competencies by framework ID. + * + * This method is reserved to core usage. + * This method does not trigger the after_delete event. + * This method does not delete related objects such as related competencies and evidences. + * + * @param int $id the framework ID + * @return bool Return true if delete was successful. + */ + public static function delete_by_frameworkid($id) { + global $DB; + return $DB->delete_records(self::TABLE, array('competencyframeworkid' => $id)); + } + + /** + * Get competency ancestors. + * + * @return competency[] Return array of ancestors. + */ + public function get_ancestors() { + global $DB; + $ancestors = array(); + $ancestorsids = explode('/', trim($this->get_path(), '/')); + // Drop the root item from the array /0/. + array_shift($ancestorsids); + if (!empty($ancestorsids)) { + list($insql, $params) = $DB->get_in_or_equal($ancestorsids, SQL_PARAMS_NAMED); + $ancestors = self::get_records_select("id $insql", $params); + } + return $ancestors; + } + +} diff --git a/competency/classes/competency_framework.php b/competency/classes/competency_framework.php new file mode 100644 index 00000000000..79d897929bf --- /dev/null +++ b/competency/classes/competency_framework.php @@ -0,0 +1,513 @@ +. + +/** + * Class for loading/storing competency frameworks from the DB. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use context; +use lang_string; +use stdClass; + +require_once($CFG->libdir . '/grade/grade_scale.php'); + +/** + * Class for loading/storing competency frameworks from the DB. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_framework extends persistent { + + const TABLE = 'competency_framework'; + + /** Taxonomy constant. */ + const TAXONOMY_BEHAVIOUR = 'behaviour'; + /** Taxonomy constant. */ + const TAXONOMY_COMPETENCY = 'competency'; + /** Taxonomy constant. */ + const TAXONOMY_CONCEPT = 'concept'; + /** Taxonomy constant. */ + const TAXONOMY_DOMAIN = 'domain'; + /** Taxonomy constant. */ + const TAXONOMY_INDICATOR = 'indicator'; + /** Taxonomy constant. */ + const TAXONOMY_LEVEL = 'level'; + /** Taxonomy constant. */ + const TAXONOMY_OUTCOME = 'outcome'; + /** Taxonomy constant. */ + const TAXONOMY_PRACTICE = 'practice'; + /** Taxonomy constant. */ + const TAXONOMY_PROFICIENCY = 'proficiency'; + /** Taxonomy constant. */ + const TAXONOMY_SKILL = 'skill'; + /** Taxonomy constant. */ + const TAXONOMY_VALUE = 'value'; + + /** @var static The object before it was updated. */ + protected $beforeupdate; + + /** + * Get the context. + * + * @return context The context + */ + public function get_context() { + return context::instance_by_id($this->get_contextid()); + } + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'shortname' => array( + 'type' => PARAM_TEXT + ), + 'idnumber' => array( + 'type' => PARAM_TEXT + ), + 'description' => array( + 'type' => PARAM_RAW, + 'default' => '' + ), + 'descriptionformat' => array( + 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), + 'type' => PARAM_INT, + 'default' => FORMAT_HTML + ), + 'visible' => array( + 'type' => PARAM_BOOL, + 'default' => 1 + ), + 'scaleid' => array( + 'type' => PARAM_INT + ), + 'scaleconfiguration' => array( + 'type' => PARAM_RAW + ), + 'contextid' => array( + 'type' => PARAM_INT + ), + 'taxonomies' => array( + 'type' => PARAM_RAW, + 'default' => '' + ) + ); + } + + /** + * Hook to execute before validate. + * + * @return void + */ + protected function before_validate() { + $this->beforeupdate = null; + + // During update. + if ($this->get_id()) { + $this->beforeupdate = new competency_framework($this->get_id()); + } + + } + + /** + * Return the scale. + * + * @return \grade_scale + */ + public function get_scale() { + $scale = \grade_scale::fetch(array('id' => $this->get_scaleid())); + $scale->load_items(); + return $scale; + } + + /** + * Get the constant name for a level. + * + * @param int $level The level of the term. + * @return string + */ + public function get_taxonomy($level) { + $taxonomies = $this->get_taxonomies(); + + if (empty($taxonomies[$level])) { + // If for some reason we cannot find the level, we fallback onto competency. + $constant = self::TAXONOMY_COMPETENCY; + } else { + $constant = $taxonomies[$level]; + } + + return $constant; + } + + /** + * Return the taxonomy constants indexed by level. + * + * @return array Contains the list of taxonomy constants indexed by level. + */ + public function get_taxonomies() { + $taxonomies = explode(',', $this->get('taxonomies')); + + // Indexing first level at 1. + array_unshift($taxonomies, null); + unset($taxonomies[0]); + + // Ensure that we do not return empty levels. + for ($i = 1; $i <= self::get_taxonomies_max_level(); $i++) { + if (empty($taxonomies[$i])) { + $taxonomies[$i] = self::TAXONOMY_COMPETENCY; + } + } + + return $taxonomies; + } + + /** + * Returns true when some competencies of the framework have user competencies. + * + * This is useful to determine if the framework, or part of it, should be locked down. + * + * @return boolean + */ + public function has_user_competencies() { + return user_competency::has_records_for_framework($this->get_id()) || + user_competency_plan::has_records_for_framework($this->get_id()); + } + + /** + * Convenience method to set taxonomies from an array or string. + * + * @param string|array $taxonomies A string, or an array where the values are the term constants. + */ + public function set_taxonomies($taxonomies) { + if (is_array($taxonomies)) { + $taxonomies = implode(',', $taxonomies); + } + $this->set('taxonomies', $taxonomies); + } + + /** + * Validate the context ID. + * + * @param int $value The context ID. + * @return bool|lang_string + */ + protected function validate_contextid($value) { + global $DB; + + $context = context::instance_by_id($value, IGNORE_MISSING); + if (!$context) { + return new lang_string('invalidcontext', 'error'); + } else if ($context->contextlevel != CONTEXT_SYSTEM && $context->contextlevel != CONTEXT_COURSECAT) { + return new lang_string('invalidcontext', 'error'); + } + + // During update. + if ($this->get_id()) { + + // The context must never change. + $oldcontextid = $DB->get_field(self::TABLE, 'contextid', array('id' => $this->get_id()), MUST_EXIST); + if ($this->get_contextid() != $oldcontextid) { + return new lang_string('invalidcontext', 'error'); + } + } + + return true; + } + + /** + * Validate the id number. + * + * @param string $value The id number. + * @return bool|lang_string + */ + protected function validate_idnumber($value) { + global $DB; + + $params = array( + 'id' => $this->get('id'), + 'idnumber' => $value, + ); + + if ($DB->record_exists_select(self::TABLE, 'idnumber = :idnumber AND id <> :id', $params)) { + return new lang_string('idnumbertaken', 'error'); + } + + return true; + } + + /** + * Validate the scale ID. + * + * @param string $value The scale ID. + * @return bool|lang_string + */ + protected function validate_scaleid($value) { + global $DB; + + // Always validate that the scale exists. + if (!$DB->record_exists_select('scale', 'id = :id', array('id' => $value))) { + return new lang_string('invalidscaleid', 'error'); + } + + // During update. + if ($this->get_id()) { + + // Validate that we can only change the scale when it is not used yet. + if ($this->beforeupdate->get_scaleid() != $value) { + if ($this->beforeupdate->has_user_competencies()) { + return new lang_string('errorscalealreadyused', 'core_competency'); + } + } + + } + + return true; + } + + /** + * Validate the scale configuration. + * + * @param string $value The scale configuration. + * @return bool|lang_string + */ + protected function validate_scaleconfiguration($value) { + $scaledefaultselected = false; + $proficientselected = false; + $scaleconfigurations = json_decode($value); + + if (is_array($scaleconfigurations)) { + + // The first element of the array contains the scale ID. + $scaleinfo = array_shift($scaleconfigurations); + if (empty($scaleinfo) || !isset($scaleinfo->scaleid) || $scaleinfo->scaleid != $this->get('scaleid')) { + // This should never happen. + return new lang_string('errorscaleconfiguration', 'core_competency'); + } + + // Walk through the array to find proficient and default values. + foreach ($scaleconfigurations as $scaleconfiguration) { + if (isset($scaleconfiguration->scaledefault) && $scaleconfiguration->scaledefault) { + $scaledefaultselected = true; + } + if (isset($scaleconfiguration->proficient) && $scaleconfiguration->proficient) { + $proficientselected = true; + } + } + } + + if (!$scaledefaultselected || !$proficientselected) { + return new lang_string('errorscaleconfiguration', 'core_competency'); + } + + return true; + } + + /** + * Validate taxonomies. + * + * @param mixed $value The taxonomies. + * @return true|lang_string + */ + protected function validate_taxonomies($value) { + $terms = explode(',', $value); + + if (count($terms) > self::get_taxonomies_max_level()) { + return new lang_string('invaliddata', 'error'); + } + + foreach ($terms as $term) { + if (!empty($term) && !array_key_exists($term, self::get_taxonomies_list())) { + return new lang_string('invalidtaxonomy', 'core_competency', $term); + } + } + + return true; + } + + /** + * Extract the default grade from a scale configuration. + * + * Returns an array where the first element is the grade, and the second + * is a boolean representing whether or not this grade is considered 'proficient'. + * + * @param string $config JSON encoded config. + * @return array(int grade, int proficient) + */ + public static function get_default_grade_from_scale_configuration($config) { + $config = json_decode($config); + if (!is_array($config)) { + throw new coding_exception('Unexpected scale configuration.'); + } + + // Remove the scale ID from the config. + array_shift($config); + + foreach ($config as $part) { + if ($part->scaledefault) { + return array((int) $part->id, (int) $part->proficient); + } + } + + throw new coding_exception('Invalid scale configuration, default not found.'); + } + + /** + * Extract the proficiency of a grade from a scale configuration. + * + * @param string $config JSON encoded config. + * @param int $grade The grade. + * @return int Representing a boolean + */ + public static function get_proficiency_of_grade_from_scale_configuration($config, $grade) { + $config = json_decode($config); + if (!is_array($config)) { + throw new coding_exception('Unexpected scale configuration.'); + } + + // Remove the scale ID from the config. + array_shift($config); + + foreach ($config as $part) { + if ($part->id == $grade) { + return (int) $part->proficient; + } + } + + return 0; + } + + /** + * Get the string of a taxonomy from a constant + * + * @param string $constant The taxonomy constant. + * @return lang_string + */ + public static function get_taxonomy_from_constant($constant) { + return self::get_taxonomies_list()[$constant]; + } + + /** + * Return the maximum number of taxonomy levels. + * + * This is a method and not a constant because we want to make it easy to adapt + * to the number of levels desired in the future. + * + * @return int + */ + public static function get_taxonomies_max_level() { + return 6; + } + + /** + * Get the list of all taxonomies. + * + * @return array Where the key is the taxonomy constant, and the value its translation. + */ + public static function get_taxonomies_list() { + static $list = null; + + // At some point we'll have to switch to not using static cache, mainly for Unit Tests in case we + // decide to allow more taxonomies to be added dynamically from a CFG variable for instance. + if ($list === null) { + $list = array( + self::TAXONOMY_BEHAVIOUR => new lang_string('taxonomy_' . self::TAXONOMY_BEHAVIOUR, 'core_competency'), + self::TAXONOMY_COMPETENCY => new lang_string('taxonomy_' . self::TAXONOMY_COMPETENCY, 'core_competency'), + self::TAXONOMY_CONCEPT => new lang_string('taxonomy_' . self::TAXONOMY_CONCEPT, 'core_competency'), + self::TAXONOMY_DOMAIN => new lang_string('taxonomy_' . self::TAXONOMY_DOMAIN, 'core_competency'), + self::TAXONOMY_INDICATOR => new lang_string('taxonomy_' . self::TAXONOMY_INDICATOR, 'core_competency'), + self::TAXONOMY_LEVEL => new lang_string('taxonomy_' . self::TAXONOMY_LEVEL, 'core_competency'), + self::TAXONOMY_OUTCOME => new lang_string('taxonomy_' . self::TAXONOMY_OUTCOME, 'core_competency'), + self::TAXONOMY_PRACTICE => new lang_string('taxonomy_' . self::TAXONOMY_PRACTICE, 'core_competency'), + self::TAXONOMY_PROFICIENCY => new lang_string('taxonomy_' . self::TAXONOMY_PROFICIENCY, 'core_competency'), + self::TAXONOMY_SKILL => new lang_string('taxonomy_' . self::TAXONOMY_SKILL, 'core_competency'), + self::TAXONOMY_VALUE => new lang_string('taxonomy_' . self::TAXONOMY_VALUE, 'core_competency'), + ); + } + + return $list; + } + + /** + * Get a uniq idnumber. + * + * @param string $idnumber the framework idnumber + * @return string + */ + public static function get_unused_idnumber($idnumber) { + global $DB; + + $currentidnumber = $idnumber; + $counter = 0; + // Iteratere while the idnumber exists. + while ($DB->record_exists_select(static::TABLE, 'idnumber = ?', array($currentidnumber))) { + $suffixidnumber = '_' . ++$counter; + $currentidnumber = substr($idnumber, 0, 100 - strlen($suffixidnumber)).$suffixidnumber; + } + + // Return the uniq idnumber. + return $currentidnumber; + } + + /** + * Whether or not the current user can manage the framework. + * + * @return bool + */ + public function can_manage() { + return self::can_manage_context($this->get_context()); + } + + /** + * Whether or not the current user can manage the framework. + * + * @param context $context + * @return bool + */ + public static function can_manage_context($context) { + return has_capability('moodle/competency:competencymanage', $context); + } + + /** + * Whether or not the current user can read the framework. + * + * @return bool + */ + public function can_read() { + return self::can_read_context($this->get_context()); + } + + /** + * Whether or not the current user can read the framework. + * + * @param context $context + * @return bool + */ + public static function can_read_context($context) { + return has_capability('moodle/competency:competencyview', $context) || self::can_manage_context($context); + } + +} diff --git a/competency/classes/competency_rule.php b/competency/classes/competency_rule.php new file mode 100644 index 00000000000..49f4f40ccd7 --- /dev/null +++ b/competency/classes/competency_rule.php @@ -0,0 +1,107 @@ +. + +/** + * Competency rule base. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; + +/** + * Competency rule base abstract class. + * + * Rules are attached to a competency and then tested against a user competency + * to determine whether or not it matches. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class competency_rule { + + /** @var competency The competency. */ + protected $competency; + + /** + * Constructor. + * + * @param competency $competency The competency. + */ + public function __construct(competency $competency) { + $class = $competency->get_ruletype(); + if (!$class || !($this instanceof $class)) { + throw new coding_exception('This competency does not use this rule.'); + } + + $this->competency = $competency; + } + + /** + * Get the rule config. + * + * @return mixed + */ + protected function get_config() { + return $this->competency->get_ruleconfig(); + } + + /** + * Whether or not the rule is matched. + * + * @param user_competency $usercompetency The user competency to test against. + * @return bool + */ + abstract public function matches(user_competency $usercompetency); + + /** + * Validate the rule config. + * + * @param string $value The value to validate. + * @return bool + */ + abstract public function validate_config($value); + + /** + * The name of the rule. + * + * @return lang_string + */ + public static function get_name() { + throw new coding_exception('Method not implemented.'); + } + + /** + * Migrate rule config from one set of competencies to another. + * + * Exceptions should be thrown when the migration can not be performed. + * + * @param string $config Original config rule of a competency. + * @param array $mappings Array that matches the original competency IDs with the new competencies objects. + * @return string New configuration. + * @throws Exception + */ + public static function migrate_config($config, $mappings) { + return $config; + } + +} diff --git a/competency/classes/competency_rule_all.php b/competency/classes/competency_rule_all.php new file mode 100644 index 00000000000..69f88582fca --- /dev/null +++ b/competency/classes/competency_rule_all.php @@ -0,0 +1,92 @@ +. + +/** + * Competency rule all. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use lang_string; + +/** + * Competency rule all class. + * + * This rule is considered matched when all the children of a competency are completed. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_rule_all extends competency_rule { + + /** + * Whether or not the rule is matched. + * + * @param user_competency $usercompetency The user competency. + * @return bool + */ + public function matches(user_competency $usercompetency) { + global $DB; + + // TODO Improve performance here, perhaps the caller could already provide records. + $children = competency::get_records(array('parentid' => $this->competency->get_id())); + + if (empty($children)) { + // Leaves are not compatible with this rule. + return false; + } + + $ids = array(); + foreach ($children as $child) { + $ids[] = $child->get_id(); + } + + list($insql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); + $sql = "userid = :userid + AND proficiency = :proficiency + AND competencyid $insql"; + $params['userid'] = $usercompetency->get_userid(); + $params['proficiency'] = 1; + + // Is the user is marked as proficient in all children? + return user_competency::count_records_select($sql, $params) === count($ids); + } + + /** + * Validate the rule config. + * + * @param string $value The value to validate. + * @return bool + */ + public function validate_config($value) { + return $value === null; + } + + /** + * The name of the rule. + * + * @return lang_string + */ + public static function get_name() { + return new lang_string('allchildrenarecomplete', 'core_competency'); + } +} diff --git a/competency/classes/competency_rule_points.php b/competency/classes/competency_rule_points.php new file mode 100644 index 00000000000..7d727bdff56 --- /dev/null +++ b/competency/classes/competency_rule_points.php @@ -0,0 +1,218 @@ +. + +/** + * Competency rule points based. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use lang_string; + + +/** + * Competency rule points based class. + * + * This rule matches when related competencies contribute for a required number of points. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_rule_points extends competency_rule { + + /** + * Get the rule config. + * + * @return mixed + */ + protected function get_config() { + $config = parent::get_config(); + return json_decode($config); + } + + /** + * Whether or not the rule is matched. + * + * @param user_competency $usercompetency The user competency. + * @return bool + */ + public function matches(user_competency $usercompetency) { + global $DB; + + $config = $this->get_config(); + $pointsrequired = $config->base->points; + + // Index by competency ID and extract required. + $compsrules = array(); + $requiredids = array(); + foreach ($config->competencies as $comp) { + $compsrules[$comp->id] = $comp; + if ($comp->required) { + $requiredids[$comp->id] = $comp->id; + } + } + + // Find all the user competency records. + list($insql, $params) = $DB->get_in_or_equal(array_keys($compsrules), SQL_PARAMS_NAMED); + $sql = "userid = :userid + AND proficiency = :proficiency + AND competencyid $insql"; + $params['userid'] = $usercompetency->get_userid(); + $params['proficiency'] = 1; + $ucs = user_competency::get_records_select($sql, $params, '', 'competencyid'); + + // Check that all the required are found. + if (!empty($requiredids)) { + $unmetrequired = array_diff_key($requiredids, $ucs); + if (!empty($unmetrequired)) { + return false; + } + } + + // Check that we have enough points. + $points = 0; + foreach ($compsrules as $compid => $comp) { + if (array_key_exists($compid, $ucs)) { + $points += $comp->points; + } + } + + return $points >= $pointsrequired; + } + + /** + * Validate the rule config. + * + * @param string $value The value to validate. + * @return bool + */ + public function validate_config($value) { + $compids = array(); + $config = json_decode($value); + if ($config === null || !isset($config->base) || !isset($config->competencies)) { + return false; + } + + if (!isset($config->base->points)) { + return false; + } + + try { + $requiredpoints = validate_param($config->base->points, PARAM_INT); + } catch (\invalid_parameter_exception $e) { + return false; + } + + if ($requiredpoints < 1) { + return false; + } + + $totalpoints = 0; + + // Validate the competency info. + foreach ($config->competencies as $competency) { + + // Cannot include self. + if ($competency->id == $this->competency->get_id()) { + return false; + } + + // Check for duplicates. + if (in_array($competency->id, $compids)) { + return false; + } + + // Check for required fields. + if (!isset($competency->id) + || !isset($competency->points) + || !isset($competency->required)) { + return false; + } + + // Validate the parameters. + try { + validate_param($competency->id, PARAM_INT); + $points = validate_param($competency->points, PARAM_INT); + validate_param($competency->required, PARAM_BOOL); + } catch (\invalid_parameter_exception $e) { + return false; + } + + $totalpoints += $points; + if ($points < 0) { + return false; + } + + $compids[] = $competency->id; + } + + // No competencies, that's strange. + if (empty($compids)) { + return false; + } + + // Impossible to reach the points required. + if ($requiredpoints > $totalpoints) { + return false; + } + + // Check that all the competencies are children of the competency. + // We may want to relax this check at a later stage if we want to allow competencies + // to be linked throughout the whole framework. + return $this->competency->is_parent_of($compids); + } + + /** + * The name of the rule. + * + * @return lang_string + */ + public static function get_name() { + return new lang_string('pointsrequiredaremet', 'core_competency'); + } + + /** + * Migrate rule config when duplicate competency based on mapping competencies ids. + * + * @param string $config the config rule of a competency + * @param array $mappings array that match the old competency ids with the new competencies + * @return string + */ + public static function migrate_config($config, $mappings) { + $ruleconfig = json_decode($config, true); + if (is_array($ruleconfig)) { + foreach ($ruleconfig['competencies'] as $key => $rulecomp) { + $rulecmpid = $rulecomp['id']; + if (array_key_exists($rulecmpid, $mappings)) { + $ruleconfig['competencies'][$key]['id'] = $mappings[$rulecmpid]->get_id(); + } else { + throw new coding_exception("The competency id is not found in the matchids."); + } + } + } else { + throw new coding_exception("Invalid JSON config rule."); + } + + return json_encode($ruleconfig); + } +} diff --git a/competency/classes/course_competency.php b/competency/classes/course_competency.php new file mode 100644 index 00000000000..6ec21af8990 --- /dev/null +++ b/competency/classes/course_competency.php @@ -0,0 +1,408 @@ +. + +/** + * Class for loading/storing competencies from the DB. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use stdClass; +use lang_string; + +/** + * Class for loading/storing course_competencies from the DB. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_competency extends persistent { + + const TABLE = 'competency_coursecomp'; + + /** Course competency ruleoutcome constant. */ + const OUTCOME_NONE = 0; + /** Course competency ruleoutcome constant. */ + const OUTCOME_EVIDENCE = 1; + /** Course competency ruleoutcome constant. */ + const OUTCOME_RECOMMEND = 2; + /** Course competency ruleoutcome constant. */ + const OUTCOME_COMPLETE = 3; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'courseid' => array( + 'type' => PARAM_INT + ), + 'competencyid' => array( + 'type' => PARAM_INT + ), + 'sortorder' => array( + 'type' => PARAM_INT + ), + 'ruleoutcome' => array( + 'choices' => array(self::OUTCOME_NONE, + self::OUTCOME_EVIDENCE, + self::OUTCOME_RECOMMEND, + self::OUTCOME_COMPLETE + ), + 'default' => self::OUTCOME_EVIDENCE, + 'type' => PARAM_INT, + ), + ); + } + + /** + * Hook to execute before validate. + * + * @return void + */ + protected function before_validate() { + if (($this->get_id() && $this->get_sortorder() === null) || !$this->get_id()) { + $this->set('sortorder', $this->count_records(array('courseid' => $this->get_courseid()))); + } + } + + /** + * Return the courses where both competency and user are. + * + * A user is considered being in a course when they are enrolled, the enrolment is valid, + * the enrolment instance is enabled, and the enrolment plugin is enabled.. + * + * @param int $competencyid The competency ID. + * @param int $userid The user ID. + * @return array Indexed by course ID. + */ + public static function get_courses_with_competency_and_user($competencyid, $userid) { + global $CFG, $DB; + + if (!$plugins = explode(',', $CFG->enrol_plugins_enabled)) { + return array(); + } + + $ctxfields = \context_helper::get_preload_record_columns_sql('ctx'); + list($plugins, $params) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee'); + $params['competencyid'] = $competencyid; + $params['userid'] = $userid; + $params['enabled'] = ENROL_INSTANCE_ENABLED; + $params['active'] = ENROL_USER_ACTIVE; + $params['contextlevel'] = CONTEXT_COURSE; + + // Heavily based on enrol_get_shared_courses(). + $sql = "SELECT c.*, $ctxfields + FROM {course} c + JOIN {" . static::TABLE . "} cc + ON cc.courseid = c.id + AND cc.competencyid = :competencyid + JOIN ( + SELECT DISTINCT c.id + FROM {enrol} e + JOIN {user_enrolments} ue + ON ue.enrolid = e.id + AND ue.status = :active + AND ue.userid = :userid + JOIN {course} c + ON c.id = e.courseid + WHERE e.status = :enabled + AND e.enrol $plugins + ) ec ON ec.id = c.id + LEFT JOIN {context} ctx + ON ctx.instanceid = c.id + AND ctx.contextlevel = :contextlevel + ORDER BY c.id"; + + $courses = $DB->get_records_sql($sql, $params); + array_map('context_helper::preload_from_record', $courses); + return $courses; + } + + /** + * Return a list of rules. + * + * @return array Indexed by outcome value. + */ + public static function get_ruleoutcome_list() { + static $list = null; + + if ($list === null) { + $list = array( + self::OUTCOME_NONE => self::get_ruleoutcome_name(self::OUTCOME_NONE), + self::OUTCOME_EVIDENCE => self::get_ruleoutcome_name(self::OUTCOME_EVIDENCE), + self::OUTCOME_RECOMMEND => self::get_ruleoutcome_name(self::OUTCOME_RECOMMEND), + self::OUTCOME_COMPLETE => self::get_ruleoutcome_name(self::OUTCOME_COMPLETE)); + } + + return $list; + } + + /** + * Human readable rule name. + * + * @param int $ruleoutcome The value of ruleoutcome. + * @return lang_string + */ + public static function get_ruleoutcome_name($ruleoutcome) { + + switch ($ruleoutcome) { + case self::OUTCOME_NONE: + $strname = 'none'; + break; + case self::OUTCOME_EVIDENCE: + $strname = 'evidence'; + break; + case self::OUTCOME_RECOMMEND: + $strname = 'recommend'; + break; + case self::OUTCOME_COMPLETE: + $strname = 'complete'; + break; + default: + throw new \moodle_exception('errorcoursecompetencyrule', 'core_competency', '', $ruleoutcome); + break; + } + + return new lang_string('coursecompetencyoutcome_' . $strname, 'core_competency'); + } + + /** + * Validate course ID. + * + * @param int $data The course ID. + * @return true|lang_string + */ + protected function validate_courseid($data) { + global $DB; + if (!$DB->record_exists('course', array('id' => $data))) { + return new lang_string('invalidcourseid', 'error'); + } + return true; + } + + /** + * Validate competency ID. + * + * @param int $data The competency ID. + * @return true|lang_string + */ + protected function validate_competencyid($data) { + if (!competency::record_exists($data)) { + return new lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Return the course IDs and visible flags that include this competency. + * + * Only the ids and visible flag are returned, for the full records use list_courses. + * + * @param int $competencyid The competency id + * @return array containing courseid and visible. + */ + public static function list_courses_min($competencyid) { + global $DB; + + $results = $DB->get_records_sql('SELECT course.id as id, course.visible as visible + FROM {' . self::TABLE . '} coursecomp + JOIN {course} course + ON coursecomp.courseid = course.id + WHERE coursecomp.competencyid = ? ', array($competencyid)); + + return $results; + } + + /** + * Return partial course records foreach course that contains this competency. + * + * @param int $competencyid The competency id + * @return array[stdClass] Array of course records containg id, visible, shortname, idnumber, fullname + */ + public static function list_courses($competencyid) { + global $DB; + + $results = $DB->get_records_sql('SELECT course.id, course.visible, course.shortname, course.idnumber, course.fullname + FROM {course} course + JOIN {' . self::TABLE . '} coursecomp + ON coursecomp.courseid = course.id + WHERE coursecomp.competencyid = ? ', array($competencyid)); + + return $results; + } + + /** + * Count the competencies in this course. + * + * @param int $courseid The course id + * @return int + */ + public static function count_competencies($courseid) { + global $DB; + + $sql = 'SELECT COUNT(comp.id) + FROM {' . self::TABLE . '} coursecomp + JOIN {' . competency::TABLE . '} comp + ON coursecomp.competencyid = comp.id + WHERE coursecomp.courseid = ? '; + $params = array($courseid); + + $results = $DB->count_records_sql($sql, $params); + + return $results; + } + + /** + * List the competencies in this course. + * + * @param int $courseid The course id + * @return competency[] Indexed by competency ID. + */ + public static function list_competencies($courseid) { + global $DB; + + $sql = 'SELECT comp.* + FROM {' . competency::TABLE . '} comp + JOIN {' . self::TABLE . '} coursecomp + ON coursecomp.competencyid = comp.id + WHERE coursecomp.courseid = ?'; + $params = array($courseid); + + $sql .= ' ORDER BY coursecomp.sortorder ASC'; + $results = $DB->get_recordset_sql($sql, $params); + $instances = array(); + foreach ($results as $result) { + $comp = new competency(0, $result); + $instances[$comp->get_id()] = $comp; + } + $results->close(); + + return $instances; + } + + /** + * Get a single competency from the course (only if it is really in the course). + * + * @param int $courseid The course id + * @param int $competencyid The competency id + * @return competency + */ + public static function get_competency($courseid, $competencyid) { + global $DB; + + $sql = 'SELECT comp.* + FROM {' . competency::TABLE . '} comp + JOIN {' . self::TABLE . '} crscomp + ON crscomp.competencyid = comp.id + WHERE crscomp.courseid = ? AND crscomp.competencyid = ?'; + $params = array($courseid, $competencyid); + + $result = $DB->get_record_sql($sql, $params); + if (!$result) { + throw new coding_exception('The competency does not belong to this course: ' . $competencyid . ', ' . $courseid); + } + + return new competency(0, $result); + } + + /** + * Hook to execute after delete. + * + * @param bool $result Whether or not the delete was successful. + * @return void + */ + protected function after_delete($result) { + global $DB; + if (!$result) { + return; + } + + $table = '{' . self::TABLE . '}'; + $sql = "UPDATE $table SET sortorder = sortorder -1 WHERE courseid = ? AND sortorder > ?"; + $DB->execute($sql, array($this->get_courseid(), $this->get_sortorder())); + } + + /** + * Get the specified course_competency in this course. + * + * @param int $courseid The course id + * @param int $competencyid The competency id + * @return course_competency + */ + public static function get_course_competency($courseid, $competencyid) { + global $DB; + + $sql = 'SELECT crscomp.* + FROM {' . self::TABLE . '} crscomp + WHERE crscomp.courseid = ? AND crscomp.competencyid = ?'; + $params = array($courseid, $competencyid); + + $result = $DB->get_record_sql($sql, $params); + if (!$result) { + throw new coding_exception('The competency does not belong to this course: ' . $competencyid . ', ' . $courseid); + } + + return new course_competency(0, $result); + } + + /** + * List the course_competencies in this course. + * + * @param int $courseid The course id + * @return course_competency[] + */ + public static function list_course_competencies($courseid) { + global $DB; + + $sql = 'SELECT coursecomp.* + FROM {' . self::TABLE . '} coursecomp + JOIN {' . competency::TABLE . '} comp + ON coursecomp.competencyid = comp.id + WHERE coursecomp.courseid = ?'; + $params = array($courseid); + + $sql .= ' ORDER BY coursecomp.sortorder ASC'; + $results = $DB->get_recordset_sql($sql, $params); + $instances = array(); + foreach ($results as $result) { + array_push($instances, new course_competency(0, $result)); + } + $results->close(); + + return $instances; + } + + /** + * Check if course competency has records for competencies. + * + * @param array $competencyids Array of competencies ids. + * @return boolean Return true if one or more than a competency was found in a course. + */ + public static function has_records_for_competencies($competencyids) { + global $DB; + list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED); + return self::record_exists_select("competencyid $insql", $params); + } + +} diff --git a/competency/classes/course_competency_settings.php b/competency/classes/course_competency_settings.php new file mode 100644 index 00000000000..79a8cc55338 --- /dev/null +++ b/competency/classes/course_competency_settings.php @@ -0,0 +1,141 @@ +. + +/** + * Class for course_competency_settings persistence. + * + * @package core_competency + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; + +use lang_string; +use context_course; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class for course_competency_settings persistence. + * + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_competency_settings extends persistent { + + /** Table name for plan_competency persistency */ + const TABLE = 'competency_coursecompsetting'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'courseid' => array( + 'type' => PARAM_INT, + ), + 'pushratingstouserplans' => array( + 'type' => PARAM_BOOL, + 'default' => function() { + return get_config('core_competency', 'pushcourseratingstouserplans'); + } + ), + ); + } + + /** + * Get a the course settings for a single course. + * + * @param int $courseid The course id + * @return course_competency_settings + */ + public static function get_by_courseid($courseid) { + global $DB; + + $params = array( + 'courseid' => $courseid + ); + + $settings = new static(null, (object) $params); + if ($record = $DB->get_record(self::TABLE, $params)) { + $settings->from_record($record); + } + + return $settings; + } + + /** + * Can the current user view competency settings for this course. + * + * @param int $courseid The course ID. + * @return bool + */ + public static function can_read($courseid) { + $context = context_course::instance($courseid); + + $capabilities = array('moodle/competency:coursecompetencyview'); + + return has_any_capability($capabilities, $context); + } + + /** + * Can the current user change competency settings for this course. + * + * @param int $courseid The course ID. + * @return bool + */ + public static function can_manage_course($courseid) { + $context = context_course::instance($courseid); + + $capabilities = array('moodle/competency:coursecompetencyconfigure'); + + return has_any_capability($capabilities, $context); + } + + /** + * Can the current user change competency settings for this course. + * + * @return bool + */ + public function can_manage() { + return static::can_manage_course($this->get_courseid()); + } + + /** + * Validate course ID. + * + * @param int $data The course ID. + * @return true|lang_string + */ + protected function validate_courseid($data) { + global $DB; + if (!$DB->record_exists('course', array('id' => $data))) { + return new lang_string('invalidcourseid', 'error'); + } + return true; + } + + /** + * Get the context. + * + * @return context The context + */ + public function get_context() { + return context_course::instance($this->get_courseid()); + } +} diff --git a/competency/classes/course_module_competency.php b/competency/classes/course_module_competency.php new file mode 100644 index 00000000000..de6363e4faa --- /dev/null +++ b/competency/classes/course_module_competency.php @@ -0,0 +1,301 @@ +. + +/** + * Class for loading/storing competencies from the DB. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use stdClass; +use lang_string; + +/** + * Class for loading/storing course_module_competencies from the DB. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_competency extends persistent { + + const TABLE = 'competency_modulecomp'; + + /** Course competency ruleoutcome constant. */ + const OUTCOME_NONE = 0; + /** Course competency ruleoutcome constant. */ + const OUTCOME_EVIDENCE = 1; + /** Course competency ruleoutcome constant. */ + const OUTCOME_RECOMMEND = 2; + /** Course competency ruleoutcome constant. */ + const OUTCOME_COMPLETE = 3; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'cmid' => array( + 'type' => PARAM_INT + ), + 'competencyid' => array( + 'type' => PARAM_INT + ), + 'sortorder' => array( + 'type' => PARAM_INT + ), + 'ruleoutcome' => array( + 'choices' => array(self::OUTCOME_NONE, + self::OUTCOME_EVIDENCE, + self::OUTCOME_RECOMMEND, + self::OUTCOME_COMPLETE + ), + 'default' => self::OUTCOME_EVIDENCE, + 'type' => PARAM_INT, + ), + ); + } + + /** + * Hook to execute before validate. + * + * @return void + */ + protected function before_validate() { + if (($this->get_id() && $this->get_sortorder() === null) || !$this->get_id()) { + $this->set('sortorder', $this->count_records(array('cmid' => $this->get_cmid()))); + } + } + + /** + * Return a list of rules. + * + * @return array Indexed by outcome value. + */ + public static function get_ruleoutcome_list() { + static $list = null; + + if ($list === null) { + $list = array( + self::OUTCOME_NONE => self::get_ruleoutcome_name(self::OUTCOME_NONE), + self::OUTCOME_EVIDENCE => self::get_ruleoutcome_name(self::OUTCOME_EVIDENCE), + self::OUTCOME_RECOMMEND => self::get_ruleoutcome_name(self::OUTCOME_RECOMMEND), + self::OUTCOME_COMPLETE => self::get_ruleoutcome_name(self::OUTCOME_COMPLETE)); + } + + return $list; + } + + /** + * Human readable rule name. + * + * @param int $ruleoutcome The value of ruleoutcome. + * @return lang_string + */ + public static function get_ruleoutcome_name($ruleoutcome) { + + switch ($ruleoutcome) { + case self::OUTCOME_NONE: + $strname = 'none'; + break; + case self::OUTCOME_EVIDENCE: + $strname = 'evidence'; + break; + case self::OUTCOME_RECOMMEND: + $strname = 'recommend'; + break; + case self::OUTCOME_COMPLETE: + $strname = 'complete'; + break; + default: + throw new \moodle_exception('errorcompetencyrule', 'core_competency', '', $ruleoutcome); + break; + } + + return new lang_string('coursemodulecompetencyoutcome_' . $strname, 'core_competency'); + } + + /** + * Validate cmid ID. + * + * @param int $data The CM ID. + * @return true|lang_string + */ + protected function validate_cmid($data) { + global $DB; + if (!$DB->record_exists('course_modules', array('id' => $data))) { + return new lang_string('invalidmodule', 'error'); + } + return true; + } + + /** + * Validate competency ID. + * + * @param int $data The competency ID. + * @return true|lang_string + */ + protected function validate_competencyid($data) { + if (!competency::record_exists($data)) { + return new lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Return the module IDs and visible flags that include this competency in a single course. + * + * @param int $competencyid The competency id + * @param int $courseid The course ID. + * @return array of ints (cmids) + */ + public static function list_course_modules($competencyid, $courseid) { + global $DB; + + $results = $DB->get_records_sql('SELECT coursemodules.id as id + FROM {' . self::TABLE . '} modcomp + JOIN {course_modules} coursemodules + ON modcomp.cmid = coursemodules.id + WHERE modcomp.competencyid = ? AND coursemodules.course = ?', + array($competencyid, $courseid)); + + return array_keys($results); + } + + /** + * Count the competencies in this course module. + * + * @param int $cmid The course module id. + * @return int + */ + public static function count_competencies($cmid) { + global $DB; + + $sql = 'SELECT COUNT(comp.id) + FROM {' . self::TABLE . '} coursemodulecomp + JOIN {' . competency::TABLE . '} comp + ON coursecomp.competencyid = comp.id + WHERE coursecomp.cmid = ? '; + $params = array($cmid); + + $results = $DB->count_records_sql($sql, $params); + + return $results; + } + + /** + * List the competencies in this course module. + * + * @param int $cmid The course module id + * @return competency[] Indexed by competency ID. + */ + public static function list_competencies($cmid) { + global $DB; + + $sql = 'SELECT comp.* + FROM {' . competency::TABLE . '} comp + JOIN {' . self::TABLE . '} coursemodulecomp + ON coursemodulecomp.competencyid = comp.id + WHERE coursemodulecomp.cmid = ? + ORDER BY coursemodulecomp.sortorder ASC'; + $params = array($cmid); + + $results = $DB->get_recordset_sql($sql, $params); + $instances = array(); + foreach ($results as $result) { + $comp = new competency(0, $result); + $instances[$comp->get_id()] = $comp; + } + $results->close(); + + return $instances; + } + + /** + * Get a single competency from the course module (only if it is really in the course module). + * + * @param int $cmid The course module id + * @param int $competencyid The competency id + * @return competency + */ + public static function get_competency($cmid, $competencyid) { + global $DB; + + $sql = 'SELECT comp.* + FROM {' . competency::TABLE . '} comp + JOIN {' . self::TABLE . '} crsmodcomp + ON crsmodcomp.competencyid = comp.id + WHERE crsmodcomp.cmid = ? AND crsmodcomp.competencyid = ?'; + $params = array($cmid, $competencyid); + + $result = $DB->get_record_sql($sql, $params); + if (!$result) { + throw new \coding_exception('The competency does not belong to this course module: ' . $competencyid . ', ' . $cmid); + } + + return new competency(0, $result); + } + + /** + * Hook to execute after delete. + * + * @param bool $result Whether or not the delete was successful. + * @return void + */ + protected function after_delete($result) { + global $DB; + if (!$result) { + return; + } + + $table = '{' . self::TABLE . '}'; + $sql = "UPDATE $table SET sortorder = sortorder -1 WHERE cmid = ? AND sortorder > ?"; + $DB->execute($sql, array($this->get_cmid(), $this->get_sortorder())); + } + + /** + * List the course_module_competencies in this course module. + * + * @param int $cmid The course module id + * @return course_module_competency[] + */ + public static function list_course_module_competencies($cmid) { + global $DB; + + $sql = 'SELECT coursemodcomp.* + FROM {' . self::TABLE . '} coursemodcomp + JOIN {' . competency::TABLE . '} comp + ON coursemodcomp.competencyid = comp.id + WHERE coursemodcomp.cmid = ? + ORDER BY coursemodcomp.sortorder ASC'; + $params = array($cmid); + + $results = $DB->get_recordset_sql($sql, $params); + $instances = array(); + foreach ($results as $result) { + array_push($instances, new course_module_competency(0, $result)); + } + $results->close(); + + return $instances; + } + +} diff --git a/competency/classes/evidence.php b/competency/classes/evidence.php new file mode 100644 index 00000000000..ed3445dd910 --- /dev/null +++ b/competency/classes/evidence.php @@ -0,0 +1,287 @@ +. + +/** + * Evidence persistent file. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use context; +use context_user; +use lang_string; +use moodle_exception; +use stdClass; + +/** + * Evidence persistent class. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class evidence extends persistent { + + const TABLE = 'competency_evidence'; + + /** Action logging. */ + const ACTION_LOG = 0; + /** Action rating a competency when no rating is set. */ + const ACTION_COMPLETE = 2; + /** Action rating a competency. */ + const ACTION_OVERRIDE = 3; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'usercompetencyid' => array( + 'type' => PARAM_INT + ), + 'contextid' => array( + 'type' => PARAM_INT + ), + 'action' => array( + 'type' => PARAM_INT, + 'choices' => array(self::ACTION_LOG, self::ACTION_COMPLETE, self::ACTION_OVERRIDE) + ), + 'actionuserid' => array( + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED + ), + 'descidentifier' => array( + 'type' => PARAM_STRINGID + ), + 'desccomponent' => array( + 'type' => PARAM_COMPONENT + ), + 'desca' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED + ), + 'url' => array( + 'type' => PARAM_URL, + 'default' => null, + 'null' => NULL_ALLOWED + ), + 'grade' => array( + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED + ), + 'note' => array( + 'type' => PARAM_NOTAGS, + 'default' => null, + 'null' => NULL_ALLOWED + ) + ); + } + + /** + * Return the competency linked to this. + * + * @return competency + */ + public function get_competency() { + return user_competency::get_competency_by_usercompetencyid($this->get_usercompetencyid()); + } + + /** + * Convenience method to get the description $a. + * + * @return mixed + */ + public function get_desca() { + $value = $this->get('desca'); + if ($value !== null) { + $value = json_decode($value); + } + return $value; + } + + /** + * Convenience method to get the description. + * + * @return lang_string + */ + public function get_description() { + return new lang_string($this->get('descidentifier'), $this->get('desccomponent'), $this->get_desca()); + } + + /** + * Convenience method to set the description $a. + * + * @param mixed $value + * @return mixed + */ + public function set_desca($value) { + if ($value !== null) { + if (!is_scalar($value) && !is_array($value) && !($value instanceof stdClass)) { + throw new coding_exception('$a format not supported.'); + } + $value = json_encode($value); + } + $this->set('desca', $value); + } + + /** + * Convenience method handling moodle_urls. + * + * @param null|string|moodle_url $url The URL. + */ + public function set_url($url) { + if ($url instanceof \moodle_url) { + $url = $url->out(false); + } + $this->set('url', $url); + } + + /** + * Validate the action user ID. + * + * @param int $value A user ID. + * @return true|lang_string + */ + protected function validate_actionuserid($value) { + if ($value !== null && !\core_user::is_real_user($value)) { + return new lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Validate the context ID. + * + * @param int $value + * @return true|lang_string + */ + protected function validate_contextid($value) { + try { + context::instance_by_id($value); + } catch (moodle_exception $e) { + // That does not look good... + return new lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Validate the description $a. + * + * @param string $value + * @return true|lang_string + */ + protected function validate_desca($value) { + if ($value === null) { + return true; + } + + $desc = json_decode($value); + if ($desc === null && json_last_error() !== JSON_ERROR_NONE) { + return new lang_string('invaliddata', 'error'); + } + + return true; + } + + /** + * Validate the description identifier. + * + * Only validate string existence during create. If the string is removed later on we should + * not prevent this model from being updated. Alternatively we could check if the string has + * changed before performing the check but this overhead is not required for now. + * An evidence should usually never be updated anyway. + * + * @param string $value + * @return true|lang_string + */ + protected function validate_descidentifier($value) { + if (!$this->get_id() && !get_string_manager()->string_exists($value, $this->get('desccomponent'))) { + return new lang_string('invalidevidencedesc', 'core_competency'); + } + + return true; + } + + /** + * Validate the grade. + * + * For performance reason we do not validate that the grade is a valid item of the + * scale associated with the competency or framework. + * + * @param int $value The value. + * @return true|lang_string + */ + protected function validate_grade($value) { + if ($value !== null && $value <= 0) { + return new lang_string('invalidgrade', 'core_competency'); + } + + $action = $this->get('action'); + if ($value === null && $action == self::ACTION_COMPLETE) { + return new lang_string('invalidgrade', 'core_competency'); + + } else if ($value !== null && $action == self::ACTION_LOG) { + return new lang_string('invalidgrade', 'core_competency'); + } + + if ($value !== null) { + // TODO MDL-52243 Use a core method to validate the grade_scale item. + // Check if grade exist in the scale item values. + $competency = $this->get_competency(); + if (!array_key_exists($value - 1, $competency->get_scale()->scale_items)) { + return new lang_string('invalidgrade', 'core_competency'); + } + } + + return true; + } + + /** + * Validate the user competency. + * + * @param int $value + * @return true|lang_string + */ + protected function validate_usercompetencyid($value) { + if (!user_competency::record_exists($value)) { + return new lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Whether the current user can delete an evidence in the context of a user. + * + * @param int $userid The user ID the evidence belongs to. + * @return bool + */ + public static function can_delete_user($userid) { + return has_capability('moodle/competency:evidencedelete', context_user::instance($userid)); + } + +} diff --git a/competency/classes/external.php b/competency/classes/external.php new file mode 100644 index 00000000000..9f4bbbb69b4 --- /dev/null +++ b/competency/classes/external.php @@ -0,0 +1,4320 @@ +. + +/** + * External API. + * + * @package core_competency + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->libdir/externallib.php"); +require_once("$CFG->libdir/grade/grade_scale.php"); + +use context; +use context_system; +use context_course; +use context_helper; +use context_user; +use coding_exception; +use external_api; +use external_function_parameters; +use external_value; +use external_format_value; +use external_single_structure; +use external_multiple_structure; +use invalid_parameter_exception; +use required_capability_exception; +use grade_scale; + +use core_competency\external\competency_exporter; +use core_competency\external\competency_framework_exporter; +use core_competency\external\course_competency_exporter; +use core_competency\external\course_competency_settings_exporter; +use core_competency\external\evidence_exporter; +use core_competency\external\plan_exporter; +use core_competency\external\template_exporter; +use core_competency\external\user_competency_exporter; +use core_competency\external\user_competency_plan_exporter; +use core_competency\external\user_evidence_competency_exporter; +use core_competency\external\user_evidence_exporter; +use core_competency\external\user_summary_exporter; + +/** + * External API class. + * + * @package core_competency + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class external extends external_api { + + /** + * Returns a prepared structure to use a context parameters. + * @return external_single_structure + */ + protected static function get_context_parameters() { + $id = new external_value( + PARAM_INT, + 'Context ID. Either use this value, or level and instanceid.', + VALUE_DEFAULT, + 0 + ); + $level = new external_value( + PARAM_ALPHA, + 'Context level. To be used with instanceid.', + VALUE_DEFAULT, + '' + ); + $instanceid = new external_value( + PARAM_INT, + 'Context instance ID. To be used with level', + VALUE_DEFAULT, + 0 + ); + return new external_single_structure(array( + 'contextid' => $id, + 'contextlevel' => $level, + 'instanceid' => $instanceid, + )); + } + + /** + * Returns description of create_competency_framework() parameters. + * + * @return \external_function_parameters + */ + public static function create_competency_framework_parameters() { + $structure = competency_framework_exporter::get_create_structure(); + $params = array('competencyframework' => $structure); + return new external_function_parameters($params); + } + + /** + * Create a new competency framework + * + * @param array $competencyframework A single param with all the fields for a competency framework. + * @return \stdClass The new record + */ + public static function create_competency_framework($competencyframework) { + global $PAGE; + + $params = self::validate_parameters(self::create_competency_framework_parameters(), array( + 'competencyframework' => $competencyframework + )); + $params = $params['competencyframework']; + + $context = self::get_context_from_params($params); + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + + unset($params['contextlevel']); + unset($params['instanceid']); + $params['contextid'] = $context->id; + + $params = (object) $params; + $result = api::create_framework($params); + $exporter = new competency_framework_exporter($result); + $record = $exporter->export($output); + return $record; + } + + /** + * Returns description of create_competency_framework() result value. + * + * @return \external_description + */ + public static function create_competency_framework_returns() { + return competency_framework_exporter::get_read_structure(); + } + + /** + * Returns description of read_competency_framework() parameters. + * + * @return \external_function_parameters + */ + public static function read_competency_framework_parameters() { + $id = new external_value( + PARAM_INT, + 'Data base record id for the framework', + VALUE_REQUIRED + ); + + $params = array( + 'id' => $id, + ); + return new external_function_parameters($params); + } + + /** + * Read a competency framework by id. + * + * @param int $id The id of the framework. + * @return \stdClass + */ + public static function read_competency_framework($id) { + global $PAGE; + + $params = self::validate_parameters(self::read_competency_framework_parameters(), array( + 'id' => $id, + )); + + $framework = api::read_framework($params['id']); + self::validate_context($framework->get_context()); + $output = $PAGE->get_renderer('core'); + $exporter = new competency_framework_exporter($framework); + $record = $exporter->export($output); + return $record; + } + + /** + * Returns description of read_competency_framework() result value. + * + * @return \external_description + */ + public static function read_competency_framework_returns() { + return competency_framework_exporter::get_read_structure(); + } + + /** + * Returns description of competency_viewed() parameters. + * + * @return \external_function_parameters + */ + public static function competency_viewed_parameters() { + $id = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'id' => $id + ); + return new external_function_parameters($params); + } + + /** + * Log event competency viewed. + * + * @param int $id The competency ID. + * @return boolean + */ + public static function competency_viewed($id) { + $params = self::validate_parameters(self::competency_viewed_parameters(), array( + 'id' => $id + )); + return api::competency_viewed($params['id']); + } + + /** + * Returns description of competency_viewed() result value. + * + * @return \external_description + */ + public static function competency_viewed_returns() { + return new external_value(PARAM_BOOL, 'True if the event competency viewed was logged'); + } + + /** + * Returns description of duplicate_competency_framework() parameters. + * + * @return \external_function_parameters + */ + public static function duplicate_competency_framework_parameters() { + $id = new external_value( + PARAM_INT, + 'Data base record id for the framework', + VALUE_REQUIRED + ); + + $params = array( + 'id' => $id, + ); + return new external_function_parameters($params); + } + + /** + * Duplicate a competency framework + * + * @param int $id The competency framework id + * @return boolean + */ + public static function duplicate_competency_framework($id) { + global $PAGE; + $params = self::validate_parameters(self::duplicate_competency_framework_parameters(), array( + 'id' => $id, + )); + + $framework = api::read_framework($params['id']); + self::validate_context($framework->get_context()); + + $output = $PAGE->get_renderer('core'); + $framework = api::duplicate_framework($params['id']); + $exporter = new competency_framework_exporter($framework); + $record = $exporter->export($output); + return $record; + } + + /** + * Returns description of duplicate_competency_framework() result value. + * + * @return \external_description + */ + public static function duplicate_competency_framework_returns() { + return competency_framework_exporter::get_read_structure(); + } + + /** + * Returns description of delete_competency_framework() parameters. + * + * @return \external_function_parameters + */ + public static function delete_competency_framework_parameters() { + $id = new external_value( + PARAM_INT, + 'Data base record id for the framework', + VALUE_REQUIRED + ); + + $params = array( + 'id' => $id, + ); + return new external_function_parameters($params); + } + + /** + * Delete a competency framework + * + * @param int $id The competency framework id + * @return boolean + */ + public static function delete_competency_framework($id) { + $params = self::validate_parameters(self::delete_competency_framework_parameters(), array( + 'id' => $id, + )); + + $framework = api::read_framework($params['id']); + self::validate_context($framework->get_context()); + + return api::delete_framework($params['id']); + } + + /** + * Returns description of delete_competency_framework() result value. + * + * @return \external_description + */ + public static function delete_competency_framework_returns() { + return new external_value(PARAM_BOOL, 'True if the delete was successful'); + } + + /** + * Returns description of update_competency_framework() parameters. + * + * @return \external_function_parameters + */ + public static function update_competency_framework_parameters() { + $structure = competency_framework_exporter::get_update_structure(); + $params = array('competencyframework' => $structure); + return new external_function_parameters($params); + } + + /** + * Update an existing competency framework + * + * @param array $competencyframework An array with all the fields for a competency framework. + * @return boolean + */ + public static function update_competency_framework($competencyframework) { + + $params = self::validate_parameters(self::update_competency_framework_parameters(), array( + 'competencyframework' => $competencyframework + )); + $params = $params['competencyframework']; + + $framework = api::read_framework($params['id']); + self::validate_context($framework->get_context()); + + $params = (object) $params; + + return api::update_framework($params); + } + + /** + * Returns description of update_competency_framework() result value. + * + * @return \external_description + */ + public static function update_competency_framework_returns() { + return new external_value(PARAM_BOOL, 'True if the update was successful'); + } + + /** + * Returns description of list_competency_frameworks() parameters. + * + * @return \external_function_parameters + */ + public static function list_competency_frameworks_parameters() { + $sort = new external_value( + PARAM_ALPHANUMEXT, + 'Column to sort by.', + VALUE_DEFAULT, + 'shortname' + ); + $order = new external_value( + PARAM_ALPHA, + 'Sort direction. Should be either ASC or DESC', + VALUE_DEFAULT, + '' + ); + $skip = new external_value( + PARAM_INT, + 'Skip this number of records before returning results', + VALUE_DEFAULT, + 0 + ); + $limit = new external_value( + PARAM_INT, + 'Return this number of records at most.', + VALUE_DEFAULT, + 0 + ); + $includes = new external_value( + PARAM_ALPHA, + 'What other contextes to fetch the frameworks from. (children, parents, self)', + VALUE_DEFAULT, + 'children' + ); + $onlyvisible = new external_value( + PARAM_BOOL, + 'Only visible frameworks will be returned if visible true', + VALUE_DEFAULT, + false + ); + $query = new external_value( + PARAM_RAW, + 'A query string to filter the results', + VALUE_DEFAULT, + '' + ); + + $params = array( + 'sort' => $sort, + 'order' => $order, + 'skip' => $skip, + 'limit' => $limit, + 'context' => self::get_context_parameters(), + 'includes' => $includes, + 'onlyvisible' => $onlyvisible, + 'query' => $query, + ); + return new external_function_parameters($params); + } + + /** + * List the existing competency frameworks + * + * @param int $sort + * @param string $order + * @param string $skip + * @param int $limit + * @param array $context + * @param bool $includes + * @param bool $onlyvisible + * @param string $query + * + * @return array + * @throws \required_capability_exception + * @throws invalid_parameter_exception + */ + public static function list_competency_frameworks($sort, $order, $skip, $limit, $context, $includes, $onlyvisible, + $query = '') { + global $PAGE; + + $params = self::validate_parameters(self::list_competency_frameworks_parameters(), array( + 'sort' => $sort, + 'order' => $order, + 'skip' => $skip, + 'limit' => $limit, + 'context' => $context, + 'includes' => $includes, + 'onlyvisible' => $onlyvisible, + 'query' => $query, + )); + + $context = self::get_context_from_params($params['context']); + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + + if ($params['order'] !== '' && $params['order'] !== 'ASC' && $params['order'] !== 'DESC') { + throw new invalid_parameter_exception('Invalid order param. Must be ASC, DESC or empty.'); + } + + $results = api::list_frameworks($params['sort'], + $params['order'], + $params['skip'], + $params['limit'], + $context, + $params['includes'], + $params['onlyvisible'], + $params['query']); + $records = array(); + foreach ($results as $result) { + $exporter = new competency_framework_exporter($result); + $record = $exporter->export($output); + array_push($records, $record); + } + return $records; + } + + /** + * Returns description of list_competency_frameworks() result value. + * + * @return \external_description + */ + public static function list_competency_frameworks_returns() { + return new external_multiple_structure(competency_framework_exporter::get_read_structure()); + } + + /** + * Returns description of count_competency_frameworks() parameters. + * + * @return \external_function_parameters + */ + public static function count_competency_frameworks_parameters() { + $includes = new external_value( + PARAM_ALPHA, + 'What other contextes to fetch the frameworks from. (children, parents, self)', + VALUE_DEFAULT, + 'children' + ); + + $params = array( + 'context' => self::get_context_parameters(), + 'includes' => $includes + ); + return new external_function_parameters($params); + } + + /** + * Count the existing competency frameworks + * + * @param array $context + * @param string $includes + * @return int + */ + public static function count_competency_frameworks($context, $includes) { + $params = self::validate_parameters(self::count_competency_frameworks_parameters(), array( + 'context' => $context, + 'includes' => $includes + )); + + $context = self::get_context_from_params($params['context']); + self::validate_context($context); + + return api::count_frameworks($context, $params['includes']); + } + + /** + * Returns description of count_competency_frameworks() result value. + * + * @return \external_description + */ + public static function count_competency_frameworks_returns() { + return new external_value(PARAM_INT, 'The number of competency frameworks found.'); + } + + /** + * Returns description of competency_framework_viewed() parameters. + * + * @return \external_function_parameters + */ + public static function competency_framework_viewed_parameters() { + $id = new external_value( + PARAM_INT, + 'The competency framework id', + VALUE_REQUIRED + ); + + $params = array( + 'id' => $id + ); + return new external_function_parameters($params); + } + + /** + * Log event competency framework viewed. + * + * @param int $id The competency framework ID. + * @return boolean + */ + public static function competency_framework_viewed($id) { + $params = self::validate_parameters(self::competency_framework_viewed_parameters(), array( + 'id' => $id + )); + return api::competency_framework_viewed($params['id']); + + } + + /** + * Returns description of competency_framework_viewed() result value. + * + * @return \external_description + */ + public static function competency_framework_viewed_returns() { + return new external_value(PARAM_BOOL, 'True if the event competency framework was logged'); + } + + /** + * Returns description of create_competency() parameters. + * + * @return \external_function_parameters + */ + public static function create_competency_parameters() { + $structure = competency_exporter::get_create_structure(); + $params = array('competency' => $structure); + return new external_function_parameters($params); + } + + /** + * Create a new competency + * + * @param array $competency All the fields for a competency record (including id) + * @return array the competency + */ + public static function create_competency($competency) { + global $PAGE; + + $params = self::validate_parameters(self::create_competency_parameters(), array( + 'competency' => $competency + )); + + $params = $params['competency']; + $framework = api::read_framework($params['competencyframeworkid']); + $context = $framework->get_context(); + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + + $params = (object) $params; + $result = api::create_competency($params); + $exporter = new competency_exporter($result, array('context' => $context)); + $record = $exporter->export($output); + return $record; + } + + /** + * Returns description of create_competency() result value. + * + * @return \external_description + */ + public static function create_competency_returns() { + return competency_exporter::get_read_structure(); + } + + /** + * Returns description of read_competency() parameters. + * + * @return \external_function_parameters + */ + public static function read_competency_parameters() { + $id = new external_value( + PARAM_INT, + 'Data base record id for the competency', + VALUE_REQUIRED + ); + + $params = array( + 'id' => $id, + ); + return new external_function_parameters($params); + } + + /** + * Read a competency by id. + * + * @param int $id The id of the competency + * @return \stdClass + */ + public static function read_competency($id) { + global $PAGE; + + $params = self::validate_parameters(self::read_competency_parameters(), array( + 'id' => $id, + )); + + $competency = api::read_competency($params['id']); + $context = $competency->get_context(); + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + $exporter = new competency_exporter($competency, array('context' => $context)); + $record = $exporter->export($output); + return $record; + } + + /** + * Returns description of read_competency() result value. + * + * @return \external_description + */ + public static function read_competency_returns() { + return competency_exporter::get_read_structure(); + } + + /** + * Returns description of delete_competency() parameters. + * + * @return \external_function_parameters + */ + public static function delete_competency_parameters() { + $id = new external_value( + PARAM_INT, + 'Data base record id for the competency', + VALUE_REQUIRED + ); + + $params = array( + 'id' => $id, + ); + return new external_function_parameters($params); + } + + /** + * Delete a competency + * + * @param int $id The competency id + * @return boolean + */ + public static function delete_competency($id) { + $params = self::validate_parameters(self::delete_competency_parameters(), array( + 'id' => $id, + )); + + $competency = api::read_competency($params['id']); + $context = $competency->get_context(); + self::validate_context($context); + + return api::delete_competency($params['id']); + } + + /** + * Returns description of delete_competency() result value. + * + * @return \external_description + */ + public static function delete_competency_returns() { + return new external_value(PARAM_BOOL, 'True if the delete was successful'); + } + + /** + * Returns description of update_competency() parameters. + * + * @return \external_function_parameters + */ + public static function update_competency_parameters() { + $structure = competency_exporter::get_update_structure(); + $params = array('competency' => $structure); + return new external_function_parameters($params); + } + + /** + * Update an existing competency + * + * @param array $competency The array of competency fields (id is required). + * @return boolean + */ + public static function update_competency($competency) { + $params = self::validate_parameters(self::update_competency_parameters(), array( + 'competency' => $competency + )); + $params = $params['competency']; + + $competency = api::read_competency($params['id']); + self::validate_context($competency->get_context()); + + $params = (object) $params; + + return api::update_competency($params); + } + + /** + * Returns description of update_competency_framework() result value. + * + * @return \external_description + */ + public static function update_competency_returns() { + return new external_value(PARAM_BOOL, 'True if the update was successful'); + } + + /** + * Returns description of list_competencies() parameters. + * + * @return \external_function_parameters + */ + public static function list_competencies_parameters() { + $filters = new external_multiple_structure(new external_single_structure( + array( + 'column' => new external_value(PARAM_ALPHANUMEXT, 'Column name to filter by'), + 'value' => new external_value(PARAM_TEXT, 'Value to filter by. Must be exact match') + ) + )); + $sort = new external_value( + PARAM_ALPHANUMEXT, + 'Column to sort by.', + VALUE_DEFAULT, + '' + ); + $order = new external_value( + PARAM_ALPHA, + 'Sort direction. Should be either ASC or DESC', + VALUE_DEFAULT, + '' + ); + $skip = new external_value( + PARAM_INT, + 'Skip this number of records before returning results', + VALUE_DEFAULT, + 0 + ); + $limit = new external_value( + PARAM_INT, + 'Return this number of records at most.', + VALUE_DEFAULT, + 0 + ); + + $params = array( + 'filters' => $filters, + 'sort' => $sort, + 'order' => $order, + 'skip' => $skip, + 'limit' => $limit + ); + return new external_function_parameters($params); + } + + /** + * List the existing competency. + * + * @param string $filters + * @param int $sort + * @param string $order + * @param string $skip + * @param int $limit + * + * @return array + * @throws \required_capability_exception + * @throws invalid_parameter_exception + */ + public static function list_competencies($filters, $sort, $order, $skip, $limit) { + global $PAGE; + + $params = self::validate_parameters(self::list_competencies_parameters(), array( + 'filters' => $filters, + 'sort' => $sort, + 'order' => $order, + 'skip' => $skip, + 'limit' => $limit + )); + + if ($params['order'] !== '' && $params['order'] !== 'ASC' && $params['order'] !== 'DESC') { + throw new invalid_parameter_exception('Invalid order param. Must be ASC, DESC or empty.'); + } + + $safefilters = array(); + $validcolumns = array('id', 'shortname', 'description', 'sortorder', 'idnumber', + 'parentid', 'competencyframeworkid'); + foreach ($params['filters'] as $filter) { + if (!in_array($filter->column, $validcolumns)) { + throw new invalid_parameter_exception('Filter column was invalid'); + } + $safefilters[$filter->column] = $filter->value; + } + + $context = null; + if (isset($safefilters['competencyframeworkid'])) { + $framework = api::read_framework($safefilters['competencyframeworkid']); + $context = $framework->get_context(); + } else { + $context = context_system::instance(); + } + + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + + $results = api::list_competencies( + $safefilters, + $params['sort'], + $params['order'], + $params['skip'], + $params['limit'] + ); + + $records = array(); + foreach ($results as $result) { + $exporter = new competency_exporter($result, array('context' => $context)); + $record = $exporter->export($output); + array_push($records, $record); + } + return $records; + } + + /** + * Returns description of list_competencies() result value. + * + * @return \external_description + */ + public static function list_competencies_returns() { + return new external_multiple_structure(competency_exporter::get_read_structure()); + } + + /** + * Returns description of search_competencies() parameters. + * + * @return \external_function_parameters + */ + public static function search_competencies_parameters() { + $searchtext = new external_value( + PARAM_RAW, + 'Text to search for', + VALUE_REQUIRED + ); + $frameworkid = new external_value( + PARAM_INT, + 'Competency framework id', + VALUE_REQUIRED + ); + + $params = array( + 'searchtext' => $searchtext, + 'competencyframeworkid' => $frameworkid + ); + return new external_function_parameters($params); + } + + /** + * List the existing competency frameworks + * + * @param string $searchtext Text to search. + * @param int $competencyframeworkid Framework id. + * + * @return array + */ + public static function search_competencies($searchtext, $competencyframeworkid) { + global $PAGE; + + $params = self::validate_parameters(self::search_competencies_parameters(), array( + 'searchtext' => $searchtext, + 'competencyframeworkid' => $competencyframeworkid + )); + + $framework = api::read_framework($params['competencyframeworkid']); + $context = $framework->get_context(); + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + + $results = api::search_competencies($params['searchtext'], $params['competencyframeworkid']); + $records = array(); + foreach ($results as $result) { + $exporter = new competency_exporter($result, array('context' => $context)); + $record = $exporter->export($output); + + array_push($records, $record); + } + + return $records; + } + + /** + * Returns description of search_competencies() result value. + * + * @return \external_description + */ + public static function search_competencies_returns() { + return new external_multiple_structure(competency_exporter::get_read_structure()); + } + + /** + * Returns description of count_competencies() parameters. + * + * @return \external_function_parameters + */ + public static function count_competencies_parameters() { + $filters = new external_multiple_structure(new external_single_structure( + array( + 'column' => new external_value(PARAM_ALPHANUMEXT, 'Column name to filter by'), + 'value' => new external_value(PARAM_TEXT, 'Value to filter by. Must be exact match') + ) + )); + + $params = array( + 'filters' => $filters, + ); + return new external_function_parameters($params); + } + + /** + * Count the existing competency frameworks. + * + * @param string $filters Filters to use. + * @return boolean + */ + public static function count_competencies($filters) { + $params = self::validate_parameters(self::count_competencies_parameters(), array( + 'filters' => $filters + )); + + $safefilters = array(); + $validcolumns = array('id', 'shortname', 'description', 'sortorder', 'idnumber', 'parentid', 'competencyframeworkid'); + foreach ($params['filters'] as $filter) { + if (!in_array($filter['column'], $validcolumns)) { + throw new invalid_parameter_exception('Filter column was invalid'); + } + $safefilters[$filter['column']] = $filter['value']; + } + + $context = null; + if (isset($safefilters['competencyframeworkid'])) { + $framework = api::read_framework($safefilters['competencyframeworkid']); + $context = $framework->get_context(); + } else { + $context = context_system::instance(); + } + + self::validate_context($context); + + return api::count_competencies($safefilters); + } + + /** + * Returns description of count_competencies() result value. + * + * @return \external_description + */ + public static function count_competencies_returns() { + return new external_value(PARAM_INT, 'The number of competencies found.'); + } + + /** + * Returns description of set_parent_competency() parameters. + * + * @return \external_function_parameters + */ + public static function set_parent_competency_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $parentid = new external_value( + PARAM_INT, + 'The new competency parent id', + VALUE_REQUIRED + ); + $params = array( + 'competencyid' => $competencyid, + 'parentid' => $parentid + ); + return new external_function_parameters($params); + } + + /** + * Move the competency to a new parent. + * + * @param int $competencyid Competency id. + * @param int $parentid Parent id. + * + * @return bool + */ + public static function set_parent_competency($competencyid, $parentid) { + $params = self::validate_parameters(self::set_parent_competency_parameters(), array( + 'competencyid' => $competencyid, + 'parentid' => $parentid + )); + + $competency = api::read_competency($params['competencyid']); + self::validate_context($competency->get_context()); + + return api::set_parent_competency($params['competencyid'], $params['parentid']); + } + + /** + * Returns description of set_parent_competency() result value. + * + * @return \external_description + */ + public static function set_parent_competency_returns() { + return new external_value(PARAM_BOOL, 'True if the update was successful'); + } + + /** + * Returns description of move_up_competency() parameters. + * + * @return \external_function_parameters + */ + public static function move_up_competency_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'id' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * Change the sort order of a competency. + * + * @param int $competencyid Competency id. + * @return boolean + */ + public static function move_up_competency($competencyid) { + $params = self::validate_parameters(self::move_up_competency_parameters(), array( + 'id' => $competencyid, + )); + + $competency = api::read_competency($params['id']); + self::validate_context($competency->get_context()); + + return api::move_up_competency($params['id']); + } + + /** + * Returns description of move_up_competency() result value. + * + * @return \external_description + */ + public static function move_up_competency_returns() { + return new external_value(PARAM_BOOL, 'True if the update was successful'); + } + + /** + * Returns description of move_down_competency() parameters. + * + * @return \external_function_parameters + */ + public static function move_down_competency_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'id' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * Change the sort order of a competency. + * + * @param int $competencyid Competency id. + * @return boolean + */ + public static function move_down_competency($competencyid) { + $params = self::validate_parameters(self::move_down_competency_parameters(), array( + 'id' => $competencyid, + )); + + $competency = api::read_competency($params['id']); + self::validate_context($competency->get_context()); + + return api::move_down_competency($params['id']); + } + + /** + * Returns description of move_down_competency() result value. + * + * @return \external_description + */ + public static function move_down_competency_returns() { + return new external_value(PARAM_BOOL, 'True if the update was successful'); + } + + /** + * Returns description of count_courses_using_competency() parameters. + * + * @return \external_function_parameters + */ + public static function count_courses_using_competency_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'id' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * Count the courses (visible to this user) that use this competency. + * + * @param int $competencyid Competency id. + * @return int + */ + public static function count_courses_using_competency($competencyid) { + $params = self::validate_parameters(self::count_courses_using_competency_parameters(), array( + 'id' => $competencyid, + )); + + $competency = api::read_competency($params['id']); + self::validate_context($competency->get_context()); + + return api::count_courses_using_competency($params['id']); + } + + /** + * Returns description of count_courses_using_competency() result value. + * + * @return \external_description + */ + public static function count_courses_using_competency_returns() { + return new external_value(PARAM_INT, 'The number of courses using this competency'); + } + + /** + * Returns description of count_competencies_in_course() parameters. + * + * @return \external_function_parameters + */ + public static function count_competencies_in_course_parameters() { + $courseid = new external_value( + PARAM_INT, + 'The course id', + VALUE_REQUIRED + ); + $params = array( + 'id' => $courseid, + ); + return new external_function_parameters($params); + } + + /** + * Count the competencies (visible to this user) in this course. + * + * @param int $courseid The course id to check. + * @return int + */ + public static function count_competencies_in_course($courseid) { + $params = self::validate_parameters(self::count_competencies_in_course_parameters(), array( + 'id' => $courseid, + )); + + self::validate_context(context_course::instance($params['id'])); + + return api::count_competencies_in_course($params['id']); + } + + /** + * Returns description of count_competencies_in_course() result value. + * + * @return \external_description + */ + public static function count_competencies_in_course_returns() { + return new external_value(PARAM_INT, 'The number of competencies in this course.'); + } + + /** + * Returns description of list_course_module_competencies() parameters. + * + * @return \external_function_parameters + */ + public static function list_course_module_competencies_parameters() { + $cmid = new external_value( + PARAM_INT, + 'The course module id', + VALUE_REQUIRED + ); + $params = array( + 'cmid' => $cmid + ); + return new external_function_parameters($params); + } + + /** + * List the course modules using this competency (visible to this user) in this course. + * + * @param int $cmid The course module id to check. + * @return array + */ + public static function list_course_module_competencies($cmid) { + global $PAGE; + + $params = self::validate_parameters(self::list_course_module_competencies_parameters(), array( + 'cmid' => $cmid + )); + + $context = context_module::instance($params['cmid']); + self::validate_context($context); + + $output = $PAGE->get_renderer('core'); + + $apiresult = api::list_course_module_competencies($params['cmid']); + $result = array(); + + foreach ($apiresult as $cmrecord) { + $one = new \stdClass(); + $exporter = new competency_exporter($cmrecord['competency']); + $one->competency = $exporter->export($output); + $exporter = new course_module_competency_exporter($cmrecord['coursemodulecompetency']); + $one->coursemodulecompetency = $exporter->export($output); + + $result[] = (array) $one; + } + + return $result; + } + + /** + * Returns description of list_course_module_competencies() result value. + * + * @return \external_description + */ + public static function list_course_module_competencies_returns() { + return new external_multiple_structure( + new external_single_structure(array( + 'competency' => competency_exporter::get_read_structure(), + 'coursemodulecompetency' => course_module_competency_exporter::get_read_structure() + )) + ); + } + + /** + * Returns description of list_course_competencies() parameters. + * + * @return \external_function_parameters + */ + public static function list_course_competencies_parameters() { + $courseid = new external_value( + PARAM_INT, + 'The course id', + VALUE_REQUIRED + ); + $params = array( + 'id' => $courseid, + ); + return new external_function_parameters($params); + } + + /** + * List the competencies (visible to this user) in this course. + * + * @param int $courseid The course id to check. + * @return array + */ + public static function list_course_competencies($courseid) { + global $PAGE; + + $params = self::validate_parameters(self::list_course_competencies_parameters(), array( + 'id' => $courseid, + )); + + $coursecontext = context_course::instance($params['id']); + self::validate_context($coursecontext); + + $output = $PAGE->get_renderer('core'); + + $competencies = api::list_course_competencies($params['id']); + $result = array(); + + $contextcache = array(); + foreach ($competencies as $competency) { + if (!isset($contextcache[$competency['competency']->get_competencyframeworkid()])) { + $contextcache[$competency['competency']->get_competencyframeworkid()] = $competency['competency']->get_context(); + } + $context = $contextcache[$competency['competency']->get_competencyframeworkid()]; + $exporter = new competency_exporter($competency['competency'], array('context' => $context)); + $competencyrecord = $exporter->export($output); + $exporter = new course_competency_exporter($competency['coursecompetency'], array('context' => $context)); + $coursecompetencyrecord = $exporter->export($output); + + $result[] = array( + 'competency' => $competencyrecord, + 'coursecompetency' => $coursecompetencyrecord + ); + } + + return $result; + } + + /** + * Returns description of list_course_competencies() result value. + * + * @return \external_description + */ + public static function list_course_competencies_returns() { + return new external_multiple_structure( + new external_single_structure(array( + 'competency' => competency_exporter::get_read_structure(), + 'coursecompetency' => course_competency_exporter::get_read_structure() + )) + ); + } + + /** + * Returns description of add_competency_to_course() parameters. + * + * @return \external_function_parameters + */ + public static function add_competency_to_course_parameters() { + $courseid = new external_value( + PARAM_INT, + 'The course id', + VALUE_REQUIRED + ); + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'courseid' => $courseid, + 'competencyid' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * Count the competencies (visible to this user) in this course. + * + * @param int $courseid The course id to check. + * @param int $competencyid Competency id. + * @return int + */ + public static function add_competency_to_course($courseid, $competencyid) { + $params = self::validate_parameters(self::add_competency_to_course_parameters(), array( + 'courseid' => $courseid, + 'competencyid' => $competencyid, + )); + self::validate_context(context_course::instance($params['courseid'])); + return api::add_competency_to_course($params['courseid'], $params['competencyid']); + } + + /** + * Returns description of add_competency_to_course() result value. + * + * @return \external_description + */ + public static function add_competency_to_course_returns() { + return new external_value(PARAM_BOOL, 'True if successful.'); + } + + /** + * Returns description of remove_competency_from_course() parameters. + * + * @return \external_function_parameters + */ + public static function remove_competency_from_course_parameters() { + $courseid = new external_value( + PARAM_INT, + 'The course id', + VALUE_REQUIRED + ); + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'courseid' => $courseid, + 'competencyid' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * Count the competencies (visible to this user) in this course. + * + * @param int $courseid The course id to check. + * @param int $competencyid Competency id. + * @return int + */ + public static function remove_competency_from_course($courseid, $competencyid) { + $params = self::validate_parameters(self::remove_competency_from_course_parameters(), array( + 'courseid' => $courseid, + 'competencyid' => $competencyid, + )); + self::validate_context(context_course::instance($params['courseid'])); + return api::remove_competency_from_course($params['courseid'], $params['competencyid']); + } + + /** + * Returns description of remove_competency_from_course() result value. + * + * @return \external_description + */ + public static function remove_competency_from_course_returns() { + return new external_value(PARAM_BOOL, 'True if successful.'); + } + + /** + * Returns description of reorder_course_competency() parameters. + * + * @return \external_function_parameters + */ + public static function reorder_course_competency_parameters() { + $courseid = new external_value( + PARAM_INT, + 'The course id', + VALUE_REQUIRED + ); + $competencyidfrom = new external_value( + PARAM_INT, + 'The competency id we are moving', + VALUE_REQUIRED + ); + $competencyidto = new external_value( + PARAM_INT, + 'The competency id we are moving to', + VALUE_REQUIRED + ); + $params = array( + 'courseid' => $courseid, + 'competencyidfrom' => $competencyidfrom, + 'competencyidto' => $competencyidto, + ); + return new external_function_parameters($params); + } + + /** + * Change the order of course competencies. + * + * @param int $courseid The course id + * @param int $competencyidfrom The competency to move. + * @param int $competencyidto The competency to move to. + * @return bool + */ + public static function reorder_course_competency($courseid, $competencyidfrom, $competencyidto) { + $params = self::validate_parameters(self::reorder_course_competency_parameters(), array( + 'courseid' => $courseid, + 'competencyidfrom' => $competencyidfrom, + 'competencyidto' => $competencyidto, + )); + self::validate_context(context_course::instance($params['courseid'])); + return api::reorder_course_competency($params['courseid'], $params['competencyidfrom'], $params['competencyidto']); + } + + /** + * Returns description of reorder_course_competency() result value. + * + * @return \external_description + */ + public static function reorder_course_competency_returns() { + return new external_value(PARAM_BOOL, 'True if successful.'); + } + + /** + * Returns description of reorder_template_competency() parameters. + * + * @return \external_function_parameters + */ + public static function reorder_template_competency_parameters() { + $templateid = new external_value( + PARAM_INT, + 'The template id', + VALUE_REQUIRED + ); + $competencyidfrom = new external_value( + PARAM_INT, + 'The competency id we are moving', + VALUE_REQUIRED + ); + $competencyidto = new external_value( + PARAM_INT, + 'The competency id we are moving to', + VALUE_REQUIRED + ); + $params = array( + 'templateid' => $templateid, + 'competencyidfrom' => $competencyidfrom, + 'competencyidto' => $competencyidto, + ); + return new external_function_parameters($params); + } + + /** + * Change the order of template competencies. + * + * @param int $templateid The template id + * @param int $competencyidfrom The competency to move. + * @param int $competencyidto The competency to move to. + * @return bool + */ + public static function reorder_template_competency($templateid, $competencyidfrom, $competencyidto) { + $params = self::validate_parameters(self::reorder_template_competency_parameters(), array( + 'templateid' => $templateid, + 'competencyidfrom' => $competencyidfrom, + 'competencyidto' => $competencyidto, + )); + + $template = api::read_template($params['templateid']); + self::validate_context($template->get_context()); + + return api::reorder_template_competency($params['templateid'], $params['competencyidfrom'], $params['competencyidto']); + } + + /** + * Returns description of reorder_template_competency() result value. + * + * @return \external_description + */ + public static function reorder_template_competency_returns() { + return new external_value(PARAM_BOOL, 'True if successful.'); + } + + /** + * Returns description of create_template() parameters. + * + * @return \external_function_parameters + */ + public static function create_template_parameters() { + $structure = template_exporter::get_create_structure(); + $params = array('template' => $structure); + return new external_function_parameters($params); + } + + /** + * Create a new learning plan template + * + * @param array $template The list of fields for the template. + * @return \stdClass Record of new template. + */ + public static function create_template($template) { + global $PAGE; + + $params = self::validate_parameters(self::create_template_parameters(), array('template' => $template)); + $params = $params['template']; + $context = self::get_context_from_params($params); + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + + unset($params['contextlevel']); + unset($params['instanceid']); + $params = (object) $params; + $params->contextid = $context->id; + + $result = api::create_template($params); + $exporter = new template_exporter($result); + $record = $exporter->export($output); + return $record; + } + + /** + * Returns description of create_template() result value. + * + * @return \external_description + */ + public static function create_template_returns() { + return template_exporter::get_read_structure(); + } + + /** + * Returns description of read_template() parameters. + * + * @return \external_function_parameters + */ + public static function read_template_parameters() { + $id = new external_value( + PARAM_INT, + 'Data base record id for the template', + VALUE_REQUIRED + ); + + $params = array( + 'id' => $id, + ); + return new external_function_parameters($params); + } + + /** + * Read a learning plan template by id. + * + * @param int $id The id of the template. + * @return \stdClass + */ + public static function read_template($id) { + global $PAGE; + + $params = self::validate_parameters(self::read_template_parameters(), array('id' => $id)); + $template = api::read_template($params['id']); + self::validate_context($template->get_context()); + $output = $PAGE->get_renderer('core'); + + $exporter = new template_exporter($template); + $record = $exporter->export($output); + return $record; + } + + /** + * Returns description of read_template() result value. + * + * @return \external_description + */ + public static function read_template_returns() { + return template_exporter::get_read_structure(); + } + + /** + * Returns description of delete_template() parameters. + * + * @return \external_function_parameters + */ + public static function delete_template_parameters() { + $id = new external_value( + PARAM_INT, + 'Data base record id for the template', + VALUE_REQUIRED + ); + + $deleteplans = new external_value( + PARAM_BOOL, + 'Boolean to indicate if plans must be deleted', + VALUE_REQUIRED + ); + + $params = array( + 'id' => $id, + 'deleteplans' => $deleteplans + ); + return new external_function_parameters($params); + } + + /** + * Delete a learning plan template + * + * @param int $id The learning plan template id + * @param boolean $deleteplans True to delete the plans associated to template or false to unlink them + * @return boolean + */ + public static function delete_template($id, $deleteplans = true) { + $params = self::validate_parameters(self::delete_template_parameters(), array( + 'id' => $id, + 'deleteplans' => $deleteplans, + )); + + $template = api::read_template($params['id']); + self::validate_context($template->get_context()); + + return api::delete_template($params['id'], $params['deleteplans']); + } + + /** + * Returns description of delete_template() result value. + * + * @return \external_description + */ + public static function delete_template_returns() { + return new external_value(PARAM_BOOL, 'True if the delete was successful'); + } + + /** + * Returns description of update_template() parameters. + * + * @return \external_function_parameters + */ + public static function update_template_parameters() { + $structure = template_exporter::get_update_structure(); + $params = array('template' => $structure); + return new external_function_parameters($params); + } + + /** + * Update an existing learning plan template + * + * @param array $template The list of fields for the template. + * @return boolean + */ + public static function update_template($template) { + + $params = self::validate_parameters(self::update_template_parameters(), array('template' => $template)); + $params = $params['template']; + $template = api::read_template($params['id']); + self::validate_context($template->get_context()); + + $params = (object) $params; + + return api::update_template($params); + } + + /** + * Returns description of update_template() result value. + * + * @return \external_description + */ + public static function update_template_returns() { + return new external_value(PARAM_BOOL, 'True if the update was successful'); + } + + /** + * Returns description of duplicate_template() parameters. + * + * @return \external_function_parameters + */ + public static function duplicate_template_parameters() { + $templateid = new external_value( + PARAM_INT, + 'The template id', + VALUE_REQUIRED + ); + + $params = array( + 'id' => $templateid + ); + return new external_function_parameters($params); + } + + /** + * Duplicate a learning plan template. + * + * @param int $id the id of the learning plan template to duplicate + * @return boolean Record of new template. + */ + public static function duplicate_template($id) { + global $PAGE; + + $params = self::validate_parameters(self::duplicate_template_parameters(), array('id' => $id)); + + $template = api::read_template($params['id']); + self::validate_context($template->get_context()); + $output = $PAGE->get_renderer('core'); + + $result = api::duplicate_template($params['id']); + $exporter = new template_exporter($result); + return $exporter->export($output); + } + + /** + * Returns description of duplicate_template() result value. + * + * @return \external_description + */ + public static function duplicate_template_returns() { + return template_exporter::get_read_structure(); + } + + /** + * Returns description of list_templates() parameters. + * + * @return \external_function_parameters + */ + public static function list_templates_parameters() { + $sort = new external_value( + PARAM_ALPHANUMEXT, + 'Column to sort by.', + VALUE_DEFAULT, + '' + ); + $order = new external_value( + PARAM_ALPHA, + 'Sort direction. Should be either ASC or DESC', + VALUE_DEFAULT, + '' + ); + $skip = new external_value( + PARAM_INT, + 'Skip this number of records before returning results', + VALUE_DEFAULT, + 0 + ); + $limit = new external_value( + PARAM_INT, + 'Return this number of records at most.', + VALUE_DEFAULT, + 0 + ); + $includes = new external_value( + PARAM_ALPHA, + 'What other contexts to fetch the templates from. (children, parents, self)', + VALUE_DEFAULT, + 'children' + ); + $onlyvisible = new external_value( + PARAM_BOOL, + 'If should list only visible templates', + VALUE_DEFAULT, + false + ); + + $params = array( + 'sort' => $sort, + 'order' => $order, + 'skip' => $skip, + 'limit' => $limit, + 'context' => self::get_context_parameters(), + 'includes' => $includes, + 'onlyvisible' => $onlyvisible + ); + return new external_function_parameters($params); + } + + /** + * List the existing learning plan templates + * + * @param string $sort Field to sort by. + * @param string $order Sort order. + * @param int $skip Limitstart. + * @param int $limit Number of rows to return. + * @param array $context + * @param bool $includes + * @param bool $onlyvisible + * + * @return array + */ + public static function list_templates($sort, $order, $skip, $limit, $context, $includes, $onlyvisible) { + global $PAGE; + + $params = self::validate_parameters(self::list_templates_parameters(), array( + 'sort' => $sort, + 'order' => $order, + 'skip' => $skip, + 'limit' => $limit, + 'context' => $context, + 'includes' => $includes, + 'onlyvisible' => $onlyvisible + )); + + $context = self::get_context_from_params($params['context']); + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + + if ($params['order'] !== '' && $params['order'] !== 'ASC' && $params['order'] !== 'DESC') { + throw new invalid_parameter_exception('Invalid order param. Must be ASC, DESC or empty.'); + } + + $results = api::list_templates($params['sort'], + $params['order'], + $params['skip'], + $params['limit'], + $context, + $params['includes'], + $params['onlyvisible']); + $records = array(); + foreach ($results as $result) { + $exporter = new template_exporter($result); + $record = $exporter->export($output); + array_push($records, $record); + } + return $records; + } + + /** + * Returns description of list_templates() result value. + * + * @return \external_description + */ + public static function list_templates_returns() { + return new external_multiple_structure(template_exporter::get_read_structure()); + } + + /** + * Returns description of count_templates() parameters. + * + * @return \external_function_parameters + */ + public static function count_templates_parameters() { + $includes = new external_value( + PARAM_ALPHA, + 'What other contextes to fetch the frameworks from. (children, parents, self)', + VALUE_DEFAULT, + 'children' + ); + + $params = array( + 'context' => self::get_context_parameters(), + 'includes' => $includes + ); + return new external_function_parameters($params); + } + + /** + * Count the existing learning plan templates + * + * @param array $context + * @param string $includes + * @return int + */ + public static function count_templates($context, $includes) { + $params = self::validate_parameters(self::count_templates_parameters(), array( + 'context' => $context, + 'includes' => $includes + )); + $context = self::get_context_from_params($params['context']); + self::validate_context($context); + + return api::count_templates($context, $includes); + } + + /** + * Returns description of count_templates() result value. + * + * @return \external_description + */ + public static function count_templates_returns() { + return new external_value(PARAM_INT, 'The number of learning plan templates found.'); + } + + /** + * Returns description of count_templates_using_competency() parameters. + * + * @return \external_function_parameters + */ + public static function count_templates_using_competency_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'id' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * Count the learning plan templates (visible to this user) that use this competency. + * + * @param int $competencyid Competency id. + * @return int + */ + public static function count_templates_using_competency($competencyid) { + $params = self::validate_parameters(self::count_templates_using_competency_parameters(), array( + 'id' => $competencyid, + )); + + $competency = api::read_competency($params['id']); + self::validate_context($competency->get_context()); + + return api::count_templates_using_competency($params['id']); + } + + /** + * Returns description of count_templates_using_competency() result value. + * + * @return \external_description + */ + public static function count_templates_using_competency_returns() { + return new external_value(PARAM_INT, 'The number of learning plan templates using this competency'); + } + + /** + * Returns description of list_templates_using_competency() parameters. + * + * @return \external_function_parameters + */ + public static function list_templates_using_competency_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'id' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * List the learning plan templates (visible to this user) that use this competency. + * + * @param int $competencyid Competency id. + * @return array + */ + public static function list_templates_using_competency($competencyid) { + global $PAGE; + + $params = self::validate_parameters(self::list_templates_using_competency_parameters(), array( + 'id' => $competencyid, + )); + + $competency = api::read_competency($params['id']); + self::validate_context($competency->get_context()); + $output = $PAGE->get_renderer('core'); + + $templates = api::list_templates_using_competency($params['id']); + $records = array(); + + foreach ($templates as $template) { + $exporter = new template_exporter($template); + $record = $exporter->export($output); + $records[] = $record; + } + + return $records; + } + + /** + * Returns description of list_templates_using_competency() result value. + * + * @return \external_description + */ + public static function list_templates_using_competency_returns() { + return new external_multiple_structure(template_exporter::get_read_structure()); + } + + /** + * Returns description of count_competencies_in_template() parameters. + * + * @return \external_function_parameters + */ + public static function count_competencies_in_template_parameters() { + $templateid = new external_value( + PARAM_INT, + 'The template id', + VALUE_REQUIRED + ); + $params = array( + 'id' => $templateid, + ); + return new external_function_parameters($params); + } + + /** + * Count the competencies (visible to this user) in this learning plan template. + * + * @param int $templateid The template id to check + * @return int + */ + public static function count_competencies_in_template($templateid) { + $params = self::validate_parameters(self::count_competencies_in_template_parameters(), array( + 'id' => $templateid, + )); + $template = api::read_template($params['id']); + self::validate_context($template->get_context()); + + return api::count_competencies_in_template($params['id']); + } + + /** + * Returns description of count_competencies_in_template() result value. + * + * @return \external_description + */ + public static function count_competencies_in_template_returns() { + return new external_value(PARAM_INT, 'The number of competencies in this learning plan template.'); + } + + /** + * Returns description of list_competencies_in_template() parameters. + * + * @return \external_function_parameters + */ + public static function list_competencies_in_template_parameters() { + $templateid = new external_value( + PARAM_INT, + 'The template id', + VALUE_REQUIRED + ); + $params = array( + 'id' => $templateid, + ); + return new external_function_parameters($params); + } + + /** + * List the competencies (visible to this user) in this learning plan template. + * + * @param int $templateid Template id. + * @return array + */ + public static function list_competencies_in_template($templateid) { + global $PAGE; + + $params = self::validate_parameters(self::list_competencies_in_template_parameters(), array( + 'id' => $templateid, + )); + + $template = api::read_template($params['id']); + self::validate_context($template->get_context()); + $output = $PAGE->get_renderer('core'); + + $competencies = api::list_competencies_in_template($params['id']); + $results = array(); + $contextcache = array(); + + foreach ($competencies as $competency) { + if (!isset($contextcache[$competency->get_competencyframeworkid()])) { + $contextcache[$competency->get_competencyframeworkid()] = $competency->get_context(); + } + $context = $contextcache[$competency->get_competencyframeworkid()]; + $exporter = new competency_exporter($competency, array('context' => $context)); + $record = $exporter->export($output); + array_push($results, $record); + } + return $results; + } + + /** + * Returns description of list_competencies_in_template() result value. + * + * @return \external_description + */ + public static function list_competencies_in_template_returns() { + return new external_multiple_structure(competency_exporter::get_read_structure()); + } + + /** + * Returns description of add_competency_to_template() parameters. + * + * @return \external_function_parameters + */ + public static function add_competency_to_template_parameters() { + $templateid = new external_value( + PARAM_INT, + 'The template id', + VALUE_REQUIRED + ); + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'templateid' => $templateid, + 'competencyid' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * Count the competencies (visible to this user) in this template. + * + * @param int $templateid Template id. + * @param int $competencyid Competency id. + * @return int + */ + public static function add_competency_to_template($templateid, $competencyid) { + global $PAGE; + $params = self::validate_parameters(self::add_competency_to_template_parameters(), array( + 'templateid' => $templateid, + 'competencyid' => $competencyid, + )); + + $template = api::read_template($params['templateid']); + self::validate_context($template->get_context()); + + return api::add_competency_to_template($params['templateid'], $params['competencyid']); + } + + /** + * Returns description of add_competency_to_template() result value. + * + * @return \external_description + */ + public static function add_competency_to_template_returns() { + return new external_value(PARAM_BOOL, 'True if successful.'); + } + + /** + * Returns description of add_competency_to_plan() parameters. + * + * @return \external_function_parameters + */ + public static function add_competency_to_plan_parameters() { + $planid = new external_value( + PARAM_INT, + 'The plan id', + VALUE_REQUIRED + ); + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'planid' => $planid, + 'competencyid' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * add competency to a learning plan. + * + * @param int $planid Plan id. + * @param int $competencyid Competency id. + * @return int + */ + public static function add_competency_to_plan($planid, $competencyid) { + $params = self::validate_parameters(self::add_competency_to_plan_parameters(), array( + 'planid' => $planid, + 'competencyid' => $competencyid, + )); + + $plan = api::read_plan($params['planid']); + self::validate_context($plan->get_context()); + + return api::add_competency_to_plan($params['planid'], $params['competencyid']); + } + + /** + * Returns description of add_competency_to_plan() result value. + * + * @return \external_description + */ + public static function add_competency_to_plan_returns() { + return new external_value(PARAM_BOOL, 'True if successful.'); + } + + /** + * Returns description of remove_competency_from_plan() parameters. + * + * @return \external_function_parameters + */ + public static function remove_competency_from_plan_parameters() { + $planid = new external_value( + PARAM_INT, + 'The plan id', + VALUE_REQUIRED + ); + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'planid' => $planid, + 'competencyid' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * Remove a competency from plan. + * + * @param int $planid Plan id. + * @param int $competencyid Competency id. + * @return int + */ + public static function remove_competency_from_plan($planid, $competencyid) { + $params = self::validate_parameters(self::remove_competency_from_plan_parameters(), array( + 'planid' => $planid, + 'competencyid' => $competencyid, + )); + $plan = api::read_plan($params['planid']); + self::validate_context($plan->get_context()); + + return api::remove_competency_from_plan($params['planid'], $params['competencyid']); + } + + /** + * Returns description of remove_competency_from_plan() result value. + * + * @return \external_description + */ + public static function remove_competency_from_plan_returns() { + return new external_value(PARAM_BOOL, 'True if successful.'); + } + + /** + * Returns description of remove_competency_from_template() parameters. + * + * @return \external_function_parameters + */ + public static function remove_competency_from_template_parameters() { + $templateid = new external_value( + PARAM_INT, + 'The template id', + VALUE_REQUIRED + ); + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $params = array( + 'templateid' => $templateid, + 'competencyid' => $competencyid, + ); + return new external_function_parameters($params); + } + + /** + * Returns description of reorder_plan_competency() parameters. + * + * @return \external_function_parameters + */ + public static function reorder_plan_competency_parameters() { + $planid = new external_value( + PARAM_INT, + 'The plan id', + VALUE_REQUIRED + ); + $competencyidfrom = new external_value( + PARAM_INT, + 'The competency id we are moving', + VALUE_REQUIRED + ); + $competencyidto = new external_value( + PARAM_INT, + 'The competency id we are moving to', + VALUE_REQUIRED + ); + $params = array( + 'planid' => $planid, + 'competencyidfrom' => $competencyidfrom, + 'competencyidto' => $competencyidto, + ); + return new external_function_parameters($params); + } + + /** + * Change the order of plan competencies. + * + * @param int $planid The plan id + * @param int $competencyidfrom The competency to move. + * @param int $competencyidto The competency to move to. + * @return bool + */ + public static function reorder_plan_competency($planid, $competencyidfrom, $competencyidto) { + $params = self::validate_parameters(self::reorder_plan_competency_parameters(), array( + 'planid' => $planid, + 'competencyidfrom' => $competencyidfrom, + 'competencyidto' => $competencyidto, + )); + + $plan = api::read_plan($params['planid']); + self::validate_context($plan->get_context()); + + return api::reorder_plan_competency($params['planid'], $params['competencyidfrom'], $params['competencyidto']); + } + + /** + * Returns description of reorder_plan_competency() result value. + * + * @return \external_description + */ + public static function reorder_plan_competency_returns() { + return new external_value(PARAM_BOOL, 'True if successful.'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function user_competency_cancel_review_request_parameters() { + return new external_function_parameters(array( + 'userid' => new external_value(PARAM_INT, 'The user ID'), + 'competencyid' => new external_value(PARAM_INT, 'The competency ID'), + )); + } + + /** + * External function user_competency_cancel_review_request. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @return boolean + */ + public static function user_competency_cancel_review_request($userid, $competencyid) { + $params = self::validate_parameters(self::user_competency_cancel_review_request_parameters(), array( + 'userid' => $userid, + 'competencyid' => $competencyid + )); + + $context = context_user::instance($params['userid']); + self::validate_context($context); + + return api::user_competency_cancel_review_request($userid, $competencyid); + } + + /** + * Returns description of external function result value. + * + * @return \external_function_parameters + */ + public static function user_competency_cancel_review_request_returns() { + return new external_value(PARAM_BOOL, 'The success'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function user_competency_request_review_parameters() { + return new external_function_parameters(array( + 'userid' => new external_value(PARAM_INT, 'The user ID'), + 'competencyid' => new external_value(PARAM_INT, 'The competency ID'), + )); + } + + /** + * External function user_competency_request_review. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @return boolean + */ + public static function user_competency_request_review($userid, $competencyid) { + $params = self::validate_parameters(self::user_competency_request_review_parameters(), array( + 'userid' => $userid, + 'competencyid' => $competencyid, + )); + + $context = context_user::instance($params['userid']); + self::validate_context($context); + + return api::user_competency_request_review($userid, $competencyid); + } + + /** + * Returns description of external function result value. + * + * @return \external_function_parameters + */ + public static function user_competency_request_review_returns() { + return new external_value(PARAM_BOOL, 'The success'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function user_competency_start_review_parameters() { + return new external_function_parameters(array( + 'userid' => new external_value(PARAM_INT, 'The user ID'), + 'competencyid' => new external_value(PARAM_INT, 'The competency ID'), + )); + } + + /** + * External function user_competency_start_review. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @return boolean + */ + public static function user_competency_start_review($userid, $competencyid) { + $params = self::validate_parameters(self::user_competency_start_review_parameters(), array( + 'userid' => $userid, + 'competencyid' => $competencyid + )); + + $context = context_user::instance($params['userid']); + self::validate_context($context); + + return api::user_competency_start_review($userid, $competencyid); + } + + /** + * Returns description of external function result value. + * + * @return \external_function_parameters + */ + public static function user_competency_start_review_returns() { + return new external_value(PARAM_BOOL, 'The success'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function user_competency_stop_review_parameters() { + return new external_function_parameters(array( + 'userid' => new external_value(PARAM_INT, 'The user ID'), + 'competencyid' => new external_value(PARAM_INT, 'The competency ID'), + )); + } + + /** + * External function user_competency_stop_review. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @return boolean + */ + public static function user_competency_stop_review($userid, $competencyid) { + $params = self::validate_parameters(self::user_competency_stop_review_parameters(), array( + 'userid' => $userid, + 'competencyid' => $competencyid + )); + + $context = context_user::instance($params['userid']); + self::validate_context($context); + + return api::user_competency_stop_review($userid, $competencyid); + } + + /** + * Returns description of external function result value. + * + * @return \external_function_parameters + */ + public static function user_competency_stop_review_returns() { + return new external_value(PARAM_BOOL, 'The success'); + } + + /** + * Returns description of template_has_related_data() parameters. + * + * @return \external_function_parameters + */ + public static function template_has_related_data_parameters() { + $templateid = new external_value( + PARAM_INT, + 'The template id', + VALUE_REQUIRED + ); + $params = array( + 'id' => $templateid, + ); + return new external_function_parameters($params); + } + + /** + * Check if template has related data. + * + * @param int $templateid Template id. + * @return boolean + */ + public static function template_has_related_data($templateid) { + $params = self::validate_parameters(self::template_has_related_data_parameters(), array( + 'id' => $templateid, + )); + + $template = api::read_template($params['id']); + self::validate_context($template->get_context()); + + return api::template_has_related_data($params['id']); + } + + /** + * Returns description of template_has_related_data() result value. + * + * @return \external_description + */ + public static function template_has_related_data_returns() { + return new external_value(PARAM_BOOL, 'True if the template has related data'); + } + + /** + * Count the competencies (visible to this user) in this learning plan template. + * + * @param int $templateid Template id. + * @param int $competencyid Competency id. + * @return int + */ + public static function remove_competency_from_template($templateid, $competencyid) { + $params = self::validate_parameters(self::remove_competency_from_template_parameters(), array( + 'templateid' => $templateid, + 'competencyid' => $competencyid, + )); + $template = api::read_template($params['templateid']); + self::validate_context($template->get_context()); + + return api::remove_competency_from_template($params['templateid'], $params['competencyid']); + } + + /** + * Returns description of remove_competency_from_template() result value. + * + * @return \external_description + */ + public static function remove_competency_from_template_returns() { + return new external_value(PARAM_BOOL, 'True if successful.'); + } + + /** + * Returns description of create_plan() parameters. + * + * @return \external_function_parameters + */ + public static function create_plan_parameters() { + $structure = plan_exporter::get_create_structure(); + $params = array('plan' => $structure); + return new external_function_parameters($params); + } + + /** + * Create a new learning plan. + * + * @param array $plan List of fields for the plan. + * @return array New plan record. + */ + public static function create_plan($plan) { + global $PAGE; + + $params = self::validate_parameters(self::create_plan_parameters(), array('plan' => $plan)); + $params = $params['plan']; + + $context = context_user::instance($params['userid']); + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + + $params = (object) $params; + + $result = api::create_plan($params); + $exporter = new plan_exporter($result, array('template' => null)); + return $exporter->export($output); + } + + /** + * Returns description of create_plan() result value. + * + * @return \external_description + */ + public static function create_plan_returns() { + return plan_exporter::get_read_structure(); + } + + /** + * Returns description of update_plan() parameters. + * + * @return \external_function_parameters + */ + public static function update_plan_parameters() { + $structure = plan_exporter::get_update_structure(); + $params = array('plan' => $structure); + return new external_function_parameters($params); + } + + /** + * Updates a new learning plan. + * + * @param array $plan Fields for the plan (id is required) + * @return mixed + */ + public static function update_plan($plan) { + global $PAGE; + + $params = self::validate_parameters(self::update_plan_parameters(), array('plan' => $plan)); + + $params = $params['plan']; + + $plan = api::read_plan($params['id']); + self::validate_context($plan->get_context()); + $output = $PAGE->get_renderer('core'); + + $params = (object) $params; + $result = api::update_plan($params); + $exporter = plan_exporter($result); + $record = $exporter->export($output); + return external_api::clean_returnvalue(self::update_plan_returns(), $record); + } + + /** + * Returns description of update_plan() result value. + * + * @return \external_description + */ + public static function update_plan_returns() { + return plan_exporter::get_read_structure(); + } + + /** + * Returns description of complete_plan() parameters. + * + * @return \external_function_parameters + */ + public static function complete_plan_parameters() { + $planid = new external_value( + PARAM_INT, + 'The plan id', + VALUE_REQUIRED + ); + $params = array('planid' => $planid); + return new external_function_parameters($params); + } + + /** + * Complete Learning plan. + * + * @param int $planid plan id (id is required) + * @return boolean + */ + public static function complete_plan($planid) { + $params = self::validate_parameters(self::complete_plan_parameters(), array( + 'planid' => $planid + )); + + return api::complete_plan($params['planid']); + } + + /** + * Returns description of complete_plan() result value. + * + * @return \external_description + */ + public static function complete_plan_returns() { + return new external_value(PARAM_BOOL, 'True if completing learning plan was successful'); + } + + /** + * Returns description of reopen_plan() parameters. + * + * @return \external_function_parameters + */ + public static function reopen_plan_parameters() { + $planid = new external_value( + PARAM_INT, + 'The plan id', + VALUE_REQUIRED + ); + $params = array('planid' => $planid); + return new external_function_parameters($params); + } + + /** + * Reopen Learning plan. + * + * @param int $planid plan id (id is required) + * @return boolean + */ + public static function reopen_plan($planid) { + $params = self::validate_parameters(self::reopen_plan_parameters(), array( + 'planid' => $planid + )); + + return api::reopen_plan($params['planid']); + } + + /** + * Returns description of reopen_plan() result value. + * + * @return \external_description + */ + public static function reopen_plan_returns() { + return new external_value(PARAM_BOOL, 'True if reopening learning plan was successful'); + } + + /** + * Returns description of read_plan() parameters. + * + * @return \external_function_parameters + */ + public static function read_plan_parameters() { + $id = new external_value( + PARAM_INT, + 'Data base record id for the plan', + VALUE_REQUIRED + ); + return new external_function_parameters(array('id' => $id)); + } + + /** + * Read a plan by id. + * + * @param int $id The id of the plan. + * @return \stdClass + */ + public static function read_plan($id) { + global $PAGE; + + $params = self::validate_parameters(self::read_plan_parameters(), array( + 'id' => $id, + )); + + $plan = api::read_plan($params['id']); + self::validate_context($plan->get_context()); + $output = $PAGE->get_renderer('core'); + + $exporter = new plan_exporter($plan, array('template' => $plan->get_template())); + $record = $exporter->export($output); + return external_api::clean_returnvalue(self::read_plan_returns(), $record); + } + + /** + * Returns description of read_plan() result value. + * + * @return \external_description + */ + public static function read_plan_returns() { + return plan_exporter::get_read_structure(); + } + + /** + * Returns description of delete_plan() parameters. + * + * @return \external_function_parameters + */ + public static function delete_plan_parameters() { + $id = new external_value( + PARAM_INT, + 'Data base record id for the learning plan', + VALUE_REQUIRED + ); + + $params = array( + 'id' => $id, + ); + return new external_function_parameters($params); + } + + /** + * Delete a plan. + * + * @param int $id The plan id + * @return boolean + */ + public static function delete_plan($id) { + $params = self::validate_parameters(self::delete_plan_parameters(), array( + 'id' => $id, + )); + + $plan = api::read_plan($params['id']); + self::validate_context($plan->get_context()); + + return external_api::clean_returnvalue(self::delete_plan_returns(), api::delete_plan($params['id'])); + } + + /** + * Returns description of delete_plan() result value. + * + * @return \external_description + */ + public static function delete_plan_returns() { + return new external_value(PARAM_BOOL, 'True if the delete was successful'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function plan_cancel_review_request_parameters() { + return new external_function_parameters(array( + 'id' => new external_value(PARAM_INT, 'The plan ID'), + )); + } + + /** + * External function plan_cancel_review_request. + * + * @param int $id The plan ID. + * @return boolean + */ + public static function plan_cancel_review_request($id) { + $params = self::validate_parameters(self::plan_cancel_review_request_parameters(), array( + 'id' => $id + )); + + $plan = api::read_plan($id); + self::validate_context($plan->get_context()); + + return api::plan_cancel_review_request($plan); + } + + /** + * Returns description of external function result value. + * + * @return \external_function_parameters + */ + public static function plan_cancel_review_request_returns() { + return new external_value(PARAM_BOOL, 'The success'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function plan_request_review_parameters() { + return new external_function_parameters(array( + 'id' => new external_value(PARAM_INT, 'The plan ID'), + )); + } + + /** + * External function plan_request_review. + * + * @param int $id The plan ID. + * @return boolean + */ + public static function plan_request_review($id) { + $params = self::validate_parameters(self::plan_request_review_parameters(), array( + 'id' => $id + )); + + $plan = api::read_plan($id); + self::validate_context($plan->get_context()); + + return api::plan_request_review($plan); + } + + /** + * Returns description of external function result value. + * + * @return \external_function_parameters + */ + public static function plan_request_review_returns() { + return new external_value(PARAM_BOOL, 'The success'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function plan_start_review_parameters() { + return new external_function_parameters(array( + 'id' => new external_value(PARAM_INT, 'The plan ID'), + )); + } + + /** + * External function plan_start_review. + * + * @param int $id The plan ID. + * @return boolean + */ + public static function plan_start_review($id) { + $params = self::validate_parameters(self::plan_start_review_parameters(), array( + 'id' => $id + )); + + $plan = api::read_plan($id); + self::validate_context($plan->get_context()); + + return api::plan_start_review($plan); + } + + /** + * Returns description of external function result value. + * + * @return \external_function_parameters + */ + public static function plan_start_review_returns() { + return new external_value(PARAM_BOOL, 'The success'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function plan_stop_review_parameters() { + return new external_function_parameters(array( + 'id' => new external_value(PARAM_INT, 'The plan ID'), + )); + } + + /** + * External function plan_stop_review. + * + * @param int $id The plan ID. + * @return boolean + */ + public static function plan_stop_review($id) { + $params = self::validate_parameters(self::plan_stop_review_parameters(), array( + 'id' => $id + )); + + $plan = api::read_plan($id); + self::validate_context($plan->get_context()); + + return api::plan_stop_review($plan); + } + + /** + * Returns description of external function result value. + * + * @return \external_function_parameters + */ + public static function plan_stop_review_returns() { + return new external_value(PARAM_BOOL, 'The success'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function approve_plan_parameters() { + return new external_function_parameters(array( + 'id' => new external_value(PARAM_INT, 'The plan ID'), + )); + } + + /** + * External function approve_plan. + * + * @param int $id The plan ID. + * @return boolean + */ + public static function approve_plan($id) { + $params = self::validate_parameters(self::approve_plan_parameters(), array( + 'id' => $id, + )); + + $plan = api::read_plan($id); + self::validate_context($plan->get_context()); + + return api::approve_plan($plan); + } + + /** + * Returns description of external function result value. + * + * @return \external_function_parameters + */ + public static function approve_plan_returns() { + return new external_value(PARAM_BOOL, 'The success'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function unapprove_plan_parameters() { + return new external_function_parameters(array( + 'id' => new external_value(PARAM_INT, 'The plan ID'), + )); + } + + /** + * External function unapprove_plan. + * + * @param int $id The plan ID. + * @return boolean + */ + public static function unapprove_plan($id) { + $params = self::validate_parameters(self::unapprove_plan_parameters(), array( + 'id' => $id, + )); + + $plan = api::read_plan($id); + self::validate_context($plan->get_context()); + + return api::unapprove_plan($plan); + } + + /** + * Returns description of external function result value. + * + * @return \external_function_parameters + */ + public static function unapprove_plan_returns() { + return new external_value(PARAM_BOOL, 'The success'); + } + + /** + * External function parameters structure. + * + * @return \external_description + */ + public static function list_plan_competencies_parameters() { + return new external_function_parameters(array( + 'id' => new external_value(PARAM_INT, 'The plan ID.') + )); + } + + /** + * List plan competencies. + * @param int $id The plan ID. + * @return array + */ + public static function list_plan_competencies($id) { + global $PAGE; + + $params = self::validate_parameters(self::list_plan_competencies_parameters(), array('id' => $id)); + $id = $params['id']; + $plan = api::read_plan($id); + $usercontext = $plan->get_context(); + self::validate_context($usercontext); + $output = $PAGE->get_renderer('core'); + + $result = api::list_plan_competencies($plan); + + if ($plan->get_status() == plan::STATUS_COMPLETE) { + $ucproperty = 'usercompetencyplan'; + } else { + $ucproperty = 'usercompetency'; + } + + $contextcache = array(); + $scalecache = array(); + + foreach ($result as $key => $r) { + if (!isset($scalecache[$r->competency->get_competencyframeworkid()])) { + $scalecache[$r->competency->get_competencyframeworkid()] = $r->competency->get_framework()->get_scale(); + } + $scale = $scalecache[$r->competency->get_competencyframeworkid()]; + + if (!isset($contextcache[$r->competency->get_competencyframeworkid()])) { + $contextcache[$r->competency->get_competencyframeworkid()] = $r->competency->get_context(); + } + $context = $contextcache[$r->competency->get_competencyframeworkid()]; + + $exporter = new competency_exporter($r->competency, array('context' => $context)); + $r->competency = $exporter->export($output); + + if ($r->usercompetency) { + $exporter = new user_competency_exporter($r->usercompetency, array('scale' => $scale)); + $r->usercompetency = $exporter->export($output); + unset($r->usercompetencyplan); + } else { + $exporter = new user_competency_plan_exporter($r->usercompetencyplan, array('scale' => $scale)); + $r->usercompetencyplan = $exporter->export($output); + unset($r->usercompetency); + } + } + return $result; + } + + /** + * External function return structure. + * + * @return \external_description + */ + public static function list_plan_competencies_returns() { + $uc = user_competency_exporter::get_read_structure(); + $ucp = user_competency_plan_exporter::get_read_structure(); + + $uc->required = VALUE_OPTIONAL; + $ucp->required = VALUE_OPTIONAL; + + return new external_multiple_structure( + new external_single_structure(array( + 'competency' => competency_exporter::get_read_structure(), + 'usercompetency' => $uc, + 'usercompetencyplan' => $ucp + )) + ); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function list_user_plans_parameters() { + return new external_function_parameters(array( + 'userid' => new external_value(PARAM_INT, 'The user ID'), + )); + } + + /** + * External function list_user_plans. + * + * @param int $userid The user ID. + * @return boolean + */ + public static function list_user_plans($userid) { + global $PAGE; + $params = self::validate_parameters(self::list_user_plans_parameters(), array( + 'userid' => $userid + )); + + $context = context_user::instance($params['userid']); + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + + $response = array(); + $plans = api::list_user_plans($params['userid']); + foreach ($plans as $plan) { + $exporter = new plan_exporter($plan, array('template' => $plan->get_template())); + $response[] = $exporter->export($output); + } + + return $response; + } + + /** + * Returns description of external function result value. + * + * @return \external_function_parameters + */ + public static function list_user_plans_returns() { + return new external_multiple_structure( + plan_exporter::get_read_structure() + ); + } + + /** + * Returns description of external function parameters. + * + * @return \external_description + */ + public static function read_user_evidence_parameters() { + return new external_function_parameters(array( + 'id' => new external_value(PARAM_INT, 'The user evidence ID.'), + )); + } + + /** + * Delete a user evidence. + * + * @param int $id The evidence id + * @return boolean + */ + public static function read_user_evidence($id) { + global $PAGE; + $params = self::validate_parameters(self::read_user_evidence_parameters(), array('id' => $id)); + + $userevidence = api::read_user_evidence($params['id']); + $context = $userevidence->get_context(); + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + + $exporter = new user_evidence_exporter($userevidence, array('context' => $context, + 'competencies' => $userevidence->get_competencies())); + return $exporter->export($output); + } + + /** + * Returns description of external function result value. + * + * @return \external_description + */ + public static function read_user_evidence_returns() { + return user_evidence_exporter::get_read_structure(); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function delete_user_evidence_parameters() { + return new external_function_parameters(array( + 'id' => new external_value(PARAM_INT, 'The user evidence ID.'), + )); + } + + /** + * Delete a user evidence. + * + * @param int $id The evidence id + * @return boolean + */ + public static function delete_user_evidence($id) { + $params = self::validate_parameters(self::delete_user_evidence_parameters(), array('id' => $id)); + + $userevidence = api::read_user_evidence($params['id']); + self::validate_context($userevidence->get_context()); + + return api::delete_user_evidence($userevidence->get_id()); + } + + /** + * Returns description of external function result value. + * + * @return \external_description + */ + public static function delete_user_evidence_returns() { + return new external_value(PARAM_BOOL, 'True if the delete was successful'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function create_user_evidence_competency_parameters() { + return new external_function_parameters(array( + 'userevidenceid' => new external_value(PARAM_INT, 'The user evidence ID.'), + 'competencyid' => new external_value(PARAM_INT, 'The competency ID.'), + )); + } + + /** + * Delete a user evidence competency relationship. + * + * @param int $userevidenceid The user evidence id. + * @param int $competencyid The competency id. + * @return boolean + */ + public static function create_user_evidence_competency($userevidenceid, $competencyid) { + global $PAGE; + $params = self::validate_parameters(self::create_user_evidence_competency_parameters(), array( + 'userevidenceid' => $userevidenceid, + 'competencyid' => $competencyid, + )); + + $userevidence = api::read_user_evidence($params['userevidenceid']); + self::validate_context($userevidence->get_context()); + + $relation = api::create_user_evidence_competency($userevidence, $competencyid); + $exporter = new user_evidence_competency_exporter($relation); + return $exporter->export($PAGE->get_renderer('core')); + } + + /** + * Returns description of external function result value. + * + * @return \external_description + */ + public static function create_user_evidence_competency_returns() { + return user_evidence_competency_exporter::get_read_structure(); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function delete_user_evidence_competency_parameters() { + return new external_function_parameters(array( + 'userevidenceid' => new external_value(PARAM_INT, 'The user evidence ID.'), + 'competencyid' => new external_value(PARAM_INT, 'The competency ID.'), + )); + } + + /** + * Delete a user evidence competency relationship. + * + * @param int $userevidenceid The user evidence id. + * @param int $competencyid The competency id. + * @return boolean + */ + public static function delete_user_evidence_competency($userevidenceid, $competencyid) { + $params = self::validate_parameters(self::delete_user_evidence_competency_parameters(), array( + 'userevidenceid' => $userevidenceid, + 'competencyid' => $competencyid, + )); + + $userevidence = api::read_user_evidence($params['userevidenceid']); + self::validate_context($userevidence->get_context()); + + return api::delete_user_evidence_competency($userevidence, $params['competencyid']); + } + + /** + * Returns description of external function result value. + * + * @return \external_description + */ + public static function delete_user_evidence_competency_returns() { + return new external_value(PARAM_BOOL, 'True if the delete was successful'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function request_review_of_user_evidence_linked_competencies_parameters() { + return new external_function_parameters(array( + 'id' => new external_value(PARAM_INT, 'The user evidence ID.') + )); + } + + /** + * Send user evidence competencies to review. + * + * @param int $id The user evidence id. + * @return boolean + */ + public static function request_review_of_user_evidence_linked_competencies($id) { + $params = self::validate_parameters(self::request_review_of_user_evidence_linked_competencies_parameters(), array( + 'id' => $id + )); + + $userevidence = api::read_user_evidence($id); + self::validate_context($userevidence->get_context()); + + return api::request_review_of_user_evidence_linked_competencies($id); + } + + /** + * Returns description of external function result value. + * + * @return \external_description + */ + public static function request_review_of_user_evidence_linked_competencies_returns() { + return new external_value(PARAM_BOOL, 'True if all competencies were send to review'); + } + + + /** + * Returns the description of the get_scale_values() parameters. + * + * @return external_function_parameters. + */ + public static function get_scale_values_parameters() { + $scaleid = new external_value( + PARAM_INT, + 'The scale id', + VALUE_REQUIRED + ); + $params = array('scaleid' => $scaleid); + return new external_function_parameters($params); + } + + /** + * Get the values associated with a scale. + * + * @param int $scaleid Scale ID + * @return array Values for a scale. + */ + public static function get_scale_values($scaleid) { + global $DB; + $params = self::validate_parameters(self::get_scale_values_parameters(), + array( + 'scaleid' => $scaleid, + ) + ); + $context = context_system::instance(); + self::validate_context($context); + // The following section is not learning plan specific and so has not been moved to the api. + // Retrieve the scale value from the database. + $scale = grade_scale::fetch(array('id' => $scaleid)); + $scalevalues = $scale->load_items(); + foreach ($scalevalues as $key => $value) { + // Add a key (make the first value 1). + $scalevalues[$key] = array( + 'id' => $key + 1, + 'name' => external_format_string($value, $context->id) + ); + } + return $scalevalues; + } + + /** + * Returns description of get_scale_values() result value. + * + * @return external_multiple_structure + */ + public static function get_scale_values_returns() { + return new external_multiple_structure( + new external_single_structure(array( + 'id' => new external_value(PARAM_INT, 'Scale value ID'), + 'name' => new external_value(PARAM_RAW, 'Scale value name') + )) + ); + } + + + /** + * Returns the description of the add_related_competency_parameters() parameters. + * + * @return external_function_parameters. + */ + public static function add_related_competency_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $relatedcompetencyid = new external_value( + PARAM_INT, + 'The related competency id', + VALUE_REQUIRED + ); + $params = array( + 'competencyid' => $competencyid, + 'relatedcompetencyid' => $relatedcompetencyid + ); + return new external_function_parameters($params); + } + + /** + * Adds a related competency. + * + * @param int $competencyid + * @param int $relatedcompetencyid + * @return bool + */ + public static function add_related_competency($competencyid, $relatedcompetencyid) { + $params = self::validate_parameters(self::add_related_competency_parameters(), array( + 'competencyid' => $competencyid, + 'relatedcompetencyid' => $relatedcompetencyid + )); + $competency = api::read_competency($params['competencyid']); + self::validate_context($competency->get_context()); + + return api::add_related_competency($params['competencyid'], $params['relatedcompetencyid']); + } + + /** + * Returns description of add_related_competency_returns() result value. + * + * @return external_description + */ + public static function add_related_competency_returns() { + return new external_value(PARAM_BOOL, 'True if successful.'); + } + + /** + * Returns the description of the remove_related_competency_parameters() parameters. + * + * @return external_function_parameters. + */ + public static function remove_related_competency_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $relatedcompetencyid = new external_value( + PARAM_INT, + 'The related competency id', + VALUE_REQUIRED + ); + $params = array( + 'competencyid' => $competencyid, + 'relatedcompetencyid' => $relatedcompetencyid + ); + return new external_function_parameters($params); + } + + /** + * Removes a related competency. + * + * @param int $competencyid + * @param int $relatedcompetencyid + * @return bool + */ + public static function remove_related_competency($competencyid, $relatedcompetencyid) { + $params = self::validate_parameters(self::remove_related_competency_parameters(), array( + 'competencyid' => $competencyid, + 'relatedcompetencyid' => $relatedcompetencyid + )); + $competency = api::read_competency($params['competencyid']); + self::validate_context($competency->get_context()); + + return api::remove_related_competency($params['competencyid'], $params['relatedcompetencyid']); + } + + /** + * Returns description of remove_related_competency_returns() result value. + * + * @return external_description + */ + public static function remove_related_competency_returns() { + return new external_value(PARAM_BOOL, 'True if successful.'); + } + + /** + * Returns description of update_ruleoutcome_course_competency() parameters. + * + * @return \external_function_parameters + */ + public static function set_course_competency_ruleoutcome_parameters() { + $coursecompetencyid = new external_value( + PARAM_INT, + 'Data base record id for the course competency', + VALUE_REQUIRED + ); + + $ruleoutcome = new external_value( + PARAM_INT, + 'Ruleoutcome value', + VALUE_REQUIRED + ); + + $params = array( + 'coursecompetencyid' => $coursecompetencyid, + 'ruleoutcome' => $ruleoutcome, + ); + return new external_function_parameters($params); + } + + /** + * Change the ruleoutcome of a course competency. + * + * @param int $coursecompetencyid The course competency id + * @param int $ruleoutcome The ruleoutcome value + * @return bool + */ + public static function set_course_competency_ruleoutcome($coursecompetencyid, $ruleoutcome) { + $params = self::validate_parameters(self::set_course_competency_ruleoutcome_parameters(), array( + 'coursecompetencyid' => $coursecompetencyid, + 'ruleoutcome' => $ruleoutcome, + )); + + $coursecompetency = new course_competency($params['coursecompetencyid']); + self::validate_context(context_course::instance($coursecompetency->get_courseid())); + + return api::set_course_competency_ruleoutcome($coursecompetency, $params['ruleoutcome']); + } + + /** + * Returns description of update_ruleoutcome_course_competency() result value. + * + * @return \external_value + */ + public static function set_course_competency_ruleoutcome_returns() { + return new external_value(PARAM_BOOL, 'True if the update was successful'); + } + + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function grade_competency_parameters() { + $userid = new external_value( + PARAM_INT, + 'User ID', + VALUE_REQUIRED + ); + $competencyid = new external_value( + PARAM_INT, + 'Competency ID', + VALUE_REQUIRED + ); + $grade = new external_value( + PARAM_INT, + 'New grade', + VALUE_REQUIRED + ); + $note = new external_value( + PARAM_NOTAGS, + 'A note to attach to the evidence', + VALUE_DEFAULT + ); + + $params = array( + 'userid' => $userid, + 'competencyid' => $competencyid, + 'grade' => $grade, + 'note' => $note, + ); + return new external_function_parameters($params); + } + + /** + * Grade a competency. + * + * @param int $userid The user ID. + * @param int $competencyid The competency id + * @param int $grade The new grade value + * @param string $note A note to attach to the evidence + * @return bool + */ + public static function grade_competency($userid, $competencyid, $grade, $note = null) { + global $USER, $PAGE; + $params = self::validate_parameters(self::grade_competency_parameters(), array( + 'userid' => $userid, + 'competencyid' => $competencyid, + 'grade' => $grade, + 'note' => $note + )); + + $uc = api::get_user_competency($params['userid'], $params['competencyid']); + self::validate_context($uc->get_context()); + + $output = $PAGE->get_renderer('core'); + $evidence = api::grade_competency( + $uc->get_userid(), + $uc->get_competencyid(), + $params['grade'], + $params['note'] + ); + + $scale = $uc->get_competency()->get_scale(); + $exporter = new evidence_exporter($evidence, [ + 'actionuser' => $USER, + 'scale' => $scale, + 'usercompetency' => $uc, + 'usercompetencyplan' => null, + ]); + return $exporter->export($output); + } + + /** + * Returns description of external function result value. + * + * @return \external_value + */ + public static function grade_competency_returns() { + return evidence_exporter::get_read_structure(); + } + + /** + * Returns description of grade_competency_in_plan() parameters. + * + * @return \external_function_parameters + */ + public static function grade_competency_in_plan_parameters() { + $planid = new external_value( + PARAM_INT, + 'Plan id', + VALUE_REQUIRED + ); + $competencyid = new external_value( + PARAM_INT, + 'Competency id', + VALUE_REQUIRED + ); + $grade = new external_value( + PARAM_INT, + 'New grade', + VALUE_REQUIRED + ); + $note = new external_value( + PARAM_NOTAGS, + 'A note to attach to the evidence', + VALUE_DEFAULT + ); + + $params = array( + 'planid' => $planid, + 'competencyid' => $competencyid, + 'grade' => $grade, + 'note' => $note + ); + return new external_function_parameters($params); + } + + /** + * Grade a competency in a plan. + * + * @param int $planid The plan id + * @param int $competencyid The competency id + * @param int $grade The new grade value + * @param string $note A note to add to the evidence + * @return bool + */ + public static function grade_competency_in_plan($planid, $competencyid, $grade, $note = null) { + global $USER, $PAGE; + + $params = self::validate_parameters(self::grade_competency_in_plan_parameters(), array( + 'planid' => $planid, + 'competencyid' => $competencyid, + 'grade' => $grade, + 'note' => $note + )); + + $plan = new plan($params['planid']); + $context = $plan->get_context(); + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + + $evidence = api::grade_competency_in_plan( + $plan->get_id(), + $params['competencyid'], + $params['grade'], + $params['note'] + ); + $competency = api::read_competency($params['competencyid']); + $scale = $competency->get_scale(); + $exporter = new evidence_exporter($evidence, [ + 'actionuser' => $USER, + 'scale' => $scale, + 'usercompetency' => null, + 'usercompetencyplan' => null, + ]); + return $exporter->export($output); + } + + /** + * Returns description of grade_competency_in_plan() result value. + * + * @return \external_value + */ + public static function grade_competency_in_plan_returns() { + return evidence_exporter::get_read_structure(); + } + + /** + * Returns description of user_competency_viewed() parameters. + * + * @return \external_function_parameters + */ + public static function user_competency_viewed_parameters() { + $usercompetencyid = new external_value( + PARAM_INT, + 'The user competency id', + VALUE_REQUIRED + ); + $params = array( + 'usercompetencyid' => $usercompetencyid + ); + return new external_function_parameters($params); + } + + /** + * Log user competency viewed event. + * + * @param int $usercompetencyid The user competency ID. + * @return boolean + */ + public static function user_competency_viewed($usercompetencyid) { + $params = self::validate_parameters(self::user_competency_viewed_parameters(), array( + 'usercompetencyid' => $usercompetencyid + )); + + $uc = api::get_user_competency_by_id($params['usercompetencyid']); + $result = api::user_competency_viewed($uc); + + return $result; + } + + /** + * Returns description of user_competency_viewed() result value. + * + * @return \external_description + */ + public static function user_competency_viewed_returns() { + return new external_value(PARAM_BOOL, 'True if the event user competency viewed was logged'); + } + + /** + * Returns description of user_competency_viewed_in_plan() parameters. + * + * @return \external_function_parameters + */ + public static function user_competency_viewed_in_plan_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $userid = new external_value( + PARAM_INT, + 'The user id', + VALUE_REQUIRED + ); + $planid = new external_value( + PARAM_INT, + 'The plan id', + VALUE_REQUIRED + ); + $params = array( + 'competencyid' => $competencyid, + 'userid' => $userid, + 'planid' => $planid + ); + return new external_function_parameters($params); + } + + /** + * Log user competency viewed in plan event. + * + * @param int $competencyid The competency ID. + * @param int $userid The user ID. + * @param int $planid The plan ID. + * @return boolean + */ + public static function user_competency_viewed_in_plan($competencyid, $userid, $planid) { + $params = self::validate_parameters(self::user_competency_viewed_in_plan_parameters(), array( + 'competencyid' => $competencyid, + 'userid' => $userid, + 'planid' => $planid + )); + $pl = api::get_plan_competency($params['planid'], $params['competencyid']); + $result = api::user_competency_viewed_in_plan($pl->usercompetency, $params['planid']); + + return $result; + } + + /** + * Returns description of user_competency_viewed_in_plan() result value. + * + * @return \external_description + */ + public static function user_competency_viewed_in_plan_returns() { + return new external_value(PARAM_BOOL, 'True if the event user competency viewed in plan was logged'); + } + + /** + * Returns description of user_competency_viewed_in_course() parameters. + * + * @return \external_function_parameters + */ + public static function user_competency_viewed_in_course_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $userid = new external_value( + PARAM_INT, + 'The user id', + VALUE_REQUIRED + ); + $courseid = new external_value( + PARAM_INT, + 'The course id', + VALUE_REQUIRED + ); + $params = array( + 'competencyid' => $competencyid, + 'userid' => $userid, + 'courseid' => $courseid + ); + return new external_function_parameters($params); + } + + /** + * Log user competency viewed in course event. + * + * @param int $competencyid The competency ID. + * @param int $userid The user ID. + * @param int $courseid The course ID. + * @return boolean + */ + public static function user_competency_viewed_in_course($competencyid, $userid, $courseid) { + $params = self::validate_parameters(self::user_competency_viewed_in_course_parameters(), array( + 'competencyid' => $competencyid, + 'userid' => $userid, + 'courseid' => $courseid + )); + $ucc = api::get_user_competency_in_course($params['courseid'], $params['userid'], $params['competencyid']); + $result = api::user_competency_viewed_in_course($ucc); + + return $result; + } + + /** + * Returns description of user_competency_viewed_in_course() result value. + * + * @return \external_description + */ + public static function user_competency_viewed_in_course_returns() { + return new external_value(PARAM_BOOL, 'True if the event user competency viewed in course was logged'); + } + + /** + * Returns description of user_competency_plan_viewed() parameters. + * + * @return \external_function_parameters + */ + public static function user_competency_plan_viewed_parameters() { + $competencyid = new external_value( + PARAM_INT, + 'The competency id', + VALUE_REQUIRED + ); + $userid = new external_value( + PARAM_INT, + 'The user id', + VALUE_REQUIRED + ); + $planid = new external_value( + PARAM_INT, + 'The plan id', + VALUE_REQUIRED + ); + $params = array( + 'competencyid' => $competencyid, + 'userid' => $userid, + 'planid' => $planid + ); + return new external_function_parameters($params); + } + + /** + * Log user competency plan viewed event. + * + * @param int $competencyid The competency ID. + * @param int $userid The user ID. + * @param int $planid The plan ID. + * @return boolean + */ + public static function user_competency_plan_viewed($competencyid, $userid, $planid) { + $params = self::validate_parameters(self::user_competency_viewed_in_plan_parameters(), array( + 'competencyid' => $competencyid, + 'userid' => $userid, + 'planid' => $planid + )); + $pl = api::get_plan_competency($params['planid'], $params['competencyid']); + $result = api::user_competency_plan_viewed($pl->usercompetencyplan); + + return $result; + } + + /** + * Returns description of user_competency_plan_viewed() result value. + * + * @return \external_description + */ + public static function user_competency_plan_viewed_returns() { + return new external_value(PARAM_BOOL, 'True if the event user competency plan viewed was logged'); + } + + /** + * Returns description of grade_competency_in_course() parameters. + * + * @return \external_function_parameters + */ + public static function grade_competency_in_course_parameters() { + $courseid = new external_value( + PARAM_INT, + 'Course id', + VALUE_REQUIRED + ); + $userid = new external_value( + PARAM_INT, + 'User id', + VALUE_REQUIRED + ); + $competencyid = new external_value( + PARAM_INT, + 'Competency id', + VALUE_REQUIRED + ); + $grade = new external_value( + PARAM_INT, + 'New grade', + VALUE_REQUIRED + ); + $note = new external_value( + PARAM_NOTAGS, + 'A note to attach to the evidence', + VALUE_DEFAULT + ); + + $params = array( + 'courseid' => $courseid, + 'userid' => $userid, + 'competencyid' => $competencyid, + 'grade' => $grade, + 'note' => $note, + ); + return new external_function_parameters($params); + } + + /** + * Grade a competency in a course. + * + * @param int $courseid The course id + * @param int $userid The user id + * @param int $competencyid The competency id + * @param int $grade The new grade value + * @param string $note A note to add to the evidence + * @return bool + */ + public static function grade_competency_in_course($courseid, $userid, $competencyid, $grade, $note = null) { + global $USER, $PAGE, $DB; + + $params = self::validate_parameters(self::grade_competency_in_course_parameters(), array( + 'courseid' => $courseid, + 'userid' => $userid, + 'competencyid' => $competencyid, + 'grade' => $grade, + 'note' => $note + )); + + $course = $DB->get_record('course', array('id' => $params['courseid'])); + $context = context_course::instance($course->id); + self::validate_context($context); + $output = $PAGE->get_renderer('core'); + + $evidence = api::grade_competency_in_course( + $params['courseid'], + $params['userid'], + $params['competencyid'], + $params['grade'], + $params['note'] + ); + $competency = api::read_competency($params['competencyid']); + $scale = $competency->get_scale(); + $exporter = new evidence_exporter($evidence, array( + 'actionuser' => $USER, + 'scale' => $scale, + 'usercompetency' => null, + 'usercompetencyplan' => null, + )); + return $exporter->export($output); + } + + /** + * Returns description of grade_competency_in_course() result value. + * + * @return \external_value + */ + public static function grade_competency_in_course_returns() { + return evidence_exporter::get_read_structure(); + } + + /** + * Returns description of unlink_plan_from_template_() parameters. + * + * @return \external_function_parameters + */ + public static function unlink_plan_from_template_parameters() { + $planid = new external_value( + PARAM_INT, + 'Data base record id for the plan', + VALUE_REQUIRED + ); + + $params = array( + 'planid' => $planid, + ); + return new external_function_parameters($params); + } + + /** + * Unlink the plan from the template. + * + * @param int $planid The plan id + * @return bool + */ + public static function unlink_plan_from_template($planid) { + $params = self::validate_parameters(self::unlink_plan_from_template_parameters(), array( + 'planid' => $planid, + )); + + $plan = new plan($params['planid']); + self::validate_context($plan->get_context()); + + return api::unlink_plan_from_template($plan); + } + + /** + * Returns description of unlink_plan_from_template_() result value. + * + * @return \external_value + */ + public static function unlink_plan_from_template_returns() { + return new external_value(PARAM_BOOL, 'True if the unlink was successful'); + } + + /** + * Returns description of template_viewed() parameters. + * + * @return \external_function_parameters + */ + public static function template_viewed_parameters() { + $id = new external_value( + PARAM_INT, + 'Data base record id for the template', + VALUE_REQUIRED + ); + + $params = array( + 'id' => $id, + ); + return new external_function_parameters($params); + } + + /** + * Log the template viewed event. + * + * @param int $id the template id + * @return array of warnings and status result + * @throws moodle_exception + */ + public static function template_viewed($id) { + $params = self::validate_parameters(self::view_book_parameters(), array( + 'id' => $id + )); + + $template = api::read_template($params['id']); + self::validate_context($template->get_context()); + + return api::template_viewed($params['id']); + } + + /** + * Returns description of template_viewed() result value. + * + * @return \external_value + */ + public static function template_viewed_returns() { + return new external_value(PARAM_BOOL, 'True if the log of the view was successful'); + } + + /** + * Returns description of update_course_competency_settings() parameters. + * + * @return \external_function_parameters + */ + public static function update_course_competency_settings_parameters() { + $courseid = new external_value( + PARAM_INT, + 'Course id for the course to update', + VALUE_REQUIRED + ); + $pushratingstouserplans = new external_value( + PARAM_BOOL, + 'New value of the setting', + VALUE_REQUIRED + ); + $settings = new external_single_structure(array( + 'pushratingstouserplans' => $pushratingstouserplans + )); + $params = array( + 'courseid' => $courseid, + 'settings' => $settings, + ); + return new external_function_parameters($params); + } + + /** + * Update the course competency settings + * + * @param int $courseid the course id + * @param stdClass $settings The list of settings (currently only pushratingstouserplans). + * @throws moodle_exception + */ + public static function update_course_competency_settings($courseid, $settings) { + $params = self::validate_parameters(self::update_course_competency_settings_parameters(), array( + 'courseid' => $courseid, + 'settings' => $settings + )); + + $context = context_course::instance($params['courseid']); + self::validate_context($context); + $result = api::update_course_competency_settings($params['courseid'], $params['settings']); + + return $result; + } + + /** + * Returns description of update_course_competency_settings() result value. + * + * @return \external_value + */ + public static function update_course_competency_settings_returns() { + return new external_value(PARAM_BOOL, 'True if the update was successful.'); + } + + /** + * Returns description of external function parameters. + * + * @return \external_function_parameters + */ + public static function delete_evidence_parameters() { + return new external_function_parameters(array( + 'id' => new external_value(PARAM_INT, 'The evidence ID'), + )); + } + + /** + * External function delete_evidence. + * + * @param int $id The evidence ID. + * @return boolean + */ + public static function delete_evidence($id) { + $params = self::validate_parameters(self::delete_evidence_parameters(), array( + 'id' => $id + )); + + $evidence = api::read_evidence($params['id']); + $uc = api::get_user_competency_by_id($evidence->get_usercompetencyid()); + self::validate_context($uc->get_context()); + + return api::delete_evidence($evidence); + } + + /** + * Returns description of external function result value. + * + * @return \external_function_parameters + */ + public static function delete_evidence_returns() { + return new external_value(PARAM_BOOL, 'The success'); + } + +} diff --git a/competency/classes/external/comment_area_exporter.php b/competency/classes/external/comment_area_exporter.php new file mode 100644 index 00000000000..638ff45edad --- /dev/null +++ b/competency/classes/external/comment_area_exporter.php @@ -0,0 +1,141 @@ +. + +/** + * Exporting a comment area. + * + * A comment area is the set of information about a defined comments area. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +require_once($CFG->dirroot . '/comment/lib.php'); + +// TODO MDL-52243 Move this to core. +defined('MOODLE_INTERNAL') || die(); + +use comment; +use renderer_base; +use stdClass; + +/** + * Class for exporting a comment area. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class comment_area_exporter extends exporter { + + /** @var comment The comment instance. */ + protected $comment = null; + + public function __construct(comment $comment, $related = array()) { + $this->comment = $comment; + $data = new stdClass(); + $data->component = $comment->get_component(); + $data->commentarea = $comment->get_commentarea(); + $data->itemid = $comment->get_itemid(); + $data->courseid = $comment->get_courseid(); + $data->contextid = $comment->get_context()->id; + $data->cid = $comment->get_cid(); + + parent::__construct($data, $related); + } + + protected static function define_properties() { + return array( + 'component' => array( + 'type' => PARAM_COMPONENT, + ), + 'commentarea' => array( + 'type' => PARAM_AREA, + ), + 'itemid' => array( + 'type' => PARAM_INT, + ), + 'courseid' => array( + 'type' => PARAM_INT, + ), + 'contextid' => array( + 'type' => PARAM_INT, + ), + 'cid' => array( + 'type' => PARAM_ALPHANUMEXT, + ), + ); + } + + protected static function define_other_properties() { + return array( + 'autostart' => array( + 'type' => PARAM_BOOL, + ), + 'canpost' => array( + 'type' => PARAM_BOOL, + ), + 'canview' => array( + 'type' => PARAM_BOOL, + ), + 'count' => array( + 'type' => PARAM_INT, + ), + 'collapsediconurl' => array( + 'type' => PARAM_URL, + ), + 'displaytotalcount' => array( + 'type' => PARAM_BOOL, + ), + 'displaycancel' => array( + 'type' => PARAM_BOOL, + ), + 'fullwidth' => array( + 'type' => PARAM_BOOL, + ), + 'linktext' => array( + 'type' => PARAM_RAW, + ), + 'notoggle' => array( + 'type' => PARAM_BOOL, + ), + 'template' => array( + 'type' => PARAM_RAW, + ), + 'canpostorhascomments' => array( + 'type' => PARAM_BOOL + ) + ); + } + + public function get_other_values(renderer_base $output) { + $values = array(); + $values['autostart'] = $this->comment->get_autostart(); + $values['canpost'] = $this->comment->can_post(); + $values['canview'] = $this->comment->can_view(); + $values['collapsediconurl'] = $output->pix_url(right_to_left() ? 't/collapsed_rtl' : 't/collapsed')->out(false); + $values['count'] = $this->comment->count(); + $values['displaycancel'] = $this->comment->get_displaycancel(); + $values['displaytotalcount'] = $this->comment->get_displaytotalcount(); + $values['fullwidth'] = $this->comment->get_fullwidth(); + $values['linktext'] = $this->comment->get_linktext(); + $values['notoggle'] = $this->comment->get_notoggle(); + $values['template'] = $this->comment->get_template(); + $values['canpostorhascomments'] = $values['canpost'] || ($values['canview'] && $values['count'] > 0); + return $values; + } +} diff --git a/competency/classes/external/competency_exporter.php b/competency/classes/external/competency_exporter.php new file mode 100644 index 00000000000..6455e433e52 --- /dev/null +++ b/competency/classes/external/competency_exporter.php @@ -0,0 +1,43 @@ +. + +/** + * Class for exporting competency data. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +/** + * Class for exporting competency data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\competency'; + } + + protected static function define_related() { + // We cache the context so it does not need to be retrieved from the framework every time. + return array('context' => '\\context'); + } +} diff --git a/competency/classes/external/competency_framework_exporter.php b/competency/classes/external/competency_framework_exporter.php new file mode 100644 index 00000000000..3782a2924c5 --- /dev/null +++ b/competency/classes/external/competency_framework_exporter.php @@ -0,0 +1,86 @@ +. + +/** + * Class for exporting competency_framework data. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +use core_competency\api; +use renderer_base; + +/** + * Class for exporting competency_framework data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_framework_exporter extends persistent_exporter { + + /** + * Define the name of persistent class. + * + * @return string + */ + protected static function define_class() { + return 'core_competency\\competency_framework'; + } + + /** + * Get other values that do not belong to the basic persisent. + * + * @param renderer_base $output + * @return Array + */ + protected function get_other_values(renderer_base $output) { + $filters = array('competencyframeworkid' => $this->persistent->get_id()); + $context = $this->persistent->get_context(); + return array( + 'canmanage' => has_capability('moodle/competency:competencymanage', $context), + 'competenciescount' => api::count_competencies($filters), + 'contextname' => $context->get_context_name(), + 'contextnamenoprefix' => $context->get_context_name(false) + ); + } + + /** + * Define other properties that do not belong to the basic persisent. + * + * @return Array + */ + protected static function define_other_properties() { + return array( + 'canmanage' => array( + 'type' => PARAM_BOOL + ), + 'competenciescount' => array( + 'type' => PARAM_INT + ), + 'contextname' => array( + 'type' => PARAM_TEXT + ), + 'contextnamenoprefix' => array( + 'type' => PARAM_TEXT + ) + ); + } + +} diff --git a/competency/classes/external/course_competency_exporter.php b/competency/classes/external/course_competency_exporter.php new file mode 100644 index 00000000000..b5f4aaf0bb9 --- /dev/null +++ b/competency/classes/external/course_competency_exporter.php @@ -0,0 +1,38 @@ +. + +/** + * Class for exporting course competency data. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +/** + * Class for exporting course competency data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_competency_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\course_competency'; + } +} diff --git a/competency/classes/external/course_competency_settings_exporter.php b/competency/classes/external/course_competency_settings_exporter.php new file mode 100644 index 00000000000..49f37f258a1 --- /dev/null +++ b/competency/classes/external/course_competency_settings_exporter.php @@ -0,0 +1,39 @@ +. + +/** + * Class for exporting course_competency_settings data. + * + * @package core_competency + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; + +/** + * Class for exporting course_competency_settings data. + * + * @package core_competency + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_competency_settings_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\course_competency_settings'; + } + +} diff --git a/competency/classes/external/course_module_competency_exporter.php b/competency/classes/external/course_module_competency_exporter.php new file mode 100644 index 00000000000..8d02c3fbf56 --- /dev/null +++ b/competency/classes/external/course_module_competency_exporter.php @@ -0,0 +1,38 @@ +. + +/** + * Class for exporting course module competency data. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +/** + * Class for exporting course module competency data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_competency_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\course_module_competency'; + } +} diff --git a/competency/classes/external/evidence_exporter.php b/competency/classes/external/evidence_exporter.php new file mode 100644 index 00000000000..5782e632ff3 --- /dev/null +++ b/competency/classes/external/evidence_exporter.php @@ -0,0 +1,106 @@ +. + +/** + * Class for exporting evidence data. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; + +use renderer_base; +use core_competency\evidence; +use core_competency\user_competency; + +/** + * Class for exporting evidence data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class evidence_exporter extends persistent_exporter { + + protected static function define_related() { + return array( + 'actionuser' => 'stdClass?', + 'scale' => 'grade_scale', + 'usercompetency' => 'core_competency\\user_competency?', + 'usercompetencyplan' => 'core_competency\\user_competency_plan?', + ); + } + + protected static function define_class() { + return 'core_competency\\evidence'; + } + + protected function get_other_values(renderer_base $output) { + $other = array(); + + if (!empty($this->related['actionuser'])) { + $exporter = new user_summary_exporter($this->related['actionuser']); + $actionuser = $exporter->export($output); + $other['actionuser'] = $actionuser; + } + + $other['description'] = $this->persistent->get_description(); + + $other['userdate'] = userdate($this->persistent->get_timecreated()); + + if ($this->persistent->get_grade() === null) { + $gradename = '-'; + } else { + $gradename = $this->related['scale']->scale_items[$this->persistent->get_grade() - 1]; + } + $other['gradename'] = $gradename; + + // Try to guess the user from the user competency. + $userid = null; + if ($this->related['usercompetency']) { + $userid = $this->related['usercompetency']->get_userid(); + } else if ($this->related['usercompetencyplan']) { + $userid = $this->related['usercompetencyplan']->get_userid(); + } else { + $uc = user_competency::get_record(['id' => $this->persistent->get_usercompetencyid()]); + $userid = $uc->get_userid(); + } + $other['candelete'] = evidence::can_delete_user($userid); + + return $other; + } + + public static function define_other_properties() { + return array( + 'actionuser' => array( + 'type' => user_summary_exporter::read_properties_definition(), + 'optional' => true + ), + 'description' => array( + 'type' => PARAM_TEXT, + ), + 'gradename' => array( + 'type' => PARAM_TEXT, + ), + 'userdate' => array( + 'type' => PARAM_TEXT + ), + 'candelete' => array( + 'type' => PARAM_BOOL + ) + ); + } +} diff --git a/competency/classes/external/exporter.php b/competency/classes/external/exporter.php new file mode 100644 index 00000000000..1f8f7f72b10 --- /dev/null +++ b/competency/classes/external/exporter.php @@ -0,0 +1,526 @@ +. + +/** + * Generic exporter to take a stdClass and prepare it for return by webservice. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/externallib.php'); + +use stdClass; +use renderer_base; +use context; +use context_system; +use coding_exception; +use external_single_structure; +use external_multiple_structure; +use external_value; +use external_format_value; + +/** + * Generic exporter to take a stdClass and prepare it for return by webservice, or as the context for a template. + * + * templatable classes implementing export_for_template, should always use a standard exporter if it exists. + * External functions should always use a standard exporter if it exists. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class exporter { + + /** @var array $related List of related objects used to avoid DB queries. */ + protected $related = array(); + + /** @var stdClass|array The data of this exporter. */ + protected $data = null; + + /** + * Constructor - saves the persistent object, and the related objects. + * + * @param mixed $data - Either an stdClass or an array of values. + * @param array $related - An optional list of pre-loaded objects related to this object. + */ + public function __construct($data, $related = array()) { + $this->data = $data; + // Cache the valid related objects. + foreach (static::define_related() as $key => $classname) { + $isarray = false; + $nullallowed = false; + + // Allow ? to mean null is allowed. + if (substr($classname, -1) === '?') { + $classname = substr($classname, 0, -1); + $nullallowed = true; + } + + // Allow [] to mean an array of values. + if (substr($classname, -2) === '[]') { + $classname = substr($classname, 0, -2); + $isarray = true; + } + + $missingdataerr = 'Exporter class is missing required related data: (' . get_called_class() . ') '; + + if ($nullallowed && array_key_exists($key, $related) && $related[$key] === null) { + $this->related[$key] = $related[$key]; + + } else if ($isarray) { + if (array_key_exists($key, $related) && is_array($related[$key])) { + foreach ($related[$key] as $index => $value) { + if (!$value instanceof $classname) { + throw new coding_exception($missingdataerr . $key . ' => ' . $classname . '[]'); + } + } + $this->related[$key] = $related[$key]; + } else { + throw new coding_exception($missingdataerr . $key . ' => ' . $classname . '[]'); + } + + } else { + if (array_key_exists($key, $related) && $related[$key] instanceof $classname) { + $this->related[$key] = $related[$key]; + } else { + throw new coding_exception($missingdataerr . $key . ' => ' . $classname); + } + } + } + } + + /** + * Function to export the renderer data in a format that is suitable for a + * mustache template. This means raw records are generated as in to_record, + * but all strings are correctly passed through external_format_text (or external_format_string). + * + * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. + * @return stdClass + */ + final public function export(renderer_base $output) { + $data = new stdClass(); + $properties = self::read_properties_definition(); + $context = $this->get_context(); + $values = (array) $this->data; + + $othervalues = $this->get_other_values($output); + if (array_intersect_key($values, $othervalues)) { + // Attempt to replace a standard property. + throw new coding_exception('Cannot override a standard property value.'); + } + $values += $othervalues; + $record = (object) $values; + + foreach ($properties as $property => $definition) { + if (isset($data->$property)) { + // This happens when we have already defined the format properties. + continue; + } else if (!property_exists($record, $property) && array_key_exists('default', $definition)) { + // We have a default value for this property. + $record->$property = $definition['default']; + } else if (!property_exists($record, $property) && !empty($definition['optional'])) { + // Fine, this property can be omitted. + continue; + } else if (!property_exists($record, $property)) { + // Whoops, we got something that wasn't defined. + throw new coding_exception('Unexpected property ' . $property); + } + + $data->$property = $record->$property; + + // If the field is PARAM_RAW and has a format field. + if ($propertyformat = self::get_format_field($properties, $property)) { + if (!property_exists($record, $propertyformat)) { + // Whoops, we got something that wasn't defined. + throw new coding_exception('Unexpected property ' . $propertyformat); + } + $format = $record->$propertyformat; + list($text, $format) = external_format_text($data->$property, $format, $context->id, 'core_competency', '', 0); + $data->$property = $text; + $data->$propertyformat = $format; + + } else if ($definition['type'] === PARAM_TEXT) { + if (!empty($definition['multiple'])) { + foreach ($data->$property as $key => $value) { + $data->$property[$key] = external_format_string($value, $context->id); + } + } else { + $data->$property = external_format_string($data->$property, $context->id); + } + } + } + + return $data; + } + + /** + * Function to guess the correct context, falling back to system context. + * + * @return context + */ + protected function get_context() { + $context = null; + if (isset($this->related['context']) && $this->related['context'] instanceof context) { + $context = $this->related['context']; + } else { + $context = context_system::instance(); + } + return $context; + } + + /** + * Get the additional values to inject while exporting. + * + * These are additional generated values that are not passed in through $data + * to the exporter. For a persistent exporter - these are generated values that + * do not exist in the persistent class. For your convenience the format_text or + * format_string functions do not need to be applied to PARAM_TEXT fields, + * it will be done automatically during export. + * + * These values are only used when returning data via {@link self::export()}, + * they are not used when generating any of the different external structures. + * + * Note: These must be defined in {@link self::define_other_properties()}. + * + * @param renderer_base $output The renderer. + * @return array Keys are the property names, values are their values. + */ + protected function get_other_values(renderer_base $output) { + return array(); + } + + /** + * Get the read properties definition of this exporter. Read properties combines the + * default properties from the model (persistent or stdClass) with the properties defined + * by {@link self::define_other_properties()}. + * + * @return array Keys are the property names, and value their definition. + */ + final public static function read_properties_definition() { + $properties = static::properties_definition(); + $customprops = static::define_other_properties(); + foreach ($customprops as $property => $definition) { + // Ensures that null is set to its default. + if (!isset($definition['null'])) { + $customprops[$property]['null'] = NULL_NOT_ALLOWED; + } + } + $properties += $customprops; + return $properties; + } + + /** + * Get the properties definition of this exporter used for create, and update structures. + * The read structures are returned by: {@link self::read_properties_definition()}. + * + * @return array Keys are the property names, and value their definition. + */ + final public static function properties_definition() { + $properties = static::define_properties(); + foreach ($properties as $property => $definition) { + // Ensures that null is set to its default. + if (!isset($definition['null'])) { + $properties[$property]['null'] = NULL_NOT_ALLOWED; + } + } + return $properties; + } + + /** + * Return the list of additional properties used only for display. + * + * Additional properties are only ever used for the read structure, and during + * export of the persistent data. + * + * The format of the array returned by this method has to match the structure + * defined in {@link \core_competency\persistent::define_properties()}. The display properties + * can however do some more fancy things. They can define 'multiple' => true to wrap + * values in an external_multiple_structure automatically - or they can define the + * type as a nested array of more properties in order to generate a nested + * external_single_structure. + * + * You can specify an array of values by including a 'multiple' => true array value. This + * will result in a nested external_multiple_structure. + * E.g. + * + * 'arrayofbools' => array( + * 'type' => PARAM_BOOL, + * 'multiple' => true + * ), + * + * You can return a nested array in the type field, which will result in a nested external_single_structure. + * E.g. + * 'competency' => array( + * 'type' => competency_exporter::read_properties_definition() + * ), + * + * Other properties can be specifically marked as optional, in which case they do not need + * to be included in the export in {@link self::get_other_values()}. This is useful when exporting + * a substructure which cannot be set as null due to webservices protocol constraints. + * E.g. + * 'competency' => array( + * 'type' => competency_exporter::read_properties_definition(), + * 'optional' => true + * ), + * + * @return array + */ + protected static function define_other_properties() { + return array(); + } + + /** + * Return the list of properties. + * + * The format of the array returned by this method has to match the structure + * defined in {@link \core_competency\persistent::define_properties()}. + * + * @return array + */ + protected static function define_properties() { + return array(); + } + + /** + * Returns a list of objects that are related to this persistent. + * + * Only objects listed here can be cached in this object. + * + * The class name can be suffixed: + * - with [] to indicate an array of values. + * - with ? to indicate that 'null' is allowed. + * + * @return array of 'propertyname' => array('type' => classname, 'required' => true) + */ + protected static function define_related() { + return array(); + } + + /** + * Get the context structure. + * + * @return external_single_structure + */ + final protected static function get_context_structure() { + return array( + 'contextid' => new external_value(PARAM_INT, 'The context id', VALUE_OPTIONAL), + 'contextlevel' => new external_value(PARAM_ALPHA, 'The context level', VALUE_OPTIONAL), + 'instanceid' => new external_value(PARAM_INT, 'The Instance id', VALUE_OPTIONAL), + ); + } + + /** + * Get the format field name. + * + * @param array $definitions List of properties definitions. + * @param string $property The name of the property that may have a format field. + * @return bool|string False, or the name of the format property. + */ + final protected static function get_format_field($definitions, $property) { + $formatproperty = $property . 'format'; + if ($definitions[$property]['type'] == PARAM_RAW && isset($definitions[$formatproperty]) + && $definitions[$formatproperty]['type'] == PARAM_INT) { + return $formatproperty; + } + return false; + } + + /** + * Get the format structure. + * + * @param string $property The name of the property on which the format applies. + * @param array $definition The definition of the format property. + * @param int $required Constant VALUE_*. + * @return external_format_value + */ + final protected static function get_format_structure($property, $definition, $required = VALUE_REQUIRED) { + if (array_key_exists('default', $definition)) { + $required = VALUE_DEFAULT; + } + return new external_format_value($property, $required); + } + + /** + * Returns the create structure. + * + * @return external_single_structure + */ + final public static function get_create_structure() { + $properties = self::properties_definition(); + $returns = array(); + + foreach ($properties as $property => $definition) { + if ($property == 'id') { + // The can not be set on create. + continue; + + } else if (isset($returns[$property]) && substr($property, -6) === 'format') { + // We've already treated the format. + continue; + } + + $required = VALUE_REQUIRED; + $default = null; + + // We cannot use isset here because we want to detect nulls. + if (array_key_exists('default', $definition)) { + $required = VALUE_DEFAULT; + $default = $definition['default']; + } + + // Magically treat the contextid fields. + if ($property == 'contextid') { + if (isset($properties['context'])) { + throw new coding_exception('There cannot be a context and a contextid column'); + } + $returns += self::get_context_structure(); + + } else { + $returns[$property] = new external_value($definition['type'], $property, $required, $default, $definition['null']); + + // Magically treat the format properties. + if ($formatproperty = self::get_format_field($properties, $property)) { + if (isset($returns[$formatproperty])) { + throw new coding_exception('The format for \'' . $property . '\' is already defined.'); + } + $returns[$formatproperty] = self::get_format_structure($property, + $properties[$formatproperty], VALUE_REQUIRED); + } + } + } + + return new external_single_structure($returns); + } + + /** + * Returns the read structure. + * + * @return external_single_structure + */ + final public static function get_read_structure() { + $properties = self::read_properties_definition(); + + return self::get_read_structure_from_properties($properties); + } + + /** + * Returns the read structure from a set of properties (recursive). + * + * @param array $properties The properties. + * @param int $required Whether is required. + * @param mixed $default The default value. + * @return external_single_structure + */ + final protected static function get_read_structure_from_properties($properties, $required = VALUE_REQUIRED, $default = null) { + $returns = array(); + foreach ($properties as $property => $definition) { + if (isset($returns[$property]) && substr($property, -6) === 'format') { + // We've already treated the format. + continue; + } + $thisvalue = null; + + $type = $definition['type']; + $proprequired = VALUE_REQUIRED; + $propdefault = null; + if (array_key_exists('default', $definition)) { + $propdefault = $definition['default']; + } + if (array_key_exists('optional', $definition)) { + // Mark as optional. Note that this should only apply to "reading" "other" properties. + $proprequired = VALUE_OPTIONAL; + } + + if (is_array($type)) { + // This is a nested array of more properties. + $thisvalue = self::get_read_structure_from_properties($type, $proprequired, $propdefault); + } else { + if ($definition['type'] == PARAM_TEXT) { + // PARAM_TEXT always becomes PARAM_RAW because filters may be applied. + $type = PARAM_RAW; + } + $thisvalue = new external_value($type, $property, $proprequired, $propdefault, $definition['null']); + } + if (!empty($definition['multiple'])) { + $returns[$property] = new external_multiple_structure($thisvalue, '', $proprequired, $propdefault); + } else { + $returns[$property] = $thisvalue; + + // Magically treat the format properties (not possible for arrays). + if ($formatproperty = self::get_format_field($properties, $property)) { + if (isset($returns[$formatproperty])) { + throw new coding_exception('The format for \'' . $property . '\' is already defined.'); + } + $returns[$formatproperty] = self::get_format_structure($property, $properties[$formatproperty]); + } + } + } + + return new external_single_structure($returns, '', $required, $default); + } + + /** + * Returns the update structure. + * + * This structure can never be included at the top level for an external function signature + * because it contains optional parameters. + * + * @return external_single_structure + */ + final public static function get_update_structure() { + $properties = self::properties_definition(); + $returns = array(); + + foreach ($properties as $property => $definition) { + if (isset($returns[$property]) && substr($property, -6) === 'format') { + // We've already treated the format. + continue; + } + + $default = null; + $required = VALUE_OPTIONAL; + if ($property == 'id') { + $required = VALUE_REQUIRED; + } + + // Magically treat the contextid fields. + if ($property == 'contextid') { + if (isset($properties['context'])) { + throw new coding_exception('There cannot be a context and a contextid column'); + } + $returns += self::get_context_structure(); + + } else { + $returns[$property] = new external_value($definition['type'], $property, $required, $default, $definition['null']); + + // Magically treat the format properties. + if ($formatproperty = self::get_format_field($properties, $property)) { + if (isset($returns[$formatproperty])) { + throw new coding_exception('The format for \'' . $property . '\' is already defined.'); + } + $returns[$formatproperty] = self::get_format_structure($property, + $properties[$formatproperty], VALUE_OPTIONAL); + } + } + } + + return new external_single_structure($returns); + } + +} diff --git a/competency/classes/external/persistent_exporter.php b/competency/classes/external/persistent_exporter.php new file mode 100644 index 00000000000..598ca7fb3d0 --- /dev/null +++ b/competency/classes/external/persistent_exporter.php @@ -0,0 +1,83 @@ +. + +/** + * Abstract class for core_competency objects saved to the DB. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/externallib.php'); + +use coding_exception; + +/** + * An extended version of the persistent class with a default implementation of export + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class persistent_exporter extends exporter { + + /** @var \core_competency\persistent The persistent object we will export. */ + protected $persistent = null; + + /** + * Constructor - saves the persistent object, and the related objects. + * + * @param \core_competency\persistent $persistent The persistent object to export. + * @param array $related - An optional list of pre-loaded objects related to this persistent. + */ + public final function __construct(\core_competency\persistent $persistent, $related = array()) { + $classname = static::define_class(); + if (!$persistent instanceof $classname) { + throw new coding_exception('Invalid type for persistent. ' . + 'Expected: ' . $classname . ' got: ' . get_class($persistent)); + } + $this->persistent = $persistent; + + if (method_exists($this->persistent, 'get_context') && !isset($this->related['context'])) { + $this->related['context'] = $this->persistent->get_context(); + } + + $data = $persistent->to_record(); + parent::__construct($data, $related); + } + + /** + * Persistent exporters get their standard properties from the persistent class. + * + * @return array Keys are the property names, and value their definition. + */ + protected final static function define_properties() { + $classname = static::define_class(); + return $classname::properties_definition(); + } + + /** + * Returns the specific class the persistent should be an instance of. + * + * @return string + */ + protected static function define_class() { + throw new coding_exception('define_class() must be overidden.'); + } + +} diff --git a/competency/classes/external/plan_competency_exporter.php b/competency/classes/external/plan_competency_exporter.php new file mode 100644 index 00000000000..8c7ea957aa7 --- /dev/null +++ b/competency/classes/external/plan_competency_exporter.php @@ -0,0 +1,38 @@ +. + +/** + * Class for exporting plan competency data. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +/** + * Class for exporting plan competency data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plan_competency_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\plan_competency'; + } +} diff --git a/competency/classes/external/plan_exporter.php b/competency/classes/external/plan_exporter.php new file mode 100644 index 00000000000..a0a34a8bf94 --- /dev/null +++ b/competency/classes/external/plan_exporter.php @@ -0,0 +1,189 @@ +. + +/** + * Class for exporting plan data. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +use core_user; +use renderer_base; +use stdClass; +use moodle_url; +use core_competency\url; + +/** + * Class for exporting plan data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plan_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\plan'; + } + + protected static function define_related() { + return array('template' => 'core_competency\\template?'); + } + + protected function get_other_values(renderer_base $output) { + $classname = static::define_class(); + $status = $this->persistent->get_status(); + + $values = new stdClass(); + + $values->statusname = $this->persistent->get_statusname(); + $values->isbasedontemplate = $this->persistent->is_based_on_template(); + + $values->canmanage = $this->persistent->can_manage(); + $values->canrequestreview = $this->persistent->can_request_review(); + $values->canreview = $this->persistent->can_review(); + $values->canbeedited = $this->persistent->can_be_edited(); + + $values->isactive = $status == $classname::STATUS_ACTIVE; + $values->isdraft = $status == $classname::STATUS_DRAFT; + $values->iscompleted = $status == $classname::STATUS_COMPLETE; + $values->isinreview = $status == $classname::STATUS_IN_REVIEW; + $values->iswaitingforreview = $status == $classname::STATUS_WAITING_FOR_REVIEW; + + $values->isreopenallowed = $values->canmanage && $values->iscompleted; + $values->iscompleteallowed = $values->canmanage && $values->isactive; + $values->isunlinkallowed = $values->canmanage && !$values->iscompleted && $values->isbasedontemplate; + + $values->isrequestreviewallowed = false; + $values->iscancelreviewrequestallowed = false; + $values->isstartreviewallowed = false; + $values->isstopreviewallowed = false; + $values->isapproveallowed = false; + $values->isunapproveallowed = false; + if (!$values->isbasedontemplate) { + $values->isrequestreviewallowed = $values->canrequestreview && $values->isdraft; + $values->iscancelreviewrequestallowed = $values->canrequestreview && $values->iswaitingforreview; + $values->isstartreviewallowed = $values->canreview && $values->iswaitingforreview; + $values->isstopreviewallowed = $values->canreview && $values->isinreview; + $values->isapproveallowed = $values->canreview && !$values->iscompleted && !$values->isactive; + $values->isunapproveallowed = $values->canreview && $values->isactive; + } + + $values->duedateformatted = userdate($this->persistent->get_duedate()); + + if ($this->persistent->is_based_on_template()) { + $exporter = new template_exporter($this->related['template']); + $values->template = $exporter->export($output); + } + + if (!empty($values->isinreview)) { + // TODO Make this more efficient. + $userexporter = new user_summary_exporter(core_user::get_user($this->persistent->get_reviewerid(), '*', MUST_EXIST)); + $values->reviewer = $userexporter->export($output); + } + + $commentareaexporter = new comment_area_exporter($this->persistent->get_comment_object()); + $values->commentarea = $commentareaexporter->export($output); + $values->url = url::plan($this->persistent->get_id())->out(false); + + return (array) $values; + } + + public static function define_other_properties() { + return array( + 'statusname' => array( + 'type' => PARAM_RAW, + ), + 'isbasedontemplate' => array( + 'type' => PARAM_BOOL, + ), + 'canmanage' => array( + 'type' => PARAM_BOOL, + ), + 'canrequestreview' => array( + 'type' => PARAM_BOOL, + ), + 'canreview' => array( + 'type' => PARAM_BOOL, + ), + 'canbeedited' => array( + 'type' => PARAM_BOOL, + ), + 'isactive' => array( + 'type' => PARAM_BOOL + ), + 'isdraft' => array( + 'type' => PARAM_BOOL + ), + 'iscompleted' => array( + 'type' => PARAM_BOOL + ), + 'isinreview' => array( + 'type' => PARAM_BOOL + ), + 'iswaitingforreview' => array( + 'type' => PARAM_BOOL + ), + 'isreopenallowed' => array( + 'type' => PARAM_BOOL + ), + 'iscompleteallowed' => array( + 'type' => PARAM_BOOL + ), + 'isunlinkallowed' => array( + 'type' => PARAM_BOOL + ), + 'isrequestreviewallowed' => array( + 'type' => PARAM_BOOL + ), + 'iscancelreviewrequestallowed' => array( + 'type' => PARAM_BOOL + ), + 'isstartreviewallowed' => array( + 'type' => PARAM_BOOL + ), + 'isstopreviewallowed' => array( + 'type' => PARAM_BOOL + ), + 'isapproveallowed' => array( + 'type' => PARAM_BOOL + ), + 'isunapproveallowed' => array( + 'type' => PARAM_BOOL + ), + 'duedateformatted' => array( + 'type' => PARAM_TEXT + ), + 'commentarea' => array( + 'type' => comment_area_exporter::read_properties_definition(), + ), + 'reviewer' => array( + 'type' => user_summary_exporter::read_properties_definition(), + 'optional' => true + ), + 'template' => array( + 'type' => template_exporter::read_properties_definition(), + 'optional' => true, + ), + 'url' => array( + 'type' => PARAM_URL + ) + ); + } +} diff --git a/competency/classes/external/related_competency_exporter.php b/competency/classes/external/related_competency_exporter.php new file mode 100644 index 00000000000..b5ee549b681 --- /dev/null +++ b/competency/classes/external/related_competency_exporter.php @@ -0,0 +1,38 @@ +. + +/** + * Class for exporting plan data. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +/** + * Class for exporting related competency data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class related_competency_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\related_competency'; + } +} diff --git a/competency/classes/external/stored_file_exporter.php b/competency/classes/external/stored_file_exporter.php new file mode 100644 index 00000000000..a73cee58bf0 --- /dev/null +++ b/competency/classes/external/stored_file_exporter.php @@ -0,0 +1,172 @@ +. + +/** + * Class for exporting stored_file data. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; + +use coding_exception; +use core_text; +use moodle_url; +use renderer_base; +use stdClass; +use stored_file; + +/** + * Class for exporting stored_file data. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class stored_file_exporter extends exporter { + + /** @var stored_file */ + protected $file; + + public function __construct(stored_file $file, $related = array()) { + $this->file = $file; + + $data = new stdClass(); + $data->contextid = $file->get_contextid(); + $data->component = $file->get_component(); + $data->filearea = $file->get_filearea(); + $data->itemid = $file->get_itemid(); + $data->filepath = $file->get_filepath(); + $data->filename = $file->get_filename(); + $data->isdir = $file->is_directory(); + $data->timemodified = $file->get_timemodified(); + $data->timecreated = $file->get_timecreated(); + $data->filesize = $file->get_filesize(); + $data->author = $file->get_author(); + $data->license = $file->get_license(); + + if ($related['context']->id != $data->contextid) { + throw new coding_exception('Unexpected context ID received.'); + } + + parent::__construct($data, $related); + } + + protected static function define_related() { + return array('context' => 'context'); + } + + protected static function define_properties() { + return array( + 'contextid' => array( + 'type' => PARAM_INT + ), + 'component' => array( + 'type' => PARAM_COMPONENT + ), + 'filearea' => array( + 'type' => PARAM_AREA + ), + 'itemid' => array( + 'type' => PARAM_INT + ), + 'filepath' => array( + 'type' => PARAM_PATH + ), + 'filename' => array( + 'type' => PARAM_FILE + ), + 'isdir' => array( + 'type' => PARAM_BOOL + ), + 'timemodified' => array( + 'type' => PARAM_INT + ), + 'timecreated' => array( + 'type' => PARAM_INT + ), + 'filesize' => array( + 'type' => PARAM_INT + ), + 'author' => array( + 'type' => PARAM_TEXT + ), + 'license' => array( + 'type' => PARAM_TEXT + ) + ); + } + + protected static function define_other_properties() { + return array( + 'filenameshort' => array( + 'type' => PARAM_RAW, + ), + 'filesizeformatted' => array( + 'type' => PARAM_RAW + ), + 'icon' => array( + 'type' => PARAM_RAW, + ), + 'iconurl' => array( + 'type' => PARAM_URL, + ), + 'timecreatedformatted' => array( + 'type' => PARAM_RAW + ), + 'timemodifiedformatted' => array( + 'type' => PARAM_RAW + ), + 'url' => array( + 'type' => PARAM_URL + ), + ); + } + + protected function get_other_values(renderer_base $output) { + $filename = $this->file->get_filename(); + $filenameshort = $filename; + if (core_text::strlen($filename) > 25) { + $filenameshort = shorten_text(substr($filename, 0, -4), 21, true, '..'); + $filenameshort .= substr($filename, -4); + } + + $icon = $this->file->is_directory() ? file_folder_icon() : file_file_icon($this->file); + $iconurl = $output->pix_url($icon, 'core'); + + $url = moodle_url::make_pluginfile_url( + $this->file->get_contextid(), + $this->file->get_component(), + $this->file->get_filearea(), + $this->file->get_itemid(), + $this->file->get_filepath(), + $this->file->get_filename(), + true + ); + + return array( + 'filenameshort' => $filenameshort, + 'filesizeformatted' => display_size((int) $this->file->get_filesize()), + 'icon' => $icon, + 'iconurl' => $iconurl->out(false), + 'url' => $url->out(false), + 'timecreatedformatted' => userdate($this->file->get_timecreated()), + 'timemodifiedformatted' => userdate($this->file->get_timemodified()), + ); + } + +} diff --git a/competency/classes/external/template_competency_exporter.php b/competency/classes/external/template_competency_exporter.php new file mode 100644 index 00000000000..1aabf226695 --- /dev/null +++ b/competency/classes/external/template_competency_exporter.php @@ -0,0 +1,38 @@ +. + +/** + * Class for exporting template competency data. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +/** + * Class for exporting template competency data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_competency_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\template_competency'; + } +} diff --git a/competency/classes/external/template_exporter.php b/competency/classes/external/template_exporter.php new file mode 100644 index 00000000000..ac287a114a0 --- /dev/null +++ b/competency/classes/external/template_exporter.php @@ -0,0 +1,82 @@ +. + +/** + * Class for exporting template data. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; +use renderer_base; +use core_competency\plan; +use core_competency\template_cohort; + +/** + * Class for exporting template data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\template'; + } + + protected function get_other_values(renderer_base $output) { + $context = $this->persistent->get_context(); + return array( + 'duedateformatted' => userdate($this->persistent->get_duedate()), + 'cohortscount' => template_cohort::count_records(array('templateid' => $this->persistent->get_id())), + 'planscount' => plan::count_records(array('templateid' => $this->persistent->get_id())), + 'canmanage' => $this->persistent->can_manage(), + 'canread' => $this->persistent->can_read(), + 'contextname' => $context->get_context_name(), + 'contextnamenoprefix' => $context->get_context_name(false) + ); + } + + protected static function define_other_properties() { + return array( + 'duedateformatted' => array( + 'type' => PARAM_RAW + ), + 'cohortscount' => array( + 'type' => PARAM_INT + ), + 'planscount' => array( + 'type' => PARAM_INT + ), + 'canmanage' => array( + 'type' => PARAM_BOOL + ), + 'canread' => array( + 'type' => PARAM_BOOL + ), + 'contextname' => array( + 'type' => PARAM_TEXT, + ), + 'contextnamenoprefix' => array( + 'type' => PARAM_TEXT, + ) + ); + } +} diff --git a/competency/classes/external/user_competency_course_exporter.php b/competency/classes/external/user_competency_course_exporter.php new file mode 100644 index 00000000000..f3dc1212eb4 --- /dev/null +++ b/competency/classes/external/user_competency_course_exporter.php @@ -0,0 +1,78 @@ +. + +/** + * Class for exporting user competency course data. + * + * @package core_competency + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +use core_user; +use renderer_base; +use stdClass; + +/** + * Class for exporting user competency course data. + * + * @copyright 2016 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency_course_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\user_competency_course'; + } + + protected static function define_related() { + // We cache the scale so it does not need to be retrieved from the framework every time. + return array('scale' => 'grade_scale'); + } + + protected function get_other_values(renderer_base $output) { + $result = new stdClass(); + + if ($this->persistent->get_grade() === null) { + $gradename = '-'; + } else { + $gradename = $this->related['scale']->scale_items[$this->persistent->get_grade() - 1]; + } + $result->gradename = $gradename; + + if ($this->persistent->get_proficiency() === null) { + $proficiencyname = get_string('no'); + } else { + $proficiencyname = get_string($this->persistent->get_proficiency() ? 'yes' : 'no'); + } + $result->proficiencyname = $proficiencyname; + + return (array) $result; + } + + protected static function define_other_properties() { + return array( + 'gradename' => array( + 'type' => PARAM_TEXT + ), + 'proficiencyname' => array( + 'type' => PARAM_RAW + ) + ); + } +} diff --git a/competency/classes/external/user_competency_exporter.php b/competency/classes/external/user_competency_exporter.php new file mode 100644 index 00000000000..ca78fb35241 --- /dev/null +++ b/competency/classes/external/user_competency_exporter.php @@ -0,0 +1,143 @@ +. + +/** + * Class for exporting user competency data. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +use core_user; +use renderer_base; +use stdClass; +use core_competency\url; +use core_competency\user_competency; + +/** + * Class for exporting user competency data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\user_competency'; + } + + protected static function define_related() { + // We cache the scale so it does not need to be retrieved from the framework every time. + return array('scale' => 'grade_scale'); + } + + protected function get_other_values(renderer_base $output) { + $result = new stdClass(); + + if ($this->persistent->get_grade() === null) { + $gradename = '-'; + } else { + $gradename = $this->related['scale']->scale_items[$this->persistent->get_grade() - 1]; + } + $result->gradename = $gradename; + + if ($this->persistent->get_proficiency() === null) { + $proficiencyname = get_string('no'); + } else { + $proficiencyname = get_string($this->persistent->get_proficiency() ? 'yes' : 'no'); + } + $result->proficiencyname = $proficiencyname; + + $statusname = '-'; + if ($this->persistent->get_status() != user_competency::STATUS_IDLE) { + $statusname = (string) user_competency::get_status_name($this->persistent->get_status()); + } + $result->statusname = $statusname; + + $result->canrequestreview = $this->persistent->can_request_review(); + $result->canreview = $this->persistent->can_review(); + + $result->isstatusidle = $this->persistent->get_status() == user_competency::STATUS_IDLE; + $result->isstatusinreview = $this->persistent->get_status() == user_competency::STATUS_IN_REVIEW; + $result->isstatuswaitingforreview = $this->persistent->get_status() == user_competency::STATUS_WAITING_FOR_REVIEW; + + $result->isrequestreviewallowed = $result->canrequestreview && $result->isstatusidle; + $result->iscancelreviewrequestallowed = $result->canrequestreview && $result->isstatuswaitingforreview; + $result->isstartreviewallowed = $result->canreview && $result->isstatuswaitingforreview; + $result->isstopreviewallowed = $result->canreview && $result->isstatusinreview; + + if (!empty($result->isstatusinreview)) { + // TODO Make this more efficient. + $userexporter = new user_summary_exporter(core_user::get_user($this->persistent->get_reviewerid(), '*', MUST_EXIST)); + $result->reviewer = $userexporter->export($output); + } + + $result->url = url::user_competency($this->persistent->get_id())->out(false); + + return (array) $result; + } + + protected static function define_other_properties() { + return array( + 'canrequestreview' => array( + 'type' => PARAM_BOOL, + ), + 'canreview' => array( + 'type' => PARAM_BOOL, + ), + 'gradename' => array( + 'type' => PARAM_TEXT + ), + 'isrequestreviewallowed' => array( + 'type' => PARAM_BOOL, + ), + 'iscancelreviewrequestallowed' => array( + 'type' => PARAM_BOOL, + ), + 'isstartreviewallowed' => array( + 'type' => PARAM_BOOL, + ), + 'isstopreviewallowed' => array( + 'type' => PARAM_BOOL, + ), + 'isstatusidle' => array( + 'type' => PARAM_BOOL, + ), + 'isstatusinreview' => array( + 'type' => PARAM_BOOL, + ), + 'isstatuswaitingforreview' => array( + 'type' => PARAM_BOOL, + ), + 'proficiencyname' => array( + 'type' => PARAM_RAW + ), + 'reviewer' => array( + 'type' => user_summary_exporter::read_properties_definition(), + 'optional' => true + ), + 'statusname' => array( + 'type' => PARAM_RAW + ), + 'url' => array( + 'type' => PARAM_URL + ), + ); + } +} diff --git a/competency/classes/external/user_competency_plan_exporter.php b/competency/classes/external/user_competency_plan_exporter.php new file mode 100644 index 00000000000..5be33d4ab73 --- /dev/null +++ b/competency/classes/external/user_competency_plan_exporter.php @@ -0,0 +1,77 @@ +. + +/** + * Class for exporting plan competency data. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +use renderer_base; +use stdClass; + +/** + * Class for exporting plan competency data. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency_plan_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\user_competency_plan'; + } + + protected static function define_related() { + // We cache the scale so it does not need to be retrieved from the framework every time. + return array('scale' => 'grade_scale'); + } + + protected function get_other_values(renderer_base $output) { + $result = new stdClass(); + + if ($this->persistent->get_grade() === null) { + $gradename = '-'; + } else { + $gradename = $this->related['scale']->scale_items[$this->persistent->get_grade() - 1]; + } + $result->gradename = $gradename; + + if ($this->persistent->get_proficiency() === null) { + $proficiencyname = get_string('no'); + } else { + $proficiencyname = get_string($this->persistent->get_proficiency() ? 'yes' : 'no'); + } + $result->proficiencyname = $proficiencyname; + + return (array) $result; + } + + protected static function define_other_properties() { + return array( + 'gradename' => array( + 'type' => PARAM_TEXT + ), + 'proficiencyname' => array( + 'type' => PARAM_RAW + ), + ); + } +} diff --git a/competency/classes/external/user_evidence_competency_exporter.php b/competency/classes/external/user_evidence_competency_exporter.php new file mode 100644 index 00000000000..f1a06c36f05 --- /dev/null +++ b/competency/classes/external/user_evidence_competency_exporter.php @@ -0,0 +1,41 @@ +. + +/** + * User evidence competency exporter. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +/** + * User evidence competency exporter class. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_evidence_competency_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\user_evidence_competency'; + } + +} diff --git a/competency/classes/external/user_evidence_exporter.php b/competency/classes/external/user_evidence_exporter.php new file mode 100644 index 00000000000..d708785a455 --- /dev/null +++ b/competency/classes/external/user_evidence_exporter.php @@ -0,0 +1,121 @@ +. + +/** + * Class for exporting user_evidence data. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; + +use moodle_url; +use renderer_base; + +/** + * Class for exporting user_evidence data. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_evidence_exporter extends persistent_exporter { + + protected static function define_class() { + return 'core_competency\\user_evidence'; + } + + protected static function define_other_properties() { + return array( + 'canmanage' => array( + 'type' => PARAM_BOOL + ), + 'competencycount' => array( + 'type' => PARAM_INT + ), + 'competencies' => array( + 'type' => competency_exporter::read_properties_definition(), + 'multiple' => true + ), + 'filecount' => array( + 'type' => PARAM_INT + ), + 'files' => array( + 'type' => stored_file_exporter::read_properties_definition(), + 'multiple' => true + ), + 'hasurlorfiles' => array( + 'type' => PARAM_BOOL + ), + 'urlshort' => array( + 'type' => PARAM_TEXT + ), + ); + } + + protected static function define_related() { + return array( + 'context' => 'context', + 'competencies' => 'core_competency\\competency[]' + ); + } + + protected function get_other_values(renderer_base $output) { + $contextcache = array(); + + $competencies = array(); + foreach ($this->related['competencies'] as $competency) { + if (!isset($contextcache[$competency->get_competencyframeworkid()])) { + $contextcache[$competency->get_competencyframeworkid()] = $competency->get_context(); + } + $context = $contextcache[$competency->get_competencyframeworkid()]; + + $compexporter = new competency_exporter($competency, array('context' => $context)); + $competencies[] = $compexporter->export($output); + } + + $urlshort = ''; + $url = $this->persistent->get_url(); + if (!empty($url)) { + $murl = new moodle_url($url); + $shorturl = preg_replace('@^https?://(www\.)?@', '', $murl->out(false)); + $urlshort = shorten_text($shorturl, 30, true); + } + + $files = array(); + $storedfiles = $this->persistent->get_files(); + if (!empty($storedfiles)) { + foreach ($storedfiles as $storedfile) { + $fileexporter = new stored_file_exporter($storedfile, array('context' => $this->related['context'])); + $files[] = $fileexporter->export($output); + } + } + + $values = array( + 'canmanage' => $this->persistent->can_manage(), + 'competencycount' => count($competencies), + 'competencies' => $competencies, + 'filecount' => count($files), + 'files' => $files, + 'hasurlorfiles' => !empty($files) || !empty($url), + 'urlshort' => $urlshort + ); + + return $values; + } + +} diff --git a/competency/classes/external/user_summary_exporter.php b/competency/classes/external/user_summary_exporter.php new file mode 100644 index 00000000000..d09b1423a98 --- /dev/null +++ b/competency/classes/external/user_summary_exporter.php @@ -0,0 +1,120 @@ +. + +/** + * Class for exporting a user summary from an stdClass. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency\external; +defined('MOODLE_INTERNAL') || die(); + +use renderer_base; +use moodle_url; + +/** + * Class for exporting a user summary from an stdClass. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_summary_exporter extends exporter { + + protected function get_other_values(renderer_base $output) { + global $PAGE, $CFG; + + // Add user picture. + $userpicture = new \user_picture($this->data); + $userpicture->size = 1; // Size f1. + $profileimageurl = $userpicture->get_url($PAGE)->out(false); + $userpicture->size = 0; // Size f2. + $profileimageurlsmall = $userpicture->get_url($PAGE)->out(false); + + $profileurl = (new moodle_url('/user/profile.php', array('id' => $this->data->id)))->out(false); + + $identityfields = array_flip(explode(',', $CFG->showuseridentity)); + $data = $this->data; + foreach ($identityfields as $field => $index) { + if (!empty($data->$field)) { + $identityfields[$field] = $data->$field; + } else { + unset($identityfields[$field]); + } + } + $identity = implode(', ', $identityfields); + return array( + 'fullname' => fullname($this->data), + 'profileimageurl' => $profileimageurl, + 'profileimageurlsmall' => $profileimageurlsmall, + 'profileurl' => $profileurl, + 'identity' => $identity + ); + } + + public static function define_properties() { + return array( + 'id' => array( + 'type' => PARAM_INT, + ), + 'email' => array( + 'type' => PARAM_TEXT, + 'default' => '' + ), + 'idnumber' => array( + 'type' => PARAM_NOTAGS, + 'default' => '' + ), + 'phone1' => array( + 'type' => PARAM_NOTAGS, + 'default' => '' + ), + 'phone2' => array( + 'type' => PARAM_NOTAGS, + 'default' => '' + ), + 'department' => array( + 'type' => PARAM_TEXT, + 'default' => '' + ), + 'institution' => array( + 'type' => PARAM_TEXT, + 'default' => '' + ) + ); + } + + public static function define_other_properties() { + return array( + 'fullname' => array( + 'type' => PARAM_TEXT + ), + 'identity' => array( + 'type' => PARAM_TEXT + ), + 'profileurl' => array( + 'type' => PARAM_URL + ), + 'profileimageurl' => array( + 'type' => PARAM_URL + ), + 'profileimageurlsmall' => array( + 'type' => PARAM_URL + ), + ); + } +} diff --git a/competency/classes/invalid_persistent_exception.php b/competency/classes/invalid_persistent_exception.php new file mode 100644 index 00000000000..49d97e257bd --- /dev/null +++ b/competency/classes/invalid_persistent_exception.php @@ -0,0 +1,46 @@ +. + +/** + * Invalid persistent exception. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_competency; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Invalid persistent exception class. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class invalid_persistent_exception extends \moodle_exception { + + public function __construct(array $errors = array()) { + $debuginfo = array(); + foreach ($errors as $key => $message) { + $debuginfo[] = "$key: $message"; + } + parent::__construct('invalidpersistent', 'core_competency', null, null, implode(' - ', $debuginfo)); + } + +} diff --git a/competency/classes/persistent.php b/competency/classes/persistent.php new file mode 100644 index 00000000000..7db1c4858b9 --- /dev/null +++ b/competency/classes/persistent.php @@ -0,0 +1,857 @@ +. + +/** + * Abstract class for core_competency objects saved to the DB. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use invalid_parameter_exception; +use lang_string; +use ReflectionMethod; +use stdClass; +use renderer_base; + +/** + * Abstract class for core_competency objects saved to the DB. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class persistent { + + /** The table name. */ + const TABLE = null; + + /** @var array The model data. */ + private $data = array(); + + /** @var array The list of validation errors. */ + private $errors = array(); + + /** @var boolean If the data was already validated. */ + private $validated = false; + + /** + * Create an instance of this class. + * + * @param int $id If set, this is the id of an existing record, used to load the data. + * @param stdClass $record If set will be passed to {@link self::from_record()}. + */ + public function __construct($id = 0, stdClass $record = null) { + if ($id > 0) { + $this->set('id', $id); + $this->read(); + } + if (!empty($record)) { + $this->from_record($record); + } + } + + /** + * Magic method to capture getters and setters. + * + * @param string $method Callee. + * @param array $arguments List of arguments. + * @return mixed + */ + final public function __call($method, $arguments) { + if (strpos($method, 'get_') === 0) { + return $this->get(substr($method, 4)); + } else if (strpos($method, 'set_') === 0) { + return $this->set(substr($method, 4), $arguments[0]); + } + throw new coding_exception('Unexpected method call: ' . $method); + } + + /** + * Data getter. + * + * This is the main getter for all the properties. Developers can implement their own getters + * but they should be calling {@link self::get()} in order to retrieve the value. Essentially + * the getters defined by the developers would only ever be used as helper methods and will not + * be called internally at this stage. In other words, do not expect {@link self::to_record()} or + * {@link self::from_record()} to use them. + * + * This is protected because we wouldn't want the developers to get into the habit of + * using $persistent->get('property_name'), the lengthy getters must be used. + * + * @param string $property The property name. + * @return mixed + */ + final protected function get($property) { + if (!static::has_property($property)) { + throw new coding_exception('Unexpected property \'' . s($property) .'\' requested.'); + } + if (!array_key_exists($property, $this->data) && !static::is_property_required($property)) { + $this->set($property, static::get_property_default_value($property)); + } + return isset($this->data[$property]) ? $this->data[$property] : null; + } + + /** + * Data setter. + * + * This is the main setter for all the properties. Developers can implement their own setters + * but they should always be calling {@link self::set()} in order to set the value. Essentially + * the setters defined by the developers are helper methods and will not be called internally + * at this stage. In other words do not expect {@link self::to_record()} or + * {@link self::from_record()} to use them. + * + * This is protected because we wouldn't want the developers to get into the habit of + * using $persistent->set('property_name', ''), the lengthy setters must be used. + * + * @param string $property The property name. + * @param mixed $value The value. + * @return mixed + */ + final protected function set($property, $value) { + if (!static::has_property($property)) { + throw new coding_exception('Unexpected property \'' . s($property) .'\' requested.'); + } + if (!array_key_exists($property, $this->data) || $this->data[$property] != $value) { + // If the value is changing, we invalidate the model. + $this->validated = false; + } + $this->data[$property] = $value; + } + + /** + * Return the custom definition of the properties of this model. + * + * Each property MUST be listed here. + * + * The result of this method is cached internally for the whole request. + * + * The 'default' value can be a Closure when its value may change during a single request. + * For example if the default value is based on a $CFG property, then it should be wrapped in a closure + * to avoid running into scenarios where the true value of $CFG is not reflected in the definition. + * Do not abuse closures as they obviously add some overhead. + * + * Examples: + * + * array( + * 'property_name' => array( + * 'default' => 'Default value', // When not set, the property is considered as required. + * 'message' => new lang_string(...), // Defaults to invalid data error message. + * 'null' => NULL_ALLOWED, // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOW_ALLOWED or NULL_ALLOWED. + * 'type' => PARAM_TYPE, // Mandatory. + * 'choices' => array(1, 2, 3) // An array of accepted values. + * ) + * ) + * + * array( + * 'dynamic_property_name' => array( + * 'default' => function() { + * return $CFG->something; + * }, + * 'type' => PARAM_INT, + * ) + * ) + * + * @return array Where keys are the property names. + */ + protected static function define_properties() { + return array(); + } + + /** + * Get the properties definition of this model.. + * + * @return array + */ + final public static function properties_definition() { + global $CFG; + + static $def = null; + if ($def !== null) { + return $def; + } + + $def = static::define_properties(); + $def['id'] = array( + 'default' => 0, + 'type' => PARAM_INT, + ); + $def['timecreated'] = array( + 'default' => 0, + 'type' => PARAM_INT, + ); + $def['timemodified'] = array( + 'default' => 0, + 'type' => PARAM_INT + ); + $def['usermodified'] = array( + 'default' => 0, + 'type' => PARAM_INT + ); + + // List of reserved property names. Mostly because we have methods (getters/setters) which would confict with them. + // Think about backwards compability before adding new ones here! + $reserved = array('errors', 'formatted_properties', 'records', 'records_select', 'property_default_value', + 'property_error_message', 'sql_fields'); + + foreach ($def as $property => $definition) { + + // Ensures that the null property is always set. + if (!array_key_exists('null', $definition)) { + $def[$property]['null'] = NULL_NOT_ALLOWED; + } + + // Warn the developers when they are doing something wrong. + if ($CFG->debugdeveloper) { + if (!array_key_exists('type', $definition)) { + throw new coding_exception('Missing type for: ' . $property); + + } else if (isset($definition['message']) && !($definition['message'] instanceof lang_string)) { + throw new coding_exception('Invalid error message for: ' . $property); + + } else if (in_array($property, $reserved)) { + throw new coding_exception('This property cannot be defined: ' . $property); + + } + } + } + + return $def; + } + + /** + * Gets all the formatted properties. + * + * Formatted properties are properties which have a format associated with them. + * + * @return array Keys are property names, values are property format names. + */ + final public static function get_formatted_properties() { + $properties = static::properties_definition(); + + $formatted = array(); + foreach ($properties as $property => $definition) { + $propertyformat = $property . 'format'; + if ($definition['type'] == PARAM_RAW && array_key_exists($propertyformat, $properties) + && $properties[$propertyformat]['type'] == PARAM_INT) { + $formatted[$property] = $propertyformat; + } + } + + return $formatted; + } + + /** + * Gets the default value for a property. + * + * This assumes that the property exists. + * + * @param string $property The property name. + * @return mixed + */ + final protected static function get_property_default_value($property) { + $properties = static::properties_definition(); + if (!isset($properties[$property]['default'])) { + return null; + } + $value = $properties[$property]['default']; + if ($value instanceof \Closure) { + return $value(); + } + return $value; + } + + /** + * Gets the error message for a property. + * + * This assumes that the property exists. + * + * @param string $property The property name. + * @return lang_string + */ + final protected static function get_property_error_message($property) { + $properties = static::properties_definition(); + if (!isset($properties[$property]['message'])) { + return new lang_string('invaliddata', 'error'); + } + return $properties[$property]['message']; + } + + /** + * Returns whether or not a property was defined. + * + * @param string $property The property name. + * @return boolean + */ + final public static function has_property($property) { + $properties = static::properties_definition(); + return isset($properties[$property]); + } + + /** + * Returns whether or not a property is required. + * + * By definition a property with a default value is not required. + * + * @param string $property The property name. + * @return boolean + */ + final public static function is_property_required($property) { + $properties = static::properties_definition(); + return !array_key_exists('default', $properties[$property]); + } + + /** + * Populate this class with data from a DB record. + * + * Note that this does not use any custom setter because the data here is intended to + * represent what is stored in the database. + * + * @param \stdClass $record A DB record. + * @return persistent + */ + final public function from_record(stdClass $record) { + $record = (array) $record; + foreach ($record as $property => $value) { + $this->set($property, $value); + } + return $this; + } + + /** + * Create a DB record from this class. + * + * Note that this does not use any custom getter because the data here is intended to + * represent what is stored in the database. + * + * @return \stdClass + */ + final public function to_record() { + $data = new stdClass(); + $properties = static::properties_definition(); + foreach ($properties as $property => $definition) { + $data->$property = $this->get($property); + } + return $data; + } + + /** + * Load the data from the DB. + * + * @return persistent + */ + final public function read() { + global $DB; + + if ($this->get_id() <= 0) { + throw new coding_exception('id is required to load'); + } + $record = $DB->get_record(static::TABLE, array('id' => $this->get_id()), '*', MUST_EXIST); + $this->from_record($record); + + // Validate the data as it comes from the database. + $this->validated = true; + + return $this; + } + + /** + * Hook to execute before a create. + * + * Please note that at this stage the data has already been validated and therefore + * any new data being set will not be validated before it is sent to the database. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @return void + */ + protected function before_create() { + } + + /** + * Insert a record in the DB. + * + * @return persistent + */ + final public function create() { + global $DB, $USER; + + if ($this->get_id()) { + // The validation methods rely on the ID to know if we're updating or not, the ID should be + // falsy whenever we are creating an object. + throw new coding_exception('Cannot create an object that has an ID defined.'); + } + + if (!$this->is_valid()) { + throw new invalid_persistent_exception($this->get_errors()); + } + + // Before create hook. + $this->before_create(); + + // We can safely set those values bypassing the validation because we know what we're doing. + $now = time(); + $this->set('timecreated', $now); + $this->set('timemodified', $now); + $this->set('usermodified', $USER->id); + + $record = $this->to_record(); + unset($record->id); + + $id = $DB->insert_record(static::TABLE, $record); + $this->set('id', $id); + + // We ensure that this is flagged as validated. + $this->validated = true; + + // After create hook. + $this->after_create(); + + return $this; + } + + /** + * Hook to execute after a create. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @return void + */ + protected function after_create() { + } + + /** + * Hook to execute before an update. + * + * Please note that at this stage the data has already been validated and therefore + * any new data being set will not be validated before it is sent to the database. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @return void + */ + protected function before_update() { + } + + /** + * Update the existing record in the DB. + * + * @return bool True on success. + */ + final public function update() { + global $DB, $USER; + + if ($this->get_id() <= 0) { + throw new coding_exception('id is required to update'); + } else if (!$this->is_valid()) { + throw new invalid_persistent_exception($this->get_errors()); + } + + // Before update hook. + $this->before_update(); + + // We can safely set those values after the validation because we know what we're doing. + $this->set('timemodified', time()); + $this->set('usermodified', $USER->id); + + $record = $this->to_record(); + unset($record->timecreated); + $record = (array) $record; + + // Save the record. + $result = $DB->update_record(static::TABLE, $record); + + // We ensure that this is flagged as validated. + $this->validated = true; + + // After update hook. + $this->after_update($result); + + return $result; + } + + /** + * Hook to execute after an update. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @param bool $result Whether or not the update was successful. + * @return void + */ + protected function after_update($result) { + } + + /** + * Hook to execute before a delete. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @return void + */ + protected function before_delete() { + } + + /** + * Delete an entry from the database. + * + * @return bool True on success. + */ + final public function delete() { + global $DB; + + if ($this->get_id() <= 0) { + throw new coding_exception('id is required to delete'); + } + + // Hook before delete. + $this->before_delete(); + + $result = $DB->delete_records(static::TABLE, array('id' => $this->get_id())); + + // Hook after delete. + $this->after_delete($result); + + // Reset the ID to avoid any confusion, this also invalidates the model's data. + if ($result) { + $this->set('id', 0); + } + + return $result; + } + + /** + * Hook to execute after a delete. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @param bool $result Whether or not the delete was successful. + * @return void + */ + protected function after_delete($result) { + } + + /** + * Hook to execute before the validation. + * + * This hook will not affect the validation results in any way but is useful to + * internally set properties which will need to be validated. + * + * This is only intended to be used by child classes, do not put any logic here! + * + * @return void + */ + protected function before_validate() { + } + + /** + * Validates the data. + * + * Developers can implement addition validation by defining a method as follows. Note that + * the method MUST return a lang_string() when there is an error, and true when the data is valid. + * + * protected function validate_propertyname($value) { + * if ($value !== 'My expected value') { + * return new lang_string('invaliddata', 'error'); + * } + * return true + * } + * + * It is OK to use other properties in your custom validation methods when you need to, however note + * they might not have been validated yet, so try not to rely on them too much. + * + * Note that the validation methods should be protected. Validating just one field is not + * recommended because of the possible dependencies between one field and another,also the + * field ID can be used to check whether the object is being updated or created. + * + * When validating foreign keys the persistent should only check that the associated model + * exists. The validation methods should not be used to check for a change in that relationship. + * The API method setting the attributes on the model should be responsible for that. + * E.g. On a course model, the method validate_categoryid will check that the category exists. + * However, if a course can never be moved outside of its category it would be up to the calling + * code to ensure that the category ID will not be altered. + * + * @return array|true Returns true when the validation passed, or an array of properties with errors. + */ + final public function validate() { + global $CFG; + + // Before validate hook. + $this->before_validate(); + + // If this object has not been validated yet. + if ($this->validated !== true) { + + $errors = array(); + $properties = static::properties_definition(); + foreach ($properties as $property => $definition) { + + // Get the data, bypassing the potential custom getter which could alter the data. + $value = $this->get($property); + + // Check if the property is required. + if ($value === null && static::is_property_required($property)) { + $errors[$property] = new lang_string('requiredelement', 'form'); + continue; + } + + // Check that type of value is respected. + try { + if ($definition['type'] === PARAM_BOOL && $value === false) { + // Validate_param() does not like false with PARAM_BOOL, better to convert it to int. + $value = 0; + } + validate_param($value, $definition['type'], $definition['null']); + } catch (invalid_parameter_exception $e) { + $errors[$property] = static::get_property_error_message($property); + continue; + } + + // Check that the value is part of a list of allowed values. + if (isset($definition['choices']) && !in_array($value, $definition['choices'])) { + $errors[$property] = static::get_property_error_message($property); + continue; + } + + // Call custom validation method. + $method = 'validate_' . $property; + if (method_exists($this, $method)) { + + // Warn the developers when they are doing something wrong. + if ($CFG->debugdeveloper) { + $reflection = new ReflectionMethod($this, $method); + if (!$reflection->isProtected()) { + throw new coding_exception('The method ' . get_class($this) . '::'. $method . ' should be protected.'); + } + } + + $valid = $this->{$method}($value); + if ($valid !== true) { + if (!($valid instanceof lang_string)) { + throw new coding_exception('Unexpected error message.'); + } + $errors[$property] = $valid; + continue; + } + } + } + + $this->validated = true; + $this->errors = $errors; + } + + return empty($this->errors) ? true : $this->errors; + } + + /** + * Returns whether or not the model is valid. + * + * @return boolean True when it is. + */ + final public function is_valid() { + return $this->validate() === true; + } + + /** + * Returns the validation errors. + * + * @return array + */ + final public function get_errors() { + $this->validate(); + return $this->errors; + } + + /** + * Extract a record from a row of data. + * + * Most likely used in combination with {@link self::get_sql_fields()}. This method is + * simple enough to be used by non-persistent classes, keep that in mind when modifying it. + * + * e.g. persistent::extract_record($row, 'user'); should work. + * + * @param stdClass $row The row of data. + * @param string $prefix The prefix the data fields are prefixed with, defaults to the table name followed by underscore. + * @return stdClass The extracted data. + */ + public static function extract_record($row, $prefix = null) { + if ($prefix === null) { + $prefix = str_replace('_', '', static::TABLE) . '_'; + } + $prefixlength = strlen($prefix); + + $data = new stdClass(); + foreach ($row as $property => $value) { + if (strpos($property, $prefix) === 0) { + $propertyname = substr($property, $prefixlength); + $data->$propertyname = $value; + } + } + + return $data; + } + + /** + * Load a list of records. + * + * @param array $filters Filters to apply. + * @param string $sort Field to sort by. + * @param string $order Sort order. + * @param int $skip Limitstart. + * @param int $limit Number of rows to return. + * + * @return \core_competency\persistent[] + */ + public static function get_records($filters = array(), $sort = '', $order = 'ASC', $skip = 0, $limit = 0) { + global $DB; + + $orderby = ''; + if (!empty($sort)) { + $orderby = $sort . ' ' . $order; + } + + $records = $DB->get_records(static::TABLE, $filters, $orderby, '*', $skip, $limit); + $instances = array(); + + foreach ($records as $record) { + $newrecord = new static(0, $record); + array_push($instances, $newrecord); + } + return $instances; + } + + /** + * Load a single record. + * + * @param array $filters Filters to apply. + * @return false|\core_competency\persistent + */ + public static function get_record($filters = array()) { + global $DB; + + $record = $DB->get_record(static::TABLE, $filters); + return $record ? new static(0, $record) : false; + } + + /** + * Load a list of records based on a select query. + * + * @param string $select + * @param array $params + * @param string $sort + * @param string $fields + * @param int $limitfrom + * @param int $limitnum + * @return \core_competency\persistent[] + */ + public static function get_records_select($select, $params = null, $sort = '', $fields = '*', $limitfrom = 0, $limitnum = 0) { + global $DB; + + $records = $DB->get_records_select(static::TABLE, $select, $params, $sort, $fields, $limitfrom, $limitnum); + + // We return class instances. + $instances = array(); + foreach ($records as $key => $record) { + $instances[$key] = new static(0, $record); + } + + return $instances; + + } + + /** + * Return the list of fields for use in a SELECT clause. + * + * Having the complete list of fields prefixed allows for multiple persistents to be fetched + * in a single query. Use {@link self::extract_record()} to extract the records from the query result. + * + * @param string $alias The alias used for the table. + * @param string $prefix The prefix to use for each field, defaults to the table name followed by underscore. + * @return string The SQL fragment. + */ + public static function get_sql_fields($alias, $prefix = null) { + $fields = array(); + + if ($prefix === null) { + $prefix = str_replace('_', '', static::TABLE) . '_'; + } + + // Get the properties and move ID to the top. + $properties = static::properties_definition(); + $id = $properties['id']; + unset($properties['id']); + $properties = array('id' => $id) + $properties; + + foreach ($properties as $property => $definition) { + $fields[] = $alias . '.' . $property . ' AS ' . $prefix . $property; + } + + return implode(', ', $fields); + } + + /** + * Count a list of records. + * + * @param array $conditions An array of conditions. + * @return int + */ + public static function count_records(array $conditions = array()) { + global $DB; + + $count = $DB->count_records(static::TABLE, $conditions); + return $count; + } + + /** + * Count a list of records. + * + * @param string $select + * @param array $params + * @return int + */ + public static function count_records_select($select, $params = null) { + global $DB; + + $count = $DB->count_records_select(static::TABLE, $select, $params); + return $count; + } + + /** + * Check if a record exists by ID. + * + * @param int $id Record ID. + * @return bool + */ + public static function record_exists($id) { + global $DB; + return $DB->record_exists(static::TABLE, array('id' => $id)); + } + + /** + * Check if a records exists. + * + * @param string $select + * @param array $params + * @return bool + */ + public static function record_exists_select($select, array $params = null) { + global $DB; + return $DB->record_exists_select(static::TABLE, $select, $params); + } + +} diff --git a/competency/classes/plan.php b/competency/classes/plan.php new file mode 100644 index 00000000000..91f397d7698 --- /dev/null +++ b/competency/classes/plan.php @@ -0,0 +1,714 @@ +. + +/** + * Class for plans persistence. + * + * @package core_competency + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use comment; +use context_user; +use dml_missing_record_exception; +use lang_string; + +/** + * Class for loading/storing plans from the DB. + * + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plan extends persistent { + + const TABLE = 'competency_plan'; + + /** Draft status. */ + const STATUS_DRAFT = 0; + + /** Active status. */ + const STATUS_ACTIVE = 1; + + /** Complete status. */ + const STATUS_COMPLETE = 2; + + /** Waiting for review. */ + const STATUS_WAITING_FOR_REVIEW = 3; + + /** In review. */ + const STATUS_IN_REVIEW = 4; + + /** 10 minutes threshold **/ + const DUEDATE_THRESHOLD = 600; + + /** @var plan object before update. */ + protected $beforeupdate = null; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'name' => array( + 'type' => PARAM_TEXT, + ), + 'description' => array( + 'type' => PARAM_RAW, + 'default' => '' + ), + 'descriptionformat' => array( + 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), + 'type' => PARAM_INT, + 'default' => FORMAT_HTML, + ), + 'userid' => array( + 'type' => PARAM_INT, + ), + 'templateid' => array( + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'origtemplateid' => array( + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'status' => array( + 'choices' => array(self::STATUS_DRAFT, self::STATUS_COMPLETE, self::STATUS_ACTIVE, + self::STATUS_WAITING_FOR_REVIEW, self::STATUS_IN_REVIEW), + 'type' => PARAM_INT, + 'default' => self::STATUS_DRAFT, + ), + 'duedate' => array( + 'type' => PARAM_INT, + 'default' => 0, + ), + 'reviewerid' => array( + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ) + ); + } + + /** + * Hook to execute before validate. + * + * @return void + */ + protected function before_validate() { + $this->beforeupdate = null; + + // During update. + if ($this->get_id()) { + $this->beforeupdate = new self($this->get_id()); + } + } + + /** + * Whether the current user can comment on this plan. + * + * @return bool + */ + public function can_comment() { + return static::can_comment_user($this->get_userid()); + } + + /** + * Whether the current user can manage the plan. + * + * @return bool + */ + public function can_manage() { + if ($this->is_draft()) { + return self::can_manage_user_draft($this->get_userid()); + } + return self::can_manage_user($this->get_userid()); + } + + /** + * Whether the current user can read the plan. + * + * @return bool + */ + public function can_read() { + if ($this->is_draft()) { + return self::can_read_user_draft($this->get_userid()); + } + return self::can_read_user($this->get_userid()); + } + + /** + * Whether the current user can read comments on this plan. + * + * @return bool + */ + public function can_read_comments() { + return $this->can_read(); + } + + /** + * Whether the current user can request a review of the plan. + * + * @return bool + */ + public function can_request_review() { + return self::can_request_review_user($this->get_userid()); + } + + /** + * Whether the current user can review the plan. + * + * @return bool + */ + public function can_review() { + return self::can_review_user($this->get_userid()); + } + + /** + * Get the comment object. + * + * @return comment + */ + public function get_comment_object() { + global $CFG; + require_once($CFG->dirroot . '/comment/lib.php'); + + if (!$this->get_id()) { + throw new \coding_exception('The plan must exist.'); + } + + $comment = new comment((object) array( + 'client_id' => 'plancommentarea' . $this->get_id(), + 'context' => $this->get_context(), + 'component' => 'competency', // This cannot be named 'core_competency'. + 'itemid' => $this->get_id(), + 'area' => 'plan', + 'showcount' => true, + )); + $comment->set_fullwidth(true); + return $comment; + } + + /** + * Get the competencies in this plan. + * + * @return competency[] + */ + public function get_competencies() { + $competencies = array(); + + if ($this->get_status() == self::STATUS_COMPLETE) { + // Get the competencies from the archive of the plan. + $competencies = user_competency_plan::list_competencies($this->get_id(), $this->get_userid()); + } else if ($this->is_based_on_template()) { + // Get the competencies from the template. + $competencies = template_competency::list_competencies($this->get_templateid()); + } else { + // Get the competencies from the plan. + $competencies = plan_competency::list_competencies($this->get_id()); + } + + return $competencies; + } + + /** + * Get a single competency from this plan. + * + * This will throw an exception if the competency does not belong to the plan. + * + * @param int $competencyid The competency ID. + * @return competency + */ + public function get_competency($competencyid) { + $competency = null; + + if ($this->get_status() == self::STATUS_COMPLETE) { + // Get the competency from the archive of the plan. + $competency = user_competency_plan::get_competency_by_planid($this->get_id(), $competencyid); + } else if ($this->is_based_on_template()) { + // Get the competency from the template. + $competency = template_competency::get_competency($this->get_templateid(), $competencyid); + } else { + // Get the competency from the plan. + $competency = plan_competency::get_competency($this->get_id(), $competencyid); + } + return $competency; + } + + /** + * Get the context in which the plan is attached. + * + * @return context_user + */ + public function get_context() { + return context_user::instance($this->get_userid()); + } + + /** + * Human readable status name. + * + * @return string + */ + public function get_statusname() { + + $status = $this->get_status(); + + switch ($status) { + case self::STATUS_DRAFT: + $strname = 'draft'; + break; + case self::STATUS_IN_REVIEW: + $strname = 'inreview'; + break; + case self::STATUS_WAITING_FOR_REVIEW: + $strname = 'waitingforreview'; + break; + case self::STATUS_ACTIVE: + $strname = 'active'; + break; + case self::STATUS_COMPLETE: + $strname = 'complete'; + break; + default: + throw new \moodle_exception('errorplanstatus', 'core_competency', '', $status); + break; + } + + return get_string('planstatus' . $strname, 'core_competency'); + } + + /** + * Get the plan template. + * + * @return template|null + */ + public function get_template() { + $templateid = $this->get_templateid(); + if ($templateid === null) { + return null; + } + return new template($templateid); + } + + /** + * Is the plan in draft mode? + * + * This method is convenient to know if the plan is a draft because whilst a draft + * is being reviewed its status is not "draft" any more, but it still is a draft nonetheless. + * + * @return boolean + */ + public function is_draft() { + return in_array($this->get_status(), static::get_draft_statuses()); + } + + /** + * Validate the template ID. + * + * @param mixed $value The value. + * @return true|lang_string + */ + protected function validate_templateid($value) { + + // Checks that the template exists. + if (!empty($value) && !template::record_exists($value)) { + return new lang_string('invaliddata', 'error'); + } + + return true; + } + + /** + * Validate the user ID. + * + * @param int $value + * @return true|lang_string + */ + protected function validate_userid($value) { + global $DB; + + // During create. + if (!$this->get_id()) { + + // Check that the user exists. We do not need to do that on update because + // the userid of a plan should never change. + if (!$DB->record_exists('user', array('id' => $value))) { + return new lang_string('invaliddata', 'error'); + } + + } + + return true; + } + + /** + * Can the current user comment on a user's plan? + * + * @param int $planuserid The user ID the plan belongs to. + * @return bool + */ + public static function can_comment_user($planuserid) { + global $USER; + + $capabilities = array('moodle/competency:plancomment'); + if ($USER->id == $planuserid) { + $capabilities[] = 'moodle/competency:plancommentown'; + } + + return has_any_capability($capabilities, context_user::instance($planuserid)); + } + + /** + * Can the current user manage a user's plan? + * + * @param int $planuserid The user to whom the plan would belong. + * @return bool + */ + public static function can_manage_user($planuserid) { + global $USER; + $context = context_user::instance($planuserid); + + $capabilities = array('moodle/competency:planmanage'); + if ($context->instanceid == $USER->id) { + $capabilities[] = 'moodle/competency:planmanageown'; + } + + return has_any_capability($capabilities, $context); + } + + /** + * Can the current user manage a user's draft plan? + * + * @param int $planuserid The user to whom the plan would belong. + * @return bool + */ + public static function can_manage_user_draft($planuserid) { + global $USER; + $context = context_user::instance($planuserid); + + $capabilities = array('moodle/competency:planmanagedraft'); + if ($context->instanceid == $USER->id) { + $capabilities[] = 'moodle/competency:planmanageowndraft'; + } + + return has_any_capability($capabilities, $context); + } + + /** + * Can the current user read the comments on a user's plan? + * + * @param int $planuserid The user ID the plan belongs to. + * @return bool + */ + public static function can_read_comments_user($planuserid) { + // Everyone who can read the plan can read the comments. + return static::can_read_user($planuserid); + } + + /** + * Can the current user view a user's plan? + * + * @param int $planuserid The user to whom the plan would belong. + * @return bool + */ + public static function can_read_user($planuserid) { + global $USER; + $context = context_user::instance($planuserid); + + $capabilities = array('moodle/competency:planview'); + if ($context->instanceid == $USER->id) { + $capabilities[] = 'moodle/competency:planviewown'; + } + + return has_any_capability($capabilities, $context) + || self::can_manage_user($planuserid); + } + + /** + * Can the current user view a user's draft plan? + * + * @param int $planuserid The user to whom the plan would belong. + * @return bool + */ + public static function can_read_user_draft($planuserid) { + global $USER; + $context = context_user::instance($planuserid); + + $capabilities = array('moodle/competency:planviewdraft'); + if ($context->instanceid == $USER->id) { + $capabilities[] = 'moodle/competency:planviewowndraft'; + } + + return has_any_capability($capabilities, $context) + || self::can_manage_user_draft($planuserid); + } + + /** + * Can the current user request the draft to be reviewed. + * + * @param int $planuserid The user to whom the plan would belong. + * @return bool + */ + public static function can_request_review_user($planuserid) { + global $USER; + + $capabilities = array('moodle/competency:planrequestreview'); + if ($USER->id == $planuserid) { + $capabilities[] = 'moodle/competency:planrequestreviewown'; + } + + return has_any_capability($capabilities, context_user::instance($planuserid)); + } + + /** + * Can the current user review the plan. + * + * This means being able to send the plan from draft to active, and vice versa. + * + * @param int $planuserid The user to whom the plan would belong. + * @return bool + */ + public static function can_review_user($planuserid) { + return has_capability('moodle/competency:planreview', context_user::instance($planuserid)) + || self::can_manage_user($planuserid); + } + + /** + * Get the plans of a user containing a specific competency. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @return plans[] + */ + public static function get_by_user_and_competency($userid, $competencyid) { + global $DB; + + $sql = 'SELECT p.* + FROM {' . self::TABLE . '} p + LEFT JOIN {' . plan_competency::TABLE . '} pc + ON pc.planid = p.id + AND pc.competencyid = :competencyid1 + LEFT JOIN {' . user_competency_plan::TABLE . '} ucp + ON ucp.planid = p.id + AND ucp.competencyid = :competencyid2 + LEFT JOIN {' . template_competency::TABLE . '} tc + ON tc.templateid = p.templateid + AND tc.competencyid = :competencyid3 + WHERE p.userid = :userid + AND (pc.id IS NOT NULL + OR ucp.id IS NOT NULL + OR tc.id IS NOT NULL) + ORDER BY p.id ASC'; + + $params = array( + 'competencyid1' => $competencyid, + 'competencyid2' => $competencyid, + 'competencyid3' => $competencyid, + 'userid' => $userid + ); + + $plans = array(); + $records = $DB->get_records_sql($sql, $params); + foreach ($records as $record) { + $plans[$record->id] = new plan(0, $record); + } + + return $plans; + } + + /** + * Get the list of draft statuses. + * + * @return array Contains the status constants. + */ + public static function get_draft_statuses() { + return array(self::STATUS_DRAFT, self::STATUS_WAITING_FOR_REVIEW, self::STATUS_IN_REVIEW); + } + + /** + * Get the recordset of the plans that are due, incomplete and not draft. + * + * @return \moodle_recordset + */ + public static function get_recordset_for_due_and_incomplete() { + global $DB; + $sql = "duedate > 0 AND duedate < :now AND status = :status"; + $params = array('now' => time(), 'status' => self::STATUS_ACTIVE); + return $DB->get_recordset_select(self::TABLE, $sql, $params); + } + + /** + * Return a list of status depending on capabilities. + * + * @param int $userid The user to whom the plan would belong. + * @return array + */ + public static function get_status_list($userid) { + $status = array(); + if (self::can_manage_user_draft($userid)) { + $status[self::STATUS_DRAFT] = get_string('planstatusdraft', 'core_competency'); + } + if (self::can_manage_user($userid)) { + $status[self::STATUS_ACTIVE] = get_string('planstatusactive', 'core_competency'); + } + return $status; + } + + /** + * Update from template. + * + * Bulk update a lot of plans from a template + * + * @param template $template + * @return bool + */ + public static function update_multiple_from_template(template $template) { + global $DB; + if (!$template->is_valid()) { + // As we will bypass this model's validation we rely on the template being validated. + throw new \coding_exception('The template must be validated before updating plans.'); + } + + $params = array( + 'templateid' => $template->get_id(), + 'status' => self::STATUS_COMPLETE, + + 'name' => $template->get_shortname(), + 'description' => $template->get_description(), + 'descriptionformat' => $template->get_descriptionformat(), + 'duedate' => $template->get_duedate(), + ); + + $sql = "UPDATE {" . self::TABLE . "} + SET name = :name, + description = :description, + descriptionformat = :descriptionformat, + duedate = :duedate + WHERE templateid = :templateid + AND status != :status"; + + return $DB->execute($sql, $params); + } + + /** + * Check if a template is associated to the plan. + * + * @return bool + */ + public function is_based_on_template() { + return $this->get_templateid() !== null; + } + + /** + * Check if plan can be edited. + * + * @return bool + */ + public function can_be_edited() { + return !$this->is_based_on_template() && $this->get_status() != self::STATUS_COMPLETE && $this->can_manage(); + } + + /** + * Validate the due date. + * When setting a due date it must not exceed the DUEDATE_THRESHOLD. + * + * @param int $value The due date. + * @return bool|lang_string + */ + protected function validate_duedate($value) { + + // We do not check duedate when plan is draft, complete, unset, or based on a template. + if ($this->is_based_on_template() + || $this->is_draft() + || $this->get_status() == self::STATUS_COMPLETE + || empty($value)) { + return true; + } + + // During update. + if ($this->get_id()) { + $before = $this->beforeupdate->get_duedate(); + $beforestatus = $this->beforeupdate->get_status(); + + // The value has not changed, then it's always OK. Though if we're going + // from draft to active it has to has to be validated. + if ($before == $value && !in_array($beforestatus, self::get_draft_statuses())) { + return true; + } + } + + if ($value <= time()) { + // We cannot set the date in the past. + return new lang_string('errorcannotsetduedateinthepast', 'core_competency'); + } + + if ($value <= time() + self::DUEDATE_THRESHOLD) { + // We cannot set the date too soon, but we can leave it empty. + return new lang_string('errorcannotsetduedatetoosoon', 'core_competency'); + } + + return true; + } + + /** + * Checks if a template has user plan records. + * + * @param int $templateid The template ID + * @return boolean + */ + public static function has_records_for_template($templateid) { + return self::record_exists_select('templateid = ?', array($templateid)); + } + + /** + * Count the number of plans for a template, optionally filtering by status. + * + * @param int $templateid The template ID + * @param int $status The plan status. 0 means all statuses. + * @return int + */ + public static function count_records_for_template($templateid, $status) { + $filters = array('templateid' => $templateid); + if ($status > 0) { + $filters['status'] = $status; + } + return self::count_records($filters); + } + + /** + * Get the plans for a template, optionally filtering by status. + * + * @param int $templateid The template ID + * @param int $status The plan status. 0 means all statuses. + * @param int $skip The number of plans to skip + * @param int $limit The max number of plans to return + * @return int + */ + public static function get_records_for_template($templateid, $status = 0, $skip = 0, $limit = 100) { + $filters = array('templateid' => $templateid); + if ($status > 0) { + $filters['status'] = $status; + } + return self::get_records($filters, $skip, $limit); + } +} diff --git a/competency/classes/plan_competency.php b/competency/classes/plan_competency.php new file mode 100644 index 00000000000..d45ec0180c8 --- /dev/null +++ b/competency/classes/plan_competency.php @@ -0,0 +1,179 @@ +. + +/** + * Class for plan_competency persistence. + * + * @package core_competency + * @copyright 2015 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use lang_string; + +/** + * Class for managing competencies in the plan (add/remove competencies for given plan). + * + * @copyright 2015 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plan_competency extends persistent { + + /** Table name for plan_competency persistency */ + const TABLE = 'competency_plancomp'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'planid' => array( + 'type' => PARAM_INT, + ), + 'competencyid' => array( + 'type' => PARAM_INT, + ), + 'sortorder' => array( + 'type' => PARAM_INT, + 'default' => null, + ), + ); + } + + /** + * List the competencies in this plan. + * + * @param int $planid The plan id + * @return array[competency] + */ + public static function list_competencies($planid) { + global $DB; + + $sql = 'SELECT comp.* + FROM {' . competency::TABLE . '} comp + JOIN {' . self::TABLE . '} plancomp + ON plancomp.competencyid = comp.id + WHERE plancomp.planid = ? + ORDER BY plancomp.sortorder ASC'; + $params = array($planid); + + // TODO MDL-52229 Handle hidden competencies. + $results = $DB->get_records_sql($sql, $params); + + $instances = array(); + foreach ($results as $result) { + array_push($instances, new competency(0, $result)); + } + + return $instances; + } + + /** + * Get a single competency from the plan (only if it is really in the plan). + * + * @param int $planid The plan id + * @param int $competencyid The competency id + * @return competency + */ + public static function get_competency($planid, $competencyid) { + global $DB; + + $sql = 'SELECT comp.* + FROM {' . competency::TABLE . '} comp + JOIN {' . self::TABLE . '} plncomp + ON plncomp.competencyid = comp.id + WHERE plncomp.planid = ? AND plncomp.competencyid = ?'; + $params = array($planid, $competencyid); + + $result = $DB->get_record_sql($sql, $params); + if (!$result) { + throw new \coding_exception('The competency does not belong to this plan: ' . $competencyid . ', ' . $planid); + } + + return new competency(0, $result); + } + + /** + * Hook to execute before validate. + * + * @return void + */ + protected function before_validate() { + if (($this->get_id() && $this->get_sortorder() === null) || !$this->get_id()) { + $this->set_sortorder($this->count_records(array('planid' => $this->get_planid()))); + } + } + + /** + * Validate competencyid. + * + * @param int $value ID. + * @return true|lang_string + */ + protected function validate_competencyid($value) { + if (!competency::record_exists($value)) { + return new lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Validate planid. + * + * @param int $value ID. + * @return true|lang_string + */ + protected function validate_planid($value) { + if (!plan::record_exists($value)) { + return new lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Hook to execute after delete. + * + * @param bool $result Whether or not the delete was successful. + * @return void + */ + protected function after_delete($result) { + global $DB; + if (!$result) { + return; + } + + $table = '{' . self::TABLE . '}'; + $sql = "UPDATE $table SET sortorder = sortorder -1 WHERE planid = ? AND sortorder > ?"; + $DB->execute($sql, array($this->get_planid(), $this->get_sortorder())); + } + + /** + * Check if plan competency has records for competencies. + * + * @param array $competencyids The competences IDs + * @return boolean + */ + public static function has_records_for_competencies($competencyids) { + global $DB; + list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED); + return self::record_exists_select("competencyid $insql", $params); + } + +} diff --git a/competency/classes/related_competency.php b/competency/classes/related_competency.php new file mode 100644 index 00000000000..677118235e5 --- /dev/null +++ b/competency/classes/related_competency.php @@ -0,0 +1,212 @@ +. + +/** + * Class for loading/storing related competencies from the DB. + * + * @package core_competency + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use lang_string; +use stdClass; + +/** + * Class for loading/storing related_competencies from the DB. + * + * @package core_competency + * @copyright 2015 David Monllao + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class related_competency extends persistent { + + const TABLE = 'competency_relatedcomp'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'competencyid' => array( + 'type' => PARAM_INT + ), + 'relatedcompetencyid' => array( + 'type' => PARAM_INT + ), + ); + } + + /** + * Validate competency ID. + * + * @param int $data The competency ID. + * @return true|lang_string + */ + protected function validate_competencyid($data) { + if (!competency::record_exists($data)) { + return new lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Validate related competency ID. + * + * @param int $data The related competency ID. + * @return true|lang_string + */ + protected function validate_relatedcompetencyid($data) { + + if ($this->get_competencyid() == $data) { + // A competency cannot be related to itself. + return new lang_string('invaliddata', 'error'); + + } if ($this->get_competencyid() > $data) { + // The competency ID must be lower than the related competency ID. + return new lang_string('invaliddata', 'error'); + + } else if (!competency::record_exists($data)) { + return new lang_string('invaliddata', 'error'); + + } else if (!competency::share_same_framework(array($data, $this->get_competencyid()))) { + // The competencies must belong to the same framework. + return new lang_string('invaliddata', 'error'); + } + + return true; + } + + /** + * Get relation specifying both competencies. + * + * This does not perform any validation on the data passed. If the relation exists in the database + * then it is loaded in a the model, if not then it is up to the developer to save the model. + * + * @param int $competencyid + * @param int $relatedcompetencyid + * @return related_competency + */ + public static function get_relation($competencyid, $relatedcompetencyid) { + global $DB; + + // Lower id always as competencyid so we know which one is competencyid and which one relatedcompetencyid. + $relation = new static(); + if ($competencyid > $relatedcompetencyid) { + $relation->set_competencyid($relatedcompetencyid); + $relation->set_relatedcompetencyid($competencyid); + } else { + $relation->set_competencyid($competencyid); + $relation->set_relatedcompetencyid($relatedcompetencyid); + } + + // We can do it because we have bidirectional relations in the DB. + $params = array( + 'competencyid' => $relation->get_competencyid(), + 'relatedcompetencyid' => $relation->get_relatedcompetencyid() + ); + if ($record = $DB->get_record(self::TABLE, $params)) { + $relation->from_record($record); + } + + return $relation; + } + + /** + * Get the competencies related to a competency. + * + * @param int $competencyid The competency ID. + * @return competency[] + */ + public static function get_related_competencies($competencyid) { + global $DB; + + $sql = "(SELECT c.*, " . $DB->sql_concat('rc.relatedcompetencyid', "'_'", 'rc.competencyid') . " AS rid + FROM {" . self::TABLE . "} rc + JOIN {" . competency::TABLE . "} c + ON c.id = rc.relatedcompetencyid + WHERE rc.competencyid = :cid) + UNION ALL + (SELECT c.*, " . $DB->sql_concat('rc.competencyid', "'_'", 'rc.relatedcompetencyid') . " AS rid + FROM {" . self::TABLE . "} rc + JOIN {" . competency::TABLE . "} c + ON c.id = rc.competencyid + WHERE rc.relatedcompetencyid = :cid2) + ORDER BY path, sortorder ASC"; + + $competencies = array(); + $records = $DB->get_recordset_sql($sql, array('cid' => $competencyid, 'cid2' => $competencyid)); + foreach ($records as $record) { + unset($record->rid); + $competencies[$record->id] = new competency(null, $record); + } + $records->close(); + + return $competencies; + } + + /** + * Get the related competencies from competency ids. + * + * @param int[] $competencyids Array of competency ids. + * @return related_competency[] + */ + public static function get_multiple_relations($competencyids) { + global $DB; + + if (empty($competencyids)) { + return array(); + } + + list($insql, $params) = $DB->get_in_or_equal($competencyids); + + $records = $DB->get_records_select(self::TABLE, + "competencyid $insql OR relatedcompetencyid $insql", + array_merge($params, $params) + ); + + $relatedcompetencies = array(); + foreach ($records as $record) { + unset($record->id); + $relatedcompetencies[] = new related_competency(null, $record); + } + return $relatedcompetencies; + } + + /** + * Delete relations using competencies. + * + * @param array $competencyids Array of competencies ids. + * @return bool True if relations were deleted successfully. + */ + public static function delete_multiple_relations($competencyids) { + global $DB; + if (empty($competencyids)) { + return true; + } + + list($insql, $params) = $DB->get_in_or_equal($competencyids); + return $DB->delete_records_select(self::TABLE, + "competencyid $insql OR relatedcompetencyid $insql", + array_merge($params, $params) + ); + } + +} diff --git a/competency/classes/template.php b/competency/classes/template.php new file mode 100644 index 00000000000..93d54b520f3 --- /dev/null +++ b/competency/classes/template.php @@ -0,0 +1,205 @@ +. + +/** + * Class for loading/storing learning plan templates from the DB. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use context; +use lang_string; +use stdClass; + +/** + * Class for loading/storing learning plan templates from the DB. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template extends persistent { + + const TABLE = 'competency_template'; + + /** @var template object before update. */ + protected $beforeupdate = null; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'shortname' => array( + 'type' => PARAM_TEXT, + ), + 'description' => array( + 'default' => '', + 'type' => PARAM_RAW, + ), + 'descriptionformat' => array( + 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), + 'type' => PARAM_INT, + 'default' => FORMAT_HTML + ), + 'duedate' => array( + 'default' => 0, + 'type' => PARAM_INT, + ), + 'visible' => array( + 'default' => 1, + 'type' => PARAM_BOOL, + ), + 'contextid' => array( + 'type' => PARAM_INT + ), + ); + } + + /** + * Hook to execute after an update. + * + * @param bool $result Whether or not the update was successful. + * @return void + */ + protected function after_update($result) { + $this->beforeupdate = null; + } + + /** + * Hook to execute before validate. + * + * @return void + */ + protected function before_validate() { + $this->beforeupdate = null; + + // During update. + if ($this->get_id()) { + $this->beforeupdate = new self($this->get_id()); + } + } + + /** + * Whether or not the current user can read the template. + * + * @return bool + */ + public function can_manage() { + return self::can_manage_context($this->get_context()); + } + + /** + * Whether or not the current user can manage the template. + * + * @param context $context + * @return bool + */ + public static function can_manage_context($context) { + return has_capability('moodle/competency:templatemanage', $context); + } + + /** + * Whether or not the current user can read the template. + * + * @return bool + */ + public function can_read() { + return self::can_read_context($this->get_context()); + } + + /** + * Whether or not the current user can read the template. + * + * @param context $context + * @return bool + */ + public static function can_read_context($context) { + return has_capability('moodle/competency:templateview', $context) || self::can_manage_context($context); + } + + /** + * Get the context. + * + * @return context The context + */ + public function get_context() { + return context::instance_by_id($this->get_contextid()); + } + + /** + * Validate the context ID. + * + * @param int $value The context ID. + * @return bool|lang_string + */ + protected function validate_contextid($value) { + $context = context::instance_by_id($value, IGNORE_MISSING); + if (!$context) { + return new lang_string('invalidcontext', 'error'); + } else if ($context->contextlevel != CONTEXT_SYSTEM && $context->contextlevel != CONTEXT_COURSECAT) { + return new lang_string('invalidcontext', 'error'); + } + return true; + } + + /** + * Validate the due date. + * + * The due date can always be changed, but when it is it must be: + * - unset + * - set in the future. + * + * @param int $value The due date. + * @return bool|lang_string + */ + protected function validate_duedate($value) { + + // During update. + if ($this->get_id()) { + $before = $this->beforeupdate->get_duedate(); + + // The value has not changed, then it's always OK. + if ($before == $value) { + return true; + } + } + + // During create and update, the date must be set in the future, or not set. + if (!empty($value) && $value <= time() - 600) { + // We cannot set the date in the past. But we allow for 10 minutes of margin so that + // a user can set the due date to "now" without risking to hit a validation error. + return new lang_string('errorcannotsetduedateinthepast', 'core_competency'); + } + + return true; + } + + /** + * Returns true when the template has user learning plans. + * + * @return boolean + */ + public function has_plans() { + return plan::has_records_for_template($this->get_id()); + } + +} diff --git a/competency/classes/template_cohort.php b/competency/classes/template_cohort.php new file mode 100644 index 00000000000..5387c06d8f0 --- /dev/null +++ b/competency/classes/template_cohort.php @@ -0,0 +1,234 @@ +. + +/** + * Template cohort persistent. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use lang_string; +use core_competency\template; + +/** + * Template cohort persistent. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_cohort extends persistent { + + const TABLE = 'competency_templatecohort'; + + /** 2 hours of threshold to prevent expired plans **/ + const DUEDATE_THRESHOLD = 7200; + + /** + * Return the custom definition of the properties of this model. + * + * @return array Where keys are the property names. + */ + protected static function define_properties() { + return array( + 'templateid' => array( + 'type' => PARAM_INT + ), + 'cohortid' => array( + 'type' => PARAM_INT + ) + ); + } + + /** + * Validate the cohort ID. + * + * @param int $value The cohort ID. + * @return true|lang_string + */ + protected function validate_cohortid($value) { + global $DB; + if (!$DB->record_exists('cohort', array('id' => $value))) { + return new lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Validate the template ID. + * + * @param int $value The template ID. + * @return true|lang_string + */ + protected function validate_templateid($value) { + if (!template::record_exists($value)) { + return new lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Return an array of user IDs for which the plans are missing. + * + * Plans are considered as missing when a member of a cohort does not have a plan created. + * When the parameter $unlinkedaremissing is set to false, plans that were unlinked from + * their template will be ignored so that we do not recreate unlinked plans endlessly. + * + * This method ignores the due date of the template. + * + * @param int $templateid The template ID. + * @param int $cohortid The cohort ID. + * @param boolean $unlinkedaremissing When true, unlinked plans are considered as missing. + * @return int[] User IDs. + */ + public static function get_missing_plans($templateid, $cohortid, $unlinkedaremissing = false) { + global $DB; + + $skipsql = ''; + $skipparams = array(); + if (!$unlinkedaremissing) { + $skipsql = 'OR p.origtemplateid = :origtemplateid'; + $skipparams = array('origtemplateid' => $templateid); + } + + $sql = "SELECT cm.userid + FROM {cohort_members} cm + LEFT JOIN {" . plan::TABLE . "} p + ON p.userid = cm.userid + AND (p.templateid = :templateid + $skipsql) + WHERE cm.cohortid = :cohortid + AND p.id IS NULL"; + $params = array('templateid' => $templateid, 'cohortid' => $cohortid) + $skipparams; + + return $DB->get_fieldset_sql($sql, $params); + } + + /** + * Get a relation. + * + * This does not perform any validation on the data passed. If the relation exists in the database + * then it is loaded in a the model, if not then it is up to the developer to save the model. + * + * @param int $templateid + * @param int $cohortid + * @return template_cohort + */ + public static function get_relation($templateid, $cohortid) { + global $DB; + + $params = array( + 'templateid' => $templateid, + 'cohortid' => $cohortid + ); + + $relation = new static(null, (object) $params); + if ($record = $DB->get_record(self::TABLE, $params)) { + $relation->from_record($record); + } + + return $relation; + } + + /** + * Get a relations by templateid. + * + * This does not perform any validation on the data passed. If the relation exists in the database + * then it is loaded in a the model, if not then it is up to the developer to save the model. + * + * @param int $templateid + * @return template_cohort[] array of template cohort + */ + public static function get_relations_by_templateid($templateid) { + global $DB; + + $params = array( + 'templateid' => $templateid + ); + + $relations = array(); + $records = $DB->get_records(self::TABLE, $params); + foreach ($records as $record) { + $relations[] = new template_cohort(0, $record); + } + + return $relations; + } + + /** + * Return an array of templates persistent with their missing userids. + * + * Note that only cohorts associated with visible templates are considered, + * as well as only templates with a due date in the future, or no due date. + * + * @param int $lastruntime The last time the Cohort ssync task ran. + * @param bool $unlinkedaremissing When true, unlinked plans are considered as missing. + * @return array( array( + * 'template' => \core_competency\template, + * 'userids' => array + * )) + */ + public static function get_all_missing_plans($lastruntime = 0, $unlinkedaremissing = false) { + global $DB; + + $planwhereclause = " WHERE (p.id is NULL + AND (cm.timeadded >= :lastruntime1 + OR tc.timecreated >= :lastruntime3 + OR t.timemodified >= :lastruntime4))"; + + if ($unlinkedaremissing) { + $planwhereclause .= " OR (p.origtemplateid IS NOT NULL AND cm.timeadded < :lastruntime2)"; + } + + $sql = "SELECT " . $DB->sql_concat('cm.userid', 'tc.templateid') . " as uniqueid, cm.userid, t.* + FROM {cohort_members} cm + JOIN {" . self::TABLE . "} tc ON cm.cohortid = tc.cohortid + JOIN {" . template::TABLE . "} t + ON (tc.templateid = t.id AND t.visible = 1) + AND (t.duedate = 0 OR t.duedate > :time1) + LEFT JOIN {" . plan::TABLE . "} p ON (cm.userid = p.userid AND (t.id = p.templateid OR t.id = p.origtemplateid)) + $planwhereclause + ORDER BY t.id"; + + $params = array('time1' => time(), 'time2' => time(), + 'lastruntime1' => $lastruntime, 'lastruntime2' => $lastruntime, + 'lastruntime3' => $lastruntime, 'lastruntime4' => $lastruntime); + $results = $DB->get_records_sql($sql, $params); + + $missingplans = array(); + foreach ($results as $usertemplate) { + $userid = $usertemplate->userid; + + // Check if template already exist in the array. + if (isset($missingplans[$usertemplate->id])) { + $missingplans[$usertemplate->id]['userids'][] = $userid; + } else { + unset($usertemplate->userid); + unset($usertemplate->uniqueid); + $template = new template(0, $usertemplate); + $missingplans[$template->get_id()]['template'] = $template; + $missingplans[$template->get_id()]['userids'][] = $userid; + } + } + return array_values($missingplans); + } + +} diff --git a/competency/classes/template_competency.php b/competency/classes/template_competency.php new file mode 100644 index 00000000000..8b24882008b --- /dev/null +++ b/competency/classes/template_competency.php @@ -0,0 +1,295 @@ +. + +/** + * Class for loading/storing competencies from the DB. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use stdClass; + +/** + * Class for loading/storing template_competencies from the DB. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class template_competency extends persistent { + + const TABLE = 'competency_templatecomp'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'templateid' => array( + 'type' => PARAM_INT, + 'default' => 0, + ), + 'competencyid' => array( + 'type' => PARAM_INT, + 'default' => 0, + ), + 'sortorder' => array( + 'type' => PARAM_INT, + 'default' => null, + ), + ); + } + + /** + * Count the templates using a competency. + * + * @param int $competencyid The competency id + * @param bool $onlyvisible If true, only count visible templates using this competency. + * @return int + */ + public static function count_templates($competencyid, $onlyvisible) { + global $DB; + + $sql = 'SELECT COUNT(tpl.id) + FROM {' . self::TABLE . '} tplcomp + JOIN {' . template::TABLE . '} tpl + ON tplcomp.templateid = tpl.id + WHERE tplcomp.competencyid = ? '; + $params = array($competencyid); + + if ($onlyvisible) { + $sql .= ' AND tpl.visible = ?'; + $params[] = 1; + } + + $results = $DB->count_records_sql($sql, $params); + + return $results; + } + + /** + * List the templates using a competency. + * + * @param int $competencyid The competency id + * @param bool $onlyvisible If true, only count visible templates using this competency. + * @return array[competency] + */ + public static function list_templates($competencyid, $onlyvisible) { + global $DB; + + $sql = 'SELECT tpl.* + FROM {' . template::TABLE . '} tpl + JOIN {' . self::TABLE . '} tplcomp + ON tplcomp.templateid = tpl.id + WHERE tplcomp.competencyid = ? '; + $params = array($competencyid); + + if ($onlyvisible) { + $sql .= ' AND tpl.visible = ?'; + $params[] = 1; + } + + $results = $DB->get_records_sql($sql, $params); + + $instances = array(); + foreach ($results as $result) { + array_push($instances, new template(0, $result)); + } + + return $instances; + } + + /** + * Count the competencies in a template. + * + * @param int $templateid The template id + * @return int + */ + public static function count_competencies($templateid) { + global $DB; + + $sql = 'SELECT COUNT(comp.id) + FROM {' . self::TABLE . '} tplcomp + JOIN {' . competency::TABLE . '} comp + ON tplcomp.competencyid = comp.id + WHERE tplcomp.templateid = ? '; + $params = array($templateid); + + $results = $DB->count_records_sql($sql, $params); + + return $results; + } + + /** + * Count the competencies in a template with no links to courses. + * + * @param int $templateid The template id + * @return int + */ + public static function count_competencies_with_no_courses($templateid) { + global $DB; + + $sql = 'SELECT COUNT(comp.id) + FROM {' . self::TABLE . '} tplcomp + JOIN {' . competency::TABLE . '} comp + ON tplcomp.competencyid = comp.id + LEFT JOIN {' . course_competency::TABLE . '} crscomp + ON crscomp.competencyid = comp.id + WHERE tplcomp.templateid = ? AND crscomp.id IS NULL'; + $params = array($templateid); + + $results = $DB->count_records_sql($sql, $params); + + return $results; + } + + /** + * Get a single competency from the template (only if it is really in the template). + * + * @param int $templateid The template id + * @param int $competencyid The competency id + * @return competency + */ + public static function get_competency($templateid, $competencyid) { + global $DB; + + $sql = 'SELECT comp.* + FROM {' . competency::TABLE . '} comp + JOIN {' . self::TABLE . '} tplcomp + ON tplcomp.competencyid = comp.id + WHERE tplcomp.templateid = ? AND tplcomp.competencyid = ?'; + $params = array($templateid, $competencyid); + + $result = $DB->get_record_sql($sql, $params); + if (!$result) { + throw new \coding_exception('The competency does not belong to this template: ' . $competencyid . ', ' . $templateid); + } + + return new competency(0, $result); + } + + /** + * List the competencies in this template. + * + * @param int $templateid The template id + * @return array[competency] + */ + public static function list_competencies($templateid) { + global $DB; + + $sql = 'SELECT comp.* + FROM {' . competency::TABLE . '} comp + JOIN {' . self::TABLE . '} tplcomp + ON tplcomp.competencyid = comp.id + WHERE tplcomp.templateid = ?'; + $params = array($templateid); + + $sql .= 'ORDER BY tplcomp.sortorder ASC'; + + $results = $DB->get_records_sql($sql, $params); + + $instances = array(); + foreach ($results as $result) { + array_push($instances, new competency(0, $result)); + } + + return $instances; + } + + /** + * Remove the competencies in this template. + * + * @param int $templateid The template id + * @return boolen + */ + public static function delete_by_templateid($templateid) { + global $DB; + + return $DB->delete_records(self::TABLE, array('templateid' => $templateid)); + } + + /** + * Hook to execute before validate. + * + * @return void + */ + protected function before_validate() { + if (($this->get_id() && $this->get_sortorder() === null) || !$this->get_id()) { + $this->set_sortorder($this->count_records(array('templateid' => $this->get_templateid()))); + } + } + + /** + * Validate competencyid. + * + * @param int $value ID. + * @return true|lang_string + */ + protected function validate_competencyid($value) { + if (!competency::record_exists($value)) { + return new \lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Validate templateid. + * + * @param int $value ID. + * @return true|lang_string + */ + protected function validate_templateid($value) { + if (!template::record_exists($value)) { + return new \lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Hook to execute after delete. + * + * @param bool $result Whether or not the delete was successful. + * @return void + */ + protected function after_delete($result) { + global $DB; + if (!$result) { + return; + } + + $table = '{' . self::TABLE . '}'; + $sql = "UPDATE $table SET sortorder = sortorder -1 WHERE templateid = ? AND sortorder > ?"; + $DB->execute($sql, array($this->get_templateid(), $this->get_sortorder())); + } + + /** + * Check if template competency has records for competencies. + * + * @param array $competencyids Array of competencies ids. + * @return boolean Return true if competencies were found in template_competency. + */ + public static function has_records_for_competencies($competencyids) { + global $DB; + list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED); + return self::record_exists_select("competencyid $insql", $params); + } + +} diff --git a/competency/classes/url.php b/competency/classes/url.php new file mode 100644 index 00000000000..f9d87396f30 --- /dev/null +++ b/competency/classes/url.php @@ -0,0 +1,191 @@ +. + +/** + * URL manager. + * + * @package core_competency + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; + +/** + * URL manager class. + * + * This class has to be used to get the URL to a resource, this allows for different + * alternate frontends to be used without resorting to core hacks. Note that you + * do not have to use this when you are navigating between pages of your own plugin. + * + * To set another resolver, set the following config value in config.php: + * $CFG->core_competency_url_resolver = 'your_plugin\\your_url_resolver_class'; + * + * Your URL resolver should implement the same methods as the ones listed in + * this class (except for {{@link self::get()}}) but not statically. + * + * /!\ Note, resolvers MUST NEVER assume that the resource, or the resources + * represented by the arguments, still exist. + * + * @package core_competency + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class url { + + /** @var url_resolver The URL resolver instance.*/ + protected static $resolver; + + /** + * Defer to the resolver. + * + * @param string $resource The resource type. + * @param array $args The arguments. + * @return mixed + */ + protected static function get($resource, $args) { + global $CFG; + if (!isset(static::$resolver)) { + $klass = !empty($CFG->core_competency_url_resolver) ? $CFG->core_competency_url_resolver : 'tool_lp\\url_resolver'; + static::$resolver = new $klass(); + } + if (!method_exists(static::$resolver, $resource)) { + debugging("URL for '$resource' not implemented.", DEBUG_DEVELOPER); + return new moodle_url('/'); + } + return call_user_func_array([static::$resolver, $resource], $args); + } + + /** + * The URL where the competency can be found. + * + * @param int $competencyid The competency ID. + * @param int $pagecontextid The ID of the context we are in. + * @return moodle_url + */ + public static function competency($competencyid, $pagecontextid) { + return static::get(__FUNCTION__, func_get_args()); + } + + /** + * The URL where the framework can be found. + * + * @param int $frameworkid The framework ID. + * @param int $pagecontextid The ID of the context we are in. + * @return moodle_url + */ + public static function framework($frameworkid, $pagecontextid) { + return static::get(__FUNCTION__, func_get_args()); + } + + /** + * The URL where the frameworks can be found. + * + * @param int $pagecontextid The ID of the context that we are browsing. + * @return moodle_url + */ + public static function frameworks($pagecontextid) { + return static::get(__FUNCTION__, func_get_args()); + } + + /** + * The URL where the plan can be found. + * + * @param int $planid The plan ID. + * @return moodle_url + */ + public static function plan($planid) { + return static::get(__FUNCTION__, func_get_args()); + } + + /** + * The URL where the plans of a user can be found. + * + * @param int $userid The user ID. + * @return moodle_url + */ + public static function plans($userid) { + return static::get(__FUNCTION__, func_get_args()); + } + + /** + * The URL where the template can be found. + * + * @param int $templateid The template ID. + * @param int $pagecontextid The ID of the context we are in. + * @return moodle_url + */ + public static function template($templateid, $pagecontextid) { + return static::get(__FUNCTION__, func_get_args()); + } + + /** + * The URL where the templates can be found. + * + * @param int $pagecontextid The ID of the context that we are browsing. + * @return moodle_url + */ + public function templates($pagecontextid) { + return static::get(__FUNCTION__, func_get_args()); + } + + /** + * The URL where the user competency can be found. + * + * @param int $usercompetencyid The user competency ID + * @return moodle_url + */ + public static function user_competency($usercompetencyid) { + return static::get(__FUNCTION__, func_get_args()); + } + + /** + * The URL where the user competency can be found in the context of a course. + * + * @param int $userid The user ID + * @param int $competencyid The competency ID. + * @param int $courseid The course ID. + * @return moodle_url + */ + public static function user_competency_in_course($userid, $competencyid, $courseid) { + return static::get(__FUNCTION__, func_get_args()); + } + + /** + * The URL where the user competency can be found in the context of a plan. + * + * @param int $userid The user ID + * @param int $competencyid The competency ID. + * @param int $planid The plan ID. + * @return moodle_url + */ + public static function user_competency_in_plan($userid, $competencyid, $planid) { + return static::get(__FUNCTION__, func_get_args()); + } + + /** + * The URL where the user evidence (of prior learning) can be found. + * + * @param int $userevidenceid The user evidence ID + * @return moodle_url + */ + public static function user_evidence($userevidenceid) { + return static::get(__FUNCTION__, func_get_args()); + } +} diff --git a/competency/classes/user_competency.php b/competency/classes/user_competency.php new file mode 100644 index 00000000000..f003d389900 --- /dev/null +++ b/competency/classes/user_competency.php @@ -0,0 +1,555 @@ +. + +/** + * Class for user_competency persistence. + * + * @package core_competency + * @copyright 2015 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use coding_exception; +use context_course; +use context_user; +use comment; +use lang_string; + +/** + * Class for loading/storing user_competency from the DB. + * + * @copyright 2015 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency extends persistent { + + /** Table name for user_competency persistency */ + const TABLE = 'competency_usercomp'; + + /** Idle status */ + const STATUS_IDLE = 0; + + /** Waiting for review status */ + const STATUS_WAITING_FOR_REVIEW = 1; + + /** In review status */ + const STATUS_IN_REVIEW = 2; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'userid' => array( + 'type' => PARAM_INT, + ), + 'competencyid' => array( + 'type' => PARAM_INT, + ), + 'status' => array( + 'choices' => array( + self::STATUS_IDLE, + self::STATUS_WAITING_FOR_REVIEW, + self::STATUS_IN_REVIEW, + ), + 'type' => PARAM_INT, + 'default' => self::STATUS_IDLE, + ), + 'reviewerid' => array( + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'proficiency' => array( + 'type' => PARAM_BOOL, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'grade' => array( + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + ); + } + + /** + * Whether the current user can comment on this user competency. + * + * @return bool + */ + public function can_comment() { + return static::can_comment_user($this->get_userid()); + } + + /** + * Whether the current user can read this user competency. + * + * @return bool + */ + public function can_read() { + return static::can_read_user($this->get_userid()); + } + + /** + * Whether the current user can read comments on this user competency. + * + * @return bool + */ + public function can_read_comments() { + return static::can_read_comments_user($this->get_userid()); + } + + /** + * Can the current user send the user competency for review? + * + * @return bool + */ + public function can_request_review() { + return static::can_request_review_user($this->get_userid()); + } + + /** + * Can the current user review the user competency? + * + * @return bool + */ + public function can_review() { + return static::can_review_user($this->get_userid()); + } + + /** + * Human readable status name. + * + * @param int $status The status code. + * @return lang_string + */ + public static function get_status_name($status) { + + switch ($status) { + case self::STATUS_IDLE: + $strname = 'idle'; + break; + case self::STATUS_WAITING_FOR_REVIEW: + $strname = 'waitingforreview'; + break; + case self::STATUS_IN_REVIEW: + $strname = 'inreview'; + break; + default: + throw new \moodle_exception('errorusercomptencystatus', 'core_competency', '', $status); + break; + } + + return new lang_string('usercompetencystatus_' . $strname, 'core_competency'); + } + + /** + * Get list of competency status. + * + * @return array + */ + public static function get_status_list() { + + static $list = null; + + if ($list === null) { + $list = array( + self::STATUS_IDLE => self::get_status_name(self::STATUS_IDLE), + self::STATUS_WAITING_FOR_REVIEW => self::get_status_name(self::STATUS_WAITING_FOR_REVIEW), + self::STATUS_IN_REVIEW => self::get_status_name(self::STATUS_IN_REVIEW)); + } + + return $list; + } + + /** + * Get the comment object. + * + * @return comment + */ + public function get_comment_object() { + global $CFG; + require_once($CFG->dirroot . '/comment/lib.php'); + + if (!$this->get_id()) { + throw new coding_exception('The user competency record must exist.'); + } + + $comment = new comment((object) array( + 'context' => $this->get_context(), + 'component' => 'competency', // This cannot be named 'core_competency'. + 'itemid' => $this->get_id(), + 'area' => 'user_competency', + 'showcount' => true, + )); + $comment->set_fullwidth(true); + return $comment; + } + + /** + * Return the competency Object. + * + * @return competency Competency Object + */ + public function get_competency() { + return new competency($this->get_competencyid()); + } + + /** + * Get the context. + * + * @return context The context. + */ + public function get_context() { + return context_user::instance($this->get_userid()); + } + + /** + * Find the plans for the user and this competency. + * + * Note that this: + * - does not perform any capability check. + * - may return completed plans. + * - may return an empty array. + * + * @return plans[] + */ + public function get_plans() { + return plan::get_by_user_and_competency($this->get_userid(), $this->get_competencyid()); + } + + /** + * 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 competency ID. + * + * @param int $value The value. + * @return true|lang_string + */ + protected function validate_competencyid($value) { + if (!competency::record_exists($value)) { + return new lang_string('errornocompetency', 'core_competency', $value); + } + + return true; + } + + /** + * Validate the proficiency. + * + * @param int $value The value. + * @return true|lang_string + */ + protected function validate_proficiency($value) { + $grade = $this->get('grade'); + + if ($grade !== null && $value === null) { + // We must set a proficiency when we set a grade. + return new lang_string('invaliddata', 'error'); + + } else if ($grade === null && $value !== null) { + // We must not set a proficiency when we don't set a grade. + return new lang_string('invaliddata', 'error'); + } + + return true; + } + + /** + * Validate the reviewer ID. + * + * @param int $value The value. + * @return true|lang_string + */ + protected function validate_reviewerid($value) { + global $DB; + + if ($value !== null && !$DB->record_exists('user', array('id' => $value))) { + return new lang_string('invaliduserid', 'error'); + } + + return true; + } + + /** + * Validate the grade. + * + * @param int $value The value. + * @return true|lang_string + */ + protected function validate_grade($value) { + if ($value !== null) { + if ($value <= 0) { + return new lang_string('invalidgrade', 'core_competency'); + } + + // TODO MDL-52243 Use a core method to validate the grade_scale item. + // Check if grade exist in the scale item values. + $competency = $this->get_competency(); + if (!array_key_exists($value - 1 , $competency->get_scale()->scale_items)) { + return new lang_string('invalidgrade', 'core_competency'); + } + } + + return true; + } + + /** + * Can the current user comment on a user's competency? + * + * @param int $userid The user ID the competency belongs to. + * @return bool + */ + public static function can_comment_user($userid) { + global $USER; + + $capabilities = array('moodle/competency:usercompetencycomment'); + if ($USER->id == $userid) { + $capabilities[] = 'moodle/competency:usercompetencycommentown'; + } + + if (has_any_capability($capabilities, context_user::instance($userid))) { + return true; + } + + return false; + } + + /** + * Can the current user grade a user's user competency? + * + * @param int $userid The user ID the competency belongs to. + * @return bool + */ + public static function can_grade_user($userid) { + $ratecap = 'moodle/competency:competencygrade'; + return has_capability($ratecap, context_user::instance($userid)); + } + + /** + * Can the current user grade a user's user competency in a course? + * + * @param int $userid The user ID the competency belongs to. + * @param int $courseid The course ID. + * @return bool + */ + public static function can_grade_user_in_course($userid, $courseid) { + $ratecap = 'moodle/competency:competencygrade'; + return has_capability($ratecap, context_course::instance($courseid)) + || static::can_grade_user($userid); + } + + /** + * Can the current user read the comments on a user's competency? + * + * @param int $userid The user ID the competency belongs to. + * @return bool + */ + public static function can_read_comments_user($userid) { + // Everyone who can read the user competency can read the comments. + return static::can_read_user($userid); + } + + /** + * Can the current user read the user competencies of a user in a course? + * + * @param int $userid The user ID the competency belongs to. + * @param int $courseid The course ID. + * @return bool + */ + public static function can_read_user_in_course($userid, $courseid) { + $capability = 'moodle/competency:usercompetencyview'; + return has_capability($capability, context_course::instance($courseid)) + || static::can_read_user($userid); + } + + /** + * Can the current user read a user's competency? + * + * @param int $userid The user ID the competency belongs to. + * @return bool + */ + public static function can_read_user($userid) { + $capability = 'moodle/competency:usercompetencyview'; + return has_capability($capability, context_user::instance($userid)) + || plan::can_read_user($userid); + } + + /** + * Can the current user send a user's competency for review? + * + * Note that the status 'review' is not meant to be used for student to self-assess + * themselves, then to ask the teacher to review their assessment. It is more intended + * for a student to provide evidence of prior learning and request their review. + * + * @param int $userid The user ID the competency belongs to. + * @return bool + */ + public static function can_request_review_user($userid) { + global $USER; + + $capabilities = array('moodle/competency:usercompetencyrequestreview'); + if ($USER->id == $userid) { + $capabilities[] = 'moodle/competency:usercompetencyrequestreviewown'; + } + + if (has_any_capability($capabilities, context_user::instance($userid))) { + return true; + } + + return false; + } + + /** + * Can the current user review the user competency? + * + * @param int $userid The user ID the competency belongs to. + * @return bool + */ + public static function can_review_user($userid) { + $capability = 'moodle/competency:usercompetencyreview'; + return has_capability($capability, context_user::instance($userid)); + } + + /** + * Create a new user_competency object. + * + * Note, this is intended to be used to create a blank relation, for instance when + * the record was not found in the database. This does not save the model. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @return \core_competency\user_competency + */ + public static function create_relation($userid, $competencyid) { + $relation = new user_competency(0, (object) array('userid' => $userid, 'competencyid' => $competencyid)); + return $relation; + } + + /** + * Fetch a competency by user competency ID. + * + * This is a convenience method to attempt to efficiently fetch a competency when + * the only information we have is the user_competency ID, in evidence for instance. + * + * @param int $id The user competency ID. + * @return competency + */ + public static function get_competency_by_usercompetencyid($id) { + global $DB; + $sql = "SELECT c.* + FROM {" . self::TABLE . "} uc + JOIN {" . competency::TABLE . "} c + ON c.id = uc.competencyid + WHERE uc.id = ?"; + $record = $DB->get_record_sql($sql, array($id), MUST_EXIST); + return new competency(0, $record); + } + + /** + * Get multiple user_competency for a user. + * + * @param int $userid + * @param array $competenciesorids Limit search to those competencies, or competency IDs. + * @return \core_competency\user_competency[] + */ + public static function get_multiple($userid, array $competenciesorids = null) { + global $DB; + + $params = array(); + $params['userid'] = $userid; + $sql = '1 = 1'; + + if (!empty($competenciesorids)) { + $test = reset($competenciesorids); + if (is_number($test)) { + $ids = $competenciesorids; + } else { + $ids = array(); + foreach ($competenciesorids as $comp) { + $ids[] = $comp->get_id(); + } + } + + list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); + $params += $inparams; + $sql = "competencyid $insql"; + } + + return self::get_records_select("userid = :userid AND $sql", $params); + } + + /** + * Checks if a competency has user competency records. + * + * @param int $competencyid The competency ID + * @return boolean + */ + public static function has_records_for_competency($competencyid) { + return self::record_exists_select('competencyid = ?', array($competencyid)); + } + + /** + * Checks if any of the competencies of a framework has a user competency record. + * + * @param int $frameworkid The competency framework ID. + * @return boolean + */ + public static function has_records_for_framework($frameworkid) { + global $DB; + + $sql = "SELECT 'x' + FROM {" . self::TABLE . "} uc + JOIN {" . competency::TABLE . "} c + ON uc.competencyid = c.id + WHERE c.competencyframeworkid = ?"; + $params = array($frameworkid); + + return $DB->record_exists_sql($sql, $params); + } + + /** + * Check if user competency has records for competencies. + * + * @param array $competencyids The competencies ids. + * @return boolean Return true if the delete was successfull. + */ + public static function has_records_for_competencies($competencyids) { + global $DB; + list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED); + return self::record_exists_select("competencyid $insql", $params); + } + +} diff --git a/competency/classes/user_competency_course.php b/competency/classes/user_competency_course.php new file mode 100644 index 00000000000..93e2c3c5f0e --- /dev/null +++ b/competency/classes/user_competency_course.php @@ -0,0 +1,288 @@ +. + +/** + * Class for user_competency_course persistence. + * + * @package core_competency + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use context_course; +use context_user; +use lang_string; + +/** + * Class for loading/storing user_competency_course from the DB. + * + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency_course extends persistent { + + /** Table name for user_competency persistency */ + const TABLE = 'competency_usercompcourse'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'userid' => array( + 'type' => PARAM_INT, + ), + 'courseid' => array( + 'type' => PARAM_INT + ), + 'competencyid' => array( + 'type' => PARAM_INT, + ), + 'proficiency' => array( + 'type' => PARAM_BOOL, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'grade' => array( + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + ); + } + + /** + * Return the competency Object. + * + * @return competency Competency Object + */ + public function get_competency() { + return new competency($this->get_competencyid()); + } + + /** + * Get the context. + * + * @return context The context. + */ + public function get_context() { + return context_user::instance($this->get_userid()); + } + + /** + * Create a new user_competency_course object. + * + * Note, this is intended to be used to create a blank relation, for instance when + * the record was not found in the database. This does not save the model. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @param int $courseid The course ID. + * @return \core_competency\user_competency_course + */ + public static function create_relation($userid, $competencyid, $courseid) { + $data = new \stdClass(); + $data->userid = $userid; + $data->competencyid = $competencyid; + $data->courseid = $courseid; + + $relation = new user_competency_course(0, $data); + return $relation; + } + + /** + * 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 competency ID. + * + * @param int $value The value. + * @return true|lang_string + */ + protected function validate_competencyid($value) { + if (!competency::record_exists($value)) { + return new lang_string('errornocompetency', 'core_competency', $value); + } + + return true; + } + + /** + * Validate course ID. + * + * @param int $value The course ID. + * @return true|lang_string + */ + protected function validate_courseid($value) { + if (!context_course::instance($value, IGNORE_MISSING)) { + return new lang_string('errorinvalidcourse', 'core_competency', $value); + } + + return true; + } + + /** + * Validate the proficiency. + * + * @param int $value The value. + * @return true|lang_string + */ + protected function validate_proficiency($value) { + $grade = $this->get('grade'); + + if ($grade !== null && $value === null) { + // We must set a proficiency when we set a grade. + return new lang_string('invaliddata', 'error'); + + } else if ($grade === null && $value !== null) { + // We must not set a proficiency when we don't set a grade. + return new lang_string('invaliddata', 'error'); + } + + return true; + } + + /** + * Validate the grade. + * + * @param int $value The value. + * @return true|lang_string + */ + protected function validate_grade($value) { + if ($value !== null) { + if ($value <= 0) { + return new lang_string('invalidgrade', 'core_competency'); + } + + // TODO MDL-52243 Use a core method to validate the grade_scale item. + // Check if grade exist in the scale item values. + $competency = $this->get_competency(); + if (!array_key_exists($value - 1 , $competency->get_scale()->scale_items)) { + return new lang_string('invalidgrade', 'core_competency'); + } + } + + return true; + } + + /** + * Get multiple user_competency_course for a user. + * + * @param int $userid + * @param int $courseid + * @param array $competenciesorids Limit search to those competencies, or competency IDs. + * @return \core_competency\user_competency_course[] + */ + public static function get_multiple($userid, $courseid, array $competenciesorids = null) { + global $DB; + + $params = array(); + $params['userid'] = $userid; + $params['courseid'] = $courseid; + $sql = '1 = 1'; + + if (!empty($competenciesorids)) { + $test = reset($competenciesorids); + if (is_number($test)) { + $ids = $competenciesorids; + } else { + $ids = array(); + foreach ($competenciesorids as $comp) { + $ids[] = $comp->get_id(); + } + } + + list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); + $params += $inparams; + $sql = "competencyid $insql"; + } + + return self::get_records_select("userid = :userid AND courseid = :courseid AND $sql", $params); + } + + /** + * Count the proficient competencies in this course for one user. + * + * @param int $courseid The course id + * @param int $userid The user id + * @return int + */ + public static function count_proficient_competencies($courseid, $userid) { + global $DB; + + $sql = 'SELECT COUNT(comp.id) + FROM {' . self::TABLE . '} usercoursecomp + JOIN {' . competency::TABLE . '} comp + ON usercoursecomp.competencyid = comp.id + WHERE usercoursecomp.courseid = ? AND usercoursecomp.userid = ? AND usercoursecomp.proficiency = ?'; + $params = array($courseid, $userid, true); + + $results = $DB->count_records_sql($sql, $params); + + return $results; + } + + /** + * Get the list of competencies that were completed the least times in a course. + * + * @param int $courseid + * @param int $skip The number of competencies to skip + * @param int $limit The max number of competencies to return + * @return competency[] + */ + public static function get_least_proficient_competencies_for_course($courseid, $skip = 0, $limit = 0) { + global $DB; + + $fields = competency::get_sql_fields('c'); + $params = array('courseid' => $courseid); + $sql = 'SELECT ' . $fields . ', SUM(COALESCE(ucc.proficiency, 0)) AS timesproficient ' . + ' FROM {' . competency::TABLE . '} c + JOIN {' . course_competency::TABLE . '} cc + ON c.id = cc.competencyid + LEFT JOIN {' . self::TABLE . '} ucc + ON ucc.competencyid = c.id AND ucc.courseid = cc.courseid + WHERE cc.courseid = :courseid + GROUP BY c.id + ORDER BY timesproficient ASC, c.id DESC'; + + $results = $DB->get_records_sql($sql, $params, $skip, $limit); + $a = $DB->get_records_sql('SELECT * from {' . self::TABLE . '}'); + + $comps = array(); + foreach ($results as $r) { + $c = competency::extract_record($r); + $comps[] = new competency(0, $c); + } + return $comps; + } +} diff --git a/competency/classes/user_competency_plan.php b/competency/classes/user_competency_plan.php new file mode 100644 index 00000000000..1bbe9371102 --- /dev/null +++ b/competency/classes/user_competency_plan.php @@ -0,0 +1,365 @@ +. + +/** + * Class for user_competency_plan persistence. + * + * @package core_competency + * @copyright 2015 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use lang_string; +use context_user; + +/** + * Class for loading/storing user_competency_plan from the DB. + * + * @copyright 2015 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_competency_plan extends persistent { + + /** Table name for user_competency_plan persistency */ + const TABLE = 'competency_usercompplan'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'userid' => array( + 'type' => PARAM_INT, + ), + 'competencyid' => array( + 'type' => PARAM_INT, + ), + 'proficiency' => array( + 'type' => PARAM_BOOL, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'grade' => array( + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'planid' => array( + 'type' => PARAM_INT, + ), + 'sortorder' => array( + 'type' => PARAM_INT, + 'default' => null, + ), + ); + } + + /** + * Return the competency Object. + * + * @return competency Competency Object + */ + public function get_competency() { + return new competency($this->get_competencyid()); + } + + /** + * Get the context. + * + * @return context The context. + */ + public function get_context() { + return context_user::instance($this->get_userid()); + } + + /** + * Validate the user ID. + * + * @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 competency ID. + * + * @param int $value The value. + * @return true|lang_string + */ + protected function validate_competencyid($value) { + if (!competency::record_exists($value)) { + return new lang_string('errornocompetency', 'core_competency', $value); + } + + return true; + } + + /** + * Validate the grade. + * + * @param int $value The value. + * @return true|lang_string + */ + protected function validate_grade($value) { + if ($value !== null) { + if ($value <= 0) { + return new lang_string('invalidgrade', 'core_competency'); + } + + // TODO MDL-52243 Use a core method to validate the grade_scale item. + // Check if grade exist in the scale item values. + $competency = $this->get_competency(); + if (!array_key_exists($value - 1, $competency->get_scale()->scale_items)) { + return new lang_string('invalidgrade', 'core_competency'); + } + } + + return true; + } + + /** + * Validate the plan ID. + * + * @param int $value The value. + * @return true|lang_string + */ + protected function validate_planid($value) { + if (!plan::record_exists($value) ) { + return new lang_string('invalidplan', 'core_competency'); + } + + return true; + } + + /** + * Create a new user_competency_plan object. + * + * Note, this is intended to be used to create a blank relation, for instance when + * the record was not found in the database. This does not save the model. + * + * @param int $userid The user ID. + * @param int $competencyid The competency ID. + * @param int $planid The plan ID. + * @return \core_competency\user_competency_plan + */ + public static function create_relation($userid, $competencyid, $planid) { + $relation = new user_competency_plan(0, (object) array('userid' => $userid, 'competencyid' => $competencyid, + 'planid' => $planid)); + return $relation; + } + + /** + * List the competencies in this plan. + * + * @param int $planid The plan ID + * @param int $userid The user ID + * @return competency[] + */ + public static function list_competencies($planid, $userid) { + global $DB; + + $sql = 'SELECT c.* + FROM {' . competency::TABLE . '} c + JOIN {' . self::TABLE . '} ucp + ON ucp.competencyid = c.id + AND ucp.userid = :userid + WHERE ucp.planid = :planid + ORDER BY ucp.sortorder ASC'; + $params = array('userid' => $userid, 'planid' => $planid); + + $results = $DB->get_recordset_sql($sql, $params); + $instances = array(); + foreach ($results as $key => $result) { + $instances[$key] = new competency(0, $result); + } + $results->close(); + + return $instances; + } + + /** + * Fetch a competency by plan ID. + * + * @param int $id The plan ID. + * @return competency + */ + public static function get_competency_by_planid($planid, $competencyid) { + global $DB; + + $sql = "SELECT c.* + FROM {" . self::TABLE . "} ucp + JOIN {" . competency::TABLE . "} c + ON c.id = ucp.competencyid + WHERE ucp.planid = ? + AND ucp.competencyid = ?"; + $record = $DB->get_record_sql($sql, array($planid, $competencyid)); + + if (!$record) { + throw new coding_exception('The competency does not belong to this plan: ' . $competencyid . ', ' . $planid); + } + + return new competency(0, $record); + } + + /** + * Get multiple user_competency_plan for a user. + * + * @param int $userid The user ID. + * @param int $planid The plan ID. + * @param array $competenciesorids Limit search to those competencies, or competency IDs. + * @return \core_competency\user_competency_plan[] + */ + public static function get_multiple($userid, $planid, array $competenciesorids = null) { + global $DB; + + $params = array(); + $params['userid'] = $userid; + $params['planid'] = $planid; + $sql = '1 = 1'; + + if (!empty($competenciesorids)) { + $test = reset($competenciesorids); + if (is_number($test)) { + $ids = $competenciesorids; + } else { + $ids = array(); + foreach ($competenciesorids as $comp) { + $ids[] = $comp->get_id(); + } + } + + list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED); + $params += $inparams; + $sql = "competencyid $insql"; + } + + return static::get_records_select("userid = :userid AND planid = :planid AND $sql", $params); + } + + /** + * Checks if a competency has user competency plan records. + * + * @param int $competencyid The competency ID + * @return boolean + */ + public static function has_records_for_competency($competencyid) { + return self::record_exists_select('competencyid = ?', array($competencyid)); + } + + /** + * Checks if any of the competencies of a framework has a user competency plan record. + * + * @param int $frameworkid The competency framework ID. + * @return boolean + */ + public static function has_records_for_framework($frameworkid) { + global $DB; + + $sql = "SELECT 'x' + FROM {" . self::TABLE . "} ucp + JOIN {" . competency::TABLE . "} c + ON ucp.competencyid = c.id + WHERE c.competencyframeworkid = ?"; + $params = array($frameworkid); + + return $DB->record_exists_sql($sql, $params); + } + + /** + * Check if user competency plan has records for competencies. + * + * @param array $competencyids The competences IDs + * @return boolean + */ + public static function has_records_for_competencies($competencyids) { + global $DB; + list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED); + return self::record_exists_select("competencyid $insql", $params); + } + + /** + * Count the number of records matching a specific template, optionally filtered by proficient values. + * + * @param int $templateid + * @param mixed $proficiency - If true - filter by proficiency, if false filter by not proficient, if null - do not filter. + * @return int + */ + public static function count_records_for_template($templateid, $proficiency=null) { + global $DB; + + $params = array('templateid' => $templateid); + $sql = 'SELECT ' . " COUNT('x') " . + 'FROM {' . self::TABLE . '} ucp + JOIN {' . plan::TABLE . '} p + ON ucp.planid = p.id + WHERE p.templateid = :templateid'; + if ($proficiency === true) { + $sql .= ' AND ucp.proficiency = :proficiency'; + $params['proficiency'] = true; + } else if ($proficiency === false) { + $sql .= ' AND (ucp.proficiency = :proficiency OR ucp.proficiency IS NULL)'; + $params['proficiency'] = false; + } + + return $DB->count_records_sql($sql, $params); + } + + /** + * Get the list of competencies that were completed the least times (in completed plans) from a template. + * + * @param int $templateid + * @param int $skip The number of competencies to skip + * @param int $limit The max number of competencies to return + * @return competency[] + */ + public static function get_least_proficient_competencies_for_template($templateid, $skip = 0, $limit = 0) { + global $DB; + + $fields = competency::get_sql_fields('c'); + $params = array('templateid' => $templateid, 'notproficient' => false); + $sql = 'SELECT ' . $fields . ', COUNT(c.id) AS timesnotproficient ' . + ' FROM {' . self::TABLE . '} ucp + JOIN {' . plan::TABLE . '} p + ON ucp.planid = p.id + JOIN {' . competency::TABLE . '} c + ON ucp.competencyid = c.id + WHERE p.templateid = :templateid + AND (ucp.proficiency = :notproficient OR ucp.proficiency IS NULL) + GROUP BY c.id + ORDER BY timesnotproficient DESC'; + + $results = $DB->get_records_sql($sql, $params, $skip, $limit); + + $comps = array(); + foreach ($results as $r) { + $c = competency::extract_record($r); + $comps[] = new competency(0, $c); + } + return $comps; + } +} diff --git a/competency/classes/user_evidence.php b/competency/classes/user_evidence.php new file mode 100644 index 00000000000..3470643cbf4 --- /dev/null +++ b/competency/classes/user_evidence.php @@ -0,0 +1,196 @@ +. + +/** + * User evidence persistent. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use context_user; +use lang_string; + +/** + * User evidence persistent class. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_evidence extends persistent { + + const TABLE = 'competency_userevidence'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'userid' => array( + 'type' => PARAM_INT + ), + 'name' => array( + 'type' => PARAM_TEXT + ), + 'description' => array( + 'type' => PARAM_RAW, + 'default' => '', + ), + 'descriptionformat' => array( + 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), + 'type' => PARAM_INT, + 'default' => FORMAT_HTML, + ), + 'url' => array( + 'type' => PARAM_URL, + 'default' => '', + 'message' => new lang_string('invalidurl', 'core_competency') + ) + ); + } + + /** + * Can the current user manage this user evidence? + * + * @return bool + */ + public function can_manage() { + return self::can_manage_user($this->get_userid()); + } + + /** + * Can the current user view this user evidence? + * + * @return bool + */ + public function can_read() { + return self::can_read_user($this->get_userid()); + } + + /** + * Get the context of this user evidence. + * + * @return context + */ + public function get_context() { + return context_user::instance($this->get_userid()); + } + + /** + * Get link competencies. + */ + public function get_competencies() { + return user_evidence_competency::get_competencies_by_userevidenceid($this->get_id()); + } + + /** + * Get link user competencies. + */ + public function get_user_competencies() { + return user_evidence_competency::get_user_competencies_by_userevidenceid($this->get_id()); + } + + /** + * Return the files associated with this evidence. + * + * @return object[] + */ + public function get_files() { + $fs = get_file_storage(); + $files = $fs->get_area_files($this->get_context()->id, 'core_competency', 'userevidence', $this->get_id(), + 'filename', false); + return $files; + } + + /** + * Validate the URL. + * + * @param int $value + * @return true|lang_string + */ + protected function validate_url($value) { + if (empty($value) && !is_numeric($value)) { + return true; + } + if (!preg_match('@^https?://.+@', $value)) { + return new lang_string('invalidurl', 'core_competency'); + } + return true; + } + + /** + * Validate the user ID. + * + * @param int $value + * @return true|lang_string + */ + protected function validate_userid($value) { + global $DB; + + // During create. + if (!$this->get_id()) { + + // Check that the user exists. We do not need to do that on update because + // the userid of an evidence should never change. + if (!$DB->record_exists('user', array('id' => $value))) { + return new lang_string('invaliddata', 'error'); + } + + } + + return true; + } + + /** + * Can the current user manage a user's evidence? + * + * @param int $evidenceuserid The user to whom the evidence would belong. + * @return bool + */ + public static function can_manage_user($evidenceuserid) { + global $USER; + $context = context_user::instance($evidenceuserid); + + $capabilities = array('moodle/competency:userevidencemanage'); + if ($context->instanceid == $USER->id) { + $capabilities[] = 'moodle/competency:userevidencemanageown'; + } + + return has_any_capability($capabilities, $context); + } + + /** + * Can the current user view a user's evidence? + * + * @param int $evidenceuserid The user to whom the evidence would belong. + * @return bool + */ + public static function can_read_user($evidenceuserid) { + $context = context_user::instance($evidenceuserid); + + $capabilities = array('moodle/competency:userevidenceview'); + + return has_any_capability($capabilities, $context) || self::can_manage_user($evidenceuserid); + } + +} diff --git a/competency/classes/user_evidence_competency.php b/competency/classes/user_evidence_competency.php new file mode 100644 index 00000000000..b424d06c135 --- /dev/null +++ b/competency/classes/user_evidence_competency.php @@ -0,0 +1,177 @@ +. + +/** + * User evidence competency persistent. + * + * This represent the many to many relationship between evidence of prior + * learning and competencies. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_competency; +defined('MOODLE_INTERNAL') || die(); + +use stdClass; +use lang_string; + +/** + * User evidence competency persistent class. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_evidence_competency extends persistent { + + const TABLE = 'competency_userevidencecomp'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'userevidenceid' => array( + 'type' => PARAM_INT + ), + 'competencyid' => array( + 'type' => PARAM_INT, + ), + ); + } + + /** + * Validate competency ID. + * + * @param int $value ID. + * @return true|lang_string + */ + protected function validate_competencyid($value) { + if (!competency::record_exists($value)) { + return new lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Validate user evidence ID. + * + * @param int $value ID. + * @return true|lang_string + */ + protected function validate_userevidenceid($value) { + if (!user_evidence::record_exists($value)) { + return new lang_string('invaliddata', 'error'); + } + return true; + } + + /** + * Get competencies by user evidence ID. + * + * @param int $userevidenceid The user evidence ID. + * @return competency[] + */ + public static function get_competencies_by_userevidenceid($userevidenceid) { + global $DB; + $sql = "SELECT c.* + FROM {" . self::TABLE . "} uec + JOIN {" . competency::TABLE . "} c + ON uec.userevidenceid = ? + AND uec.competencyid = c.id + ORDER BY c.shortname"; + $competencies = array(); + $records = $DB->get_recordset_sql($sql, array($userevidenceid)); + foreach ($records as $record) { + $competencies[] = new competency(0, $record); + } + $records->close(); + return $competencies; + } + + /** + * Get user competencies by user evidence ID. + * + * @param int $userevidenceid The user evidence ID. + * @return user_competency[] + */ + public static function get_user_competencies_by_userevidenceid($userevidenceid) { + global $DB; + + $sql = "SELECT uc.* + FROM {" . user_competency::TABLE . "} uc + JOIN {" . self::TABLE . "} uec + ON uc.competencyid = uec.competencyid + JOIN {" . user_evidence::TABLE . "} ue + ON uec.userevidenceid = ue.id + AND uc.userid = ue.userid + AND ue.id = ?"; + + $usercompetencies = array(); + $records = $DB->get_recordset_sql($sql, array($userevidenceid)); + foreach ($records as $record) { + $usercompetencies[] = new user_competency(0, $record); + } + $records->close(); + return $usercompetencies; + } + + /** + * Get a relation. + * + * This does not perform any validation on the data passed. If the relation exists in the database + * then it is loaded in a the model, if not then it is up to the developer to save the model. + * + * @param int $userevidenceid + * @param int $competencyid + * @return template_cohort + */ + public static function get_relation($userevidenceid, $competencyid) { + global $DB; + + $params = array( + 'userevidenceid' => $userevidenceid, + 'competencyid' => $competencyid + ); + + $relation = new static(null, (object) $params); + if ($record = $DB->get_record(static::TABLE, $params)) { + $relation->from_record($record); + } + + return $relation; + } + + /** + * Delete evidences using competencies. + * + * @param array $competencyids Array of competencies ids. + * @return bool Return true if the delete was successful. + */ + public static function delete_by_competencyids($competencyids) { + global $DB; + if (empty($competencyids)) { + return true; + } + list($insql, $params) = $DB->get_in_or_equal($competencyids); + return $DB->delete_records_select(self::TABLE, "competencyid $insql", $params); + } + +} diff --git a/competency/lib.php b/competency/lib.php new file mode 100644 index 00000000000..c3b4b05dc04 --- /dev/null +++ b/competency/lib.php @@ -0,0 +1,299 @@ +. + +/** + * Competency lib. + * + * @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(); + +use core_competency\api; +use core_competency\plan; +use core_competency\url; +use core_competency\user_competency; +use core_competency\user_evidence; + +/** + * Hook when a comment is added. + * + * @param stdClass $comment The comment. + * @param stdClass $params The parameters. + * @return array + */ +function core_competency_comment_add($comment, $params) { + global $USER; + + if (!api::is_enabled()) { + return; + } + + if ($params->commentarea == 'user_competency') { + $uc = new user_competency($params->itemid); + + // Message both the user and the reviewer, except when they are the author of the message. + $recipients = array($uc->get_userid()); + if ($uc->get_reviewerid()) { + $recipients[] = $uc->get_reviewerid(); + } + $recipients = array_diff($recipients, array($comment->userid)); + if (empty($recipients)) { + return; + } + + // Get the sender. + $user = $USER; + if ($USER->id != $comment->userid) { + $user = core_user::get_user($comment->userid); + } + $fullname = fullname($user); + + // Get the competency. + $competency = $uc->get_competency(); + $competencyname = format_string($competency->get_shortname(), true, array('context' => $competency->get_context())); + + // We want to send a message for one plan, trying to find an active one first, or the last modified one. + $plan = null; + $plans = $uc->get_plans(); + foreach ($plans as $candidate) { + if ($candidate->get_status() == plan::STATUS_ACTIVE) { + $plan = $candidate; + break; + + } else if (!empty($plan) && $plan->get_timemodified() < $candidate->get_timemodified()) { + $plan = $candidate; + + } else if (empty($plan)) { + $plan = $candidate; + } + } + + // Urls. + // TODO MDL-52749 Replace the link to the plan with the user competency page. + if (empty($plan)) { + $urlname = get_string('userplans', 'core_competency'); + $url = url::plans($uc->get_userid()); + } else { + $urlname = $competencyname; + $url = url::user_competency_in_plan($uc->get_userid(), $uc->get_competencyid(), $plan->get_id()); + } + + // Construct the message content. + $fullmessagehtml = get_string('usercommentedonacompetencyhtml', 'core_competency', array( + 'fullname' => $fullname, + 'competency' => $competencyname, + 'comment' => format_text($comment->content, $comment->format, array('context' => $params->context->id)), + 'url' => $url->out(true), + 'urlname' => $urlname, + )); + if ($comment->format == FORMAT_PLAIN || $comment->format == FORMAT_MOODLE) { + $format = FORMAT_MOODLE; + $fullmessage = get_string('usercommentedonacompetency', 'core_competency', array( + 'fullname' => $fullname, + 'competency' => $competencyname, + 'comment' => $comment->content, + 'url' => $url->out(false), + )); + } else { + $format = FORMAT_HTML; + $fullmessage = $fullmessagehtml; + } + + $message = new \core\message\message(); + $message->component = 'moodle'; + $message->name = 'competencyusercompcomment'; + $message->notification = 1; + $message->userfrom = core_user::get_noreply_user(); + $message->subject = get_string('usercommentedonacompetencysubject', 'core_competency', $fullname); + $message->fullmessage = $fullmessage; + $message->fullmessageformat = $format; + $message->fullmessagehtml = $fullmessagehtml; + $message->smallmessage = get_string('usercommentedonacompetencysmall', 'core_competency', array( + 'fullname' => $fullname, + 'competency' => $competencyname, + )); + $message->contexturl = $url->out(false); + $message->contexturlname = $urlname; + + // Message each recipient. + foreach ($recipients as $recipient) { + $msgcopy = clone($message); + $msgcopy->userto = $recipient; + message_send($msgcopy); + } + + } else if ($params->commentarea == 'plan') { + $plan = new plan($params->itemid); + + // Message both the user and the reviewer, except when they are the author of the message. + $recipients = array($plan->get_userid()); + if ($plan->get_reviewerid()) { + $recipients[] = $plan->get_reviewerid(); + } + $recipients = array_diff($recipients, array($comment->userid)); + if (empty($recipients)) { + return; + } + + // Get the sender. + $user = $USER; + if ($USER->id != $comment->userid) { + $user = core_user::get_user($comment->userid); + } + + $fullname = fullname($user); + $planname = format_string($plan->get_name(), true, array('context' => $plan->get_context())); + $urlname = $planname; + $url = url::plan($plan->get_id()); + + // Construct the message content. + $fullmessagehtml = get_string('usercommentedonaplanhtml', 'core_competency', array( + 'fullname' => $fullname, + 'plan' => $planname, + 'comment' => format_text($comment->content, $comment->format, array('context' => $params->context->id)), + 'url' => $url->out(true), + 'urlname' => $urlname, + )); + if ($comment->format == FORMAT_PLAIN || $comment->format == FORMAT_MOODLE) { + $format = FORMAT_MOODLE; + $fullmessage = get_string('usercommentedonaplan', 'core_competency', array( + 'fullname' => $fullname, + 'plan' => $planname, + 'comment' => $comment->content, + 'url' => $url->out(false), + )); + } else { + $format = FORMAT_HTML; + $fullmessage = $fullmessagehtml; + } + + $message = new \core\message\message(); + $message->component = 'moodle'; + $message->name = 'competencyplancomment'; + $message->notification = 1; + $message->userfrom = core_user::get_noreply_user(); + $message->subject = get_string('usercommentedonaplansubject', 'core_competency', $fullname); + $message->fullmessage = $fullmessage; + $message->fullmessageformat = $format; + $message->fullmessagehtml = $fullmessagehtml; + $message->smallmessage = get_string('usercommentedonaplansmall', 'core_competency', array( + 'fullname' => $fullname, + 'plan' => $planname, + )); + $message->contexturl = $url->out(false); + $message->contexturlname = $urlname; + + // Message each recipient. + foreach ($recipients as $recipient) { + $msgcopy = clone($message); + $msgcopy->userto = $recipient; + message_send($msgcopy); + } + } +} + +/** + * Return the permissions of for the comments. + * + * @param stdClass $params The parameters. + * @return array + */ +function core_competency_comment_permissions($params) { + if (!api::is_enabled()) { + return array('post' => false, 'view' => false); + } + + if ($params->commentarea == 'user_competency') { + $uc = new user_competency($params->itemid); + if ($uc->can_read()) { + return array('post' => $uc->can_comment(), 'view' => $uc->can_read_comments()); + } + } else if ($params->commentarea == 'plan') { + $plan = new plan($params->itemid); + if ($plan->can_read()) { + return array('post' => $plan->can_comment(), 'view' => $plan->can_read_comments()); + } + } + + return array('post' => false, 'view' => false); +} + +/** + * Validates comments. + * + * @param stdClass $params The parameters. + * @return bool + */ +function core_competency_comment_validate($params) { + if (!api::is_enabled()) { + return false; + } + + if ($params->commentarea == 'user_competency') { + if (!user_competency::record_exists($params->itemid)) { + return false; + } + return true; + } else if ($params->commentarea == 'plan') { + if (!plan::record_exists($params->itemid)) { + return false; + } + return true; + } + return false; +} + +/** + * File serving. + * + * @param stdClass $course The course object. + * @param stdClass $cm The cm object. + * @param context $context The context object. + * @param string $filearea The file area. + * @param array $args List of arguments. + * @param bool $forcedownload Whether or not to force the download of the file. + * @param array $options Array of options. + * @return void|false + */ +function core_competency_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { + global $CFG; + + if (!api::is_enabled()) { + return false; + } + + $fs = get_file_storage(); + $file = null; + + $itemid = array_shift($args); + $filename = array_shift($args); + $filepath = $args ? '/' .implode('/', $args) . '/' : '/'; + + if ($filearea == 'userevidence' && $context->contextlevel == CONTEXT_USER) { + if (user_evidence::can_read_user($context->instanceid)) { + $file = $fs->get_file($context->id, 'core_competency', $filearea, $itemid, $filepath, $filename); + } + } + + if (!$file) { + return false; + } + + send_stored_file($file, null, 0, $forcedownload); +} diff --git a/competency/tests/api_test.php b/competency/tests/api_test.php new file mode 100644 index 00000000000..9004d1fd633 --- /dev/null +++ b/competency/tests/api_test.php @@ -0,0 +1,4417 @@ +. + +/** + * API tests. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +use core_competency\api; +use core_competency\competency; +use core_competency\competency_framework; +use core_competency\course_competency_settings; +use core_competency\evidence; +use core_competency\user_competency; +use core_competency\plan; + +/** + * API tests. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_api_testcase extends advanced_testcase { + + public function test_get_framework_related_contexts() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $cat1 = $dg->create_category(); + $cat2 = $dg->create_category(array('parent' => $cat1->id)); + $cat3 = $dg->create_category(array('parent' => $cat2->id)); + $c1 = $dg->create_course(array('category' => $cat2->id)); // This context should not be returned. + + $cat1ctx = context_coursecat::instance($cat1->id); + $cat2ctx = context_coursecat::instance($cat2->id); + $cat3ctx = context_coursecat::instance($cat3->id); + $sysctx = context_system::instance(); + + $expected = array($cat1ctx->id => $cat1ctx); + $this->assertEquals($expected, api::get_related_contexts($cat1ctx, 'self')); + + $expected = array($cat1ctx->id => $cat1ctx, $cat2ctx->id => $cat2ctx, $cat3ctx->id => $cat3ctx); + $this->assertEquals($expected, api::get_related_contexts($cat1ctx, 'children')); + + $expected = array($sysctx->id => $sysctx, $cat1ctx->id => $cat1ctx, $cat2ctx->id => $cat2ctx); + $this->assertEquals($expected, api::get_related_contexts($cat2ctx, 'parents')); + } + + public function test_get_framework_related_contexts_with_capabilities() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $user = $dg->create_user(); + $cat1 = $dg->create_category(); + $cat2 = $dg->create_category(array('parent' => $cat1->id)); + $cat3 = $dg->create_category(array('parent' => $cat2->id)); + $c1 = $dg->create_course(array('category' => $cat2->id)); // This context should not be returned. + + $cat1ctx = context_coursecat::instance($cat1->id); + $cat2ctx = context_coursecat::instance($cat2->id); + $cat3ctx = context_coursecat::instance($cat3->id); + $sysctx = context_system::instance(); + + $roleallow = create_role('Allow', 'allow', 'Allow read'); + assign_capability('moodle/competency:competencyview', CAP_ALLOW, $roleallow, $sysctx->id); + role_assign($roleallow, $user->id, $sysctx->id); + + $roleprevent = create_role('Prevent', 'prevent', 'Prevent read'); + assign_capability('moodle/competency:competencyview', CAP_PROHIBIT, $roleprevent, $sysctx->id); + role_assign($roleprevent, $user->id, $cat2ctx->id); + + accesslib_clear_all_caches_for_unit_testing(); + $this->setUser($user); + $this->assertFalse(has_capability('moodle/competency:competencyview', $cat2ctx)); + + $requiredcap = array('moodle/competency:competencyview'); + + $expected = array(); + $this->assertEquals($expected, api::get_related_contexts($cat2ctx, 'self', $requiredcap)); + + $expected = array($cat1ctx->id => $cat1ctx); + $this->assertEquals($expected, api::get_related_contexts($cat1ctx, 'children', $requiredcap)); + + $expected = array($sysctx->id => $sysctx, $cat1ctx->id => $cat1ctx); + $this->assertEquals($expected, api::get_related_contexts($cat2ctx, 'parents', $requiredcap)); + } + + public function test_get_template_related_contexts() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $cat1 = $dg->create_category(); + $cat2 = $dg->create_category(array('parent' => $cat1->id)); + $cat3 = $dg->create_category(array('parent' => $cat2->id)); + $c1 = $dg->create_course(array('category' => $cat2->id)); // This context should not be returned. + + $cat1ctx = context_coursecat::instance($cat1->id); + $cat2ctx = context_coursecat::instance($cat2->id); + $cat3ctx = context_coursecat::instance($cat3->id); + $sysctx = context_system::instance(); + + $expected = array($cat1ctx->id => $cat1ctx); + $this->assertEquals($expected, api::get_related_contexts($cat1ctx, 'self')); + + $expected = array($cat1ctx->id => $cat1ctx, $cat2ctx->id => $cat2ctx, $cat3ctx->id => $cat3ctx); + $this->assertEquals($expected, api::get_related_contexts($cat1ctx, 'children')); + + $expected = array($sysctx->id => $sysctx, $cat1ctx->id => $cat1ctx, $cat2ctx->id => $cat2ctx); + $this->assertEquals($expected, api::get_related_contexts($cat2ctx, 'parents')); + } + + public function test_get_template_related_contexts_with_capabilities() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $user = $dg->create_user(); + $cat1 = $dg->create_category(); + $cat2 = $dg->create_category(array('parent' => $cat1->id)); + $cat3 = $dg->create_category(array('parent' => $cat2->id)); + $c1 = $dg->create_course(array('category' => $cat2->id)); // This context should not be returned. + + $cat1ctx = context_coursecat::instance($cat1->id); + $cat2ctx = context_coursecat::instance($cat2->id); + $cat3ctx = context_coursecat::instance($cat3->id); + $sysctx = context_system::instance(); + + $roleallow = create_role('Allow', 'allow', 'Allow read'); + assign_capability('moodle/competency:templateview', CAP_ALLOW, $roleallow, $sysctx->id); + role_assign($roleallow, $user->id, $sysctx->id); + + $roleprevent = create_role('Prevent', 'prevent', 'Prevent read'); + assign_capability('moodle/competency:templateview', CAP_PROHIBIT, $roleprevent, $sysctx->id); + role_assign($roleprevent, $user->id, $cat2ctx->id); + + accesslib_clear_all_caches_for_unit_testing(); + $this->setUser($user); + $this->assertFalse(has_capability('moodle/competency:templateview', $cat2ctx)); + + $requiredcap = array('moodle/competency:templateview'); + + $expected = array(); + $this->assertEquals($expected, api::get_related_contexts($cat2ctx, 'self', $requiredcap)); + + $expected = array($cat1ctx->id => $cat1ctx); + $this->assertEquals($expected, api::get_related_contexts($cat1ctx, 'children', $requiredcap)); + + $expected = array($sysctx->id => $sysctx, $cat1ctx->id => $cat1ctx); + $this->assertEquals($expected, api::get_related_contexts($cat2ctx, 'parents', $requiredcap)); + } + + /** + * Test updating a template. + */ + public function test_update_template() { + $cat = $this->getDataGenerator()->create_category(); + $this->resetAfterTest(true); + $this->setAdminUser(); + + $syscontext = context_system::instance(); + $template = api::create_template((object) array('shortname' => 'testing', 'contextid' => $syscontext->id)); + + $this->assertEquals('testing', $template->get_shortname()); + $this->assertEquals($syscontext->id, $template->get_contextid()); + + // Simple update. + api::update_template((object) array('id' => $template->get_id(), 'shortname' => 'success')); + $template = api::read_template($template->get_id()); + $this->assertEquals('success', $template->get_shortname()); + + // Trying to change the context. + $this->setExpectedException('coding_exception'); + api::update_template((object) array('id' => $template->get_id(), 'contextid' => context_coursecat::instance($cat->id))); + } + + /** + * Test listing framework with order param. + */ + public function test_list_frameworks() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + // Create a list of frameworks. + $framework1 = $lpg->create_framework(array( + 'shortname' => 'shortname_alpha', + 'idnumber' => 'idnumber_cinnamon', + 'description' => 'description', + 'descriptionformat' => FORMAT_HTML, + 'visible' => true, + 'contextid' => context_system::instance()->id + )); + + $framework2 = $lpg->create_framework(array( + 'shortname' => 'shortname_beetroot', + 'idnumber' => 'idnumber_apple', + 'description' => 'description', + 'descriptionformat' => FORMAT_HTML, + 'visible' => true, + 'contextid' => context_system::instance()->id + )); + + $framework3 = $lpg->create_framework(array( + 'shortname' => 'shortname_crisps', + 'idnumber' => 'idnumber_beer', + 'description' => 'description', + 'descriptionformat' => FORMAT_HTML, + 'visible' => false, + 'contextid' => context_system::instance()->id + )); + + // Get frameworks list order by shortname desc. + $result = api::list_frameworks('shortname', 'DESC', null, 3, context_system::instance()); + + $f = (object) array_shift($result); + $this->assertEquals($framework3->get_id(), $f->get_id()); + $f = (object) array_shift($result); + $this->assertEquals($framework2->get_id(), $f->get_id()); + $f = (object) array_shift($result); + $this->assertEquals($framework1->get_id(), $f->get_id()); + + // Get frameworks list order by idnumber asc. + $result = api::list_frameworks('idnumber', 'ASC', null, 3, context_system::instance()); + + $f = (object) array_shift($result); + $this->assertEquals($framework2->get_id(), $f->get_id()); + $f = (object) array_shift($result); + $this->assertEquals($framework3->get_id(), $f->get_id()); + $f = (object) array_shift($result); + $this->assertEquals($framework1->get_id(), $f->get_id()); + + // Repeat excluding the non-visible ones. + $result = api::list_frameworks('idnumber', 'ASC', null, 3, context_system::instance(), 'self', true); + $this->assertCount(2, $result); + $f = (object) array_shift($result); + $this->assertEquals($framework2->get_id(), $f->get_id()); + $f = (object) array_shift($result); + $this->assertEquals($framework1->get_id(), $f->get_id()); + + // Search by query string, trying match on shortname. + $result = api::list_frameworks('idnumber', 'ASC', null, 3, context_system::instance(), 'self', false, 'crisp'); + $this->assertCount(1, $result); + $f = (object) array_shift($result); + $this->assertEquals($framework3->get_id(), $f->get_id()); + + // Search by query string, trying match on shortname, but hidden. + $result = api::list_frameworks('idnumber', 'ASC', null, 3, context_system::instance(), 'self', true, 'crisp'); + $this->assertCount(0, $result); + + // Search by query string, trying match on ID number. + $result = api::list_frameworks('idnumber', 'ASC', null, 3, context_system::instance(), 'self', false, 'apple'); + $this->assertCount(1, $result); + $f = (object) array_shift($result); + $this->assertEquals($framework2->get_id(), $f->get_id()); + + // Search by query string, trying match on both. + $result = api::list_frameworks('idnumber', 'ASC', null, 3, context_system::instance(), 'self', false, 'bee'); + $this->assertCount(2, $result); + $f = (object) array_shift($result); + $this->assertEquals($framework2->get_id(), $f->get_id()); + $f = (object) array_shift($result); + $this->assertEquals($framework3->get_id(), $f->get_id()); + } + + /** + * Test duplicate a framework. + */ + public function test_duplicate_framework() { + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $this->resetAfterTest(true); + $this->setAdminUser(); + + $syscontext = context_system::instance(); + $params = array( + 'shortname' => 'shortname_a', + 'idnumber' => 'idnumber_c', + 'description' => 'description', + 'descriptionformat' => FORMAT_HTML, + 'visible' => true, + 'contextid' => $syscontext->id + ); + $framework = $lpg->create_framework($params); + $competency1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $competency2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $competency3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $competency4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $competency41 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), + 'parentid' => $competency4->get_id()) + ); + $competency42 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), + 'parentid' => $competency4->get_id()) + ); + $competencyidnumbers = array($competency1->get_idnumber(), + $competency2->get_idnumber(), + $competency3->get_idnumber(), + $competency4->get_idnumber(), + $competency41->get_idnumber(), + $competency42->get_idnumber() + ); + + $config = json_encode(array( + 'base' => array('points' => 4), + 'competencies' => array( + array('id' => $competency41->get_id(), 'points' => 3, 'required' => 0), + array('id' => $competency42->get_id(), 'points' => 2, 'required' => 1), + ) + )); + $competency4->set_ruletype('core_competency\competency_rule_points'); + $competency4->set_ruleoutcome(\core_competency\competency::OUTCOME_EVIDENCE); + $competency4->set_ruleconfig($config); + $competency4->update(); + + api::add_related_competency($competency1->get_id(), $competency2->get_id()); + api::add_related_competency($competency3->get_id(), $competency4->get_id()); + + $frameworkduplicated1 = api::duplicate_framework($framework->get_id()); + $frameworkduplicated2 = api::duplicate_framework($framework->get_id()); + + $this->assertEquals($framework->get_idnumber().'_1', $frameworkduplicated1->get_idnumber()); + $this->assertEquals($framework->get_idnumber().'_2', $frameworkduplicated2->get_idnumber()); + + $competenciesfr1 = api::list_competencies(array('competencyframeworkid' => $frameworkduplicated1->get_id())); + $competenciesfr2 = api::list_competencies(array('competencyframeworkid' => $frameworkduplicated2->get_id())); + + $competencyidsfr1 = array(); + $competencyidsfr2 = array(); + + foreach ($competenciesfr1 as $cmp) { + $competencyidsfr1[] = $cmp->get_idnumber(); + } + foreach ($competenciesfr2 as $cmp) { + $competencyidsfr2[] = $cmp->get_idnumber(); + } + + $this->assertEmpty(array_diff($competencyidsfr1, $competencyidnumbers)); + $this->assertEmpty(array_diff($competencyidsfr2, $competencyidnumbers)); + $this->assertCount(6, $competenciesfr1); + $this->assertCount(6, $competenciesfr2); + + // Test the related competencies. + reset($competenciesfr1); + $compduplicated1 = current($competenciesfr1); + $relatedcompetencies = $compduplicated1->get_related_competencies(); + $comprelated = current($relatedcompetencies); + $this->assertEquals($comprelated->get_idnumber(), $competency2->get_idnumber()); + + // Check if config rule have been ported correctly. + $competency4duplicated = competency::get_record(array( + 'idnumber' => $competency4->get_idnumber(), + 'competencyframeworkid' => $frameworkduplicated2->get_id() + )); + $configduplicated = json_decode($competency4duplicated->get_ruleconfig(), true); + $configorigin = json_decode($config, true); + // Check that the 2 config have the same base. + $this->assertEquals($configorigin['base'], $configduplicated['base']); + $this->assertEquals(count($configorigin['competencies']), count($configduplicated['competencies'])); + $competencyidsrules = array(); + foreach ($configduplicated['competencies'] as $key => $value) { + // Check that the only difference between the 2 config is id competency. + $this->assertEquals(1, count(array_diff($value, $configorigin['competencies'][$key]))); + $competencyidsrules[] = $value['id']; + } + $this->assertTrue($competency4duplicated->is_parent_of($competencyidsrules)); + + // Test duplicate an empty framework. + $emptyfrm = $lpg->create_framework(); + $emptyfrmduplicated = api::duplicate_framework($emptyfrm->get_id()); + $this->assertEquals($emptyfrm->get_idnumber().'_1', $emptyfrmduplicated->get_idnumber()); + $nbcomp = api::count_competencies(array('competencyframeworkid' => $emptyfrmduplicated->get_id())); + $this->assertEquals(0, $nbcomp); + + } + + /** + * Test update plan. + */ + public function test_update_plan() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $usermanageowndraft = $dg->create_user(); + $usermanageown = $dg->create_user(); + $usermanagedraft = $dg->create_user(); + $usermanage = $dg->create_user(); + + $syscontext = context_system::instance(); + + // Creating specific roles. + $manageowndraftrole = $dg->create_role(array( + 'name' => 'User manage own draft', + 'shortname' => 'manage-own-draft' + )); + $manageownrole = $dg->create_role(array( + 'name' => 'User manage own', + 'shortname' => 'manage-own' + )); + $managedraftrole = $dg->create_role(array( + 'name' => 'User manage draft', + 'shortname' => 'manage-draft' + )); + $managerole = $dg->create_role(array( + 'name' => 'User manage', + 'shortname' => 'manage' + )); + + assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $manageowndraftrole, $syscontext->id); + assign_capability('moodle/competency:planviewowndraft', CAP_ALLOW, $manageowndraftrole, $syscontext->id); + + assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $manageownrole, $syscontext->id); + assign_capability('moodle/competency:planviewown', CAP_ALLOW, $manageownrole, $syscontext->id); + + assign_capability('moodle/competency:planmanagedraft', CAP_ALLOW, $managedraftrole, $syscontext->id); + assign_capability('moodle/competency:planviewdraft', CAP_ALLOW, $managedraftrole, $syscontext->id); + + assign_capability('moodle/competency:planmanage', CAP_ALLOW, $managerole, $syscontext->id); + assign_capability('moodle/competency:planview', CAP_ALLOW, $managerole, $syscontext->id); + + $dg->role_assign($manageowndraftrole, $usermanageowndraft->id, $syscontext->id); + $dg->role_assign($manageownrole, $usermanageown->id, $syscontext->id); + $dg->role_assign($managedraftrole, $usermanagedraft->id, $syscontext->id); + $dg->role_assign($managerole, $usermanage->id, $syscontext->id); + + // Create first learning plan with user create draft. + $this->setUser($usermanageowndraft); + $plan = array ( + 'name' => 'plan own draft', + 'description' => 'plan own draft', + 'userid' => $usermanageowndraft->id + ); + $plan = api::create_plan((object)$plan); + $record = $plan->to_record(); + $record->name = 'plan own draft modified'; + + // Check if user create draft can edit the plan name. + $plan = api::update_plan($record); + $this->assertInstanceOf('\core_competency\plan', $plan); + + // The status cannot be changed in this method. + $record->status = \core_competency\plan::STATUS_ACTIVE; + try { + $plan = api::update_plan($record); + $this->fail('Updating the status is not allowed.'); + } catch (coding_exception $e) { + $this->assertRegExp('/To change the status of a plan use the appropriate methods./', $e->getMessage()); + } + + // Test when user with manage own plan capability try to edit other user plan. + $record->status = \core_competency\plan::STATUS_DRAFT; + $record->name = 'plan create draft modified 2'; + $this->setUser($usermanageown); + try { + $plan = api::update_plan($record); + $this->fail('User with manage own plan capability can only edit his own plan.'); + } catch (required_capability_exception $e) { + $this->assertTrue(true); + } + + // User with manage plan capability cannot edit the other user plans with status draft. + $this->setUser($usermanage); + $record->name = 'plan create draft modified 3'; + try { + $plan = api::update_plan($record); + $this->fail('User with manage plan capability cannot edit the other user plans with status draft'); + } catch (required_capability_exception $e) { + $this->assertTrue(true); + } + + // User with manage draft capability can edit other user's learning plan if the status is draft. + $this->setUser($usermanagedraft); + $record->status = \core_competency\plan::STATUS_DRAFT; + $record->name = 'plan manage draft modified 3'; + $plan = api::update_plan($record); + $this->assertInstanceOf('\core_competency\plan', $plan); + + // User with manage plan capability can create/edit learning plan if status is active/complete. + $this->setUser($usermanage); + $plan = array ( + 'name' => 'plan create', + 'description' => 'plan create', + 'userid' => $usermanage->id, + 'status' => \core_competency\plan::STATUS_ACTIVE + ); + $plan = api::create_plan((object)$plan); + + // Silently transition to complete status to avoid errors about transitioning to complete. + $plan->set_status(\core_competency\plan::STATUS_COMPLETE); + $plan->update(); + + $record = $plan->to_record(); + $record->name = 'plan create own modified'; + try { + api::update_plan($record); + $this->fail('Completed plan can not be edited'); + } catch (coding_exception $e) { + $this->assertTrue(true); + } + } + + public function test_create_plan_from_template() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $u1 = $this->getDataGenerator()->create_user(); + $tpl = $this->getDataGenerator()->get_plugin_generator('core_competency')->create_template(); + + // Creating a new plan. + $plan = api::create_plan_from_template($tpl, $u1->id); + $record = $plan->to_record(); + $this->assertInstanceOf('\core_competency\plan', $plan); + $this->assertTrue(\core_competency\plan::record_exists($plan->get_id())); + $this->assertEquals($tpl->get_id(), $plan->get_templateid()); + $this->assertEquals($u1->id, $plan->get_userid()); + $this->assertTrue($plan->is_based_on_template()); + + // Creating a plan that already exists. + $plan = api::create_plan_from_template($tpl, $u1->id); + $this->assertFalse($plan); + + // Check that api::create_plan cannot be used. + $this->setExpectedException('coding_exception'); + unset($record->id); + $plan = api::create_plan($record); + } + + public function test_update_plan_based_on_template() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + + $this->setAdminUser(); + $tpl1 = $lpg->create_template(); + $tpl2 = $lpg->create_template(); + $up1 = $lpg->create_plan(array('userid' => $u1->id, 'templateid' => $tpl1->get_id())); + $up2 = $lpg->create_plan(array('userid' => $u2->id, 'templateid' => null)); + + try { + // Trying to remove the template dependency. + $record = $up1->to_record(); + $record->templateid = null; + api::update_plan($record); + $this->fail('A plan cannot be unlinked using api::update_plan()'); + } catch (coding_exception $e) { + // All good. + } + + try { + // Trying to switch to another template. + $record = $up1->to_record(); + $record->templateid = $tpl2->get_id(); + api::update_plan($record); + $this->fail('A plan cannot be moved to another template.'); + } catch (coding_exception $e) { + // All good. + } + + try { + // Trying to switch to using a template. + $record = $up2->to_record(); + $record->templateid = $tpl1->get_id(); + api::update_plan($record); + $this->fail('A plan cannot be update to use a template.'); + } catch (coding_exception $e) { + // All good. + } + } + + public function test_unlink_plan_from_template() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + + $this->setAdminUser(); + $f1 = $lpg->create_framework(); + $f2 = $lpg->create_framework(); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2a = $lpg->create_competency(array('competencyframeworkid' => $f2->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + + $tpl1 = $lpg->create_template(); + $tpl2 = $lpg->create_template(); + + $tplc1a = $lpg->create_template_competency(array('templateid' => $tpl1->get_id(), 'competencyid' => $c1a->get_id(), + 'sortorder' => 9)); + $tplc1b = $lpg->create_template_competency(array('templateid' => $tpl1->get_id(), 'competencyid' => $c1b->get_id(), + 'sortorder' => 8)); + $tplc2a = $lpg->create_template_competency(array('templateid' => $tpl2->get_id(), 'competencyid' => $c2a->get_id())); + + $plan1 = $lpg->create_plan(array('userid' => $u1->id, 'templateid' => $tpl1->get_id(), 'status' => plan::STATUS_ACTIVE)); + $plan2 = $lpg->create_plan(array('userid' => $u2->id, 'templateid' => $tpl2->get_id())); + $plan3 = $lpg->create_plan(array('userid' => $u1->id, 'templateid' => $tpl1->get_id(), 'status' => plan::STATUS_COMPLETE)); + + // Check that we have what we expect at this stage. + $this->assertEquals(2, \core_competency\template_competency::count_records(array('templateid' => $tpl1->get_id()))); + $this->assertEquals(1, \core_competency\template_competency::count_records(array('templateid' => $tpl2->get_id()))); + $this->assertEquals(0, \core_competency\plan_competency::count_records(array('planid' => $plan1->get_id()))); + $this->assertEquals(0, \core_competency\plan_competency::count_records(array('planid' => $plan2->get_id()))); + $this->assertTrue($plan1->is_based_on_template()); + $this->assertTrue($plan2->is_based_on_template()); + + // Let's do this! + $tpl1comps = \core_competency\template_competency::list_competencies($tpl1->get_id(), true); + $tpl2comps = \core_competency\template_competency::list_competencies($tpl2->get_id(), true); + + api::unlink_plan_from_template($plan1); + + $plan1->read(); + $plan2->read(); + $this->assertCount(2, $tpl1comps); + $this->assertCount(1, $tpl2comps); + $this->assertEquals(2, \core_competency\template_competency::count_records(array('templateid' => $tpl1->get_id()))); + $this->assertEquals(1, \core_competency\template_competency::count_records(array('templateid' => $tpl2->get_id()))); + $this->assertEquals(2, \core_competency\plan_competency::count_records(array('planid' => $plan1->get_id()))); + $this->assertEquals(0, \core_competency\plan_competency::count_records(array('planid' => $plan2->get_id()))); + $this->assertFalse($plan1->is_based_on_template()); + $this->assertEquals($tpl1->get_id(), $plan1->get_origtemplateid()); + $this->assertTrue($plan2->is_based_on_template()); + $this->assertEquals(null, $plan2->get_origtemplateid()); + + // Check we can unlink draft plan. + try { + api::unlink_plan_from_template($plan2); + } catch (coding_exception $e) { + $this->fail('Fail to unlink draft plan.'); + } + + // Check we can not unlink completed plan. + try { + api::unlink_plan_from_template($plan3); + $this->fail('We can not unlink completed plan.'); + } catch (coding_exception $e) { + // All good. + } + + // Even the order remains. + $plan1comps = \core_competency\plan_competency::list_competencies($plan1->get_id()); + $before = reset($tpl1comps); + $after = reset($plan1comps); + $this->assertEquals($before->get_id(), $after->get_id()); + $this->assertEquals($before->get_sortorder(), $after->get_sortorder()); + $before = next($tpl1comps); + $after = next($plan1comps); + $this->assertEquals($before->get_id(), $after->get_id()); + $this->assertEquals($before->get_sortorder(), $after->get_sortorder()); + } + + public function test_update_template_updates_plans() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $dg = $this->getDataGenerator(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $lpg = $dg->get_plugin_generator('core_competency'); + $tpl1 = $lpg->create_template(); + $tpl2 = $lpg->create_template(); + + // Create plans with data not matching templates. + $time = time(); + $plan1 = $lpg->create_plan(array('templateid' => $tpl1->get_id(), 'userid' => $u1->id, + 'name' => 'Not good name', 'duedate' => $time + 3600, 'description' => 'Ahah', 'descriptionformat' => FORMAT_MARKDOWN)); + $plan2 = $lpg->create_plan(array('templateid' => $tpl1->get_id(), 'userid' => $u2->id, + 'name' => 'Not right name', 'duedate' => $time + 3601, 'description' => 'Ahah', 'descriptionformat' => FORMAT_PLAIN)); + $plan3 = $lpg->create_plan(array('templateid' => $tpl2->get_id(), 'userid' => $u1->id, + 'name' => 'Not sweet name', 'duedate' => $time + 3602, 'description' => 'Ahah', 'descriptionformat' => FORMAT_PLAIN)); + + // Prepare our expectations. + $plan1->read(); + $plan2->read(); + $plan3->read(); + + $this->assertEquals($tpl1->get_id(), $plan1->get_templateid()); + $this->assertEquals($tpl1->get_id(), $plan2->get_templateid()); + $this->assertEquals($tpl2->get_id(), $plan3->get_templateid()); + $this->assertNotEquals($tpl1->get_shortname(), $plan1->get_name()); + $this->assertNotEquals($tpl1->get_shortname(), $plan2->get_name()); + $this->assertNotEquals($tpl2->get_shortname(), $plan3->get_name()); + $this->assertNotEquals($tpl1->get_description(), $plan1->get_description()); + $this->assertNotEquals($tpl1->get_description(), $plan2->get_description()); + $this->assertNotEquals($tpl2->get_description(), $plan3->get_description()); + $this->assertNotEquals($tpl1->get_descriptionformat(), $plan1->get_descriptionformat()); + $this->assertNotEquals($tpl1->get_descriptionformat(), $plan2->get_descriptionformat()); + $this->assertNotEquals($tpl2->get_descriptionformat(), $plan3->get_descriptionformat()); + $this->assertNotEquals($tpl1->get_duedate(), $plan1->get_duedate()); + $this->assertNotEquals($tpl1->get_duedate(), $plan2->get_duedate()); + $this->assertNotEquals($tpl2->get_duedate(), $plan3->get_duedate()); + + // Update the template without changing critical fields does not update the plans. + $data = $tpl1->to_record(); + $data->visible = 0; + api::update_template($data); + $this->assertNotEquals($tpl1->get_shortname(), $plan1->get_name()); + $this->assertNotEquals($tpl1->get_shortname(), $plan2->get_name()); + $this->assertNotEquals($tpl2->get_shortname(), $plan3->get_name()); + $this->assertNotEquals($tpl1->get_description(), $plan1->get_description()); + $this->assertNotEquals($tpl1->get_description(), $plan2->get_description()); + $this->assertNotEquals($tpl2->get_description(), $plan3->get_description()); + $this->assertNotEquals($tpl1->get_descriptionformat(), $plan1->get_descriptionformat()); + $this->assertNotEquals($tpl1->get_descriptionformat(), $plan2->get_descriptionformat()); + $this->assertNotEquals($tpl2->get_descriptionformat(), $plan3->get_descriptionformat()); + $this->assertNotEquals($tpl1->get_duedate(), $plan1->get_duedate()); + $this->assertNotEquals($tpl1->get_duedate(), $plan2->get_duedate()); + $this->assertNotEquals($tpl2->get_duedate(), $plan3->get_duedate()); + + // Now really update the template. + $data = $tpl1->to_record(); + $data->shortname = 'Awesome!'; + $data->description = 'This is too awesome!'; + $data->descriptionformat = FORMAT_HTML; + $data->duedate = $time + 200; + api::update_template($data); + $tpl1->read(); + + // Now confirm that the right plans were updated. + $plan1->read(); + $plan2->read(); + $plan3->read(); + + $this->assertEquals($tpl1->get_id(), $plan1->get_templateid()); + $this->assertEquals($tpl1->get_id(), $plan2->get_templateid()); + $this->assertEquals($tpl2->get_id(), $plan3->get_templateid()); + + $this->assertEquals($tpl1->get_shortname(), $plan1->get_name()); + $this->assertEquals($tpl1->get_shortname(), $plan2->get_name()); + $this->assertNotEquals($tpl2->get_shortname(), $plan3->get_name()); + $this->assertEquals($tpl1->get_description(), $plan1->get_description()); + $this->assertEquals($tpl1->get_description(), $plan2->get_description()); + $this->assertNotEquals($tpl2->get_description(), $plan3->get_description()); + $this->assertEquals($tpl1->get_descriptionformat(), $plan1->get_descriptionformat()); + $this->assertEquals($tpl1->get_descriptionformat(), $plan2->get_descriptionformat()); + $this->assertNotEquals($tpl2->get_descriptionformat(), $plan3->get_descriptionformat()); + $this->assertEquals($tpl1->get_duedate(), $plan1->get_duedate()); + $this->assertEquals($tpl1->get_duedate(), $plan2->get_duedate()); + $this->assertNotEquals($tpl2->get_duedate(), $plan3->get_duedate()); + } + + /** + * Test that the method to complete a plan. + */ + public function test_complete_plan() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + + // Create a framework and assign competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Create two plans and assign competencies. + $plan = $lpg->create_plan(array('userid' => $user->id)); + $otherplan = $lpg->create_plan(array('userid' => $user->id)); + + $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c1->get_id())); + $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c2->get_id())); + $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c3->get_id())); + $lpg->create_plan_competency(array('planid' => $otherplan->get_id(), 'competencyid' => $c1->get_id())); + + $uclist = array( + $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c1->get_id(), + 'proficiency' => true, 'grade' => 1 )), + $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c2->get_id(), + 'proficiency' => false, 'grade' => 2 )) + ); + + $this->assertEquals(2, \core_competency\user_competency::count_records()); + $this->assertEquals(0, \core_competency\user_competency_plan::count_records()); + + // Change status of the plan to complete. + api::complete_plan($plan); + + // Check that user competencies are now in user_competency_plan objects and still in user_competency. + $this->assertEquals(2, \core_competency\user_competency::count_records()); + $this->assertEquals(3, \core_competency\user_competency_plan::count_records()); + + $usercompetenciesplan = \core_competency\user_competency_plan::get_records(); + + $this->assertEquals($uclist[0]->get_userid(), $usercompetenciesplan[0]->get_userid()); + $this->assertEquals($uclist[0]->get_competencyid(), $usercompetenciesplan[0]->get_competencyid()); + $this->assertEquals($uclist[0]->get_proficiency(), (bool) $usercompetenciesplan[0]->get_proficiency()); + $this->assertEquals($uclist[0]->get_grade(), $usercompetenciesplan[0]->get_grade()); + $this->assertEquals($plan->get_id(), $usercompetenciesplan[0]->get_planid()); + + $this->assertEquals($uclist[1]->get_userid(), $usercompetenciesplan[1]->get_userid()); + $this->assertEquals($uclist[1]->get_competencyid(), $usercompetenciesplan[1]->get_competencyid()); + $this->assertEquals($uclist[1]->get_proficiency(), (bool) $usercompetenciesplan[1]->get_proficiency()); + $this->assertEquals($uclist[1]->get_grade(), $usercompetenciesplan[1]->get_grade()); + $this->assertEquals($plan->get_id(), $usercompetenciesplan[1]->get_planid()); + + $this->assertEquals($user->id, $usercompetenciesplan[2]->get_userid()); + $this->assertEquals($c3->get_id(), $usercompetenciesplan[2]->get_competencyid()); + $this->assertNull($usercompetenciesplan[2]->get_proficiency()); + $this->assertNull($usercompetenciesplan[2]->get_grade()); + $this->assertEquals($plan->get_id(), $usercompetenciesplan[2]->get_planid()); + + // Check we can not add competency to completed plan. + try { + api::add_competency_to_plan($plan->get_id(), $c4->get_id()); + $this->fail('We can not add competency to completed plan.'); + } catch (coding_exception $e) { + // All good. + } + + // Check we can not remove competency to completed plan. + try { + api::remove_competency_from_plan($plan->get_id(), $c3->get_id()); + $this->fail('We can not remove competency to completed plan.'); + } catch (coding_exception $e) { + // All good. + } + + // Completing a plan that is completed throws an exception. + $this->setExpectedException('coding_exception'); + api::complete_plan($plan); + } + + /** + * Set-up the workflow data (review, active, ...). + * + * @return array + */ + protected function setup_workflow_data() { + $this->resetAfterTest(); + + $dg = $this->getDataGenerator(); + $user = $dg->create_user(); + $reviewer = $dg->create_user(); + $otheruser = $dg->create_user(); + + $syscontext = context_system::instance(); + $userrole = $dg->create_role(); + $reviewerrole = $dg->create_role(); + $otheruserrole = $dg->create_role(); + + assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $userrole, $syscontext->id); + assign_capability('moodle/competency:planmanage', CAP_ALLOW, $reviewerrole, $syscontext->id); + assign_capability('moodle/competency:planviewdraft', CAP_ALLOW, $reviewerrole, $syscontext->id); + $dg->role_assign($userrole, $user->id, $syscontext->id); + $dg->role_assign($reviewerrole, $reviewer->id, $syscontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + $lpg = $dg->get_plugin_generator('core_competency'); + $tpl = $lpg->create_template(); + $plan = $lpg->create_plan(array('userid' => $user->id)); + $tplplan = $lpg->create_plan(array('userid' => $user->id, 'templateid' => $tpl->get_id())); + + return array( + 'dg' => $dg, + 'lpg' => $lpg, + 'user' => $user, + 'reviewer' => $reviewer, + 'otheruser' => $otheruser, + 'plan' => $plan, + 'tplplan' => $tplplan, + ); + } + + /** + * Testing requesting the review of a plan. + */ + public function test_plan_request_review() { + $data = $this->setup_workflow_data(); + $dg = $data['dg']; + $lpg = $data['lpg']; + $user = $data['user']; + $reviewer = $data['reviewer']; + $otheruser = $data['otheruser']; + $plan = $data['plan']; + $tplplan = $data['tplplan']; + + $this->assertEquals(plan::STATUS_DRAFT, $plan->get_status()); + $this->assertEquals(plan::STATUS_DRAFT, $tplplan->get_status()); + + // Foreign user cannot do anything. + $this->setUser($otheruser); + try { + api::plan_request_review($plan); + $this->fail('The user can not read the plan.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Can not change a plan based on a template. + $this->setUser($user); + try { + api::plan_request_review($tplplan); + $this->fail('The plan is based on a template.'); + } catch (coding_exception $e) { + $this->assertRegExp('/Template plans cannot be reviewed./', $e->getMessage()); + } + + // Can not send for review when not draft. + $this->setUser($user); + $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + try { + api::plan_request_review($plan); + $this->fail('The plan cannot be sent for review at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan cannot be sent for review at this stage./', $e->getMessage()); + } + + // Can not send for review when not draft. + $this->setUser($user); + $plan->set_status(plan::STATUS_ACTIVE); + try { + api::plan_request_review($plan); + $this->fail('The plan cannot be sent for review at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan cannot be sent for review at this stage./', $e->getMessage()); + } + + // Can not send for review when not draft. + $this->setUser($user); + $plan->set_status(plan::STATUS_IN_REVIEW); + try { + api::plan_request_review($plan); + $this->fail('The plan cannot be sent for review at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan cannot be sent for review at this stage./', $e->getMessage()); + } + + // Can not send for review when not draft. + $this->setUser($user); + $plan->set_status(plan::STATUS_COMPLETE); + try { + api::plan_request_review($plan); + $this->fail('The plan cannot be sent for review at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan cannot be sent for review at this stage./', $e->getMessage()); + } + + // Sending for review as a reviewer. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_DRAFT); + try { + api::plan_request_review($plan); + $this->fail('The user can not request a review.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Sending for review. + $this->setUser($user); + api::plan_request_review($plan); + $plan->read(); + $this->assertEquals(plan::STATUS_WAITING_FOR_REVIEW, $plan->get_status()); + + // Sending for review by ID. + $plan->set_status(plan::STATUS_DRAFT); + $plan->update(); + api::plan_request_review($plan->get_id()); + $plan->read(); + $this->assertEquals(plan::STATUS_WAITING_FOR_REVIEW, $plan->get_status()); + } + + /** + * Testing cancelling the review request. + */ + public function test_plan_cancel_review_request() { + $data = $this->setup_workflow_data(); + $dg = $data['dg']; + $lpg = $data['lpg']; + $user = $data['user']; + $reviewer = $data['reviewer']; + $otheruser = $data['otheruser']; + $plan = $data['plan']; + $tplplan = $data['tplplan']; + + // Set waiting for review. + $tplplan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + $tplplan->update(); + $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + $plan->update(); + + // Foreign user cannot do anything. + $this->setUser($otheruser); + try { + api::plan_cancel_review_request($plan); + $this->fail('The user can not read the plan.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Can not change a plan based on a template. + $this->setUser($user); + try { + api::plan_cancel_review_request($tplplan); + $this->fail('The plan is based on a template.'); + } catch (coding_exception $e) { + $this->assertRegExp('/Template plans cannot be reviewed./', $e->getMessage()); + } + + // Can not cancel review request when not waiting for review. + $this->setUser($user); + $plan->set_status(plan::STATUS_DRAFT); + try { + api::plan_cancel_review_request($plan); + $this->fail('The plan cannot be sent for review at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan review cannot be cancelled at this stage./', $e->getMessage()); + } + + // Can not cancel review request when not waiting for review. + $this->setUser($user); + $plan->set_status(plan::STATUS_IN_REVIEW); + try { + api::plan_cancel_review_request($plan); + $this->fail('The plan review cannot be cancelled at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan review cannot be cancelled at this stage./', $e->getMessage()); + } + + // Can not cancel review request when not waiting for review. + $this->setUser($user); + $plan->set_status(plan::STATUS_ACTIVE); + try { + api::plan_cancel_review_request($plan); + $this->fail('The plan review cannot be cancelled at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan review cannot be cancelled at this stage./', $e->getMessage()); + } + + // Can not cancel review request when not waiting for review. + $this->setUser($user); + $plan->set_status(plan::STATUS_COMPLETE); + try { + api::plan_cancel_review_request($plan); + $this->fail('The plan review cannot be cancelled at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan review cannot be cancelled at this stage./', $e->getMessage()); + } + + // Cancelling as a reviewer. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + try { + api::plan_cancel_review_request($plan); + $this->fail('The user can not cancel a review request.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Cancelling review request. + $this->setUser($user); + api::plan_cancel_review_request($plan); + $plan->read(); + $this->assertEquals(plan::STATUS_DRAFT, $plan->get_status()); + + // Cancelling review request by ID. + $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + $plan->update(); + api::plan_cancel_review_request($plan->get_id()); + $plan->read(); + $this->assertEquals(plan::STATUS_DRAFT, $plan->get_status()); + } + + /** + * Testing starting the review. + */ + public function test_plan_start_review() { + $data = $this->setup_workflow_data(); + $dg = $data['dg']; + $lpg = $data['lpg']; + $user = $data['user']; + $reviewer = $data['reviewer']; + $otheruser = $data['otheruser']; + $plan = $data['plan']; + $tplplan = $data['tplplan']; + + // Set waiting for review. + $tplplan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + $tplplan->update(); + $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + $plan->update(); + + // Foreign user cannot do anything. + $this->setUser($otheruser); + try { + api::plan_start_review($plan); + $this->fail('The user can not read the plan.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Can not change a plan based on a template. + $this->setUser($reviewer); + try { + api::plan_start_review($tplplan); + $this->fail('The plan is based on a template.'); + } catch (coding_exception $e) { + $this->assertRegExp('/Template plans cannot be reviewed./', $e->getMessage()); + } + + // Can not start a review when not waiting for review. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_DRAFT); + try { + api::plan_start_review($plan); + $this->fail('The plan review cannot be started at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan review cannot be started at this stage./', $e->getMessage()); + } + + // Can not start a review when not waiting for review. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_IN_REVIEW); + try { + api::plan_start_review($plan); + $this->fail('The plan review cannot be started at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan review cannot be started at this stage./', $e->getMessage()); + } + + // Can not start a review when not waiting for review. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_ACTIVE); + try { + api::plan_start_review($plan); + $this->fail('The plan review cannot be started at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan review cannot be started at this stage./', $e->getMessage()); + } + + // Can not start a review when not waiting for review. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_COMPLETE); + try { + api::plan_start_review($plan); + $this->fail('The plan review cannot be started at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan review cannot be started at this stage./', $e->getMessage()); + } + + // Starting as the owner. + $this->setUser($user); + $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + try { + api::plan_start_review($plan); + $this->fail('The user can not start a review.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Starting review. + $this->setUser($reviewer); + api::plan_start_review($plan); + $plan->read(); + $this->assertEquals(plan::STATUS_IN_REVIEW, $plan->get_status()); + $this->assertEquals($reviewer->id, $plan->get_reviewerid()); + + // Starting review by ID. + $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + $plan->set_reviewerid(null); + $plan->update(); + api::plan_start_review($plan->get_id()); + $plan->read(); + $this->assertEquals(plan::STATUS_IN_REVIEW, $plan->get_status()); + $this->assertEquals($reviewer->id, $plan->get_reviewerid()); + } + + /** + * Testing stopping the review. + */ + public function test_plan_stop_review() { + $data = $this->setup_workflow_data(); + $dg = $data['dg']; + $lpg = $data['lpg']; + $user = $data['user']; + $reviewer = $data['reviewer']; + $otheruser = $data['otheruser']; + $plan = $data['plan']; + $tplplan = $data['tplplan']; + + // Set waiting for review. + $tplplan->set_status(plan::STATUS_IN_REVIEW); + $tplplan->update(); + $plan->set_status(plan::STATUS_IN_REVIEW); + $plan->update(); + + // Foreign user cannot do anything. + $this->setUser($otheruser); + try { + api::plan_stop_review($plan); + $this->fail('The user can not read the plan.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Can not change a plan based on a template. + $this->setUser($reviewer); + try { + api::plan_stop_review($tplplan); + $this->fail('The plan is based on a template.'); + } catch (coding_exception $e) { + $this->assertRegExp('/Template plans cannot be reviewed./', $e->getMessage()); + } + + // Can not stop a review whe not in review. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_DRAFT); + try { + api::plan_stop_review($plan); + $this->fail('The plan review cannot be stopped at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan review cannot be stopped at this stage./', $e->getMessage()); + } + + // Can not stop a review whe not in review. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + try { + api::plan_stop_review($plan); + $this->fail('The plan review cannot be stopped at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan review cannot be stopped at this stage./', $e->getMessage()); + } + + // Can not stop a review whe not in review. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_ACTIVE); + try { + api::plan_stop_review($plan); + $this->fail('The plan review cannot be stopped at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan review cannot be stopped at this stage./', $e->getMessage()); + } + + // Can not stop a review whe not in review. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_COMPLETE); + try { + api::plan_stop_review($plan); + $this->fail('The plan review cannot be stopped at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan review cannot be stopped at this stage./', $e->getMessage()); + } + + // Stopping as the owner. + $this->setUser($user); + $plan->set_status(plan::STATUS_IN_REVIEW); + try { + api::plan_stop_review($plan); + $this->fail('The user can not stop a review.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Stopping review. + $this->setUser($reviewer); + api::plan_stop_review($plan); + $plan->read(); + $this->assertEquals(plan::STATUS_DRAFT, $plan->get_status()); + + // Stopping review by ID. + $plan->set_status(plan::STATUS_IN_REVIEW); + $plan->update(); + api::plan_stop_review($plan->get_id()); + $plan->read(); + $this->assertEquals(plan::STATUS_DRAFT, $plan->get_status()); + } + + /** + * Testing approving the plan. + */ + public function test_approve_plan() { + $data = $this->setup_workflow_data(); + $dg = $data['dg']; + $lpg = $data['lpg']; + $user = $data['user']; + $reviewer = $data['reviewer']; + $otheruser = $data['otheruser']; + $plan = $data['plan']; + $tplplan = $data['tplplan']; + + // Set waiting for review. + $tplplan->set_status(plan::STATUS_IN_REVIEW); + $tplplan->update(); + $plan->set_status(plan::STATUS_IN_REVIEW); + $plan->update(); + + // Foreign user cannot do anything. + $this->setUser($otheruser); + try { + api::approve_plan($plan); + $this->fail('The user can not read the plan.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Can not change a plan based on a template. + $this->setUser($reviewer); + try { + api::approve_plan($tplplan); + $this->fail('The plan is based on a template.'); + } catch (coding_exception $e) { + $this->assertRegExp('/Template plans are already approved./', $e->getMessage()); + } + + // Can not approve a plan already approved. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_ACTIVE); + try { + api::approve_plan($plan); + $this->fail('The plan cannot be approved at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan cannot be approved at this stage./', $e->getMessage()); + } + + // Can not approve a plan already approved. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_COMPLETE); + try { + api::approve_plan($plan); + $this->fail('The plan cannot be approved at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan cannot be approved at this stage./', $e->getMessage()); + } + + // Approve as the owner. + $this->setUser($user); + $plan->set_status(plan::STATUS_IN_REVIEW); + try { + api::approve_plan($plan); + $this->fail('The user can not approve the plan.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Approve plan from in review. + $this->setUser($reviewer); + api::approve_plan($plan); + $plan->read(); + $this->assertEquals(plan::STATUS_ACTIVE, $plan->get_status()); + + // Approve plan by ID. + $plan->set_status(plan::STATUS_IN_REVIEW); + $plan->update(); + api::approve_plan($plan->get_id()); + $plan->read(); + $this->assertEquals(plan::STATUS_ACTIVE, $plan->get_status()); + + // Approve plan from draft. + $plan->set_status(plan::STATUS_DRAFT); + $plan->update(); + api::approve_plan($plan); + $plan->read(); + $this->assertEquals(plan::STATUS_ACTIVE, $plan->get_status()); + + // Approve plan from waiting for review. + $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + $plan->update(); + api::approve_plan($plan); + $plan->read(); + $this->assertEquals(plan::STATUS_ACTIVE, $plan->get_status()); + } + + /** + * Testing stopping the review. + */ + public function test_unapprove_plan() { + $data = $this->setup_workflow_data(); + $dg = $data['dg']; + $lpg = $data['lpg']; + $user = $data['user']; + $reviewer = $data['reviewer']; + $otheruser = $data['otheruser']; + $plan = $data['plan']; + $tplplan = $data['tplplan']; + + // Set waiting for review. + $tplplan->set_status(plan::STATUS_ACTIVE); + $tplplan->update(); + $plan->set_status(plan::STATUS_ACTIVE); + $plan->update(); + + // Foreign user cannot do anything. + $this->setUser($otheruser); + try { + api::unapprove_plan($plan); + $this->fail('The user can not read the plan.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Can not change a plan based on a template. + $this->setUser($reviewer); + try { + api::unapprove_plan($tplplan); + $this->fail('The plan is based on a template.'); + } catch (coding_exception $e) { + $this->assertRegExp('/Template plans are always approved./', $e->getMessage()); + } + + // Can not unapprove a non-draft plan. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_DRAFT); + try { + api::unapprove_plan($plan); + $this->fail('The plan cannot be sent back to draft at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan cannot be sent back to draft at this stage./', $e->getMessage()); + } + + // Can not unapprove a non-draft plan. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW); + try { + api::unapprove_plan($plan); + $this->fail('The plan cannot be sent back to draft at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan cannot be sent back to draft at this stage./', $e->getMessage()); + } + + // Can not unapprove a non-draft plan. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_IN_REVIEW); + try { + api::unapprove_plan($plan); + $this->fail('The plan cannot be sent back to draft at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan cannot be sent back to draft at this stage./', $e->getMessage()); + } + + // Can not unapprove a non-draft plan. + $this->setUser($reviewer); + $plan->set_status(plan::STATUS_COMPLETE); + try { + api::unapprove_plan($plan); + $this->fail('The plan cannot be sent back to draft at this stage.'); + } catch (coding_exception $e) { + $this->assertRegExp('/The plan cannot be sent back to draft at this stage./', $e->getMessage()); + } + + // Unapprove as the owner. + $this->setUser($user); + $plan->set_status(plan::STATUS_ACTIVE); + try { + api::unapprove_plan($plan); + $this->fail('The user can not unapprove the plan.'); + } catch (required_capability_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Unapprove plan. + $this->setUser($reviewer); + api::unapprove_plan($plan); + $plan->read(); + $this->assertEquals(plan::STATUS_DRAFT, $plan->get_status()); + + // Unapprove plan by ID. + $plan->set_status(plan::STATUS_ACTIVE); + $plan->update(); + api::unapprove_plan($plan->get_id()); + $plan->read(); + $this->assertEquals(plan::STATUS_DRAFT, $plan->get_status()); + } + + /** + * Test update plan and the managing of archived user competencies. + */ + public function test_update_plan_manage_archived_competencies() { + global $DB; + + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $syscontext = context_system::instance(); + + // Create users and roles for the test. + $user = $dg->create_user(); + $manageownrole = $dg->create_role(array( + 'name' => 'User manage own', + 'shortname' => 'manageown' + )); + assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $manageownrole, $syscontext->id); + assign_capability('moodle/competency:planviewowndraft', CAP_ALLOW, $manageownrole, $syscontext->id); + assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $manageownrole, $syscontext->id); + assign_capability('moodle/competency:planviewown', CAP_ALLOW, $manageownrole, $syscontext->id); + $dg->role_assign($manageownrole, $user->id, $syscontext->id); + $this->setUser($user); + + // Create a framework and assign competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Create two plans and assign competencies. + $plan = $lpg->create_plan(array('userid' => $user->id)); + $otherplan = $lpg->create_plan(array('userid' => $user->id)); + + $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c1->get_id())); + $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c2->get_id())); + $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c3->get_id())); + $lpg->create_plan_competency(array('planid' => $otherplan->get_id(), 'competencyid' => $c1->get_id())); + + $uclist = array( + $lpg->create_user_competency(array( + 'userid' => $user->id, + 'competencyid' => $c1->get_id(), + 'proficiency' => true, + 'grade' => 1 + )), + $lpg->create_user_competency(array( + 'userid' => $user->id, + 'competencyid' => $c2->get_id(), + 'proficiency' => false, + 'grade' => 2 + )) + ); + + // Change status of the plan to complete. + $record = $plan->to_record(); + $record->status = \core_competency\plan::STATUS_COMPLETE; + + try { + $plan = api::update_plan($record); + $this->fail('We cannot complete a plan using api::update_plan().'); + } catch (coding_exception $e) { + // All good. + } + api::complete_plan($plan); + + // Check that user compretencies are now in user_competency_plan objects and still in user_competency. + $this->assertEquals(2, \core_competency\user_competency::count_records()); + $this->assertEquals(3, \core_competency\user_competency_plan::count_records()); + + $usercompetenciesplan = \core_competency\user_competency_plan::get_records(); + + $this->assertEquals($uclist[0]->get_userid(), $usercompetenciesplan[0]->get_userid()); + $this->assertEquals($uclist[0]->get_competencyid(), $usercompetenciesplan[0]->get_competencyid()); + $this->assertEquals($uclist[0]->get_proficiency(), (bool) $usercompetenciesplan[0]->get_proficiency()); + $this->assertEquals($uclist[0]->get_grade(), $usercompetenciesplan[0]->get_grade()); + $this->assertEquals($plan->get_id(), $usercompetenciesplan[0]->get_planid()); + + $this->assertEquals($uclist[1]->get_userid(), $usercompetenciesplan[1]->get_userid()); + $this->assertEquals($uclist[1]->get_competencyid(), $usercompetenciesplan[1]->get_competencyid()); + $this->assertEquals($uclist[1]->get_proficiency(), (bool) $usercompetenciesplan[1]->get_proficiency()); + $this->assertEquals($uclist[1]->get_grade(), $usercompetenciesplan[1]->get_grade()); + $this->assertEquals($plan->get_id(), $usercompetenciesplan[1]->get_planid()); + + $this->assertEquals($user->id, $usercompetenciesplan[2]->get_userid()); + $this->assertEquals($c3->get_id(), $usercompetenciesplan[2]->get_competencyid()); + $this->assertNull($usercompetenciesplan[2]->get_proficiency()); + $this->assertNull($usercompetenciesplan[2]->get_grade()); + $this->assertEquals($plan->get_id(), $usercompetenciesplan[2]->get_planid()); + + // Change status of the plan to active. + $record = $plan->to_record(); + $record->status = \core_competency\plan::STATUS_ACTIVE; + + try { + api::update_plan($record); + $this->fail('Completed plan can not be edited'); + } catch (coding_exception $e) { + // All good. + } + + api::reopen_plan($record->id); + // Check that user_competency_plan objects are deleted if the plan status is changed to another status. + $this->assertEquals(2, \core_competency\user_competency::count_records()); + $this->assertEquals(0, \core_competency\user_competency_plan::count_records()); + } + + /** + * Test completing plan does not change the order of competencies. + */ + public function test_complete_plan_doesnot_change_order() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $syscontext = context_system::instance(); + + // Create users and roles for the test. + $user = $dg->create_user(); + + // Create a framework and assign competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Create two plans and assign competencies. + $plan = $lpg->create_plan(array('userid' => $user->id)); + + $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c1->get_id())); + $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c2->get_id())); + $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c3->get_id())); + + // Changing competencies order in plan competency. + api::reorder_plan_competency($plan->get_id(), $c1->get_id(), $c3->get_id()); + + $competencies = api::list_plan_competencies($plan); + $this->assertEquals($c2->get_id(), $competencies[0]->competency->get_id()); + $this->assertEquals($c3->get_id(), $competencies[1]->competency->get_id()); + $this->assertEquals($c1->get_id(), $competencies[2]->competency->get_id()); + + // Completing plan. + api::complete_plan($plan); + + $competencies = api::list_plan_competencies($plan); + + // Completing plan does not change order. + $this->assertEquals($c2->get_id(), $competencies[0]->competency->get_id()); + $this->assertEquals($c3->get_id(), $competencies[1]->competency->get_id()); + $this->assertEquals($c1->get_id(), $competencies[2]->competency->get_id()); + + // Testing plan based on template. + $template = $lpg->create_template(); + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + $lpg->create_template_competency(array( + 'templateid' => $template->get_id(), + 'competencyid' => $c1->get_id() + )); + $lpg->create_template_competency(array( + 'templateid' => $template->get_id(), + 'competencyid' => $c2->get_id() + )); + $lpg->create_template_competency(array( + 'templateid' => $template->get_id(), + 'competencyid' => $c3->get_id() + )); + // Reorder competencies in template. + api::reorder_template_competency($template->get_id(), $c1->get_id(), $c3->get_id()); + + // Create plan from template. + $plan = api::create_plan_from_template($template->get_id(), $user->id); + + $competencies = api::list_plan_competencies($plan); + + // Completing plan does not change order. + $this->assertEquals($c2->get_id(), $competencies[0]->competency->get_id()); + $this->assertEquals($c3->get_id(), $competencies[1]->competency->get_id()); + $this->assertEquals($c1->get_id(), $competencies[2]->competency->get_id()); + + // Completing plan. + api::complete_plan($plan); + + $competencies = api::list_plan_competencies($plan); + + // Completing plan does not change order. + $this->assertEquals($c2->get_id(), $competencies[0]->competency->get_id()); + $this->assertEquals($c3->get_id(), $competencies[1]->competency->get_id()); + $this->assertEquals($c1->get_id(), $competencies[2]->competency->get_id()); + } + + /** + * Test remove plan and the managing of archived user competencies. + */ + public function test_delete_plan_manage_archived_competencies() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $syscontext = context_system::instance(); + + // Create user and role for the test. + $user = $dg->create_user(); + $managerole = $dg->create_role(array( + 'name' => 'User manage own', + 'shortname' => 'manageown' + )); + assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $managerole, $syscontext->id); + assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $managerole, $syscontext->id); + $dg->role_assign($managerole, $user->id, $syscontext->id); + $this->setUser($user); + + // Create a framework and assign competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Create completed plan with records in user_competency. + $completedplan = $lpg->create_plan(array('userid' => $user->id, 'status' => \core_competency\plan::STATUS_COMPLETE)); + + $lpg->create_plan_competency(array('planid' => $completedplan->get_id(), 'competencyid' => $c1->get_id())); + $lpg->create_plan_competency(array('planid' => $completedplan->get_id(), 'competencyid' => $c2->get_id())); + + $uc1 = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c1->get_id())); + $uc2 = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c2->get_id())); + + $ucp1 = $lpg->create_user_competency_plan(array('userid' => $user->id, 'competencyid' => $c1->get_id(), + 'planid' => $completedplan->get_id())); + $ucp2 = $lpg->create_user_competency_plan(array('userid' => $user->id, 'competencyid' => $c2->get_id(), + 'planid' => $completedplan->get_id())); + + api::delete_plan($completedplan->get_id()); + + // Check that achived user competencies are deleted. + $this->assertEquals(0, \core_competency\plan::count_records()); + $this->assertEquals(2, \core_competency\user_competency::count_records()); + $this->assertEquals(0, \core_competency\user_competency_plan::count_records()); + } + + /** + * Test listing of plan competencies. + */ + public function test_list_plan_competencies_manage_archived_competencies() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $syscontext = context_system::instance(); + + // Create user and role for the test. + $user = $dg->create_user(); + $viewrole = $dg->create_role(array( + 'name' => 'User view', + 'shortname' => 'view' + )); + assign_capability('moodle/competency:planviewdraft', CAP_ALLOW, $viewrole, $syscontext->id); + assign_capability('moodle/competency:planview', CAP_ALLOW, $viewrole, $syscontext->id); + $dg->role_assign($viewrole, $user->id, $syscontext->id); + $this->setUser($user); + + // Create a framework and assign competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Create draft plan with records in user_competency. + $draftplan = $lpg->create_plan(array('userid' => $user->id)); + + $lpg->create_plan_competency(array('planid' => $draftplan->get_id(), 'competencyid' => $c1->get_id())); + $lpg->create_plan_competency(array('planid' => $draftplan->get_id(), 'competencyid' => $c2->get_id())); + $lpg->create_plan_competency(array('planid' => $draftplan->get_id(), 'competencyid' => $c3->get_id())); + + $uc1 = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c1->get_id())); + $uc2 = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c2->get_id())); + + // Check that user_competency objects are returned when plan status is not complete. + $plancompetencies = api::list_plan_competencies($draftplan); + + $this->assertCount(3, $plancompetencies); + $this->assertInstanceOf('\core_competency\user_competency', $plancompetencies[0]->usercompetency); + $this->assertEquals($uc1->get_id(), $plancompetencies[0]->usercompetency->get_id()); + $this->assertNull($plancompetencies[0]->usercompetencyplan); + + $this->assertInstanceOf('\core_competency\user_competency', $plancompetencies[1]->usercompetency); + $this->assertEquals($uc2->get_id(), $plancompetencies[1]->usercompetency->get_id()); + $this->assertNull($plancompetencies[1]->usercompetencyplan); + + $this->assertInstanceOf('\core_competency\user_competency', $plancompetencies[2]->usercompetency); + $this->assertEquals(0, $plancompetencies[2]->usercompetency->get_id()); + $this->assertNull($plancompetencies[2]->usercompetencyplan); + + // Create completed plan with records in user_competency_plan. + $completedplan = $lpg->create_plan(array('userid' => $user->id, 'status' => \core_competency\plan::STATUS_COMPLETE)); + + $pc1 = $lpg->create_plan_competency(array('planid' => $completedplan->get_id(), 'competencyid' => $c1->get_id())); + $pc2 = $lpg->create_plan_competency(array('planid' => $completedplan->get_id(), 'competencyid' => $c2->get_id())); + $pc3 = $lpg->create_plan_competency(array('planid' => $completedplan->get_id(), 'competencyid' => $c3->get_id())); + + $ucp1 = $lpg->create_user_competency_plan(array('userid' => $user->id, 'competencyid' => $c1->get_id(), + 'planid' => $completedplan->get_id())); + $ucp2 = $lpg->create_user_competency_plan(array('userid' => $user->id, 'competencyid' => $c2->get_id(), + 'planid' => $completedplan->get_id())); + $ucp3 = $lpg->create_user_competency_plan(array('userid' => $user->id, 'competencyid' => $c3->get_id(), + 'planid' => $completedplan->get_id())); + + // Check that user_competency_plan objects are returned when plan status is complete. + $plancompetencies = api::list_plan_competencies($completedplan); + + $this->assertCount(3, $plancompetencies); + $this->assertInstanceOf('\core_competency\user_competency_plan', $plancompetencies[0]->usercompetencyplan); + $this->assertEquals($ucp1->get_id(), $plancompetencies[0]->usercompetencyplan->get_id()); + $this->assertNull($plancompetencies[0]->usercompetency); + $this->assertInstanceOf('\core_competency\user_competency_plan', $plancompetencies[1]->usercompetencyplan); + $this->assertEquals($ucp2->get_id(), $plancompetencies[1]->usercompetencyplan->get_id()); + $this->assertNull($plancompetencies[1]->usercompetency); + $this->assertInstanceOf('\core_competency\user_competency_plan', $plancompetencies[2]->usercompetencyplan); + $this->assertEquals($ucp3->get_id(), $plancompetencies[2]->usercompetencyplan->get_id()); + $this->assertNull($plancompetencies[2]->usercompetency); + } + + public function test_create_template_cohort() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $c1 = $dg->create_cohort(); + $c2 = $dg->create_cohort(); + $t1 = $lpg->create_template(); + $t2 = $lpg->create_template(); + + $this->assertEquals(0, \core_competency\template_cohort::count_records()); + + // Create two relations with mixed parameters. + $result = api::create_template_cohort($t1->get_id(), $c1->id); + $result = api::create_template_cohort($t1, $c2); + + $this->assertEquals(2, \core_competency\template_cohort::count_records()); + $this->assertInstanceOf('core_competency\template_cohort', $result); + $this->assertEquals($c2->id, $result->get_cohortid()); + $this->assertEquals($t1->get_id(), $result->get_templateid()); + $this->assertEquals(2, \core_competency\template_cohort::count_records_select('templateid = :id', + array('id' => $t1->get_id()))); + $this->assertEquals(0, \core_competency\template_cohort::count_records_select('templateid = :id', + array('id' => $t2->get_id()))); + } + + public function test_create_template_cohort_permissions() { + $this->resetAfterTest(true); + + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $cat = $dg->create_category(); + $catcontext = context_coursecat::instance($cat->id); + $syscontext = context_system::instance(); + + $user = $dg->create_user(); + $role = $dg->create_role(); + assign_capability('moodle/competency:templatemanage', CAP_ALLOW, $role, $syscontext->id, true); + $dg->role_assign($role, $user->id, $syscontext->id); + + $cohortrole = $dg->create_role(); + assign_capability('moodle/cohort:view', CAP_ALLOW, $cohortrole, $syscontext->id, true); + + accesslib_clear_all_caches_for_unit_testing(); + + $c1 = $dg->create_cohort(); + $c2 = $dg->create_cohort(array('visible' => 0, 'contextid' => $catcontext->id)); + $t1 = $lpg->create_template(); + + $this->assertEquals(0, \core_competency\template_cohort::count_records()); + + $this->setUser($user); + $result = api::create_template_cohort($t1, $c1); + $this->assertInstanceOf('core_competency\\template_cohort', $result); + + try { + $result = api::create_template_cohort($t1, $c2); + $this->fail('Permission required.'); + } catch (required_capability_exception $e) { + // That's what should happen. + } + + // Try again with the right permissions. + $dg->role_assign($cohortrole, $user->id, $catcontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + $result = api::create_template_cohort($t1, $c2); + $this->assertInstanceOf('core_competency\\template_cohort', $result); + } + + public function test_delete_template() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $c1 = $dg->create_cohort(); + $c2 = $dg->create_cohort(); + $template = $lpg->create_template(); + $id = $template->get_id(); + + // Create 2 template cohorts. + $tc1 = $lpg->create_template_cohort(array('templateid' => $template->get_id(), 'cohortid' => $c1->id)); + $tc1 = $lpg->create_template_cohort(array('templateid' => $template->get_id(), 'cohortid' => $c2->id)); + + // Check pre-test. + $this->assertTrue(\core_competency\template::record_exists($id)); + $this->assertEquals(2, \core_competency\template_cohort::count_records(array('templateid' => $id))); + + $result = api::delete_template($template->get_id()); + $this->assertTrue($result); + + // Check that the template deos not exist anymore. + $this->assertFalse(\core_competency\template::record_exists($id)); + + // Test if associated cohorts are also deleted. + $this->assertEquals(0, \core_competency\template_cohort::count_records(array('templateid' => $id))); + } + + public function test_delete_template_cohort() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $c1 = $dg->create_cohort(); + $c2 = $dg->create_cohort(); + $t1 = $lpg->create_template(); + $t2 = $lpg->create_template(); + $tc1 = $lpg->create_template_cohort(array('templateid' => $t1->get_id(), 'cohortid' => $c1->id)); + $tc1 = $lpg->create_template_cohort(array('templateid' => $t2->get_id(), 'cohortid' => $c2->id)); + + $this->assertEquals(2, \core_competency\template_cohort::count_records()); + $this->assertEquals(1, \core_competency\template_cohort::count_records_select('templateid = :id', + array('id' => $t1->get_id()))); + $this->assertEquals(1, \core_competency\template_cohort::count_records_select('templateid = :id', + array('id' => $t2->get_id()))); + + // Delete existing. + $result = api::delete_template_cohort($t1->get_id(), $c1->id); + $this->assertTrue($result); + $this->assertEquals(1, \core_competency\template_cohort::count_records()); + $this->assertEquals(0, \core_competency\template_cohort::count_records_select('templateid = :id', + array('id' => $t1->get_id()))); + $this->assertEquals(1, \core_competency\template_cohort::count_records_select('templateid = :id', + array('id' => $t2->get_id()))); + + // Delete non-existant. + $result = api::delete_template_cohort($t1->get_id(), $c1->id); + $this->assertTrue($result); + $this->assertEquals(1, \core_competency\template_cohort::count_records()); + $this->assertEquals(0, \core_competency\template_cohort::count_records_select('templateid = :id', + array('id' => $t1->get_id()))); + $this->assertEquals(1, \core_competency\template_cohort::count_records_select('templateid = :id', + array('id' => $t2->get_id()))); + } + + public function test_add_evidence_log() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + $u1ctx = context_user::instance($u1->id); + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + + // Creating a standard evidence with minimal information. + $evidence = api::add_evidence($u1->id, $c1->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_LOG, + 'invaliddata', 'error'); + $evidence->read(); + $uc = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1->get_id())); + $this->assertEquals(\core_competency\user_competency::STATUS_IDLE, $uc->get_status()); + $this->assertSame(null, $uc->get_grade()); + $this->assertSame(null, $uc->get_proficiency()); + $this->assertEquals($uc->get_id(), $evidence->get_usercompetencyid()); + $this->assertEquals($u1ctx->id, $evidence->get_contextid()); + $this->assertEquals(\core_competency\evidence::ACTION_LOG, $evidence->get_action()); + $this->assertEquals('invaliddata', $evidence->get_descidentifier()); + $this->assertEquals('error', $evidence->get_desccomponent()); + $this->assertSame(null, $evidence->get_desca()); + $this->assertSame(null, $evidence->get_url()); + $this->assertSame(null, $evidence->get_grade()); + $this->assertSame(null, $evidence->get_actionuserid()); + + // Creating a standard evidence with more information. + $evidence = api::add_evidence($u1->id, $c1->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_LOG, 'invaliddata', + 'error', '$a', false, 'http://moodle.org', null, 2, 'The evidence of prior learning were reviewed.'); + $evidence->read(); + $uc = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1->get_id())); + $this->assertEquals(\core_competency\user_competency::STATUS_IDLE, $uc->get_status()); + $this->assertSame(null, $uc->get_grade()); + $this->assertSame(null, $uc->get_proficiency()); + $this->assertEquals($uc->get_id(), $evidence->get_usercompetencyid()); + $this->assertEquals($u1ctx->id, $evidence->get_contextid()); + $this->assertEquals(\core_competency\evidence::ACTION_LOG, $evidence->get_action()); + $this->assertEquals('invaliddata', $evidence->get_descidentifier()); + $this->assertEquals('error', $evidence->get_desccomponent()); + $this->assertEquals('$a', $evidence->get_desca()); + $this->assertEquals('http://moodle.org', $evidence->get_url()); + $this->assertSame(null, $evidence->get_grade()); + $this->assertEquals(2, $evidence->get_actionuserid()); + $this->assertSame('The evidence of prior learning were reviewed.', $evidence->get_note()); + + // Creating a standard evidence and send for review. + $evidence = api::add_evidence($u1->id, $c2->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_LOG, 'invaliddata', + 'error', null, true); + $evidence->read(); + $uc = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c2->get_id())); + $this->assertEquals(\core_competency\user_competency::STATUS_WAITING_FOR_REVIEW, $uc->get_status()); + + // Trying to pass a grade should fail. + try { + $evidence = api::add_evidence($u1->id, $c1->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_LOG, 'invaliddata', + 'error', null, false, null, 1); + $this->fail('A grade can not be set'); + } catch (coding_exception $e) { + $this->assertRegExp('/grade MUST NOT be set/', $e->getMessage()); + } + } + + public function test_add_evidence_complete() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + $u1ctx = context_user::instance($u1->id); + $scale = $dg->create_scale(array('scale' => 'A,B,C,D')); + $scaleconfig = array(array('scaleid' => $scale->id)); + $scaleconfig[] = array('name' => 'B', 'id' => 2, 'scaledefault' => 1, 'proficient' => 0); + $scaleconfig[] = array('name' => 'C', 'id' => 3, 'scaledefault' => 0, 'proficient' => 1); + $scaleconfig[] = array('name' => 'D', 'id' => 4, 'scaledefault' => 0, 'proficient' => 1); + $c2scaleconfig = array(array('scaleid' => $scale->id)); + $c2scaleconfig[] = array('name' => 'B', 'id' => 2, 'scaledefault' => 0, 'proficient' => 1); + $c2scaleconfig[] = array('name' => 'C', 'id' => 3, 'scaledefault' => 0, 'proficient' => 0); + $c2scaleconfig[] = array('name' => 'D', 'id' => 4, 'scaledefault' => 1, 'proficient' => 1); + $f1 = $lpg->create_framework(array('scaleid' => $scale->id, 'scaleconfiguration' => $scaleconfig)); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'scaleid' => $scale->id, + 'scaleconfiguration' => $c2scaleconfig)); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + + // Creating an evidence with minimal information. + $evidence = api::add_evidence($u1->id, $c1->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_COMPLETE, 'invaliddata', + 'error'); + $evidence->read(); + $uc = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1->get_id())); + $this->assertEquals(\core_competency\user_competency::STATUS_IDLE, $uc->get_status()); + $this->assertEquals(2, $uc->get_grade()); // The grade has been set automatically to the framework default. + $this->assertEquals(0, $uc->get_proficiency()); + $this->assertEquals($uc->get_id(), $evidence->get_usercompetencyid()); + $this->assertEquals($u1ctx->id, $evidence->get_contextid()); + $this->assertEquals(\core_competency\evidence::ACTION_COMPLETE, $evidence->get_action()); + $this->assertEquals('invaliddata', $evidence->get_descidentifier()); + $this->assertEquals('error', $evidence->get_desccomponent()); + $this->assertSame(null, $evidence->get_desca()); + $this->assertSame(null, $evidence->get_url()); + $this->assertEquals(2, $evidence->get_grade()); + $this->assertSame(null, $evidence->get_actionuserid()); + + // Creating an evidence complete on competency with custom scale. + $evidence = api::add_evidence($u1->id, $c2->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_COMPLETE, 'invaliddata', + 'error'); + $evidence->read(); + $uc = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c2->get_id())); + $this->assertEquals(\core_competency\user_competency::STATUS_IDLE, $uc->get_status()); + $this->assertEquals(4, $uc->get_grade()); // The grade has been set automatically to the competency default. + $this->assertEquals(true, $uc->get_proficiency()); + $this->assertEquals($uc->get_id(), $evidence->get_usercompetencyid()); + $this->assertEquals($u1ctx->id, $evidence->get_contextid()); + $this->assertEquals(\core_competency\evidence::ACTION_COMPLETE, $evidence->get_action()); + $this->assertEquals('invaliddata', $evidence->get_descidentifier()); + $this->assertEquals('error', $evidence->get_desccomponent()); + $this->assertSame(null, $evidence->get_desca()); + $this->assertSame(null, $evidence->get_url()); + $this->assertEquals(4, $evidence->get_grade()); + $this->assertSame(null, $evidence->get_actionuserid()); + + // Creating an evidence complete on a user competency with an existing grade. + $uc = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c3->get_id(), 'grade' => 1, + 'proficiency' => 0)); + $this->assertEquals(1, $uc->get_grade()); + $this->assertEquals(0, $uc->get_proficiency()); + $evidence = api::add_evidence($u1->id, $c3->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_COMPLETE, 'invaliddata', + 'error'); + $evidence->read(); + $uc->read(); + $this->assertEquals(\core_competency\user_competency::STATUS_IDLE, $uc->get_status()); + $this->assertEquals(1, $uc->get_grade()); // The grade has not been changed. + $this->assertEquals(0, $uc->get_proficiency()); + $this->assertEquals($uc->get_id(), $evidence->get_usercompetencyid()); + $this->assertEquals($u1ctx->id, $evidence->get_contextid()); + $this->assertEquals(\core_competency\evidence::ACTION_COMPLETE, $evidence->get_action()); + $this->assertEquals('invaliddata', $evidence->get_descidentifier()); + $this->assertEquals('error', $evidence->get_desccomponent()); + $this->assertSame(null, $evidence->get_desca()); + $this->assertSame(null, $evidence->get_url()); + $this->assertEquals(2, $evidence->get_grade()); // The complete grade has been set. + $this->assertSame(null, $evidence->get_actionuserid()); + + // Creating a standard evidence and send for review. + $evidence = api::add_evidence($u1->id, $c2->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_COMPLETE, 'invaliddata', + 'error', null, true); + $evidence->read(); + $uc = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c2->get_id())); + $this->assertEquals(\core_competency\user_competency::STATUS_WAITING_FOR_REVIEW, $uc->get_status()); + + // Trying to pass a grade should throw an exception. + try { + api::add_evidence($u1->id, $c2->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_COMPLETE, 'invaliddata', + 'error', null, false, null, 1); + } catch (coding_exception $e) { + $this->assertRegExp('/grade MUST NOT be set/', $e->getMessage()); + } + } + + public function test_add_evidence_override() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + $u1ctx = context_user::instance($u1->id); + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + + // Creating an evidence with minimal information. + $evidence = api::add_evidence($u1->id, $c1->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_OVERRIDE, 'invaliddata', + 'error'); + $evidence->read(); + $uc = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1->get_id())); + $this->assertEquals(\core_competency\user_competency::STATUS_IDLE, $uc->get_status()); + $this->assertSame(null, $uc->get_grade()); // We overrode with 'null'. + $this->assertSame(null, $uc->get_proficiency()); + $this->assertEquals($uc->get_id(), $evidence->get_usercompetencyid()); + $this->assertEquals($u1ctx->id, $evidence->get_contextid()); + $this->assertEquals(\core_competency\evidence::ACTION_OVERRIDE, $evidence->get_action()); + $this->assertEquals('invaliddata', $evidence->get_descidentifier()); + $this->assertEquals('error', $evidence->get_desccomponent()); + $this->assertSame(null, $evidence->get_desca()); + $this->assertSame(null, $evidence->get_url()); + $this->assertSame(null, $evidence->get_grade()); // We overrode with 'null'. + $this->assertSame(null, $evidence->get_actionuserid()); + + // Creating an evidence with a grade information. + $evidence = api::add_evidence($u1->id, $c1->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_OVERRIDE, 'invaliddata', + 'error', null, false, null, 3); + $evidence->read(); + $uc = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1->get_id())); + $this->assertEquals(\core_competency\user_competency::STATUS_IDLE, $uc->get_status()); + $this->assertEquals(3, $uc->get_grade()); + $this->assertEquals(true, $uc->get_proficiency()); + $this->assertEquals($uc->get_id(), $evidence->get_usercompetencyid()); + $this->assertEquals($u1ctx->id, $evidence->get_contextid()); + $this->assertEquals(\core_competency\evidence::ACTION_OVERRIDE, $evidence->get_action()); + $this->assertEquals('invaliddata', $evidence->get_descidentifier()); + $this->assertEquals('error', $evidence->get_desccomponent()); + $this->assertSame(null, $evidence->get_desca()); + $this->assertSame(null, $evidence->get_url()); + $this->assertEquals(3, $evidence->get_grade()); + $this->assertSame(null, $evidence->get_actionuserid()); + + // Creating an evidence with another grade information. + $evidence = api::add_evidence($u1->id, $c1->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_OVERRIDE, 'invaliddata', + 'error', null, false, null, 1); + $evidence->read(); + $uc = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1->get_id())); + $this->assertEquals(\core_competency\user_competency::STATUS_IDLE, $uc->get_status()); + $this->assertEquals(1, $uc->get_grade()); + $this->assertEquals(0, $uc->get_proficiency()); + $this->assertEquals($uc->get_id(), $evidence->get_usercompetencyid()); + $this->assertEquals($u1ctx->id, $evidence->get_contextid()); + $this->assertEquals(\core_competency\evidence::ACTION_OVERRIDE, $evidence->get_action()); + $this->assertEquals('invaliddata', $evidence->get_descidentifier()); + $this->assertEquals('error', $evidence->get_desccomponent()); + $this->assertSame(null, $evidence->get_desca()); + $this->assertSame(null, $evidence->get_url()); + $this->assertEquals(1, $evidence->get_grade()); + $this->assertSame(null, $evidence->get_actionuserid()); + + // Creating reverting the grade and send for review. + $evidence = api::add_evidence($u1->id, $c1->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_OVERRIDE, 'invaliddata', + 'error', null, true); + $evidence->read(); + $uc = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1->get_id())); + $this->assertSame(null, $uc->get_grade()); + $this->assertSame(null, $uc->get_proficiency()); + $this->assertEquals(\core_competency\user_competency::STATUS_WAITING_FOR_REVIEW, $uc->get_status()); + $this->assertSame(null, $evidence->get_grade()); + } + + public function test_add_evidence_and_send_for_review() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + $u1ctx = context_user::instance($u1->id); + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + + // Non-existing user competencies are created up for review. + $evidence = api::add_evidence($u1->id, $c1->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_LOG, 'invaliddata', + 'error', null, true); + $uc = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1->get_id())); + $this->assertEquals(\core_competency\user_competency::STATUS_WAITING_FOR_REVIEW, $uc->get_status()); + + // Existing user competencies sent for review don't change. + $evidence = api::add_evidence($u1->id, $c1->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_LOG, 'invaliddata', + 'error', null, true); + $uc->read(); + $this->assertEquals(\core_competency\user_competency::STATUS_WAITING_FOR_REVIEW, $uc->get_status()); + + // A user competency with a status non-idle won't change. + $uc->set_status(\core_competency\user_competency::STATUS_IN_REVIEW); + $uc->update(); + $evidence = api::add_evidence($u1->id, $c1->get_id(), $u1ctx->id, \core_competency\evidence::ACTION_LOG, 'invaliddata', + 'error', null, true); + $uc->read(); + $this->assertEquals(\core_competency\user_competency::STATUS_IN_REVIEW, $uc->get_status()); + } + + /** + * Test add evidence for existing user_competency. + */ + public function test_add_evidence_existing_user_competency() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $syscontext = context_system::instance(); + + // Create users. + $user = $dg->create_user(); + $this->setUser($user); + + // Create a framework and assign competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $uc = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c1->get_id())); + $this->assertSame(null, $uc->get_grade()); + $this->assertSame(null, $uc->get_proficiency()); + + // Create an evidence and check it was created with the right usercomptencyid and information. + $evidence = api::add_evidence($user->id, $c1->get_id(), $syscontext->id, \core_competency\evidence::ACTION_OVERRIDE, + 'invalidevidencedesc', 'core_competency', array('a' => 'b'), false, 'http://moodle.org', 1, 2); + $this->assertEquals(1, \core_competency\evidence::count_records()); + + $evidence->read(); + $uc->read(); + $this->assertEquals($uc->get_id(), $evidence->get_usercompetencyid()); + $this->assertEquals('invalidevidencedesc', $evidence->get_descidentifier()); + $this->assertEquals('core_competency', $evidence->get_desccomponent()); + $this->assertEquals((object) array('a' => 'b'), $evidence->get_desca()); + $this->assertEquals('http://moodle.org', $evidence->get_url()); + $this->assertEquals(\core_competency\evidence::ACTION_OVERRIDE, $evidence->get_action()); + $this->assertEquals(2, $evidence->get_actionuserid()); + $this->assertEquals(1, $evidence->get_grade()); + $this->assertEquals(1, $uc->get_grade()); + $this->assertEquals(0, $uc->get_proficiency()); + } + + /** + * Test add evidence for non-existing user_competency. + */ + public function test_add_evidence_no_existing_user_competency() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $syscontext = context_system::instance(); + + // Create users. + $user = $dg->create_user(); + $this->setUser($user); + + // Create a framework and assign competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $this->assertEquals(0, \core_competency\user_competency::count_records()); + + // Create an evidence without a user competency record. + $evidence = api::add_evidence($user->id, $c1->get_id(), $syscontext->id, \core_competency\evidence::ACTION_OVERRIDE, + 'invalidevidencedesc', 'core_competency', 'Hello world!', false, 'http://moodle.org', 1, 2); + $this->assertEquals(1, \core_competency\evidence::count_records()); + $this->assertEquals(1, \core_competency\user_competency::count_records()); + + $uc = \core_competency\user_competency::get_record(array('userid' => $user->id, 'competencyid' => $c1->get_id())); + $evidence->read(); + $this->assertEquals($uc->get_id(), $evidence->get_usercompetencyid()); + $this->assertEquals('invalidevidencedesc', $evidence->get_descidentifier()); + $this->assertEquals('core_competency', $evidence->get_desccomponent()); + $this->assertEquals('Hello world!', $evidence->get_desca()); + $this->assertEquals('http://moodle.org', $evidence->get_url()); + $this->assertEquals(\core_competency\evidence::ACTION_OVERRIDE, $evidence->get_action()); + $this->assertEquals(2, $evidence->get_actionuserid()); + $this->assertEquals(1, $evidence->get_grade()); + $this->assertEquals(1, $uc->get_grade()); + $this->assertEquals(0, $uc->get_proficiency()); + } + + public function test_add_evidence_applies_competency_rules() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $syscontext = context_system::instance(); + $ctxid = $syscontext->id; + + $u1 = $dg->create_user(); + + // Setting up the framework. + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c2->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c3a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c3->get_id())); + $c4 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c4a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c4->get_id())); + $c5 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + + // Setting up the rules. + $c1->set_ruletype('core_competency\\competency_rule_all'); + $c1->set_ruleoutcome(\core_competency\competency::OUTCOME_COMPLETE); + $c1->update(); + $c2->set_ruletype('core_competency\\competency_rule_all'); + $c2->set_ruleoutcome(\core_competency\competency::OUTCOME_RECOMMEND); + $c2->update(); + $c3->set_ruletype('core_competency\\competency_rule_all'); + $c3->set_ruleoutcome(\core_competency\competency::OUTCOME_EVIDENCE); + $c3->update(); + $c4->set_ruletype('core_competency\\competency_rule_all'); + $c4->set_ruleoutcome(\core_competency\competency::OUTCOME_NONE); + $c4->update(); + + // Confirm the current data. + $this->assertEquals(0, user_competency::count_records()); + $this->assertEquals(0, evidence::count_records()); + + // Let's do this! + // First let's confirm that evidence not marking a completion have no impact. + api::add_evidence($u1->id, $c1a, $ctxid, evidence::ACTION_LOG, 'commentincontext', 'core'); + $uc1a = user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1a->get_id())); + $this->assertSame(null, $uc1a->get_proficiency()); + $this->assertFalse(user_competency::record_exists_select('userid = ? AND competencyid = ?', array($u1->id, $c1->get_id()))); + + // Now let's try complete a competency but the rule won't match (not all children are complete). + // The parent (the thing with the rule) will be created but won't have any evidence attached, and not + // not be marked as completed. + api::add_evidence($u1->id, $c1a, $ctxid, evidence::ACTION_COMPLETE, 'commentincontext', 'core'); + $uc1a = user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1a->get_id())); + $this->assertEquals(true, $uc1a->get_proficiency()); + $uc1 = user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1->get_id())); + $this->assertSame(null, $uc1->get_proficiency()); + $this->assertEquals(0, evidence::count_records(array('usercompetencyid' => $uc1->get_id()))); + + // Now we complete the other child. That will mark the parent as complete with an evidence. + api::add_evidence($u1->id, $c1b, $ctxid, evidence::ACTION_COMPLETE, 'commentincontext', 'core'); + $uc1b = user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1b->get_id())); + $this->assertEquals(true, $uc1a->get_proficiency()); + $uc1 = user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c1->get_id())); + $this->assertEquals(true, $uc1->get_proficiency()); + $this->assertEquals(user_competency::STATUS_IDLE, $uc1->get_status()); + $this->assertEquals(1, evidence::count_records(array('usercompetencyid' => $uc1->get_id()))); + + // Check rule recommending. + api::add_evidence($u1->id, $c2a, $ctxid, evidence::ACTION_COMPLETE, 'commentincontext', 'core'); + $uc2a = user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c2a->get_id())); + $this->assertEquals(true, $uc1a->get_proficiency()); + $uc2 = user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c2->get_id())); + $this->assertSame(null, $uc2->get_proficiency()); + $this->assertEquals(user_competency::STATUS_WAITING_FOR_REVIEW, $uc2->get_status()); + $this->assertEquals(1, evidence::count_records(array('usercompetencyid' => $uc2->get_id()))); + + // Check rule evidence. + api::add_evidence($u1->id, $c3a, $ctxid, evidence::ACTION_COMPLETE, 'commentincontext', 'core'); + $uc3a = user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c3a->get_id())); + $this->assertEquals(true, $uc1a->get_proficiency()); + $uc3 = user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c3->get_id())); + $this->assertSame(null, $uc3->get_proficiency()); + $this->assertEquals(user_competency::STATUS_IDLE, $uc3->get_status()); + $this->assertEquals(1, evidence::count_records(array('usercompetencyid' => $uc3->get_id()))); + + // Check rule nothing. + api::add_evidence($u1->id, $c4a, $ctxid, evidence::ACTION_COMPLETE, 'commentincontext', 'core'); + $uc4a = user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c4a->get_id())); + $this->assertEquals(true, $uc1a->get_proficiency()); + $this->assertFalse(user_competency::record_exists_select('userid = ? AND competencyid = ?', array($u1->id, $c4->get_id()))); + + // Check marking on something that has no parent. This just checks that nothing breaks. + api::add_evidence($u1->id, $c5, $ctxid, evidence::ACTION_COMPLETE, 'commentincontext', 'core'); + } + + /** + * Tests for the user_competency_course data when api::add_evidence() is invoked when + * grading a user competency in the system context. + */ + public function test_add_evidence_for_user_competency_course_grade_outside_course() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $syscontext = context_system::instance(); + + // Create a student. + $student = $dg->create_user(); + + // Create a competency for the course. + $lpg = $dg->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $comp = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Add evidence. + api::add_evidence($student->id, $comp, $syscontext, evidence::ACTION_OVERRIDE, + 'commentincontext', 'core', null, false, null, 1); + + // Query for user_competency_course data. + $filterparams = array( + 'userid' => $student->id, + 'competencyid' => $comp->get_id(), + ); + $usercompcourse = \core_competency\user_competency_course::get_record($filterparams); + // There should be no user_competency_course object created when grading. + $this->assertFalse($usercompcourse); + } + + /** + * Tests for the user_competency_course data when api::add_evidence() is invoked when + * grading a user competency in a course. + */ + public function test_add_evidence_user_competency_course_grade_in_course() { + global $USER; + + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + + // Create a course. + $course = $dg->create_course(); + $record = array('courseid' => $course->id, 'pushratingstouserplans' => false); + $settings = new course_competency_settings(0, (object) $record); + $settings->create(); + $coursecontext = context_course::instance($course->id); + + // Create a student and enrol into the course. + $student = $dg->create_user(); + $studentarch = get_archetype_roles('student'); + $studentrole = array_shift($studentarch); + $dg->role_assign($studentrole->id, $student->id, $coursecontext->id); + $dg->enrol_user($student->id, $course->id, $studentrole->id); + + // Create a competency for the course. + $lpg = $dg->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + // Do not push ratings from course to user plans. + $comp = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $lpg->create_course_competency(array('courseid' => $course->id, 'competencyid' => $comp->get_id())); + + // Query for user_competency_course data. + $filterparams = array( + 'userid' => $student->id, + 'competencyid' => $comp->get_id(), + 'courseid' => $course->id + ); + + // Add evidence that sets a grade to the course. + $evidence = api::add_evidence($student->id, $comp, $coursecontext, evidence::ACTION_OVERRIDE, + 'commentincontext', 'core', null, false, null, 3, $USER->id); + // Get user competency course record. + $usercompcourse = \core_competency\user_competency_course::get_record($filterparams); + // There should be a user_competency_course object when adding a grade. + $this->assertNotEmpty($usercompcourse); + $grade = $evidence->get_grade(); + $this->assertEquals($grade, $usercompcourse->get_grade()); + $this->assertEquals(3, $usercompcourse->get_grade()); + $proficiency = $comp->get_proficiency_of_grade($grade); + $this->assertEquals($proficiency, $usercompcourse->get_proficiency()); + + // Confirm that the user competency's grade/proficiency has not been affected by the grade. + $usercompetencyparams = [ + 'userid' => $student->id, + 'competencyid' => $comp->get_id(), + ]; + $usercompetency = \core_competency\user_competency::get_record($usercompetencyparams); + $this->assertNotEmpty($usercompetency); + $this->assertNotEquals($usercompcourse->get_grade(), $usercompetency->get_grade()); + $this->assertNotEquals($usercompcourse->get_proficiency(), $usercompetency->get_proficiency()); + } + + public function test_observe_course_completed() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + // Set-up users, framework, competencies and course competencies. + $course = $dg->create_course(); + $coursectx = context_course::instance($course->id); + $u1 = $dg->create_user(); + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c4 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $cc1 = $lpg->create_course_competency(array('competencyid' => $c1->get_id(), 'courseid' => $course->id, + 'ruleoutcome' => \core_competency\course_competency::OUTCOME_NONE)); + $cc2 = $lpg->create_course_competency(array('competencyid' => $c2->get_id(), 'courseid' => $course->id, + 'ruleoutcome' => \core_competency\course_competency::OUTCOME_EVIDENCE)); + $cc3 = $lpg->create_course_competency(array('competencyid' => $c3->get_id(), 'courseid' => $course->id, + 'ruleoutcome' => \core_competency\course_competency::OUTCOME_RECOMMEND)); + $cc4 = $lpg->create_course_competency(array('competencyid' => $c4->get_id(), 'courseid' => $course->id, + 'ruleoutcome' => \core_competency\course_competency::OUTCOME_COMPLETE)); + + $event = \core\event\course_completed::create(array( + 'objectid' => 1, + 'relateduserid' => $u1->id, + 'context' => $coursectx, + 'courseid' => $course->id, + 'other' => array('relateduserid' => $u1->id) + )); + $this->assertEquals(0, \core_competency\user_competency::count_records()); + $this->assertEquals(0, \core_competency\evidence::count_records()); + + // Let's go! + api::observe_course_completed($event); + $this->assertEquals(3, \core_competency\user_competency::count_records()); + $this->assertEquals(3, \core_competency\evidence::count_records()); + + // Outcome NONE did nothing. + $this->assertFalse(\core_competency\user_competency::record_exists_select('userid = :uid AND competencyid = :cid', array( + 'uid' => $u1->id, 'cid' => $c1->get_id() + ))); + + // Outcome evidence. + $uc2 = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c2->get_id())); + $ev2 = \core_competency\evidence::get_record(array('usercompetencyid' => $uc2->get_id())); + + $this->assertEquals(null, $uc2->get_grade()); + $this->assertEquals(null, $uc2->get_proficiency()); + $this->assertEquals(\core_competency\user_competency::STATUS_IDLE, $uc2->get_status()); + + $this->assertEquals('evidence_coursecompleted', $ev2->get_descidentifier()); + $this->assertEquals('core_competency', $ev2->get_desccomponent()); + $this->assertEquals($course->shortname, $ev2->get_desca()); + $this->assertStringEndsWith('/report/completion/index.php?course=' . $course->id, $ev2->get_url()); + $this->assertEquals(null, $ev2->get_grade()); + $this->assertEquals($coursectx->id, $ev2->get_contextid()); + $this->assertEquals(\core_competency\evidence::ACTION_LOG, $ev2->get_action()); + $this->assertEquals(null, $ev2->get_actionuserid()); + + // Outcome recommend. + $uc3 = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c3->get_id())); + $ev3 = \core_competency\evidence::get_record(array('usercompetencyid' => $uc3->get_id())); + + $this->assertEquals(null, $uc3->get_grade()); + $this->assertEquals(null, $uc3->get_proficiency()); + $this->assertEquals(\core_competency\user_competency::STATUS_WAITING_FOR_REVIEW, $uc3->get_status()); + + $this->assertEquals('evidence_coursecompleted', $ev3->get_descidentifier()); + $this->assertEquals('core_competency', $ev3->get_desccomponent()); + $this->assertEquals($course->shortname, $ev3->get_desca()); + $this->assertStringEndsWith('/report/completion/index.php?course=' . $course->id, $ev3->get_url()); + $this->assertEquals(null, $ev3->get_grade()); + $this->assertEquals($coursectx->id, $ev3->get_contextid()); + $this->assertEquals(\core_competency\evidence::ACTION_LOG, $ev3->get_action()); + $this->assertEquals(null, $ev3->get_actionuserid()); + + // Outcome complete. + $uc4 = \core_competency\user_competency::get_record(array('userid' => $u1->id, 'competencyid' => $c4->get_id())); + $ev4 = \core_competency\evidence::get_record(array('usercompetencyid' => $uc4->get_id())); + + $this->assertEquals(3, $uc4->get_grade()); + $this->assertEquals(1, $uc4->get_proficiency()); + $this->assertEquals(\core_competency\user_competency::STATUS_IDLE, $uc4->get_status()); + + $this->assertEquals('evidence_coursecompleted', $ev4->get_descidentifier()); + $this->assertEquals('core_competency', $ev4->get_desccomponent()); + $this->assertEquals($course->shortname, $ev4->get_desca()); + $this->assertStringEndsWith('/report/completion/index.php?course=' . $course->id, $ev4->get_url()); + $this->assertEquals(3, $ev4->get_grade()); + $this->assertEquals($coursectx->id, $ev4->get_contextid()); + $this->assertEquals(\core_competency\evidence::ACTION_COMPLETE, $ev4->get_action()); + $this->assertEquals(null, $ev4->get_actionuserid()); + } + + public function test_list_course_modules_using_competency() { + global $SITE; + + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $course = $dg->create_course(); + $course2 = $dg->create_course(); + + $this->setAdminUser(); + $f = $lpg->create_framework(); + $c = $lpg->create_competency(array('competencyframeworkid' => $f->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f->get_id())); + $cc = api::add_competency_to_course($course->id, $c->get_id()); + $cc2 = api::add_competency_to_course($course->id, $c2->get_id()); + + // First check we get an empty list when there are no links. + $expected = array(); + $result = api::list_course_modules_using_competency($c->get_id(), $course->id); + $this->assertEquals($expected, $result); + + $pagegenerator = $this->getDataGenerator()->get_plugin_generator('mod_page'); + $page = $pagegenerator->create_instance(array('course' => $course->id)); + + $cm = get_coursemodule_from_instance('page', $page->id); + // Add a link and list again. + $ccm = api::add_competency_to_course_module($cm, $c->get_id()); + $expected = array($cm->id); + $result = api::list_course_modules_using_competency($c->get_id(), $course->id); + $this->assertEquals($expected, $result); + + // Check a different course. + $expected = array(); + $result = api::list_course_modules_using_competency($c->get_id(), $course2->id); + $this->assertEquals($expected, $result); + + // Remove the link and check again. + $result = api::remove_competency_from_course_module($cm, $c->get_id()); + $expected = true; + $this->assertEquals($expected, $result); + $expected = array(); + $result = api::list_course_modules_using_competency($c->get_id(), $course->id); + $this->assertEquals($expected, $result); + + // Now add 2 links. + api::add_competency_to_course_module($cm, $c->get_id()); + api::add_competency_to_course_module($cm, $c2->get_id()); + $result = api::list_course_module_competencies_in_course_module($cm->id); + $this->assertEquals($result[0]->get_competencyid(), $c->get_id()); + $this->assertEquals($result[1]->get_competencyid(), $c2->get_id()); + + // Now re-order. + api::reorder_course_module_competency($cm, $c->get_id(), $c2->get_id()); + $result = api::list_course_module_competencies_in_course_module($cm->id); + $this->assertEquals($result[0]->get_competencyid(), $c2->get_id()); + $this->assertEquals($result[1]->get_competencyid(), $c->get_id()); + + // And re-order again. + api::reorder_course_module_competency($cm, $c->get_id(), $c2->get_id()); + $result = api::list_course_module_competencies_in_course_module($cm->id); + $this->assertEquals($result[0]->get_competencyid(), $c->get_id()); + $this->assertEquals($result[1]->get_competencyid(), $c2->get_id()); + } + + /** + * Test update ruleoutcome for course_competency. + */ + public function test_set_ruleoutcome_course_competency() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $course = $dg->create_course(); + + $this->setAdminUser(); + $f = $lpg->create_framework(); + $c = $lpg->create_competency(array('competencyframeworkid' => $f->get_id())); + $cc = api::add_competency_to_course($course->id, $c->get_id()); + + // Check record was created with default rule value Evidence. + $this->assertEquals(1, \core_competency\course_competency::count_records()); + $recordscc = api::list_course_competencies($course->id); + $this->assertEquals(\core_competency\course_competency::OUTCOME_EVIDENCE, + $recordscc[0]['coursecompetency']->get_ruleoutcome()); + + // Check ruleoutcome value is updated to None. + $this->assertTrue(api::set_course_competency_ruleoutcome($recordscc[0]['coursecompetency']->get_id(), + \core_competency\course_competency::OUTCOME_NONE)); + $recordscc = api::list_course_competencies($course->id); + $this->assertEquals(\core_competency\course_competency::OUTCOME_NONE, $recordscc[0]['coursecompetency']->get_ruleoutcome()); + } + + /** + * Test validation on grade on user_competency. + */ + public function test_validate_grade_in_user_competency() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + + $dg->create_scale(array("id" => "1", "scale" => "value1, value2")); + $dg->create_scale(array("id" => "2", "scale" => "value3, value4, value5, value6")); + + $scaleconfiguration1 = '[{"scaleid":"1"},{"name":"value1","id":1,"scaledefault":1,"proficient":0},' . + '{"name":"value2","id":2,"scaledefault":0,"proficient":1}]'; + $scaleconfiguration2 = '[{"scaleid":"2"},{"name":"value3","id":1,"scaledefault":1,"proficient":0},' + . '{"name":"value4","id":2,"scaledefault":0,"proficient":1}]'; + + // Create a framework with scale configuration1. + $frm = array( + 'scaleid' => 1, + 'scaleconfiguration' => $scaleconfiguration1 + ); + $framework = $lpg->create_framework($frm); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Create competency with its own scale configuration. + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), + 'scaleid' => 2, + 'scaleconfiguration' => $scaleconfiguration2 + )); + + // Detecte invalid grade in competency using its framework competency scale. + try { + $usercompetency = new user_competency(0, (object) array('userid' => $user->id, 'competencyid' => $c1->get_id(), + 'proficiency' => true, 'grade' => 3 )); + $usercompetency->create(); + $this->fail('Invalid grade not detected in framework scale'); + } catch (\core_competency\invalid_persistent_exception $e) { + $this->assertTrue(true); + } + + // Detecte invalid grade in competency using its own scale. + try { + $usercompetency = new user_competency(0, (object) array('userid' => $user->id, 'competencyid' => $c2->get_id(), + 'proficiency' => true, 'grade' => 5 )); + $usercompetency->create(); + $this->fail('Invalid grade not detected in competency scale'); + } catch (\core_competency\invalid_persistent_exception $e) { + $this->assertTrue(true); + } + + // Accept valid grade in competency using its framework competency scale. + try { + $usercompetency = new user_competency(0, (object) array('userid' => $user->id, 'competencyid' => $c1->get_id(), + 'proficiency' => true, 'grade' => 1 )); + $usercompetency->create(); + $this->assertTrue(true); + } catch (\core_competency\invalid_persistent_exception $e) { + $this->fail('Valide grade rejected in framework scale'); + } + + // Accept valid grade in competency using its framework competency scale. + try { + $usercompetency = new user_competency(0, (object) array('userid' => $user->id, 'competencyid' => $c2->get_id(), + 'proficiency' => true, 'grade' => 4 )); + $usercompetency->create(); + $this->assertTrue(true); + } catch (\core_competency\invalid_persistent_exception $e) { + $this->fail('Valide grade rejected in competency scale'); + } + } + + /** + * Test when adding competency that belong to hidden framework to plan/template/course. + */ + public function test_hidden_framework() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + + // Create a course. + $cat1 = $dg->create_category(); + $course = $dg->create_course(array('category' => $cat1->id)); + // Create a template. + $template = $lpg->create_template(); + // Create a plan. + $plan = $lpg->create_plan(array('userid' => $user->id)); + + // Create a hidden framework. + $frm = array( + 'visible' => false + ); + $framework = $lpg->create_framework($frm); + $competency = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Linking competency that belong to hidden framework to course. + try { + api::add_competency_to_course($course->id, $competency->get_id()); + $this->fail('A competency belonging to hidden framework can not be linked to course'); + } catch (coding_exception $e) { + $this->assertTrue(true); + } + + // Adding competency that belong to hidden framework to template. + try { + api::add_competency_to_template($template->get_id(), $competency->get_id()); + $this->fail('A competency belonging to hidden framework can not be added to template'); + } catch (coding_exception $e) { + $this->assertTrue(true); + } + + // Adding competency that belong to hidden framework to plan. + try { + api::add_competency_to_plan($plan->get_id(), $competency->get_id()); + $this->fail('A competency belonging to hidden framework can not be added to plan'); + } catch (coding_exception $e) { + $this->assertTrue(true); + } + } + + /** + * Test when using hidden template in plan/cohort. + */ + public function test_hidden_template() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + + // Create a cohort. + $cohort = $dg->create_cohort(); + // Create a hidden template. + $template = $lpg->create_template(array('visible' => false)); + + // Can not link hidden template to plan. + try { + api::create_plan_from_template($template->get_id(), $user->id); + $this->fail('Can not link a hidden template to plan'); + } catch (coding_exception $e) { + $this->assertTrue(true); + } + + // Can associate hidden template to cohort. + $templatecohort = api::create_template_cohort($template->get_id(), $cohort->id); + $this->assertInstanceOf('\core_competency\template_cohort', $templatecohort); + } + + /** + * Test that completed plan created form a template does not change when template is modified. + */ + public function test_completed_plan_doesnot_change() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + + // Create a framework and assign competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Create template and assign competencies. + $tp = $lpg->create_template(); + $tpc1 = $lpg->create_template_competency(array('templateid' => $tp->get_id(), 'competencyid' => $c1->get_id())); + $tpc2 = $lpg->create_template_competency(array('templateid' => $tp->get_id(), 'competencyid' => $c2->get_id())); + $tpc3 = $lpg->create_template_competency(array('templateid' => $tp->get_id(), 'competencyid' => $c3->get_id())); + + // Create a plan form template and change it status to complete. + $plan = $lpg->create_plan(array('userid' => $user->id, 'templateid' => $tp->get_id())); + api::complete_plan($plan); + + // Check user competency plan created correctly. + $this->assertEquals(3, \core_competency\user_competency_plan::count_records()); + $ucp = \core_competency\user_competency_plan::get_records(); + $this->assertEquals($ucp[0]->get_competencyid(), $c1->get_id()); + $this->assertEquals($ucp[1]->get_competencyid(), $c2->get_id()); + $this->assertEquals($ucp[2]->get_competencyid(), $c3->get_id()); + + // Add and remove a competency from the template. + api::add_competency_to_template($tp->get_id(), $c4->get_id()); + api::remove_competency_from_template($tp->get_id(), $c1->get_id()); + + // Check that user competency plan did not change. + $competencies = $plan->get_competencies(); + $this->assertEquals(3, count($competencies)); + $ucp1 = array($c1->get_id(), $c2->get_id(), $c3->get_id()); + $ucp2 = array(); + foreach ($competencies as $id => $cmp) { + $ucp2[] = $id; + } + $this->assertEquals(0, count(array_diff($ucp1, $ucp2))); + } + + protected function setup_framework_for_reset_rules_tests() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $this->setAdminUser(); + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1a1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c1a1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1b1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c1b1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + + $c1->set_ruleoutcome(competency::OUTCOME_EVIDENCE); + $c1->set_ruletype('core_competency\\competency_rule_all'); + $c1->update(); + $c1a->set_ruleoutcome(competency::OUTCOME_EVIDENCE); + $c1a->set_ruletype('core_competency\\competency_rule_all'); + $c1a->update(); + $c1a1->set_ruleoutcome(competency::OUTCOME_EVIDENCE); + $c1a1->set_ruletype('core_competency\\competency_rule_all'); + $c1a1->update(); + $c1b->set_ruleoutcome(competency::OUTCOME_EVIDENCE); + $c1b->set_ruletype('core_competency\\competency_rule_all'); + $c1b->update(); + $c2->set_ruleoutcome(competency::OUTCOME_EVIDENCE); + $c2->set_ruletype('core_competency\\competency_rule_all'); + $c2->update(); + + return array( + 'f1' => $f1, + 'c1' => $c1, + 'c1a' => $c1a, + 'c1a1' => $c1a1, + 'c1a1a' => $c1a1a, + 'c1b' => $c1b, + 'c1b1' => $c1b1, + 'c1b1a' => $c1b1a, + 'c2' => $c2, + 'c2a' => $c2a, + ); + } + + public function test_moving_competency_reset_rules_updown() { + $data = $this->setup_framework_for_reset_rules_tests(); + $f1 = $data['f1']; + $c1 = $data['c1']; + $c1a = $data['c1a']; + $c1a1 = $data['c1a1']; + $c1a1a = $data['c1a1a']; + $c1b = $data['c1b']; + $c1b1 = $data['c1b1']; + $c1b1a = $data['c1b1a']; + $c2 = $data['c2']; + $c2a = $data['c2a']; + + // Moving up and down doesn't change anything. + api::move_down_competency($c1a->get_id()); + $c1->read(); + $c1a->read(); + $c1a1->read(); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1a->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1a1->get_ruleoutcome()); + api::move_up_competency($c1a->get_id()); + $c1->read(); + $c1a->read(); + $c1a1->read(); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1a->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1a1->get_ruleoutcome()); + } + + public function test_moving_competency_reset_rules_parent() { + $data = $this->setup_framework_for_reset_rules_tests(); + $f1 = $data['f1']; + $c1 = $data['c1']; + $c1a = $data['c1a']; + $c1a1 = $data['c1a1']; + $c1a1a = $data['c1a1a']; + $c1b = $data['c1b']; + $c1b1 = $data['c1b1']; + $c1b1a = $data['c1b1a']; + $c2 = $data['c2']; + $c2a = $data['c2a']; + + // Moving out of parent will reset the parent, and the destination. + api::set_parent_competency($c1a->get_id(), $c1b->get_id()); + $c1->read(); + $c1a->read(); + $c1a1->read(); + $c1b->read(); + $c2->read(); + $this->assertEquals(competency::OUTCOME_NONE, $c1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1a->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1a1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_NONE, $c1b->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c2->get_ruleoutcome()); + } + + public function test_moving_competency_reset_rules_totoplevel() { + $data = $this->setup_framework_for_reset_rules_tests(); + $f1 = $data['f1']; + $c1 = $data['c1']; + $c1a = $data['c1a']; + $c1a1 = $data['c1a1']; + $c1a1a = $data['c1a1a']; + $c1b = $data['c1b']; + $c1b1 = $data['c1b1']; + $c1b1a = $data['c1b1a']; + $c2 = $data['c2']; + $c2a = $data['c2a']; + + // Moving to top level only affects the initial parent. + api::set_parent_competency($c1a1->get_id(), 0); + $c1->read(); + $c1a->read(); + $c1a1->read(); + $c1b->read(); + $c2->read(); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_NONE, $c1a->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1a1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1b->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c2->get_ruleoutcome()); + } + + public function test_moving_competency_reset_rules_fromtoplevel() { + $data = $this->setup_framework_for_reset_rules_tests(); + $f1 = $data['f1']; + $c1 = $data['c1']; + $c1a = $data['c1a']; + $c1a1 = $data['c1a1']; + $c1a1a = $data['c1a1a']; + $c1b = $data['c1b']; + $c1b1 = $data['c1b1']; + $c1b1a = $data['c1b1a']; + $c2 = $data['c2']; + $c2a = $data['c2a']; + + // Moving from top level only affects the destination parent. + api::set_parent_competency($c2->get_id(), $c1a1->get_id()); + $c1->read(); + $c1a->read(); + $c1a1->read(); + $c1b->read(); + $c2->read(); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1a->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_NONE, $c1a1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1b->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c2->get_ruleoutcome()); + } + + public function test_moving_competency_reset_rules_child() { + $data = $this->setup_framework_for_reset_rules_tests(); + $f1 = $data['f1']; + $c1 = $data['c1']; + $c1a = $data['c1a']; + $c1a1 = $data['c1a1']; + $c1a1a = $data['c1a1a']; + $c1b = $data['c1b']; + $c1b1 = $data['c1b1']; + $c1b1a = $data['c1b1a']; + $c2 = $data['c2']; + $c2a = $data['c2a']; + + // Moving to a child of self resets self, parent and destination. + api::set_parent_competency($c1a->get_id(), $c1a1->get_id()); + $c1->read(); + $c1a->read(); + $c1a1->read(); + $c1b->read(); + $c2->read(); + $this->assertEquals(competency::OUTCOME_NONE, $c1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_NONE, $c1a->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_NONE, $c1a1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1b->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c2->get_ruleoutcome()); + } + + public function test_create_competency_reset_rules() { + $data = $this->setup_framework_for_reset_rules_tests(); + $f1 = $data['f1']; + $c1 = $data['c1']; + $c1a = $data['c1a']; + $c1a1 = $data['c1a1']; + $c1a1a = $data['c1a1a']; + $c1b = $data['c1b']; + $c1b1 = $data['c1b1']; + $c1b1a = $data['c1b1a']; + $c2 = $data['c2']; + $c2a = $data['c2a']; + + // Adding a new competency resets the rule of its parent. + api::create_competency((object) array('shortname' => 'A', 'parentid' => $c1->get_id(), 'idnumber' => 'A', + 'competencyframeworkid' => $f1->get_id())); + $c1->read(); + $c1a->read(); + $c1a1->read(); + $c1b->read(); + $c2->read(); + $this->assertEquals(competency::OUTCOME_NONE, $c1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1a->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1a1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1b->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c2->get_ruleoutcome()); + } + + public function test_delete_competency_reset_rules() { + $data = $this->setup_framework_for_reset_rules_tests(); + $f1 = $data['f1']; + $c1 = $data['c1']; + $c1a = $data['c1a']; + $c1a1 = $data['c1a1']; + $c1a1a = $data['c1a1a']; + $c1b = $data['c1b']; + $c1b1 = $data['c1b1']; + $c1b1a = $data['c1b1a']; + $c2 = $data['c2']; + $c2a = $data['c2a']; + + // Deleting a competency resets the rule of its parent. + api::delete_competency($c1a->get_id()); + $c1->read(); + $c1b->read(); + $c2->read(); + $this->assertEquals(competency::OUTCOME_NONE, $c1->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c1b->get_ruleoutcome()); + $this->assertEquals(competency::OUTCOME_EVIDENCE, $c2->get_ruleoutcome()); + } + + public function test_template_has_related_data() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $dg = $this->getDataGenerator(); + $user = $dg->create_user(); + $lpg = $dg->get_plugin_generator('core_competency'); + $tpl1 = $lpg->create_template(); + $tpl2 = $lpg->create_template(); + + // Create plans for first template. + $time = time(); + $plan1 = $lpg->create_plan(array('templateid' => $tpl1->get_id(), 'userid' => $user->id, + 'name' => 'Not good name', 'duedate' => $time + 3600, 'description' => 'Ahah', 'descriptionformat' => FORMAT_PLAIN)); + + $this->assertTrue(api::template_has_related_data($tpl1->get_id())); + $this->assertFalse(api::template_has_related_data($tpl2->get_id())); + + } + + public function test_delete_template_delete_plans() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + $f = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f->get_id())); + + $tpl = $lpg->create_template(); + + $tplc1 = $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $c1->get_id(), + 'sortorder' => 1)); + $tplc2 = $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $c2->get_id(), + 'sortorder' => 2)); + + $p1 = $lpg->create_plan(array('templateid' => $tpl->get_id(), 'userid' => $u1->id)); + + // Check pre-test. + $this->assertTrue(\core_competency\template::record_exists($tpl->get_id())); + $this->assertEquals(2, \core_competency\template_competency::count_competencies($tpl->get_id())); + $this->assertEquals(1, count(\core_competency\plan::get_records(array('templateid' => $tpl->get_id())))); + + $result = api::delete_template($tpl->get_id(), true); + $this->assertTrue($result); + + // Check that the template does not exist anymore. + $this->assertFalse(\core_competency\template::record_exists($tpl->get_id())); + + // Check that associated competencies are also deleted. + $this->assertEquals(0, \core_competency\template_competency::count_competencies($tpl->get_id())); + + // Check that associated plan are also deleted. + $this->assertEquals(0, count(\core_competency\plan::get_records(array('templateid' => $tpl->get_id())))); + } + + public function test_delete_template_unlink_plans() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + $f = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f->get_id())); + + $tpl = $lpg->create_template(); + + $tplc1 = $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $c1->get_id(), + 'sortorder' => 1)); + $tplc2 = $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $c2->get_id(), + 'sortorder' => 2)); + + $p1 = $lpg->create_plan(array('templateid' => $tpl->get_id(), 'userid' => $u1->id)); + + // Check pre-test. + $this->assertTrue(\core_competency\template::record_exists($tpl->get_id())); + $this->assertEquals(2, \core_competency\template_competency::count_competencies($tpl->get_id())); + $this->assertEquals(1, count(\core_competency\plan::get_records(array('templateid' => $tpl->get_id())))); + + $result = api::delete_template($tpl->get_id(), false); + $this->assertTrue($result); + + // Check that the template does not exist anymore. + $this->assertFalse(\core_competency\template::record_exists($tpl->get_id())); + + // Check that associated competencies are also deleted. + $this->assertEquals(0, \core_competency\template_competency::count_competencies($tpl->get_id())); + + // Check that associated plan still exist but unlink from template. + $plans = \core_competency\plan::get_records(array('id' => $p1->get_id())); + $this->assertEquals(1, count($plans)); + $this->assertEquals($plans[0]->get_origtemplateid(), $tpl->get_id()); + $this->assertNull($plans[0]->get_templateid()); + } + + public function test_delete_competency() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $u1 = $dg->create_user(); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c11b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c12b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + + // Set rules on parent competency. + $c1->set_ruleoutcome(competency::OUTCOME_EVIDENCE); + $c1->set_ruletype('core_competency\\competency_rule_all'); + $c1->update(); + + // If we delete competeny, the related competencies relations and evidences should be deleted. + // Create related competencies using one of c1a competency descendants. + $rc = $lpg->create_related_competency(array( + 'competencyid' => $c2->get_id(), + 'relatedcompetencyid' => $c11b->get_id() + )); + $this->assertEquals($c11b->get_id(), $rc->get_relatedcompetencyid()); + + // Creating a standard evidence with minimal information. + $uc2 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c11b->get_id())); + $evidence = $lpg->create_evidence(array('usercompetencyid' => $uc2->get_id())); + $this->assertEquals($uc2->get_id(), $evidence->get_usercompetencyid()); + $uc2->delete(); + + $this->assertTrue(api::delete_competency($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1a->get_id())); + + // Check that on delete, we reset the rule on parent competency. + $c1->read(); + $this->assertNull($c1->get_ruletype()); + $this->assertNull($c1->get_ruletype()); + $this->assertEquals(competency::OUTCOME_NONE, $c1->get_ruleoutcome()); + + // Check that descendants were also deleted. + $this->assertFalse(competency::record_exists($c1b->get_id())); + $this->assertFalse(competency::record_exists($c11b->get_id())); + $this->assertFalse(competency::record_exists($c12b->get_id())); + + // Check if evidence are also deleted. + $this->assertEquals(0, \core_competency\user_evidence_competency::count_records(array('competencyid' => $c11b->get_id()))); + + // Check if related conpetency relation is deleted. + $this->assertEquals(0, count(api::list_related_competencies($c2->get_id()))); + + // Delete a simple competency. + $this->assertTrue(api::delete_competency($c2->get_id())); + $this->assertFalse(competency::record_exists($c2->get_id())); + } + + public function test_delete_competency_used_in_plan() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $u1 = $dg->create_user(); + + $plan = $lpg->create_plan((object) array('userid' => $u1->id)); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c11b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c12b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + + // Add competency to plan. + $pc = $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c11b->get_id())); + // We can not delete a competency , if competency or competency children is associated to plan. + $this->assertFalse(api::delete_competency($c1a->get_id())); + + // We can delete the competency if we remove the competency from the plan. + $pc->delete(); + + $this->assertTrue(api::delete_competency($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1b->get_id())); + $this->assertFalse(competency::record_exists($c11b->get_id())); + $this->assertFalse(competency::record_exists($c12b->get_id())); + } + + public function test_delete_competency_used_in_usercompetency() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $u1 = $dg->create_user(); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c11b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c12b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + + // Create user competency. + $uc1 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c11b->get_id())); + + // We can not delete a competency , if competency or competency children exist in user competency. + $this->assertFalse(api::delete_competency($c1a->get_id())); + + // We can delete the competency if we remove the competency from user competency. + $uc1->delete(); + + $this->assertTrue(api::delete_competency($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1b->get_id())); + $this->assertFalse(competency::record_exists($c11b->get_id())); + $this->assertFalse(competency::record_exists($c12b->get_id())); + } + + public function test_delete_competency_used_in_usercompetencyplan() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $u1 = $dg->create_user(); + + $plan = $lpg->create_plan((object) array('userid' => $u1->id)); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c11b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c12b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + + // Create user competency plan. + $uc2 = $lpg->create_user_competency_plan(array( + 'userid' => $u1->id, + 'competencyid' => $c11b->get_id(), + 'planid' => $plan->get_id() + )); + + // We can not delete a competency , if competency or competency children exist in user competency plan. + $this->assertFalse(api::delete_competency($c1a->get_id())); + + // We can delete the competency if we remove the competency from user competency plan. + $uc2->delete(); + + $this->assertTrue(api::delete_competency($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1b->get_id())); + $this->assertFalse(competency::record_exists($c11b->get_id())); + $this->assertFalse(competency::record_exists($c12b->get_id())); + } + + public function test_delete_competency_used_in_template() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $template = $lpg->create_template(); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c11b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c12b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + + // Add competency to a template. + $tc = $lpg->create_template_competency(array( + 'templateid' => $template->get_id(), + 'competencyid' => $c11b->get_id() + )); + // We can not delete a competency , if competency or competency children is linked to template. + $this->assertFalse(api::delete_competency($c1a->get_id())); + + // We can delete the competency if we remove the competency from template. + $tc->delete(); + + $this->assertTrue(api::delete_competency($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1b->get_id())); + $this->assertFalse(competency::record_exists($c11b->get_id())); + $this->assertFalse(competency::record_exists($c12b->get_id())); + } + + public function test_delete_competency_used_in_course() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $cat1 = $dg->create_category(); + + $course = $dg->create_course(array('category' => $cat1->id)); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c11b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c12b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + + // Add competency to course. + $cc = $lpg->create_course_competency(array( + 'courseid' => $course->id, + 'competencyid' => $c11b->get_id() + )); + + // We can not delete a competency if the competency or competencies children is linked to a course. + $this->assertFalse(api::delete_competency($c1a->get_id())); + + // We can delete the competency if we remove the competency from course. + $cc->delete(); + + $this->assertTrue(api::delete_competency($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1b->get_id())); + $this->assertFalse(competency::record_exists($c11b->get_id())); + $this->assertFalse(competency::record_exists($c12b->get_id())); + } + + public function test_delete_framework() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $u1 = $dg->create_user(); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2id = $c2->get_id(); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c11b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c12b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + + // If we delete framework, the related competencies relations and evidences should be deleted. + // Create related competencies using one of c1a competency descendants. + $rc = $lpg->create_related_competency(array( + 'competencyid' => $c2->get_id(), + 'relatedcompetencyid' => $c11b->get_id() + )); + $this->assertEquals($c11b->get_id(), $rc->get_relatedcompetencyid()); + + // Creating a standard evidence with minimal information. + $uc2 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c11b->get_id())); + $evidence = $lpg->create_evidence(array('usercompetencyid' => $uc2->get_id())); + $this->assertEquals($uc2->get_id(), $evidence->get_usercompetencyid()); + $uc2->delete(); + + $this->assertTrue(api::delete_framework($f1->get_id())); + $this->assertFalse(competency_framework::record_exists($f1->get_id())); + + // Check that all competencies were also deleted. + $this->assertFalse(competency::record_exists($c1->get_id())); + $this->assertFalse(competency::record_exists($c2->get_id())); + $this->assertFalse(competency::record_exists($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1b->get_id())); + $this->assertFalse(competency::record_exists($c11b->get_id())); + $this->assertFalse(competency::record_exists($c12b->get_id())); + + // Check if evidence are also deleted. + $this->assertEquals(0, \core_competency\user_evidence_competency::count_records(array('competencyid' => $c11b->get_id()))); + + // Check if related conpetency relation is deleted. + $this->assertEquals(0, count(\core_competency\related_competency::get_multiple_relations(array($c2id)))); + + // Delete a simple framework. + $f2 = $lpg->create_framework(); + $this->assertTrue(api::delete_framework($f2->get_id())); + $this->assertFalse(competency_framework::record_exists($f2->get_id())); + } + + public function test_delete_framework_competency_used_in_plan() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $u1 = $dg->create_user(); + + $plan = $lpg->create_plan((object) array('userid' => $u1->id)); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2id = $c2->get_id(); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c11b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c12b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + + // Create related competencies. + $rc = $lpg->create_related_competency(array( + 'competencyid' => $c2->get_id(), + 'relatedcompetencyid' => $c11b->get_id() + )); + $this->assertEquals($c11b->get_id(), $rc->get_relatedcompetencyid()); + + // Creating a standard evidence with minimal information. + $uc2 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c11b->get_id())); + $usercompetencyid = $uc2->get_id(); + $evidence = $lpg->create_evidence(array('usercompetencyid' => $usercompetencyid)); + $this->assertEquals($uc2->get_id(), $evidence->get_usercompetencyid()); + $uc2->delete(); + + // Add competency to plan. + $pc = $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c11b->get_id())); + // We can not delete a framework , if competency or competency children is associated to plan. + $this->assertFalse(api::delete_framework($f1->get_id())); + // Check that none of associated data are deleted. + $this->assertEquals($usercompetencyid, $evidence->read()->get_usercompetencyid()); + $this->assertEquals($c2->get_id(), $rc->read()->get_competencyid()); + + // We can delete the competency if we remove the competency from the plan. + $pc->delete(); + + $this->assertTrue(api::delete_framework($f1->get_id())); + $this->assertFalse(competency::record_exists($c1->get_id())); + $this->assertFalse(competency::record_exists($c2->get_id())); + $this->assertFalse(competency::record_exists($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1b->get_id())); + $this->assertFalse(competency::record_exists($c11b->get_id())); + $this->assertFalse(competency::record_exists($c12b->get_id())); + // Check if evidence are also deleted. + $this->assertEquals(0, \core_competency\user_evidence_competency::count_records(array('competencyid' => $c11b->get_id()))); + + // Check if related conpetency relation is deleted. + $this->assertEquals(0, count(\core_competency\related_competency::get_multiple_relations(array($c2id)))); + } + + public function test_delete_framework_competency_used_in_usercompetency() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $u1 = $dg->create_user(); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2id = $c2->get_id(); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c11b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c12b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + + // Create related competencies. + $rc = $lpg->create_related_competency(array( + 'competencyid' => $c2->get_id(), + 'relatedcompetencyid' => $c11b->get_id() + )); + $this->assertEquals($c11b->get_id(), $rc->get_relatedcompetencyid()); + + // Creating a standard evidence with minimal information. + $uc1 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c11b->get_id())); + $usercompetencyid = $uc1->get_id(); + $evidence = $lpg->create_evidence(array('usercompetencyid' => $usercompetencyid)); + $this->assertEquals($uc1->get_id(), $evidence->get_usercompetencyid()); + $uc1->delete(); + + // Create user competency. + $uc2 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c11b->get_id())); + + // We can not delete a framework , if competency or competency children exist in user competency. + $this->assertFalse(api::delete_framework($f1->get_id())); + // Check that none of associated data are deleted. + $this->assertEquals($usercompetencyid, $evidence->read()->get_usercompetencyid()); + $this->assertEquals($c2->get_id(), $rc->read()->get_competencyid()); + + // We can delete the framework if we remove the competency from user competency. + $uc2->delete(); + + $this->assertTrue(api::delete_framework($f1->get_id())); + $this->assertFalse(competency::record_exists($c1->get_id())); + $this->assertFalse(competency::record_exists($c2->get_id())); + $this->assertFalse(competency::record_exists($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1b->get_id())); + $this->assertFalse(competency::record_exists($c11b->get_id())); + $this->assertFalse(competency::record_exists($c12b->get_id())); + // Check if evidence are also deleted. + $this->assertEquals(0, \core_competency\user_evidence_competency::count_records(array('competencyid' => $c11b->get_id()))); + + // Check if related conpetency relation is deleted. + $this->assertEquals(0, count(\core_competency\related_competency::get_multiple_relations(array($c2id)))); + } + + public function test_delete_framework_competency_used_in_usercompetencyplan() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $u1 = $dg->create_user(); + + $plan = $lpg->create_plan((object) array('userid' => $u1->id)); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2id = $c2->get_id(); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c11b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c12b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + + // Create related competencies. + $rc = $lpg->create_related_competency(array( + 'competencyid' => $c2->get_id(), + 'relatedcompetencyid' => $c11b->get_id() + )); + $this->assertEquals($c11b->get_id(), $rc->get_relatedcompetencyid()); + + // Creating a standard evidence with minimal information. + $uc1 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c11b->get_id())); + $usercompetencyid = $uc1->get_id(); + $evidence = $lpg->create_evidence(array('usercompetencyid' => $usercompetencyid)); + $this->assertEquals($uc1->get_id(), $evidence->get_usercompetencyid()); + $uc1->delete(); + + // Create user competency plan. + $uc2 = $lpg->create_user_competency_plan(array( + 'userid' => $u1->id, + 'competencyid' => $c11b->get_id(), + 'planid' => $plan->get_id() + )); + + // We can not delete a framework , if competency or competency children exist in user competency plan. + $this->assertFalse(api::delete_framework($f1->get_id())); + // Check that none of associated data are deleted. + $this->assertEquals($usercompetencyid, $evidence->read()->get_usercompetencyid()); + $this->assertEquals($c2->get_id(), $rc->read()->get_competencyid()); + + // We can delete the framework if we remove the competency from user competency plan. + $uc2->delete(); + + $this->assertTrue(api::delete_framework($f1->get_id())); + $this->assertFalse(competency::record_exists($c1->get_id())); + $this->assertFalse(competency::record_exists($c2->get_id())); + $this->assertFalse(competency::record_exists($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1b->get_id())); + $this->assertFalse(competency::record_exists($c11b->get_id())); + $this->assertFalse(competency::record_exists($c12b->get_id())); + // Check if evidence are also deleted. + $this->assertEquals(0, \core_competency\user_evidence_competency::count_records(array('competencyid' => $c11b->get_id()))); + + // Check if related conpetency relation is deleted. + $this->assertEquals(0, count(\core_competency\related_competency::get_multiple_relations(array($c2id)))); + } + + public function test_delete_framework_competency_used_in_template() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $u1 = $dg->create_user(); + $template = $lpg->create_template(); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2id = $c2->get_id(); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c11b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c12b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + + // Create related competencies. + $rc = $lpg->create_related_competency(array( + 'competencyid' => $c2->get_id(), + 'relatedcompetencyid' => $c11b->get_id() + )); + $this->assertEquals($c11b->get_id(), $rc->get_relatedcompetencyid()); + + // Creating a standard evidence with minimal information. + $uc1 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c11b->get_id())); + $usercompetencyid = $uc1->get_id(); + $evidence = $lpg->create_evidence(array('usercompetencyid' => $usercompetencyid)); + $this->assertEquals($uc1->get_id(), $evidence->get_usercompetencyid()); + $uc1->delete(); + + // Add competency to a template. + $tc = $lpg->create_template_competency(array( + 'templateid' => $template->get_id(), + 'competencyid' => $c11b->get_id() + )); + // We can not delete a framework , if competency or competency children is linked to template. + $this->assertFalse(api::delete_framework($f1->get_id())); + // Check that none of associated data are deleted. + $this->assertEquals($usercompetencyid, $evidence->read()->get_usercompetencyid()); + $this->assertEquals($c2->get_id(), $rc->read()->get_competencyid()); + + // We can delete the framework if we remove the competency from template. + $tc->delete(); + + $this->assertTrue(api::delete_framework($f1->get_id())); + $this->assertFalse(competency::record_exists($c1->get_id())); + $this->assertFalse(competency::record_exists($c2->get_id())); + $this->assertFalse(competency::record_exists($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1b->get_id())); + $this->assertFalse(competency::record_exists($c11b->get_id())); + $this->assertFalse(competency::record_exists($c12b->get_id())); + // Check if evidence are also deleted. + $this->assertEquals(0, \core_competency\user_evidence_competency::count_records(array('competencyid' => $c11b->get_id()))); + + // Check if related conpetency relation is deleted. + $this->assertEquals(0, count(\core_competency\related_competency::get_multiple_relations(array($c2id)))); + } + + public function test_delete_framework_competency_used_in_course() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $cat1 = $dg->create_category(); + $u1 = $dg->create_user(); + $course = $dg->create_course(array('category' => $cat1->id)); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2id = $c2->get_id(); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1a->get_id())); + $c11b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + $c12b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1b->get_id())); + + // Create related competencies. + $rc = $lpg->create_related_competency(array( + 'competencyid' => $c2->get_id(), + 'relatedcompetencyid' => $c11b->get_id() + )); + $this->assertEquals($c11b->get_id(), $rc->get_relatedcompetencyid()); + + // Creating a standard evidence with minimal information. + $uc1 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c11b->get_id())); + $usercompetencyid = $uc1->get_id(); + $evidence = $lpg->create_evidence(array('usercompetencyid' => $usercompetencyid)); + $this->assertEquals($uc1->get_id(), $evidence->get_usercompetencyid()); + $uc1->delete(); + + // Add competency to course. + $cc = $lpg->create_course_competency(array( + 'courseid' => $course->id, + 'competencyid' => $c11b->get_id() + )); + + // We can not delete a framework if the competency or competencies children is linked to a course. + $this->assertFalse(api::delete_framework($f1->get_id())); + // Check that none of associated data are deleted. + $this->assertEquals($usercompetencyid, $evidence->read()->get_usercompetencyid()); + $this->assertEquals($c2->get_id(), $rc->read()->get_competencyid()); + + // We can delete the framework if we remove the competency from course. + $cc->delete(); + + $this->assertTrue(api::delete_framework($f1->get_id())); + $this->assertFalse(competency::record_exists($c1->get_id())); + $this->assertFalse(competency::record_exists($c2->get_id())); + $this->assertFalse(competency::record_exists($c1a->get_id())); + $this->assertFalse(competency::record_exists($c1b->get_id())); + $this->assertFalse(competency::record_exists($c11b->get_id())); + $this->assertFalse(competency::record_exists($c12b->get_id())); + // Check if evidence are also deleted. + $this->assertEquals(0, \core_competency\user_evidence_competency::count_records(array('competencyid' => $c11b->get_id()))); + + // Check if related conpetency relation is deleted. + $this->assertEquals(0, count(\core_competency\related_competency::get_multiple_relations(array($c2id)))); + } + + public function test_grade_competency_in_course_permissions() { + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + + $c1 = $dg->create_course(); + $c2 = $dg->create_course(); + $sysctx = context_system::instance(); + $c1ctx = context_course::instance($c1->id); + $c2ctx = context_course::instance($c2->id); + + $teacher1 = $dg->create_user(); + $noneditingteacher = $dg->create_user(); + $student1 = $dg->create_user(); + $student2 = $dg->create_user(); + $notstudent1 = $dg->create_user(); + + $lpg = $dg->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $comp1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $lpg->create_course_competency(array('courseid' => $c1->id, 'competencyid' => $comp1->get_id())); + + $studentarch = get_archetype_roles('student'); + $studentrole = array_shift($studentarch); + + $gradablerole = $dg->create_role(); + assign_capability('moodle/competency:coursecompetencygradable', CAP_ALLOW, $gradablerole, $sysctx->id); + + $notgradablerole = $dg->create_role(); + assign_capability('moodle/competency:coursecompetencygradable', CAP_PROHIBIT, $notgradablerole, $sysctx->id); + + $canviewucrole = $dg->create_role(); + assign_capability('moodle/competency:usercompetencyview', CAP_ALLOW, $canviewucrole, $sysctx->id); + + $cannotviewcomp = $dg->create_role(); + assign_capability('moodle/competency:competencyview', CAP_PROHIBIT, $cannotviewcomp, $sysctx->id); + + $canmanagecomp = $dg->create_role(); + assign_capability('moodle/competency:competencymanage', CAP_ALLOW, $canmanagecomp, $sysctx->id); + + $cangraderole = $dg->create_role(); + assign_capability('moodle/competency:competencygrade', CAP_ALLOW, $cangraderole, $sysctx->id); + + // Enrol s1 and s2 as students in course 1. + $dg->enrol_user($student1->id, $c1->id, $studentrole->id); + $dg->enrol_user($student2->id, $c1->id, $studentrole->id); + + // Mark the s2 as not being 'gradable'. + $dg->role_assign($notgradablerole, $student2->id, $c1ctx->id); + + // Mark the 'non a student' as 'gradable' throughout the site. + $dg->role_assign($gradablerole, $notstudent1->id, $sysctx->id); + + // From now we'll iterate over each permission. + accesslib_clear_all_caches_for_unit_testing(); + $this->setUser($teacher1); + + $this->assertExceptionWithGradeCompetencyInCourse('required_capability_exception', 'View a user competency', + $c1->id, $student1->id, $comp1->get_id()); + + // Give permission to view competencies. + $dg->role_assign($canviewucrole, $teacher1->id, $c1ctx->id); + accesslib_clear_all_caches_for_unit_testing(); + $this->assertExceptionWithGradeCompetencyInCourse('required_capability_exception', 'Set competency grade', + $c1->id, $student1->id, $comp1->get_id()); + + // Give permission to rate. + $dg->role_assign($cangraderole, $teacher1->id, $c1ctx->id); + accesslib_clear_all_caches_for_unit_testing(); + $this->assertSuccessWithGradeCompetencyInCourse($c1->id, $student1->id, $comp1->get_id()); + + // Remove permssion to read competencies, this leads to error. + $dg->role_assign($cannotviewcomp, $teacher1->id, $sysctx->id); + accesslib_clear_all_caches_for_unit_testing(); + $this->assertExceptionWithGradeCompetencyInCourse('required_capability_exception', 'View competency frameworks', + $c1->id, $student1->id, $comp1->get_id()); + + // Give permssion to manage course competencies, this leads to success. + $dg->role_assign($canmanagecomp, $teacher1->id, $sysctx->id); + accesslib_clear_all_caches_for_unit_testing(); + $this->assertSuccessWithGradeCompetencyInCourse($c1->id, $student1->id, $comp1->get_id()); + + // Try to grade a user that is not gradable, lead to errors. + $this->assertExceptionWithGradeCompetencyInCourse('coding_exception', 'The competency may not be rated at this time.', + $c1->id, $student2->id, $comp1->get_id()); + + // Try to grade a competency not in the course. + $this->assertExceptionWithGradeCompetencyInCourse('coding_exception', 'The competency does not belong to this course', + $c1->id, $student1->id, $comp2->get_id()); + + // Try to grade a user that is not enrolled, even though they are 'gradable'. + $this->assertExceptionWithGradeCompetencyInCourse('coding_exception', 'The competency may not be rated at this time.', + $c1->id, $notstudent1->id, $comp1->get_id()); + + // Give permission for non-editing teacher to grade. + $dg->role_assign($canviewucrole, $noneditingteacher->id, $c1ctx->id); + $dg->role_assign($cangraderole, $noneditingteacher->id, $c1ctx->id); + $this->setUser($noneditingteacher); + + accesslib_clear_all_caches_for_unit_testing(); + $this->assertSuccessWithGradeCompetencyInCourse($c1->id, $student1->id, $comp1->get_id()); + } + + /** + * Assert that a competency was graded in a course. + * + * @param int $courseid The course ID. + * @param int $userid The user ID. + * @param int $compid The competency ID. + * @param int $grade The grade. + */ + protected function assertSuccessWithGradeCompetencyInCourse($courseid, $userid, $compid, $grade = 1) { + $beforecount = evidence::count_records(); + api::grade_competency_in_course($courseid, $userid, $compid, $grade); + $this->assertEquals($beforecount + 1, evidence::count_records()); + $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $compid)); + $records = evidence::get_records(array(), 'id', 'DESC', 0, 1); + $evidence = array_pop($records); + $this->assertEquals($uc->get_id(), $evidence->get_usercompetencyid()); + } + + /** + * Assert that grading a competency in course throws an exception. + * + * @param string $exceptiontype The exception type. + * @param string $exceptiontest The exceptiont text. + * @param int $courseid The course ID. + * @param int $userid The user ID. + * @param int $compid The competency ID. + * @param int $grade The grade. + */ + protected function assertExceptionWithGradeCompetencyInCourse($exceptiontype, $exceptiontext, $courseid, $userid, $compid, + $grade = 1) { + + $raised = false; + try { + api::grade_competency_in_course($courseid, $userid, $compid, $grade); + } catch (moodle_exception $e) { + $raised = true; + $this->assertInstanceOf($exceptiontype, $e); + $this->assertRegExp('@' . $exceptiontext . '@', $e->getMessage()); + } + + if (!$raised) { + $this->fail('Grading should not be allowed.'); + } + } + + /** + * Test list of evidences for plan completed and not completed. + */ + public function test_list_evidence() { + global $DB; + + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $currenttime = time(); + $syscontext = context_system::instance(); + + // Create users. + $user = $dg->create_user(); + $this->setUser($user); + + // Create a framework and assign competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Create 2 user plans and add competency to each plan. + $p1 = $lpg->create_plan(array('userid' => $user->id)); + $p2 = $lpg->create_plan(array('userid' => $user->id)); + $pc1 = $lpg->create_plan_competency(array('planid' => $p1->get_id(), 'competencyid' => $c1->get_id())); + $pc2 = $lpg->create_plan_competency(array('planid' => $p2->get_id(), 'competencyid' => $c1->get_id())); + + // Create user competency. Add user_evidence and associate it to the user competency. + $uc = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c1->get_id())); + $ue = $lpg->create_user_evidence(array('userid' => $user->id)); + $uec = $lpg->create_user_evidence_competency(array('userevidenceid' => $ue->get_id(), 'competencyid' => $c1->get_id())); + $e1 = $lpg->create_evidence(array('usercompetencyid' => $uc->get_id())); + + // Check both plans as one evidence. + $this->assertEquals(1, count(api::list_evidence($user->id, $c1->get_id(), $p1->get_id()))); + $this->assertEquals(1, count(api::list_evidence($user->id, $c1->get_id(), $p2->get_id()))); + + // Complete second plan. + $currenttime += 1; + $p2->set_status(plan::STATUS_COMPLETE); + $p2->update(); + $plansql = "UPDATE {" . plan::TABLE . "} SET timemodified = :currenttime WHERE id = :planid"; + $DB->execute($plansql, array('currenttime' => $currenttime, 'planid' => $p2->get_id())); + + // Add an other user evidence for the same competency. + $currenttime += 1; + $ue2 = $lpg->create_user_evidence(array('userid' => $user->id)); + $uec2 = $lpg->create_user_evidence_competency(array('userevidenceid' => $ue2->get_id(), 'competencyid' => $c1->get_id())); + $e2 = $lpg->create_evidence(array('usercompetencyid' => $uc->get_id())); + $evidencesql = "UPDATE {" . evidence::TABLE . "} SET timecreated = :currenttime WHERE id = :evidenceid"; + $DB->execute($evidencesql, array('currenttime' => $currenttime, 'evidenceid' => $e2->get_id())); + + // Check first plan which is not completed as all evidences. + $this->assertEquals(2, count(api::list_evidence($user->id, $c1->get_id(), $p1->get_id()))); + + // Check second plan completed before the new evidence as only the first evidence. + $listevidences = api::list_evidence($user->id, $c1->get_id(), $p2->get_id()); + $this->assertEquals(1, count($listevidences)); + $this->assertEquals($e1->get_id(), $listevidences[$e1->get_id()]->get_id()); + } + + /** + * Get a user competency in a course. + */ + public function test_get_user_competency_in_course() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $user = $dg->create_user(); + $c1 = $dg->create_course(); + + // Enrol the user so they can be rated in the course. + $studentarch = get_archetype_roles('student'); + $studentrole = array_shift($studentarch); + $coursecontext = context_course::instance($c1->id); + $dg->role_assign($studentrole->id, $user->id, $coursecontext->id); + $dg->enrol_user($user->id, $c1->id, $studentrole->id); + + $framework = $lpg->create_framework(); + $comp1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $lpg->create_course_competency(array('competencyid' => $comp1->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp2->get_id(), 'courseid' => $c1->id)); + + // Create a user competency for comp1. + api::grade_competency_in_course($c1, $user->id, $comp1->get_id(), 3, 'Unit test'); + + // Test for competency already exist in user_competency. + $uc = api::get_user_competency_in_course($c1->id, $user->id, $comp1->get_id()); + $this->assertEquals($comp1->get_id(), $uc->get_competencyid()); + $this->assertEquals($user->id, $uc->get_userid()); + $this->assertEquals(3, $uc->get_grade()); + $this->assertEquals(true, $uc->get_proficiency()); + + // Test for competency does not exist in user_competency. + $uc2 = api::get_user_competency_in_course($c1->id, $user->id, $comp2->get_id()); + $this->assertEquals($comp2->get_id(), $uc2->get_competencyid()); + $this->assertEquals($user->id, $uc2->get_userid()); + $this->assertEquals(null, $uc2->get_grade()); + $this->assertEquals(null, $uc2->get_proficiency()); + } + + /** + * Test course statistics api functions. + */ + public function test_course_statistics() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + $u4 = $dg->create_user(); + $c1 = $dg->create_course(); + $framework = $lpg->create_framework(); + // Enrol students in the course. + $studentarch = get_archetype_roles('student'); + $studentrole = array_shift($studentarch); + $coursecontext = context_course::instance($c1->id); + $dg->role_assign($studentrole->id, $u1->id, $coursecontext->id); + $dg->enrol_user($u1->id, $c1->id, $studentrole->id); + $dg->role_assign($studentrole->id, $u2->id, $coursecontext->id); + $dg->enrol_user($u2->id, $c1->id, $studentrole->id); + $dg->role_assign($studentrole->id, $u3->id, $coursecontext->id); + $dg->enrol_user($u3->id, $c1->id, $studentrole->id); + $dg->role_assign($studentrole->id, $u4->id, $coursecontext->id); + $dg->enrol_user($u4->id, $c1->id, $studentrole->id); + + // Create 6 competencies. + $comp1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp5 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp6 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Link 6 out of 6 to a course. + $lpg->create_course_competency(array('competencyid' => $comp1->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp2->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp3->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp4->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp5->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp6->get_id(), 'courseid' => $c1->id)); + + // Rate some competencies. + // User 1. + api::grade_competency_in_course($c1, $u1->id, $comp1->get_id(), 4, 'Unit test'); + api::grade_competency_in_course($c1, $u1->id, $comp2->get_id(), 4, 'Unit test'); + api::grade_competency_in_course($c1, $u1->id, $comp3->get_id(), 4, 'Unit test'); + api::grade_competency_in_course($c1, $u1->id, $comp4->get_id(), 4, 'Unit test'); + // User 2. + api::grade_competency_in_course($c1, $u2->id, $comp1->get_id(), 1, 'Unit test'); + api::grade_competency_in_course($c1, $u2->id, $comp2->get_id(), 1, 'Unit test'); + api::grade_competency_in_course($c1, $u2->id, $comp3->get_id(), 1, 'Unit test'); + api::grade_competency_in_course($c1, $u2->id, $comp4->get_id(), 1, 'Unit test'); + // User 3. + api::grade_competency_in_course($c1, $u3->id, $comp1->get_id(), 3, 'Unit test'); + api::grade_competency_in_course($c1, $u3->id, $comp2->get_id(), 3, 'Unit test'); + // User 4. + api::grade_competency_in_course($c1, $u4->id, $comp1->get_id(), 2, 'Unit test'); + api::grade_competency_in_course($c1, $u4->id, $comp2->get_id(), 2, 'Unit test'); + + // OK we have enough data - lets call some API functions and check for expected results. + + $result = api::count_proficient_competencies_in_course_for_user($c1->id, $u1->id); + $this->assertEquals(4, $result); + $result = api::count_proficient_competencies_in_course_for_user($c1->id, $u2->id); + $this->assertEquals(0, $result); + $result = api::count_proficient_competencies_in_course_for_user($c1->id, $u3->id); + $this->assertEquals(2, $result); + $result = api::count_proficient_competencies_in_course_for_user($c1->id, $u4->id); + $this->assertEquals(0, $result); + + $result = api::get_least_proficient_competencies_for_course($c1->id, 0, 2); + // We should get 5 and 6 in repeatable order. + $valid = false; + if (($comp5->get_id() == $result[0]->get_id()) || ($comp6->get_id() == $result[0]->get_id())) { + $valid = true; + } + $this->assertTrue($valid); + $valid = false; + if (($comp5->get_id() == $result[1]->get_id()) || ($comp6->get_id() == $result[1]->get_id())) { + $valid = true; + } + $this->assertTrue($valid); + $expected = $result[1]->get_id(); + $result = api::get_least_proficient_competencies_for_course($c1->id, 1, 1); + $this->assertEquals($result[0]->get_id(), $expected); + } + + /** + * Test template statistics api functions. + */ + public function test_template_statistics() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $this->setAdminUser(); + + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + $u4 = $dg->create_user(); + $c1 = $dg->create_course(); + $framework = $lpg->create_framework(); + + // Create 6 competencies. + $comp1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp5 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp6 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Link 5 out of 6 to a course. + $lpg->create_course_competency(array('competencyid' => $comp1->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp2->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp3->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp4->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp5->get_id(), 'courseid' => $c1->id)); + + // Put all 6 in a template. + $tpl = $this->getDataGenerator()->get_plugin_generator('core_competency')->create_template(); + $tplc1 = $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $comp1->get_id())); + $tplc2 = $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $comp2->get_id())); + $tplc3 = $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $comp3->get_id())); + $tplc4 = $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $comp4->get_id())); + $tplc5 = $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $comp5->get_id())); + $tplc6 = $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $comp6->get_id())); + + // Create some plans from the template. + $p1 = $lpg->create_plan(array('templateid' => $tpl->get_id(), 'userid' => $u1->id)); + $p2 = $lpg->create_plan(array('templateid' => $tpl->get_id(), 'userid' => $u2->id)); + $p3 = $lpg->create_plan(array('templateid' => $tpl->get_id(), 'userid' => $u3->id)); + $p4 = $lpg->create_plan(array('templateid' => $tpl->get_id(), 'userid' => $u4->id)); + + // Rate some competencies. + // User 1. + $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $comp1->get_id(), + 'proficiency' => true, 'grade' => 1 )); + $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $comp2->get_id(), + 'proficiency' => true, 'grade' => 1 )); + $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $comp3->get_id(), + 'proficiency' => true, 'grade' => 1 )); + // User 2. + $lpg->create_user_competency(array('userid' => $u2->id, 'competencyid' => $comp1->get_id(), + 'proficiency' => false, 'grade' => 1 )); + $lpg->create_user_competency(array('userid' => $u2->id, 'competencyid' => $comp2->get_id(), + 'proficiency' => false, 'grade' => 1 )); + $lpg->create_user_competency(array('userid' => $u2->id, 'competencyid' => $comp3->get_id(), + 'proficiency' => false, 'grade' => 1 )); + // User 3. + $lpg->create_user_competency(array('userid' => $u3->id, 'competencyid' => $comp2->get_id(), + 'proficiency' => false, 'grade' => 1 )); + $lpg->create_user_competency(array('userid' => $u3->id, 'competencyid' => $comp3->get_id(), + 'proficiency' => true, 'grade' => 1 )); + $lpg->create_user_competency(array('userid' => $u3->id, 'competencyid' => $comp4->get_id(), + 'proficiency' => false, 'grade' => 1 )); + $lpg->create_user_competency(array('userid' => $u3->id, 'competencyid' => $comp5->get_id(), + 'proficiency' => true, 'grade' => 1 )); + // User 4. + $lpg->create_user_competency(array('userid' => $u4->id, 'competencyid' => $comp3->get_id(), + 'proficiency' => true, 'grade' => 1 )); + $lpg->create_user_competency(array('userid' => $u4->id, 'competencyid' => $comp5->get_id(), + 'proficiency' => true, 'grade' => 1 )); + + // Complete 3 out of 4 plans. + api::complete_plan($p1->get_id()); + api::complete_plan($p2->get_id()); + api::complete_plan($p3->get_id()); + + // OK we have enough data - lets call some API functions and check for expected results. + + $result = api::count_competencies_in_template_with_no_courses($tpl->get_id()); + $this->assertEquals(1, $result); + + $result = api::count_plans_for_template($tpl->get_id()); + $this->assertEquals(4, $result); + + $result = api::count_plans_for_template($tpl->get_id(), plan::STATUS_COMPLETE); + $this->assertEquals(3, $result); + + // This counts the records of competencies in completed plans for all users with a plan from this template. + $result = api::count_user_competency_plans_for_template($tpl->get_id()); + // There should be 3 plans * 6 competencies. + $this->assertEquals(18, $result); + + // This counts the records of proficient competencies in completed plans for all users with a plan from this template. + $result = api::count_user_competency_plans_for_template($tpl->get_id(), true); + // There should be 5. + $this->assertEquals(5, $result); + + // This counts the records of not proficient competencies in completed plans for all users with a plan from this template. + $result = api::count_user_competency_plans_for_template($tpl->get_id(), false); + // There should be 13. + $this->assertEquals(13, $result); + + // This lists the plans based on this template, optionally filtered by status. + $result = api::list_plans_for_template($tpl->get_id()); + + $this->assertEquals(4, count($result)); + foreach ($result as $one) { + $this->assertInstanceOf('\core_competency\plan', $one); + } + // This lists the plans based on this template, optionally filtered by status. + $result = api::list_plans_for_template($tpl->get_id(), plan::STATUS_COMPLETE); + + $this->assertEquals(3, count($result)); + foreach ($result as $one) { + $this->assertInstanceOf('\core_competency\plan', $one); + $this->assertEquals(plan::STATUS_COMPLETE, $one->get_status()); + } + + $result = api::get_least_proficient_competencies_for_template($tpl->get_id(), 0, 2); + + // Our times completed counts should look like this: + // - comp1 - 1 + // - comp2 - 1 + // - comp3 - 2 + // - comp4 - 0 + // - comp5 - 1 + // - comp6 - 0 + // + // And this is a fullstop to make CiBoT happy. + $this->assertEquals(2, count($result)); + $leastarray = array($comp4->get_id(), $comp6->get_id()); + foreach ($result as $one) { + $this->assertInstanceOf('\core_competency\competency', $one); + $this->assertContains($one->get_id(), $leastarray); + } + } + + public function test_is_scale_used_anywhere() { + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $scale1 = $dg->create_scale(); + $scale2 = $dg->create_scale(); + $scale3 = $dg->create_scale(); + $scale4 = $dg->create_scale(); + + $this->assertFalse(api::is_scale_used_anywhere($scale1->id)); + $this->assertFalse(api::is_scale_used_anywhere($scale2->id)); + $this->assertFalse(api::is_scale_used_anywhere($scale3->id)); + $this->assertFalse(api::is_scale_used_anywhere($scale4->id)); + + // Using scale 1 in a framework. + $f1 = $lpg->create_framework([ + 'scaleid' => $scale1->id, + 'scaleconfiguration' => json_encode([ + ['scaleid' => $scale1->id], + ['id' => 1, 'scaledefault' => 1, 'proficient' => 1] + ]) + ]); + $this->assertTrue(api::is_scale_used_anywhere($scale1->id)); + $this->assertFalse(api::is_scale_used_anywhere($scale2->id)); + $this->assertFalse(api::is_scale_used_anywhere($scale3->id)); + $this->assertFalse(api::is_scale_used_anywhere($scale4->id)); + + // Using scale 2 in a competency. + $f2 = $lpg->create_framework(); + $c2 = $lpg->create_competency([ + 'competencyframeworkid' => $f2->get_id(), + 'scaleid' => $scale2->id, + 'scaleconfiguration' => json_encode([ + ['scaleid' => $scale2->id], + ['id' => 1, 'scaledefault' => 1, 'proficient' => 1] + ]) + ]); + + $this->assertTrue(api::is_scale_used_anywhere($scale1->id)); + $this->assertTrue(api::is_scale_used_anywhere($scale2->id)); + $this->assertFalse(api::is_scale_used_anywhere($scale3->id)); + $this->assertFalse(api::is_scale_used_anywhere($scale4->id)); + + // Using scale 3 in a framework, and scale 4 in a competency of that framework. + $f3 = $lpg->create_framework([ + 'scaleid' => $scale3->id, + 'scaleconfiguration' => json_encode([ + ['scaleid' => $scale3->id], + ['id' => 1, 'scaledefault' => 1, 'proficient' => 1] + ]) + ]); + $c3 = $lpg->create_competency([ + 'competencyframeworkid' => $f3->get_id(), + 'scaleid' => $scale4->id, + 'scaleconfiguration' => json_encode([ + ['scaleid' => $scale4->id], + ['id' => 1, 'scaledefault' => 1, 'proficient' => 1] + ]) + ]); + + $this->assertTrue(api::is_scale_used_anywhere($scale1->id)); + $this->assertTrue(api::is_scale_used_anywhere($scale2->id)); + $this->assertTrue(api::is_scale_used_anywhere($scale3->id)); + $this->assertTrue(api::is_scale_used_anywhere($scale4->id)); + + // Multiple occurrences of the same scale (3, and 4). + $f4 = $lpg->create_framework([ + 'scaleid' => $scale3->id, + 'scaleconfiguration' => json_encode([ + ['scaleid' => $scale3->id], + ['id' => 1, 'scaledefault' => 1, 'proficient' => 1] + ]) + ]); + $c4 = $lpg->create_competency([ + 'competencyframeworkid' => $f3->get_id(), + 'scaleid' => $scale4->id, + 'scaleconfiguration' => json_encode([ + ['scaleid' => $scale4->id], + ['id' => 1, 'scaledefault' => 1, 'proficient' => 1] + ]) + ]); + $this->assertTrue(api::is_scale_used_anywhere($scale1->id)); + $this->assertTrue(api::is_scale_used_anywhere($scale2->id)); + $this->assertTrue(api::is_scale_used_anywhere($scale3->id)); + $this->assertTrue(api::is_scale_used_anywhere($scale4->id)); + } + + public function test_delete_evidence() { + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $ccg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + $f1 = $ccg->create_framework(); + $comp1 = $ccg->create_competency(['competencyframeworkid' => $f1->get_id()]); + $uc1 = $ccg->create_user_competency(['userid' => $u1->id, 'competencyid' => $comp1->get_id()]); + + $ev1 = $ccg->create_evidence(['usercompetencyid' => $uc1->get_id()]); + $ev2 = $ccg->create_evidence(['usercompetencyid' => $uc1->get_id()]); + + $this->setAdminUser($u1); + + $this->assertEquals(2, evidence::count_records()); + api::delete_evidence($ev1); + $this->assertEquals(1, evidence::count_records()); + $this->assertFalse(evidence::record_exists($ev1->get_id())); + $this->assertTrue(evidence::record_exists($ev2->get_id())); + } + + public function test_delete_evidence_without_permissions() { + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $ccg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + $f1 = $ccg->create_framework(); + $comp1 = $ccg->create_competency(['competencyframeworkid' => $f1->get_id()]); + $uc1 = $ccg->create_user_competency(['userid' => $u1->id, 'competencyid' => $comp1->get_id()]); + $ev1 = $ccg->create_evidence(['usercompetencyid' => $uc1->get_id()]); + + $this->setUser($u1); + $this->setExpectedException('required_capability_exception'); + + api::delete_evidence($ev1); + } +} diff --git a/competency/tests/competency_rule_test.php b/competency/tests/competency_rule_test.php new file mode 100644 index 00000000000..950e25531ed --- /dev/null +++ b/competency/tests/competency_rule_test.php @@ -0,0 +1,377 @@ +. + +/** + * Competency rule tests. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +use core_competency\user_competency; +use core_competency\competency; +use core_competency\competency_rule_all; +use core_competency\competency_rule_points; + +/** + * Competency rule testcase. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_competency_rule_testcase extends externallib_advanced_testcase { + + public function test_rule_all_matching() { + $this->resetAfterTest(true); + + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $u1 = $this->getDataGenerator()->create_user(); + + // Set up the framework and competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), + 'ruletype' => 'core_competency\competency_rule_all')); + $c11 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id(), + 'ruletype' => 'core_competency\competency_rule_all')); + $c111 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c11->get_id(), + 'ruletype' => 'core_competency\competency_rule_all')); + $c112 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c11->get_id(), + 'ruletype' => 'core_competency\competency_rule_all')); + $c12 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id(), + 'ruletype' => 'core_competency\competency_rule_all')); + $c13 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id(), + 'ruletype' => 'core_competency\competency_rule_all')); + $c131 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c13->get_id(), + 'ruletype' => 'core_competency\competency_rule_all')); + + // Create some user competency records. + $uc1 = $lpg->create_user_competency(array('competencyid' => $c1->get_id(), 'userid' => $u1->id)); + $uc11 = $lpg->create_user_competency(array('competencyid' => $c11->get_id(), 'userid' => $u1->id, + 'grade' => 1, 'proficiency' => 1)); + $uc111 = $lpg->create_user_competency(array('competencyid' => $c111->get_id(), 'userid' => $u1->id, + 'grade' => 1, 'proficiency' => 1)); + $uc112 = $lpg->create_user_competency(array('competencyid' => $c112->get_id(), 'userid' => $u1->id, + 'grade' => 1, 'proficiency' => 1)); + $uc12 = $lpg->create_user_competency(array('competencyid' => $c12->get_id(), 'userid' => $u1->id)); + $uc13 = new user_competency(0, (object) array('userid' => $u1->id, 'competencyid' => $c13->get_id())); + + // Not all children are met. + $cr = new competency_rule_all($c1); + $this->assertFalse($cr->matches($uc1)); + + // All children are met. + $cr = new competency_rule_all($c11); + $this->assertTrue($cr->matches($uc11)); + + // The competency doesn't have any children. + $cr = new competency_rule_all($c12); + $this->assertFalse($cr->matches($uc12)); + + // The competency doesn't have saved user competency records. + $cr = new competency_rule_all($c13); + $this->assertFalse($cr->matches($uc13)); + } + + public function test_rule_points_validation() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $framework2 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + $c4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $cx = $lpg->create_competency(array('competencyframeworkid' => $framework2->get_id())); + + $c1->set_ruletype('core_competency\competency_rule_points'); + $rule = new competency_rule_points($c1); + + // Invalid config. + $config = json_encode(array()); + $this->assertFalse($rule->validate_config($config)); + + // Missing required points. + $config = json_encode(array( + 'base' => array(), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Invalid required points. + $config = json_encode(array( + 'base' => array('points' => 'abc'), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Less than 1 required points. + $config = json_encode(array( + 'base' => array('points' => 0), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Not enough required points. + $config = json_encode(array( + 'base' => array('points' => 3), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Duplicate competency. + $config = json_encode(array( + 'base' => array('points' => 1), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Competency includes itself. + $config = json_encode(array( + 'base' => array('points' => 1), + 'competencies' => array( + array('id' => $c1->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Cannot use negative points. + $config = json_encode(array( + 'base' => array('points' => 1), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => -1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // Not competencies set. + $config = json_encode(array( + 'base' => array('points' => 1), + 'competencies' => array( + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // There is a competency that is not a child. + $config = json_encode(array( + 'base' => array('points' => 1), + 'competencies' => array( + array('id' => $c1->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c2->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // There is a competency from another framework in there. + $config = json_encode(array( + 'base' => array('points' => 1), + 'competencies' => array( + array('id' => $cx->get_id(), 'points' => 1, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 1, 'required' => 0), + ) + )); + $this->assertFalse($rule->validate_config($config)); + + // A normal config. + $config = json_encode(array( + 'base' => array('points' => 4), + 'competencies' => array( + array('id' => $c2->get_id(), 'points' => 3, 'required' => 0), + array('id' => $c3->get_id(), 'points' => 2, 'required' => 1), + ) + )); + $this->assertTrue($rule->validate_config($config)); + } + + public function test_rule_points_matching() { + $this->resetAfterTest(true); + + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $u1 = $this->getDataGenerator()->create_user(); + + // Set up the framework and competencies. + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c1->set_ruletype('core_competency\competency_rule_points'); + $comprule = new competency_rule_points($c1); + $c11 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + $c12 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + $c13 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + $c14 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + + // Create some user competency records. + $uc1 = $lpg->create_user_competency(array('competencyid' => $c1->get_id(), 'userid' => $u1->id)); + $uc11 = $lpg->create_user_competency(array('competencyid' => $c11->get_id(), 'userid' => $u1->id, + 'grade' => 1, 'proficiency' => 1)); + $uc12 = $lpg->create_user_competency(array('competencyid' => $c12->get_id(), 'userid' => $u1->id, + 'grade' => 1, 'proficiency' => 1)); + $uc13 = $lpg->create_user_competency(array('competencyid' => $c13->get_id(), 'userid' => $u1->id)); + + // Enough points. + $rule = array( + 'base' => array('points' => 8), + 'competencies' => array( + array( + 'id' => $c11->get_id(), + 'points' => 4, + 'required' => 0 + ), + array( + 'id' => $c12->get_id(), + 'points' => 4, + 'required' => 0 + ), + ) + ); + $c1->set_ruleconfig(json_encode($rule)); + $c1->update(); + $this->assertTrue($comprule->matches($uc1)); + + // Not enough points. + $rule = array( + 'base' => array('points' => 8), + 'competencies' => array( + array( + 'id' => $c11->get_id(), + 'points' => 4, + 'required' => 0 + ), + array( + 'id' => $c13->get_id(), + 'points' => 4, + 'required' => 0 + ), + ) + ); + $c1->set_ruleconfig(json_encode($rule)); + $c1->update(); + $this->assertFalse($comprule->matches($uc1)); + + // One required that is not met but points were OK. + $rule = array( + 'base' => array('points' => 8), + 'competencies' => array( + array( + 'id' => $c11->get_id(), + 'points' => 4, + 'required' => 0 + ), + array( + 'id' => $c12->get_id(), + 'points' => 4, + 'required' => 0 + ), + array( + 'id' => $c13->get_id(), + 'points' => 4, + 'required' => 1 + ), + ) + ); + $c1->set_ruleconfig(json_encode($rule)); + $c1->update(); + $this->assertFalse($comprule->matches($uc1)); + + // One required, one not, should match. + $rule = array( + 'base' => array('points' => 8), + 'competencies' => array( + array( + 'id' => $c11->get_id(), + 'points' => 4, + 'required' => 0 + ), + array( + 'id' => $c12->get_id(), + 'points' => 4, + 'required' => 1 + ), + ) + ); + $c1->set_ruleconfig(json_encode($rule)); + $c1->update(); + $this->assertTrue($comprule->matches($uc1)); + + // All required and should match. + $rule = array( + 'base' => array('points' => 8), + 'competencies' => array( + array( + 'id' => $c11->get_id(), + 'points' => 4, + 'required' => 1 + ), + array( + 'id' => $c12->get_id(), + 'points' => 4, + 'required' => 1 + ), + ) + ); + $c1->set_ruleconfig(json_encode($rule)); + $c1->update(); + $this->assertTrue($comprule->matches($uc1)); + + // All required, but one doesn't have a user record. + $rule = array( + 'base' => array('points' => 4), + 'competencies' => array( + array( + 'id' => $c12->get_id(), + 'points' => 4, + 'required' => 1 + ), + array( + 'id' => $c14->get_id(), + 'points' => 4, + 'required' => 1 + ), + ) + ); + $c1->set_ruleconfig(json_encode($rule)); + $c1->update(); + $this->assertFalse($comprule->matches($uc1)); + } + +} diff --git a/competency/tests/course_competency_settings_test.php b/competency/tests/course_competency_settings_test.php new file mode 100644 index 00000000000..1897e8105f6 --- /dev/null +++ b/competency/tests/course_competency_settings_test.php @@ -0,0 +1,127 @@ +. + +/** + * Course competency persistent class tests. + * + * @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(); +global $CFG; + +use core_competency\course_competency; +use core_competency\api; +use core_competency\course_competency_settings; + +/** + * This test ensures that the course competency settings are applied and work correctly. + * + * @package core_competency + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_course_competency_settings_testcase extends advanced_testcase { + + public function test_who_can_change_settings() { + global $CFG, $DB; + + $this->resetAfterTest(true); + + $syscontext = context_system::instance(); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + $role = create_role('Settings changer role', 'settingschanger', 'Someone who can change course competency settings'); + assign_capability('moodle/competency:coursecompetencyconfigure', CAP_ALLOW, $role, $syscontext->id); + assign_capability('moodle/competency:competencygrade', CAP_ALLOW, $role, $syscontext->id); + assign_capability('moodle/competency:coursecompetencyview', CAP_ALLOW, $role, $syscontext->id); + assign_capability('moodle/competency:planview', CAP_ALLOW, $role, $syscontext->id); + $gradedrole = create_role('Graded role', 'graded', 'Someone who can be graded'); + assign_capability('moodle/competency:coursecompetencygradable', CAP_ALLOW, $gradedrole, $syscontext->id); + + $c1 = $dg->create_course(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + + $framework = $lpg->create_framework(); + $comp1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $lpg->create_course_competency(array('competencyid' => $comp1->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp2->get_id(), 'courseid' => $c1->id)); + + // Enrol the user. + $dg->enrol_user($u1->id, $c1->id); + role_assign($gradedrole, $u1->id, $syscontext->id); + + // Assign roles. + role_assign($role, $u2->id, $syscontext->id); + + $this->setUser($u2); + + set_config('pushcourseratingstouserplans', true, 'core_competency'); + + $coursesettings = course_competency_settings::get_by_courseid($c1->id); + $this->assertTrue((boolean)$coursesettings->get_pushratingstouserplans()); + + set_config('pushcourseratingstouserplans', false, 'core_competency'); + + $coursesettings = course_competency_settings::get_by_courseid($c1->id); + $this->assertFalse((boolean)$coursesettings->get_pushratingstouserplans()); + + api::update_course_competency_settings($c1->id, (object) array('pushratingstouserplans' => true)); + $coursesettings = course_competency_settings::get_by_courseid($c1->id); + $this->assertTrue((boolean)$coursesettings->get_pushratingstouserplans()); + + set_config('pushcourseratingstouserplans', true, 'core_competency'); + api::update_course_competency_settings($c1->id, (object) array('pushratingstouserplans' => false)); + $coursesettings = course_competency_settings::get_by_courseid($c1->id); + $this->assertFalse((boolean)$coursesettings->get_pushratingstouserplans()); + + // Right now the setting is false. + api::grade_competency_in_course($c1->id, $u1->id, $comp1->get_id(), 1, 'Note'); + $filterparams = array( + 'userid' => $u1->id, + 'competencyid' => $comp1->get_id(), + ); + $usercompcourse = \core_competency\user_competency_course::get_record($filterparams); + $usercomp = \core_competency\user_competency::get_record($filterparams); + + // No grade in plan - only a grade in the course. + $this->assertEmpty($usercomp->get_grade()); + $this->assertEquals(1, $usercompcourse->get_grade()); + + api::update_course_competency_settings($c1->id, (object) array('pushratingstouserplans' => true)); + api::grade_competency_in_course($c1->id, $u1->id, $comp1->get_id(), 2, 'Note 2'); + $filterparams = array( + 'userid' => $u1->id, + 'competencyid' => $comp1->get_id(), + ); + $usercompcourse = \core_competency\user_competency_course::get_record($filterparams); + $usercomp = \core_competency\user_competency::get_record($filterparams); + + // Updated grade in plan - updated grade in the course. + $this->assertEquals(2, $usercomp->get_grade()); + $this->assertEquals(2, $usercompcourse->get_grade()); + + $this->setUser($u3); + $this->setExpectedException('required_capability_exception'); + api::update_course_competency_settings($c1->id, (object) array('pushratingstouserplans' => false)); + } + +} diff --git a/competency/tests/course_competency_test.php b/competency/tests/course_competency_test.php new file mode 100644 index 00000000000..421c0ec1510 --- /dev/null +++ b/competency/tests/course_competency_test.php @@ -0,0 +1,137 @@ +. + +/** + * Course competency persistent class tests. + * + * @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(); +global $CFG; + +use core_competency\course_competency; + +/** + * Course competency persistent testcase. + * + * @package core_competency + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_course_competency_testcase extends advanced_testcase { + + public function test_get_courses_with_competency_and_user() { + global $CFG, $DB; + + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $c1 = $dg->create_course(); + $c2 = $dg->create_course(); + $c3 = $dg->create_course(); + $c4 = $dg->create_course(); + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + $u4 = $dg->create_user(); + + $flatfileplugin = enrol_get_plugin('flatfile'); + $flatfileinstanceid = $flatfileplugin->add_instance($c2); + + $framework = $lpg->create_framework(); + $comp1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); // In C1, and C2. + $comp2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); // In C2. + $comp3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); // In None. + $comp4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); // In C4. + $lpg->create_course_competency(array('competencyid' => $comp1->get_id(), 'courseid' => $c1->id)); + $lpg->create_course_competency(array('competencyid' => $comp1->get_id(), 'courseid' => $c2->id)); + $lpg->create_course_competency(array('competencyid' => $comp2->get_id(), 'courseid' => $c2->id)); + $lpg->create_course_competency(array('competencyid' => $comp4->get_id(), 'courseid' => $c4->id)); + + // Enrol the user 1 in C1, C2, and C3. + $dg->enrol_user($u1->id, $c1->id); + $dg->enrol_user($u1->id, $c2->id); + $dg->enrol_user($u1->id, $c3->id); + + // Enrol the user 2 in C4. + $dg->enrol_user($u2->id, $c4->id); + + // Enrol the user 3 in C1 and C2, but non active in C2. + $dg->enrol_user($u3->id, $c1->id); + $dg->enrol_user($u3->id, $c2->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED); + + // Enrol the user 4 with a plugin which will be enabled/disabled. + $dg->enrol_user($u4->id, $c2->id, null, 'flatfile'); + + // Using the competency that is not used anywhere -> no courses. + $this->assertCount(0, course_competency::get_courses_with_competency_and_user($comp3->get_id(), $u1->id)); + + // Using the competency that is used in a course where the user is not enrolled -> no courses. + $this->assertCount(0, course_competency::get_courses_with_competency_and_user($comp4->get_id(), $u1->id)); + + // Using the competency that is used in a course where the user is enrolled -> one course. + $courses = course_competency::get_courses_with_competency_and_user($comp2->get_id(), $u1->id); + $this->assertCount(1, $courses); + $this->assertArrayHasKey($c2->id, $courses); + + // Using the competency used multiple times. + $courses = course_competency::get_courses_with_competency_and_user($comp1->get_id(), $u1->id); + $this->assertCount(2, $courses); + $this->assertArrayHasKey($c1->id, $courses); + $this->assertArrayHasKey($c2->id, $courses); + + // Checking for another user where the competency is used twice, but not for them. + $courses = course_competency::get_courses_with_competency_and_user($comp1->get_id(), $u2->id); + $this->assertCount(0, $courses); + + // Checking for another user where the competency is used in their course. + $courses = course_competency::get_courses_with_competency_and_user($comp4->get_id(), $u2->id); + $this->assertCount(1, $courses); + $this->assertArrayHasKey($c4->id, $courses); + + // Checking for a user who is suspended in a course. + $courses = course_competency::get_courses_with_competency_and_user($comp1->get_id(), $u3->id); + $this->assertCount(1, $courses); + $this->assertArrayHasKey($c1->id, $courses); + + // Check for the user with plugin enabled. + $enrolplugins = explode(',', $CFG->enrol_plugins_enabled); + $enrolplugins[] = 'flatfile'; + $CFG->enrol_plugins_enabled = implode(',', array_unique($enrolplugins)); + $courses = course_competency::get_courses_with_competency_and_user($comp1->get_id(), $u4->id); + $this->assertCount(1, $courses); + $this->assertArrayHasKey($c2->id, $courses); + + // Check for the user with plugin enabled, but enrolment instance disabled. + $flatfileinstance = $DB->get_record('enrol', array('id' => $flatfileinstanceid)); + $flatfileplugin->update_status($flatfileinstance, ENROL_INSTANCE_DISABLED); + $courses = course_competency::get_courses_with_competency_and_user($comp1->get_id(), $u4->id); + $this->assertCount(0, $courses); + $flatfileplugin->update_status($flatfileinstance, ENROL_INSTANCE_ENABLED); + + // Check for the user with plugin disabled. + $enrolplugins = array_flip(explode(',', $CFG->enrol_plugins_enabled)); + unset($enrolplugins['flatfile']); + $CFG->enrol_plugins_enabled = implode(',', array_keys($enrolplugins)); + $courses = course_competency::get_courses_with_competency_and_user($comp1->get_id(), $u4->id); + $this->assertCount(0, $courses); + } + +} diff --git a/competency/tests/event_test.php b/competency/tests/event_test.php new file mode 100644 index 00000000000..95223a3a88a --- /dev/null +++ b/competency/tests/event_test.php @@ -0,0 +1,1696 @@ +. + +/** + * Event tests. + * + * @package core_competency + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->dirroot . '/comment/lib.php'); + +use core_competency\api; +use core_competency\url; + +/** + * Event tests. + * + * @package core_competency + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_event_testcase extends advanced_testcase { + + /** + * Test the competency framework created event. + * + */ + public function test_competency_framework_created() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + // Use DataGenerator to have a record framework with the right format. + $record = $lpg->create_framework()->to_record(); + $record->id = 0; + $record->shortname = "New shortname"; + $record->idnumber = "New idnumber"; + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $framework = api::create_framework((object) $record); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_framework_created', $event); + $this->assertEquals($framework->get_id(), $event->objectid); + $this->assertEquals($framework->get_contextid(), $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the competency framework deleted event. + * + */ + public function test_competency_framework_deleted() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $framework = $lpg->create_framework(); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::delete_framework($framework->get_id()); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_framework_deleted', $event); + $this->assertEquals($framework->get_id(), $event->objectid); + $this->assertEquals($framework->get_contextid(), $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the competency framework updated event. + * + */ + public function test_competency_framework_updated() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $framework = $lpg->create_framework(); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $framework->set_shortname('Shortname modified'); + api::update_framework($framework->to_record()); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_framework_updated', $event); + $this->assertEquals($framework->get_id(), $event->objectid); + $this->assertEquals($framework->get_contextid(), $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the competency framework viewed event. + * + */ + public function test_competency_framework_viewed() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $framework = $lpg->create_framework(); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::competency_framework_viewed($framework); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_framework_viewed', $event); + $this->assertEquals($framework->get_id(), $event->objectid); + $this->assertEquals($framework->get_contextid(), $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the competency viewed event. + * + */ + public function test_competency_viewed() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $competency = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::competency_viewed($competency); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_viewed', $event); + $this->assertEquals($competency->get_id(), $event->objectid); + $this->assertEquals($competency->get_context()->id, $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the template viewed event. + * + */ + public function test_template_viewed() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $template = $lpg->create_template(); + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::template_viewed($template); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_template_viewed', $event); + $this->assertEquals($template->get_id(), $event->objectid); + $this->assertEquals($template->get_contextid(), $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the template created event. + * + */ + public function test_template_created() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + // Use DataGenerator to have a template record with the right format. + $record = $lpg->create_template()->to_record(); + $record->id = 0; + $record->shortname = "New shortname"; + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $template = api::create_template((object) $record); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + $this->assertInstanceOf('\core\event\competency_template_created', $event); + $this->assertEquals($template->get_id(), $event->objectid); + $this->assertEquals($template->get_contextid(), $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the template deleted event. + * + */ + public function test_template_deleted() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $template = $lpg->create_template(); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::delete_template($template->get_id()); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_template_deleted', $event); + $this->assertEquals($template->get_id(), $event->objectid); + $this->assertEquals($template->get_contextid(), $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the template updated event. + * + */ + public function test_template_updated() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $template = $lpg->create_template(); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $template->set_shortname('Shortname modified'); + api::update_template($template->to_record()); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_template_updated', $event); + $this->assertEquals($template->get_id(), $event->objectid); + $this->assertEquals($template->get_contextid(), $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the competency updated event. + * + */ + public function test_competency_updated() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $f1 = $lpg->create_framework(); + $competency = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c12 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + $c13 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $competency->set_shortname('Shortname modified'); + api::update_competency($competency->to_record()); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_updated', $event); + $this->assertEquals($competency->get_id(), $event->objectid); + $this->assertEquals($competency->get_context()->id, $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the competency created event. + * + */ + public function test_competency_created() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $record = $c1->to_record(); + $record->id = 0; + $record->idnumber = 'comp idnumber'; + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + // Create competency should trigger a created event. + $competency = api::create_competency($record); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + $this->assertInstanceOf('\core\event\competency_created', $event); + $this->assertEquals($competency->get_id(), $event->objectid); + $this->assertEquals($competency->get_context()->id, $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the competency created event by duplicate framework. + * + */ + public function test_competency_created_by_duplicateframework() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c12 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + // Create framework should trigger a created event for competencies. + api::duplicate_framework($f1->get_id()); + + // Get our event event. + $events = $sink->get_events(); + $this->assertEquals(4, count($events)); + + $event = array_shift($events); + $this->assertInstanceOf('\core\event\competency_created', $event); + + $event = array_shift($events); + $this->assertInstanceOf('\core\event\competency_created', $event); + + $event = array_shift($events); + $this->assertInstanceOf('\core\event\competency_created', $event); + + $event = array_shift($events); + $this->assertInstanceOf('\core\event\competency_framework_created', $event); + } + + /** + * Test the competency deleted event. + * + */ + public function test_competency_deleted() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1id = $c1->get_id(); + $contextid = $c1->get_context()->id; + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + // Delete competency should trigger a deleted event. + api::delete_competency($c1id); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + $this->assertInstanceOf('\core\event\competency_deleted', $event); + $this->assertEquals($c1id, $event->objectid); + $this->assertEquals($contextid, $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the competency deleted event by delete framework. + * + */ + public function test_competency_deleted_by_deleteframework() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c12 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'parentid' => $c1->get_id())); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + // Delete framework should trigger a deleted event for competencies. + api::delete_framework($f1->get_id()); + + // Get our event event. + $events = $sink->get_events(); + $this->assertEquals(4, count($events)); + + $event = array_shift($events); + $this->assertInstanceOf('\core\event\competency_framework_deleted', $event); + + $event = array_shift($events); + $this->assertInstanceOf('\core\event\competency_deleted', $event); + + $event = array_shift($events); + $this->assertInstanceOf('\core\event\competency_deleted', $event); + + $event = array_shift($events); + $this->assertInstanceOf('\core\event\competency_deleted', $event); + } + + /** + * Test the plan created event. + * + */ + public function test_plan_created() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + $plan = array ( + 'name' => 'plan', + 'userid' => $user->id + ); + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $plan = api::create_plan((object)$plan); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_plan_created', $event); + $this->assertEquals($plan->get_id(), $event->objectid); + $this->assertEquals($plan->get_context()->id, $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the plan created event using template_cohort. + * + */ + public function test_plan_created_using_templatecohort() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $user2 = $dg->create_user(); + $c1 = $dg->create_cohort(); + // Add 2 users to the cohort. + cohort_add_member($c1->id, $user1->id); + cohort_add_member($c1->id, $user2->id); + $t1 = $lpg->create_template(); + $tc = $lpg->create_template_cohort(array( + 'templateid' => $t1->get_id(), + 'cohortid' => $c1->id + )); + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::create_plans_from_template_cohort($t1->get_id(), $c1->id); + // Get our event event. + $plans = core_competency\plan::get_records(array('templateid' => $t1->get_id()), 'id'); + $events = $sink->get_events(); + $this->assertCount(2, $events); + $this->assertCount(2, $plans); + $event = $events[0]; + $plan = $plans[0]; + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_plan_created', $event); + $this->assertEquals($plan->get_id(), $event->objectid); + $this->assertEquals($plan->get_context()->id, $event->contextid); + $event = $events[1]; + $plan = $plans[1]; + $this->assertInstanceOf('\core\event\competency_plan_created', $event); + $this->assertEquals($plan->get_id(), $event->objectid); + $this->assertEquals($plan->get_context()->id, $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the plan updated event. + * + */ + public function test_plan_updated() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user1->id)); + $record = $plan->to_record(); + $record->name = 'Plan updated'; + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $plan = api::update_plan($record); + $this->assertEquals('Plan updated', $plan->get_name()); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + $this->assertInstanceOf('\core\event\competency_plan_updated', $event); + $this->assertEquals($plan->get_id(), $event->objectid); + $this->assertEquals($plan->get_context()->id, $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the plan deleted event. + * + */ + public function test_plan_deleted() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user1->id)); + $planid = $plan->get_id(); + $contextid = $plan->get_context()->id; + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $result = api::delete_plan($plan->get_id()); + $this->assertTrue($result); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + $this->assertInstanceOf('\core\event\competency_plan_deleted', $event); + $this->assertEquals($planid, $event->objectid); + $this->assertEquals($contextid, $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the plan viewed event. + * + */ + public function test_plan_viewed() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user1->id)); + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::plan_viewed($plan); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_plan_viewed', $event); + $this->assertEquals($plan->get_id(), $event->objectid); + $this->assertEquals($plan->get_context()->id, $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the evidence of prior learning created event. + * + */ + public function test_user_evidence_created() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $user = $dg->create_user(); + // Use DataGenerator to have a user_evidence record with the right format. + $record = $userevidence = $lpg->create_user_evidence(array('userid' => $user->id))->to_record(); + $record->id = 0; + $record->name = "New name"; + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $userevidence = api::create_user_evidence((object) $record); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + $this->assertInstanceOf('\core\event\competency_user_evidence_created', $event); + $this->assertEquals($userevidence->get_id(), $event->objectid); + $this->assertEquals($userevidence->get_context()->id, $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the evidence of prior learning deleted event. + * + */ + public function test_user_evidence_deleted() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $user = $dg->create_user(); + $userevidence = $lpg->create_user_evidence(array('userid' => $user->id)); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::delete_user_evidence($userevidence->get_id()); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_user_evidence_deleted', $event); + $this->assertEquals($userevidence->get_id(), $event->objectid); + $this->assertEquals($userevidence->get_context()->id, $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the evidence of prior learning updated event. + * + */ + public function test_user_evidence_updated() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $user = $dg->create_user(); + $userevidence = $lpg->create_user_evidence(array('userid' => $user->id)); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $userevidence->set_name('Name modified'); + api::update_user_evidence($userevidence->to_record()); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_user_evidence_updated', $event); + $this->assertEquals($userevidence->get_id(), $event->objectid); + $this->assertEquals($userevidence->get_context()->id, $event->contextid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the user competency viewed event in plan. + * + */ + public function test_user_competency_viewed_in_plan() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user->id)); + $fr = $lpg->create_framework(); + $c = $lpg->create_competency(array('competencyframeworkid' => $fr->get_id())); + $pc = $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c->get_id())); + $uc = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c->get_id())); + + // Can not log the event for user competency using completed plan. + api::complete_plan($plan); + + try { + api::user_competency_viewed_in_plan($uc, $plan->get_id()); + $this->fail('To log the user competency in completed plan ' + . 'use user_competency_plan_viewed method.'); + } catch (coding_exception $e) { + $this->assertRegExp('/To log the user competency in completed plan ' + . 'use user_competency_plan_viewed method./', $e->getMessage()); + } + + api::reopen_plan($plan); + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::user_competency_viewed_in_plan($uc, $plan->get_id()); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_user_competency_viewed_in_plan', $event); + $this->assertEquals($uc->get_id(), $event->objectid); + $this->assertEquals($uc->get_context()->id, $event->contextid); + $this->assertEquals($uc->get_userid(), $event->relateduserid); + $this->assertEquals($plan->get_id(), $event->other['planid']); + $this->assertEquals($c->get_id(), $event->other['competencyid']); + + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + + // Test validation. + $params = array ( + 'objectid' => $uc->get_id(), + 'contextid' => $uc->get_context()->id, + 'other' => null + ); + + // Other value null. + try { + \core\event\competency_user_competency_viewed_in_plan::create($params)->trigger(); + $this->fail('The \'competencyid\' and \'planid\' values must be set.'); + } catch (coding_exception $e) { + $this->assertRegExp("/The 'competencyid' and 'planid' values must be set./", $e->getMessage()); + } + + $params['other']['anythingelse'] = ''; + // Missing competencyid. + try { + \core\event\competency_user_competency_viewed_in_plan::create($params)->trigger(); + $this->fail('The \'competencyid\' value must be set.'); + } catch (coding_exception $e) { + $this->assertRegExp("/The 'competencyid' value must be set./", $e->getMessage()); + } + + $params['other']['competencyid'] = $c->get_id(); + // Missing planid. + try { + \core\event\competency_user_competency_viewed_in_plan::create($params)->trigger(); + $this->fail('The \'planid\' value must be set.'); + } catch (coding_exception $e) { + $this->assertRegExp("/The 'planid' value must be set./", $e->getMessage()); + } + } + + /** + * Test the user competency viewed event in course. + * + */ + public function test_user_competency_viewed_in_course() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + $course = $dg->create_course(); + $fr = $lpg->create_framework(); + $c = $lpg->create_competency(array('competencyframeworkid' => $fr->get_id())); + $pc = $lpg->create_course_competency(array('courseid' => $course->id, 'competencyid' => $c->get_id())); + $params = array('userid' => $user->id, 'competencyid' => $c->get_id(), 'courseid' => $course->id); + $ucc = $lpg->create_user_competency_course($params); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::user_competency_viewed_in_course($ucc); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_user_competency_viewed_in_course', $event); + $this->assertEquals($ucc->get_id(), $event->objectid); + $this->assertEquals(context_course::instance($course->id)->id, $event->contextid); + $this->assertEquals($ucc->get_userid(), $event->relateduserid); + $this->assertEquals($course->id, $event->courseid); + $this->assertEquals($c->get_id(), $event->other['competencyid']); + + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + + // Test validation. + $params = array ( + 'objectid' => $ucc->get_id(), + 'contextid' => $ucc->get_context()->id, + 'other' => null + ); + + // Missing courseid. + try { + \core\event\competency_user_competency_viewed_in_course::create($params)->trigger(); + $this->fail('The \'courseid\' value must be set.'); + } catch (coding_exception $e) { + $this->assertRegExp("/The 'courseid' value must be set./", $e->getMessage()); + } + + $params['contextid'] = context_course::instance($course->id)->id; + $params['courseid'] = $course->id; + // Missing competencyid. + try { + \core\event\competency_user_competency_viewed_in_course::create($params)->trigger(); + $this->fail('The \'competencyid\' value must be set.'); + } catch (coding_exception $e) { + $this->assertRegExp("/The 'competencyid' value must be set./", $e->getMessage()); + } + } + + /** + * Test the user competency plan viewed event. + * + */ + public function test_user_competency_plan_viewed() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user->id)); + $fr = $lpg->create_framework(); + $c = $lpg->create_competency(array('competencyframeworkid' => $fr->get_id())); + $ucp = $lpg->create_user_competency_plan(array( + 'userid' => $user->id, + 'competencyid' => $c->get_id(), + 'planid' => $plan->get_id() + )); + + // Can not log the event for user competency using non completed plan. + try { + api::user_competency_plan_viewed($ucp); + $this->fail('To log the user competency in non-completed plan ' + . 'use user_competency_viewed_in_plan method.'); + } catch (coding_exception $e) { + $this->assertRegExp('/To log the user competency in non-completed plan ' + . 'use user_competency_viewed_in_plan method./', $e->getMessage()); + } + + // Complete the plan. + api::complete_plan($plan); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::user_competency_plan_viewed($ucp); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_user_competency_plan_viewed', $event); + $this->assertEquals($ucp->get_id(), $event->objectid); + $this->assertEquals($ucp->get_context()->id, $event->contextid); + $this->assertEquals($ucp->get_userid(), $event->relateduserid); + $this->assertEquals($plan->get_id(), $event->other['planid']); + $this->assertEquals($c->get_id(), $event->other['competencyid']); + + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + + // Test validation. + $params = array ( + 'objectid' => $ucp->get_id(), + 'contextid' => $ucp->get_context()->id, + 'other' => null + ); + + // Other value null. + try { + \core\event\competency_user_competency_plan_viewed::create($params)->trigger(); + $this->fail('The \'competencyid\' and \'planid\' values must be set.'); + } catch (coding_exception $e) { + $this->assertRegExp("/The 'competencyid' and 'planid' values must be set./", $e->getMessage()); + } + + $params['other']['anythingelse'] = ''; + // Missing competencyid. + try { + \core\event\competency_user_competency_plan_viewed::create($params)->trigger(); + $this->fail('The \'competencyid\' value must be set.'); + } catch (coding_exception $e) { + $this->assertRegExp("/The 'competencyid' value must be set./", $e->getMessage()); + } + + $params['other']['competencyid'] = $c->get_id(); + // Missing planid. + try { + \core\event\competency_user_competency_plan_viewed::create($params)->trigger(); + $this->fail('The \'planid\' value must be set.'); + } catch (coding_exception $e) { + $this->assertRegExp("/The 'planid' value must be set./", $e->getMessage()); + } + } + + /** + * Test the user competency viewed event. + * + */ + public function test_user_competency_viewed() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + $fr = $lpg->create_framework(); + $c = $lpg->create_competency(array('competencyframeworkid' => $fr->get_id())); + $uc = $lpg->create_user_competency(array( + 'userid' => $user->id, + 'competencyid' => $c->get_id() + )); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::user_competency_viewed($uc); + + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_user_competency_viewed', $event); + $this->assertEquals($uc->get_id(), $event->objectid); + $this->assertEquals($uc->get_context()->id, $event->contextid); + $this->assertEquals($uc->get_userid(), $event->relateduserid); + $this->assertEquals($c->get_id(), $event->other['competencyid']); + + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + + // Test validation. + $params = array ( + 'objectid' => $uc->get_id(), + 'contextid' => $uc->get_context()->id + ); + + // Missing competencyid. + try { + \core\event\competency_user_competency_viewed::create($params)->trigger(); + $this->fail('The \'competencyid\' value must be set.'); + } catch (coding_exception $e) { + $this->assertRegExp("/The 'competencyid' value must be set./", $e->getMessage()); + } + } + + /** + * Test the plan approved event. + * + */ + public function test_plan_approved() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user1->id)); + $planid = $plan->get_id(); + $contextid = $plan->get_context()->id; + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $result = api::approve_plan($plan->get_id()); + $this->assertTrue($result); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + $this->assertInstanceOf('\core\event\competency_plan_approved', $event); + $this->assertEquals($planid, $event->objectid); + $this->assertEquals($contextid, $event->contextid); + $this->assertEquals($plan->get_userid(), $event->relateduserid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the plan unapproved event. + * + */ + public function test_plan_unapproved() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user1->id, 'status' => \core_competency\plan::STATUS_ACTIVE)); + $planid = $plan->get_id(); + $contextid = $plan->get_context()->id; + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $result = api::unapprove_plan($plan->get_id()); + $this->assertTrue($result); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + $this->assertInstanceOf('\core\event\competency_plan_unapproved', $event); + $this->assertEquals($planid, $event->objectid); + $this->assertEquals($contextid, $event->contextid); + $this->assertEquals($plan->get_userid(), $event->relateduserid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the plan reopened event. + * + */ + public function test_plan_reopened() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user1->id, 'status' => \core_competency\plan::STATUS_COMPLETE)); + $planid = $plan->get_id(); + $contextid = $plan->get_context()->id; + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $result = api::reopen_plan($plan->get_id()); + $this->assertTrue($result); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + $this->assertInstanceOf('\core\event\competency_plan_reopened', $event); + $this->assertEquals($planid, $event->objectid); + $this->assertEquals($contextid, $event->contextid); + $this->assertEquals($plan->get_userid(), $event->relateduserid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the plan completed event. + * + */ + public function test_plan_completed() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user1->id, 'status' => \core_competency\plan::STATUS_ACTIVE)); + $planid = $plan->get_id(); + $contextid = $plan->get_context()->id; + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $result = api::complete_plan($plan->get_id()); + $this->assertTrue($result); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + $this->assertInstanceOf('\core\event\competency_plan_completed', $event); + $this->assertEquals($planid, $event->objectid); + $this->assertEquals($contextid, $event->contextid); + $this->assertEquals($plan->get_userid(), $event->relateduserid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the plan unlinked event. + * + */ + public function test_plan_unlinked() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $template = $lpg->create_template(); + $plan = $lpg->create_plan(array( + 'userid' => $user1->id, + 'status' => \core_competency\plan::STATUS_ACTIVE, + 'templateid' => $template->get_id() + )); + $planid = $plan->get_id(); + $contextid = $plan->get_context()->id; + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $result = api::unlink_plan_from_template($plan->get_id()); + $this->assertTrue($result); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + $this->assertInstanceOf('\core\event\competency_plan_unlinked', $event); + $this->assertEquals($planid, $event->objectid); + $this->assertEquals($contextid, $event->contextid); + $this->assertEquals($plan->get_userid(), $event->relateduserid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the plan review requested event. + * + */ + public function test_plan_review_requested() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user1->id)); + $planid = $plan->get_id(); + $contextid = $plan->get_context()->id; + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $result = api::plan_request_review($plan->get_id()); + $this->assertTrue($result); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + $this->assertInstanceOf('\core\event\competency_plan_review_requested', $event); + $this->assertEquals($planid, $event->objectid); + $this->assertEquals($contextid, $event->contextid); + $this->assertEquals($plan->get_userid(), $event->relateduserid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the plan review request cancelled event. + * + */ + public function test_plan_review_request_cancelled() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user1->id, 'status' => \core_competency\plan::STATUS_WAITING_FOR_REVIEW)); + $planid = $plan->get_id(); + $contextid = $plan->get_context()->id; + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $result = api::plan_cancel_review_request($plan->get_id()); + $this->assertTrue($result); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + $this->assertInstanceOf('\core\event\competency_plan_review_request_cancelled', $event); + $this->assertEquals($planid, $event->objectid); + $this->assertEquals($contextid, $event->contextid); + $this->assertEquals($plan->get_userid(), $event->relateduserid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the plan review started event. + * + */ + public function test_plan_review_started() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user1->id, 'status' => \core_competency\plan::STATUS_WAITING_FOR_REVIEW)); + $planid = $plan->get_id(); + $contextid = $plan->get_context()->id; + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $result = api::plan_start_review($plan->get_id()); + $this->assertTrue($result); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + $this->assertInstanceOf('\core\event\competency_plan_review_started', $event); + $this->assertEquals($planid, $event->objectid); + $this->assertEquals($contextid, $event->contextid); + $this->assertEquals($plan->get_userid(), $event->relateduserid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the plan review stopped event. + * + */ + public function test_plan_review_stopped() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user1->id, 'status' => \core_competency\plan::STATUS_IN_REVIEW)); + $planid = $plan->get_id(); + $contextid = $plan->get_context()->id; + // Trigger and capture the event. + $sink = $this->redirectEvents(); + $result = api::plan_stop_review($plan->get_id()); + $this->assertTrue($result); + // Get our event event. + $events = $sink->get_events(); + $event = reset($events); + $this->assertInstanceOf('\core\event\competency_plan_review_stopped', $event); + $this->assertEquals($planid, $event->objectid); + $this->assertEquals($contextid, $event->contextid); + $this->assertEquals($plan->get_userid(), $event->relateduserid); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test plan comment created event. + */ + public function test_plan_comment_created() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + $this->setUser($user); + $plan = $lpg->create_plan(array('userid' => $user->id)); + $context = context_user::instance($user->id); + + $cmt = new stdClass(); + $cmt->context = $context; + $cmt->area = 'plan'; + $cmt->itemid = $plan->get_id(); + $cmt->component = 'competency'; + $cmt->showcount = 1; + $manager = new comment($cmt); + $manager->set_post_permission(true); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $manager->add("New comment for plan"); + $events = $sink->get_events(); + // Add comment will trigger 2 other events message_viewed and message_sent. + $this->assertCount(1, $events); + $event = array_pop($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\core\event\competency_comment_created', $event); + $this->assertEquals($context, $event->get_context()); + $this->assertEquals($plan->get_id(), $event->other['itemid']); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test plan comment deleted event. + */ + public function test_plan_comment_deleted() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user1 = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user1->id)); + + $context = context_user::instance($user1->id); + + $cmt = new stdClass(); + $cmt->context = $context; + $cmt->area = 'plan'; + $cmt->itemid = $plan->get_id(); + $cmt->component = 'competency'; + $manager = new comment($cmt); + $newcomment = $manager->add("Comment to be deleted"); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $manager->delete($newcomment->id); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\core\event\competency_comment_deleted', $event); + $this->assertEquals($context, $event->get_context()); + $this->assertEquals($plan->get_id(), $event->other['itemid']); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test evidence_created event. + */ + public function test_evidence_created() { + global $USER; + + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $syscontext = context_system::instance(); + + // Create a student. + $student = $dg->create_user(); + + // Create a competency for the course. + $lpg = $dg->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $comp = $lpg->create_competency(['competencyframeworkid' => $framework->get_id()]); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + + // Add evidence. + $recommend = false; + $evidence = api::add_evidence($student->id, $comp, $syscontext, \core_competency\evidence::ACTION_OVERRIDE, + 'commentincontext', 'core', null, $recommend, null, 1); + + // Get event. + $events = $sink->get_events(); + $event = reset($events); + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_evidence_created', $event); + $this->assertEquals($evidence->get_contextid(), $event->contextid); + $this->assertEquals($evidence->get_id(), $event->objectid); + $this->assertEquals($evidence->get_actionuserid(), $event->userid); + $this->assertEquals($student->id, $event->relateduserid); + $this->assertEquals($evidence->get_usercompetencyid(), $event->other['usercompetencyid']); + $this->assertEquals($comp->get_id(), $event->other['competencyid']); + $this->assertEquals($evidence->get_action(), $event->other['action']); + $this->assertEquals($recommend, $event->other['recommend']); + + // Test get_name(). + $this->assertEquals(get_string('eventevidencecreated', 'core_competency'), $event->get_name()); + + // Test get_description(). + $description = "The user with id '$USER->id' created an evidence with id '{$evidence->get_id()}'."; + $this->assertEquals($description, $event->get_description()); + + // Test get_url(). + $url = url::user_competency($evidence->get_usercompetencyid()); + $this->assertEquals($url, $event->get_url()); + + // Test get_objectid_mapping(). + $this->assertEquals(\core\event\base::NOT_MAPPED, $event->get_objectid_mapping()); + + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test evidence_created event by linking an invalid user competency to an evidence. + */ + public function test_evidence_created_with_invalid_user_competency() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $syscontext = context_system::instance(); + + // Create students. + $student = $dg->create_user(); + $student2 = $dg->create_user(); + + // Create a competency for the course. + $lpg = $dg->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $comp = $lpg->create_competency(['competencyframeworkid' => $framework->get_id()]); + + // Create a different user competency. + $otheruc = \core_competency\user_competency::create_relation($student2->id, $comp->get_id()); + $otheruc->create(); + + // Add evidence. + $recommend = false; + $evidence = api::add_evidence($student->id, $comp, $syscontext, \core_competency\evidence::ACTION_OVERRIDE, + 'commentincontext', 'core', null, $recommend, null, 1); + + // We expect this to fail and throw a coding exception. + $this->setExpectedException('coding_exception', 'The user competency linked with this evidence is invalid.'); + \core\event\competency_evidence_created::create_from_evidence($evidence, $otheruc, $recommend)->trigger(); + } + + /** + * Test creation of evidence_created event with missing data. + * + * These data are validated by \core_competency\evidence_created::validate_data(). + */ + public function test_evidence_created_with_missing_data() { + $eventdata = [ + 'contextid' => 1, + 'objectid' => 1, + 'userid' => 1 + ]; + + // No relateduserid. + $errormsg = 'The \'relateduserid\' must be set.'; + try { + \core\event\competency_evidence_created::create($eventdata)->trigger(); + $this->fail('Coding exception should have been thrown: ' . $errormsg); + } catch (coding_exception $e) { + $this->assertContains($errormsg, $e->getMessage()); + } + $eventdata['relateduserid'] = 1; + + // No other['usercompetencyid']. + $errormsg = 'The \'usercompetencyid\' data in \'other\' must be set.'; + try { + \core\event\competency_evidence_created::create($eventdata)->trigger(); + $this->fail('Coding exception should have been thrown: ' . $errormsg); + } catch (coding_exception $e) { + $this->assertContains($errormsg, $e->getMessage()); + } + $eventdata['other']['usercompetencyid'] = 1; + + // No other['competencyid']. + $errormsg = 'The \'competencyid\' data in \'other\' must be set.'; + try { + \core\event\competency_evidence_created::create($eventdata)->trigger(); + $this->fail('Coding exception should have been thrown: ' . $errormsg); + } catch (coding_exception $e) { + $this->assertContains($errormsg, $e->getMessage()); + } + $eventdata['other']['competencyid'] = 1; + + // No other['action']. + $errormsg = 'The \'action\' data in \'other\' must be set.'; + try { + \core\event\competency_evidence_created::create($eventdata)->trigger(); + $this->fail('Coding exception should have been thrown: ' . $errormsg); + } catch (coding_exception $e) { + $this->assertContains($errormsg, $e->getMessage()); + } + $eventdata['other']['action'] = 1; + + // No other['recommend']. + $errormsg = 'The \'recommend\' data in \'other\' must be set.'; + try { + \core\event\competency_evidence_created::create($eventdata)->trigger(); + $this->fail('Coding exception should have been thrown: ' . $errormsg); + } catch (coding_exception $e) { + $this->assertContains($errormsg, $e->getMessage()); + } + $eventdata['other']['recommend'] = 1; + + // Event should be triggered without any problems. + \core\event\competency_evidence_created::create($eventdata)->trigger(); + } + + /** + * Test the user competency grade rated event. + * + */ + public function test_user_competency_rated() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $scale = $dg->create_scale(array('scale' => 'A,B,C,D')); + $scaleconfig = array(array('scaleid' => $scale->id)); + $scaleconfig[] = array('name' => 'A', 'id' => 1, 'scaledefault' => 0, 'proficient' => 0); + $scaleconfig[] = array('name' => 'B', 'id' => 2, 'scaledefault' => 1, 'proficient' => 0); + $scaleconfig[] = array('name' => 'C', 'id' => 3, 'scaledefault' => 0, 'proficient' => 1); + $scaleconfig[] = array('name' => 'D', 'id' => 4, 'scaledefault' => 0, 'proficient' => 1); + $fr = $lpg->create_framework(); + $c = $lpg->create_competency(array( + 'competencyframeworkid' => $fr->get_id(), + 'scaleid' => $scale->id, + 'scaleconfiguration' => $scaleconfig + )); + + $user = $dg->create_user(); + $uc = $lpg->create_user_competency(array( + 'userid' => $user->id, + 'competencyid' => $c->get_id())); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::grade_competency($user->id, $c->get_id(), 2, true); + + // Get our event event. + $events = $sink->get_events(); + // Evidence created. + $this->assertCount(2, $events); + $evidencecreatedevent = $events[0]; + $event = $events[1]; + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_evidence_created', $evidencecreatedevent); + $this->assertInstanceOf('\core\event\competency_user_competency_rated', $event); + $this->assertEquals($uc->get_id(), $event->objectid); + $this->assertEquals($uc->get_context()->id, $event->contextid); + $this->assertEquals($uc->get_userid(), $event->relateduserid); + $this->assertEquals(2, $event->other['grade']); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the user competency grade rated in course event. + * + */ + public function test_user_competency_rated_in_course() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $scale = $dg->create_scale(array('scale' => 'A,B,C,D')); + $course = $dg->create_course(); + $user = $dg->create_user(); + $studentarch = get_archetype_roles('student'); + $studentrole = array_shift($studentarch); + $scaleconfig = array(array('scaleid' => $scale->id)); + $scaleconfig[] = array('name' => 'A', 'id' => 1, 'scaledefault' => 0, 'proficient' => 0); + $scaleconfig[] = array('name' => 'B', 'id' => 2, 'scaledefault' => 1, 'proficient' => 0); + $scaleconfig[] = array('name' => 'C', 'id' => 3, 'scaledefault' => 0, 'proficient' => 1); + $scaleconfig[] = array('name' => 'D', 'id' => 4, 'scaledefault' => 0, 'proficient' => 1); + $fr = $lpg->create_framework(); + $c = $lpg->create_competency(array( + 'competencyframeworkid' => $fr->get_id(), + 'scaleid' => $scale->id, + 'scaleconfiguration' => $scaleconfig + )); + // Enrol the user as students in course. + $dg->enrol_user($user->id, $course->id, $studentrole->id); + $lpg->create_course_competency(array( + 'courseid' => $course->id, + 'competencyid' => $c->get_id())); + $uc = $lpg->create_user_competency(array( + 'userid' => $user->id, + 'competencyid' => $c->get_id())); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::grade_competency_in_course($course->id, $user->id, $c->get_id(), 2, true); + + // Get our event event. + $events = $sink->get_events(); + // Evidence created. + $this->assertCount(2, $events); + $evidencecreatedevent = $events[0]; + $event = $events[1]; + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_evidence_created', $evidencecreatedevent); + $this->assertInstanceOf('\core\event\competency_user_competency_rated_in_course', $event); + $this->assertEquals(context_course::instance($course->id)->id, $event->contextid); + $this->assertEquals($course->id, $event->courseid); + $this->assertEquals($uc->get_userid(), $event->relateduserid); + $this->assertEquals($uc->get_competencyid(), $event->other['competencyid']); + $this->assertEquals(2, $event->other['grade']); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test the user competency grade rated in plan event. + * + */ + public function test_user_competency_rated_in_plan() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $scale = $dg->create_scale(array('scale' => 'A,B,C,D')); + $user = $dg->create_user(); + $scaleconfig = array(array('scaleid' => $scale->id)); + $scaleconfig[] = array('name' => 'A', 'id' => 1, 'scaledefault' => 0, 'proficient' => 0); + $scaleconfig[] = array('name' => 'B', 'id' => 2, 'scaledefault' => 1, 'proficient' => 0); + $scaleconfig[] = array('name' => 'C', 'id' => 3, 'scaledefault' => 0, 'proficient' => 1); + $scaleconfig[] = array('name' => 'D', 'id' => 4, 'scaledefault' => 0, 'proficient' => 1); + $plan = $lpg->create_plan(array('userid' => $user->id)); + $fr = $lpg->create_framework(); + $c = $lpg->create_competency(array( + 'competencyframeworkid' => $fr->get_id(), + 'scaleid' => $scale->id, + 'scaleconfiguration' => $scaleconfig + )); + $pc = $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c->get_id())); + $uc = $lpg->create_user_competency(array( + 'userid' => $user->id, + 'competencyid' => $c->get_id())); + + // Trigger and capture the event. + $sink = $this->redirectEvents(); + api::grade_competency_in_plan($plan->get_id(), $c->get_id(), 3, true); + + // Get our event event. + $events = $sink->get_events(); + // Evidence created. + $this->assertCount(2, $events); + $evidencecreatedevent = $events[0]; + $event = $events[1]; + + // Check that the event data is valid. + $this->assertInstanceOf('\core\event\competency_evidence_created', $evidencecreatedevent); + $this->assertInstanceOf('\core\event\competency_user_competency_rated_in_plan', $event); + $this->assertEquals($uc->get_id(), $event->objectid); + $this->assertEquals($uc->get_context()->id, $event->contextid); + $this->assertEquals($uc->get_userid(), $event->relateduserid); + $this->assertEquals($uc->get_competencyid(), $event->other['competencyid']); + $this->assertEquals(3, $event->other['grade']); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test user competency comment created event. + */ + public function test_user_competency_comment_created() { + $this->resetAfterTest(true); + + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + $this->setUser($user); + $fr = $lpg->create_framework(); + $c = $lpg->create_competency(array('competencyframeworkid' => $fr->get_id())); + $uc = $lpg->create_user_competency(array( + 'userid' => $user->id, + 'competencyid' => $c->get_id() + )); + + $context = context_user::instance($user->id); + $cmt = new stdClass(); + $cmt->context = $context; + $cmt->area = 'user_competency'; + $cmt->itemid = $uc->get_id(); + $cmt->component = 'competency'; + $cmt->showcount = 1; + $manager = new comment($cmt); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $manager->add("New comment for user competency"); + $events = $sink->get_events(); + // Add comment will trigger 2 other events message_viewed and message_sent. + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\core\event\competency_comment_created', $event); + $this->assertEquals($context, $event->get_context()); + $this->assertEquals($uc->get_id(), $event->other['itemid']); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } + + /** + * Test plan comment deleted event. + */ + public function test_user_competency_comment_deleted() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + $fr = $lpg->create_framework(); + $c = $lpg->create_competency(array('competencyframeworkid' => $fr->get_id())); + $uc = $lpg->create_user_competency(array( + 'userid' => $user->id, + 'competencyid' => $c->get_id() + )); + $context = context_user::instance($user->id); + + $cmt = new stdClass(); + $cmt->context = $context; + $cmt->area = 'user_competency'; + $cmt->itemid = $uc->get_id(); + $cmt->component = 'competency'; + $manager = new comment($cmt); + $newcomment = $manager->add("Comment to be deleted"); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $manager->delete($newcomment->id); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\core\event\competency_comment_deleted', $event); + $this->assertEquals($context, $event->get_context()); + $this->assertEquals($uc->get_id(), $event->other['itemid']); + $this->assertEventContextNotUsed($event); + $this->assertDebuggingNotCalled(); + } +} diff --git a/competency/tests/exporter_test.php b/competency/tests/exporter_test.php new file mode 100644 index 00000000000..eb4830b882d --- /dev/null +++ b/competency/tests/exporter_test.php @@ -0,0 +1,161 @@ +. + +/** + * Exporter class tests. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +/** + * Exporter testcase. + * + * @package core_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_exporter_testcase extends advanced_testcase { + + protected $validrelated = null; + protected $invalidrelated = null; + protected $validdata = null; + protected $invaliddata = null; + + public function setUp() { + $s = new stdClass(); + $this->validrelated = array('simplestdClass' => $s, 'arrayofstdClass' => array($s, $s)); + $this->invalidrelated = array('simplestdClass' => 'a string', 'arrayofstdClass' => 5); + + $this->validdata = array('stringA' => 'A string', 'stringAformat' => FORMAT_HTML, 'intB' => 4); + + $this->invaliddata = array('stringA' => 'A string'); + } + + public function test_get_read_structure() { + $structure = core_competency_testable_exporter::get_read_structure(); + + $this->assertInstanceOf('external_single_structure', $structure); + $this->assertInstanceOf('external_value', $structure->keys['stringA']); + $this->assertInstanceOf('external_format_value', $structure->keys['stringAformat']); + $this->assertInstanceOf('external_value', $structure->keys['intB']); + $this->assertInstanceOf('external_value', $structure->keys['otherstring']); + $this->assertInstanceOf('external_multiple_structure', $structure->keys['otherstrings']); + } + + public function test_get_create_structure() { + $structure = core_competency_testable_exporter::get_create_structure(); + + $this->assertInstanceOf('external_single_structure', $structure); + $this->assertInstanceOf('external_value', $structure->keys['stringA']); + $this->assertInstanceOf('external_format_value', $structure->keys['stringAformat']); + $this->assertInstanceOf('external_value', $structure->keys['intB']); + $this->assertArrayNotHasKey('otherstring', $structure->keys); + $this->assertArrayNotHasKey('otherstrings', $structure->keys); + } + + public function test_get_update_structure() { + $structure = core_competency_testable_exporter::get_update_structure(); + + $this->assertInstanceOf('external_single_structure', $structure); + $this->assertInstanceOf('external_value', $structure->keys['stringA']); + $this->assertInstanceOf('external_format_value', $structure->keys['stringAformat']); + $this->assertInstanceOf('external_value', $structure->keys['intB']); + $this->assertArrayNotHasKey('otherstring', $structure->keys); + $this->assertArrayNotHasKey('otherstrings', $structure->keys); + } + + public function test_invalid_data() { + global $PAGE; + $this->setExpectedException('coding_exception'); + $exporter = new core_competency_testable_exporter($this->invaliddata, $this->validrelated); + $output = $PAGE->get_renderer('tool_lp'); + + $result = $exporter->export($output); + } + + public function test_invalid_related() { + global $PAGE; + $this->setExpectedException('coding_exception'); + $exporter = new core_competency_testable_exporter($this->validdata, $this->invalidrelated); + $output = $PAGE->get_renderer('tool_lp'); + + $result = $exporter->export($output); + } + + public function test_valid_data_and_related() { + global $PAGE; + $exporter = new core_competency_testable_exporter($this->validdata, $this->validrelated); + + $output = $PAGE->get_renderer('tool_lp'); + + $result = $exporter->export($output); + } +} + +/** + * Example persistent class. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_testable_exporter extends \core_competency\external\exporter { + + protected static function define_related() { + // We cache the context so it does not need to be retrieved from the course. + return array('simplestdClass' => 'stdClass', 'arrayofstdClass' => 'stdClass[]'); + } + + protected function get_other_values(renderer_base $output) { + return array( + 'otherstring' => 'An other string', + 'otherstrings' => array('String a', 'String b') + ); + } + + public static function define_properties() { + return array( + 'stringA' => array( + 'type' => PARAM_RAW, + ), + 'stringAformat' => array( + 'type' => PARAM_INT, + ), + 'intB' => array( + 'type' => PARAM_INT, + ) + ); + } + + public static function define_other_properties() { + return array( + 'otherstring' => array( + 'type' => PARAM_TEXT, + ), + 'otherstrings' => array( + 'type' => PARAM_TEXT, + 'multiple' => true + ) + ); + } + + +} diff --git a/competency/tests/external_test.php b/competency/tests/external_test.php new file mode 100644 index 00000000000..287ae50d566 --- /dev/null +++ b/competency/tests/external_test.php @@ -0,0 +1,2727 @@ +. + +/** + * External tests. + * + * @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(); +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +use core_competency\api; +use core_competency\course_competency_settings; +use core_competency\external; +use core_competency\invalid_persistent_exception; +use core_competency\plan; +use core_competency\plan_competency; +use core_competency\related_competency; +use core_competency\template; +use core_competency\template_competency; +use core_competency\user_competency; +use core_competency\user_competency_plan; + +/** + * External testcase. + * + * @package core_competency + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_external_testcase extends externallib_advanced_testcase { + + /** @var stdClass $creator User with enough permissions to create insystem context. */ + protected $creator = null; + + /** @var stdClass $learningplancreator User with enough permissions to create incategory context. */ + protected $catcreator = null; + + /** @var stdClass $category Category */ + protected $category = null; + + /** @var stdClass $user User with enough permissions to view insystem context */ + protected $user = null; + + /** @var stdClass $catuser User with enough permissions to view incategory context */ + protected $catuser = null; + + /** @var int Creator role id */ + protected $creatorrole = null; + + /** @var int User role id */ + protected $userrole = null; + + /** @var string scaleconfiguration */ + protected $scaleconfiguration1 = null; + + /** @var string scaleconfiguration */ + protected $scaleconfiguration2 = null; + + /** @var string catscaleconfiguration */ + protected $scaleconfiguration3 = null; + + /** @var string catscaleconfiguration */ + protected $catscaleconfiguration4 = 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. + $creator = $this->getDataGenerator()->create_user(); + $user = $this->getDataGenerator()->create_user(); + $catuser = $this->getDataGenerator()->create_user(); + $category = $this->getDataGenerator()->create_category(); + $othercategory = $this->getDataGenerator()->create_category(); + $catcreator = $this->getDataGenerator()->create_user(); + + $syscontext = context_system::instance(); + $catcontext = context_coursecat::instance($category->id); + $othercatcontext = context_coursecat::instance($othercategory->id); + + // Fetching default authenticated user role. + $userroles = get_archetype_roles('user'); + $this->assertCount(1, $userroles); + $authrole = array_pop($userroles); + + // Reset all default authenticated users permissions. + unassign_capability('moodle/competency:competencygrade', $authrole->id); + unassign_capability('moodle/competency:competencymanage', $authrole->id); + unassign_capability('moodle/competency:competencyview', $authrole->id); + unassign_capability('moodle/competency:planmanage', $authrole->id); + unassign_capability('moodle/competency:planmanagedraft', $authrole->id); + unassign_capability('moodle/competency:planmanageown', $authrole->id); + unassign_capability('moodle/competency:planview', $authrole->id); + unassign_capability('moodle/competency:planviewdraft', $authrole->id); + unassign_capability('moodle/competency:planviewown', $authrole->id); + unassign_capability('moodle/competency:planviewowndraft', $authrole->id); + unassign_capability('moodle/competency:templatemanage', $authrole->id); + unassign_capability('moodle/competency:templateview', $authrole->id); + unassign_capability('moodle/cohort:manage', $authrole->id); + unassign_capability('moodle/competency:coursecompetencyconfigure', $authrole->id); + + // Creating specific roles. + $this->creatorrole = create_role('Creator role', 'creatorrole', 'learning plan creator role description'); + $this->userrole = create_role('User role', 'userrole', 'learning plan user role description'); + + assign_capability('moodle/competency:competencymanage', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:competencycompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:competencyview', CAP_ALLOW, $this->userrole, $syscontext->id); + assign_capability('moodle/competency:planmanage', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:planmanagedraft', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:planview', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:planviewdraft', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:templatemanage', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:competencygrade', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/cohort:manage', CAP_ALLOW, $this->creatorrole, $syscontext->id); + assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $syscontext->id); + assign_capability('moodle/competency:planviewown', CAP_ALLOW, $this->userrole, $syscontext->id); + assign_capability('moodle/competency:planviewowndraft', CAP_ALLOW, $this->userrole, $syscontext->id); + + role_assign($this->creatorrole, $creator->id, $syscontext->id); + role_assign($this->creatorrole, $catcreator->id, $catcontext->id); + role_assign($this->userrole, $user->id, $syscontext->id); + role_assign($this->userrole, $catuser->id, $catcontext->id); + + $this->creator = $creator; + $this->catcreator = $catcreator; + $this->user = $user; + $this->catuser = $catuser; + $this->category = $category; + $this->othercategory = $othercategory; + + $this->getDataGenerator()->create_scale(array("id" => "1", "scale" => "value1, value2")); + $this->getDataGenerator()->create_scale(array("id" => "2", "scale" => "value3, value4")); + $this->getDataGenerator()->create_scale(array("id" => "3", "scale" => "value5, value6")); + $this->getDataGenerator()->create_scale(array("id" => "4", "scale" => "value7, value8")); + + $this->scaleconfiguration1 = '[{"scaleid":"1"},{"name":"value1","id":1,"scaledefault":1,"proficient":0},' . + '{"name":"value2","id":2,"scaledefault":0,"proficient":1}]'; + $this->scaleconfiguration2 = '[{"scaleid":"2"},{"name":"value3","id":1,"scaledefault":1,"proficient":0},' . + '{"name":"value4","id":2,"scaledefault":0,"proficient":1}]'; + $this->scaleconfiguration3 = '[{"scaleid":"3"},{"name":"value5","id":1,"scaledefault":1,"proficient":0},' . + '{"name":"value6","id":2,"scaledefault":0,"proficient":1}]'; + $this->scaleconfiguration4 = '[{"scaleid":"4"},{"name":"value8","id":1,"scaledefault":1,"proficient":0},' . + '{"name":"value8","id":2,"scaledefault":0,"proficient":1}]'; + accesslib_clear_all_caches_for_unit_testing(); + } + + + protected function create_competency_framework($number = 1, $system = true) { + $scalepropname = 'scaleconfiguration' . $number; + $framework = array( + 'shortname' => 'shortname' . $number, + 'idnumber' => 'idnumber' . $number, + 'description' => 'description' . $number, + 'descriptionformat' => FORMAT_HTML, + 'scaleid' => $number, + 'scaleconfiguration' => $this->$scalepropname, + 'visible' => true, + 'contextid' => $system ? context_system::instance()->id : context_coursecat::instance($this->category->id)->id + ); + $result = external::create_competency_framework($framework); + return (object) external_api::clean_returnvalue(external::create_competency_framework_returns(), $result); + } + + protected function create_plan($number, $userid, $templateid, $status, $duedate) { + $plan = array( + 'name' => 'name' . $number, + 'description' => 'description' . $number, + 'descriptionformat' => FORMAT_HTML, + 'userid' => $userid, + 'templateid' => empty($templateid) ? null : $templateid, + 'status' => $status, + 'duedate' => $duedate + ); + $result = external::create_plan($plan); + return (object) external_api::clean_returnvalue(external::create_plan_returns(), $result); + } + + protected function create_template($number, $system) { + $template = array( + 'shortname' => 'shortname' . $number, + 'description' => 'description' . $number, + 'descriptionformat' => FORMAT_HTML, + 'duedate' => 0, + 'visible' => true, + 'contextid' => $system ? context_system::instance()->id : context_coursecat::instance($this->category->id)->id + ); + $result = external::create_template($template); + return (object) external_api::clean_returnvalue(external::create_template_returns(), $result); + } + + protected function update_template($templateid, $number) { + $template = array( + 'id' => $templateid, + 'shortname' => 'shortname' . $number, + 'description' => 'description' . $number, + 'descriptionformat' => FORMAT_HTML, + 'visible' => true + ); + $result = external::update_template($template); + return external_api::clean_returnvalue(external::update_template_returns(), $result); + } + + protected function update_plan($planid, $number, $userid, $templateid, $status, $duedate) { + $plan = array( + 'id' => $planid, + 'name' => 'name' . $number, + 'description' => 'description' . $number, + 'descriptionformat' => FORMAT_HTML, + 'userid' => $userid, + 'templateid' => $templateid, + 'status' => $status, + 'duedate' => $duedate + ); + $result = external::update_plan($plan); + return external_api::clean_returnvalue(external::update_plan_returns(), $result); + } + + protected function update_competency_framework($id, $number = 1, $system = true) { + $scalepropname = 'scaleconfiguration' . $number; + $framework = array( + 'id' => $id, + 'shortname' => 'shortname' . $number, + 'idnumber' => 'idnumber' . $number, + 'description' => 'description' . $number, + 'descriptionformat' => FORMAT_HTML, + 'scaleid' => $number, + 'scaleconfiguration' => $this->$scalepropname, + 'visible' => true, + 'contextid' => $system ? context_system::instance()->id : context_coursecat::instance($this->category->id)->id + ); + $result = external::update_competency_framework($framework); + return external_api::clean_returnvalue(external::update_competency_framework_returns(), $result); + } + + protected function create_competency($number, $frameworkid) { + $competency = array( + 'shortname' => 'shortname' . $number, + 'idnumber' => 'idnumber' . $number, + 'description' => 'description' . $number, + 'descriptionformat' => FORMAT_HTML, + 'competencyframeworkid' => $frameworkid, + 'sortorder' => 0 + ); + $result = external::create_competency($competency); + return (object) external_api::clean_returnvalue(external::create_competency_returns(), $result); + } + + protected function update_competency($id, $number) { + $competency = array( + 'id' => $id, + 'shortname' => 'shortname' . $number, + 'idnumber' => 'idnumber' . $number, + 'description' => 'description' . $number, + 'descriptionformat' => FORMAT_HTML, + 'sortorder' => 0 + ); + $result = external::update_competency($competency); + return external_api::clean_returnvalue(external::update_competency_returns(), $result); + } + + /** + * Test we can't create a competency framework with only read permissions. + */ + public function test_create_competency_frameworks_with_read_permissions() { + $this->setExpectedException('required_capability_exception'); + $this->setUser($this->user); + + $result = $this->create_competency_framework(1, true); + } + + /** + * Test we can't create a competency framework with only read permissions. + */ + public function test_create_competency_frameworks_with_read_permissions_in_category() { + $this->setExpectedException('required_capability_exception'); + $this->setUser($this->catuser); + $result = $this->create_competency_framework(1, false); + } + + /** + * Test we can create a competency framework with manage permissions. + */ + public function test_create_competency_frameworks_with_manage_permissions() { + $this->setUser($this->creator); + $result = $this->create_competency_framework(1, true); + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname1', $result->shortname); + $this->assertEquals('idnumber1', $result->idnumber); + $this->assertEquals('description1', $result->description); + $this->assertEquals(FORMAT_HTML, $result->descriptionformat); + $this->assertEquals(1, $result->scaleid); + $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration); + $this->assertEquals(true, $result->visible); + } + + /** + * Test we can create a competency framework with manage permissions. + */ + public function test_create_competency_frameworks_with_manage_permissions_in_category() { + $this->setUser($this->catcreator); + $result = $this->create_competency_framework(1, false); + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->catcreator->id, $result->usermodified); + $this->assertEquals('shortname1', $result->shortname); + $this->assertEquals('idnumber1', $result->idnumber); + $this->assertEquals('description1', $result->description); + $this->assertEquals(FORMAT_HTML, $result->descriptionformat); + $this->assertEquals(1, $result->scaleid); + $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration); + $this->assertEquals(true, $result->visible); + + try { + $result = $this->create_competency_framework(1, true); + $this->fail('User cannot create a framework at system level.'); + } catch (required_capability_exception $e) { + // All good. + } + } + + /** + * Test we cannot create a competency framework with nasty data. + */ + public function test_create_competency_frameworks_with_nasty_data() { + $this->setUser($this->creator); + $this->setExpectedException('invalid_parameter_exception'); + $framework = array( + 'shortname' => 'short', + 'idnumber' => 'id;"number', + 'description' => 'de<>\\..scription', + 'descriptionformat' => FORMAT_HTML, + 'scaleid' => 1, + 'scaleconfiguration' => $this->scaleconfiguration1, + 'visible' => true, + 'contextid' => context_system::instance()->id + ); + $result = external::create_competency_framework($framework); + } + + /** + * Test we can read a competency framework with manage permissions. + */ + public function test_read_competency_frameworks_with_manage_permissions() { + $this->setUser($this->creator); + $result = $this->create_competency_framework(1, true); + + $id = $result->id; + $result = external::read_competency_framework($id); + $result = (object) external_api::clean_returnvalue(external::read_competency_framework_returns(), $result); + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname1', $result->shortname); + $this->assertEquals('idnumber1', $result->idnumber); + $this->assertEquals('description1', $result->description); + $this->assertEquals(FORMAT_HTML, $result->descriptionformat); + $this->assertEquals(1, $result->scaleid); + $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration); + $this->assertEquals(true, $result->visible); + } + + /** + * Test we can read a competency framework with manage permissions. + */ + public function test_read_competency_frameworks_with_manage_permissions_in_category() { + $this->setUser($this->creator); + + $insystem = $this->create_competency_framework(1, true); + $incat = $this->create_competency_framework(2, false); + + $this->setUser($this->catcreator); + $id = $incat->id; + $result = external::read_competency_framework($id); + $result = (object) external_api::clean_returnvalue(external::read_competency_framework_returns(), $result); + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname2', $result->shortname); + $this->assertEquals('idnumber2', $result->idnumber); + $this->assertEquals('description2', $result->description); + $this->assertEquals(FORMAT_HTML, $result->descriptionformat); + $this->assertEquals(2, $result->scaleid); + $this->assertEquals($this->scaleconfiguration2, $result->scaleconfiguration); + $this->assertEquals(true, $result->visible); + + try { + $id = $insystem->id; + $result = external::read_competency_framework($id); + $result = (object) external_api::clean_returnvalue(external::read_competency_framework_returns(), $result); + $this->fail('User cannot read a framework at system level.'); + } catch (required_capability_exception $e) { + // All good. + } + } + + /** + * Test we can read a competency framework with read permissions. + */ + public function test_read_competency_frameworks_with_read_permissions() { + $this->setUser($this->creator); + $result = $this->create_competency_framework(1, true); + + // Switch users to someone with less permissions. + $this->setUser($this->user); + $id = $result->id; + $result = external::read_competency_framework($id); + $result = (object) external_api::clean_returnvalue(external::read_competency_framework_returns(), $result); + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname1', $result->shortname); + $this->assertEquals('idnumber1', $result->idnumber); + $this->assertEquals('description1', $result->description); + $this->assertEquals(FORMAT_HTML, $result->descriptionformat); + $this->assertEquals(1, $result->scaleid); + $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration); + $this->assertEquals(true, $result->visible); + } + /** + * Test we can read a competency framework with read permissions. + */ + public function test_read_competency_frameworks_with_read_permissions_in_category() { + $this->setUser($this->creator); + + $insystem = $this->create_competency_framework(1, true); + $incat = $this->create_competency_framework(2, false); + + // Switch users to someone with less permissions. + $this->setUser($this->catuser); + $id = $incat->id; + $result = external::read_competency_framework($id); + $result = (object) external_api::clean_returnvalue(external::read_competency_framework_returns(), $result); + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname2', $result->shortname); + $this->assertEquals('idnumber2', $result->idnumber); + $this->assertEquals('description2', $result->description); + $this->assertEquals(FORMAT_HTML, $result->descriptionformat); + $this->assertEquals(2, $result->scaleid); + $this->assertEquals($this->scaleconfiguration2, $result->scaleconfiguration); + $this->assertEquals(true, $result->visible); + + // Switching to user with no permissions. + try { + $result = external::read_competency_framework($insystem->id); + $this->fail('Current user cannot should not be able to read the framework.'); + } catch (required_capability_exception $e) { + // All good. + } + } + + /** + * Test we can delete a competency framework with manage permissions. + */ + public function test_delete_competency_frameworks_with_manage_permissions() { + $this->setUser($this->creator); + $result = $this->create_competency_framework(1, true); + + $id = $result->id; + $result = external::delete_competency_framework($id); + $result = external_api::clean_returnvalue(external::delete_competency_framework_returns(), $result); + + $this->assertTrue($result); + } + + /** + * Test we can delete a competency framework with manage permissions. + */ + public function test_delete_competency_frameworks_with_manage_permissions_in_category() { + $this->setUser($this->creator); + + $insystem = $this->create_competency_framework(1, true); + $incat = $this->create_competency_framework(2, false); + + $this->setUser($this->catcreator); + $id = $incat->id; + $result = external::delete_competency_framework($id); + $result = external_api::clean_returnvalue(external::delete_competency_framework_returns(), $result); + + $this->assertTrue($result); + + try { + $id = $insystem->id; + $result = external::delete_competency_framework($id); + $result = external_api::clean_returnvalue(external::delete_competency_framework_returns(), $result); + $this->fail('Current user cannot should not be able to delete the framework.'); + } catch (required_capability_exception $e) { + // All good. + } + } + + /** + * Test we can delete a competency framework with read permissions. + */ + public function test_delete_competency_frameworks_with_read_permissions() { + $this->setExpectedException('required_capability_exception'); + $this->setUser($this->creator); + $result = $this->create_competency_framework(1, true); + + $id = $result->id; + // Switch users to someone with less permissions. + $this->setUser($this->user); + $result = external::delete_competency_framework($id); + } + + /** + * Test we can update a competency framework with manage permissions. + */ + public function test_update_competency_frameworks_with_manage_permissions() { + $this->setUser($this->creator); + $result = $this->create_competency_framework(1, true); + + $result = $this->update_competency_framework($result->id, 2, true); + + $this->assertTrue($result); + } + + /** + * Test we can update a competency framework with manage permissions. + */ + public function test_update_competency_frameworks_with_manage_permissions_in_category() { + $this->setUser($this->creator); + + $insystem = $this->create_competency_framework(1, true); + $incat = $this->create_competency_framework(2, false); + + $this->setUser($this->catcreator); + $id = $incat->id; + + $result = $this->update_competency_framework($incat->id, 3, false); + + $this->assertTrue($result); + + try { + $result = $this->update_competency_framework($insystem->id, 4, true); + $this->fail('Current user should not be able to update the framework.'); + } catch (required_capability_exception $e) { + // All good. + } + } + + public function test_update_framework_scale() { + $this->setUser($this->creator); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $s1 = $this->getDataGenerator()->create_scale(); + + $f1 = $lpg->create_framework(array('scaleid' => 1)); + $f2 = $lpg->create_framework(array('scaleid' => 1)); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f2->get_id())); + + $this->assertEquals(1, $f1->get_scaleid()); + + // Make the scale of f2 being used. + $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c2->get_id())); + + // Changing the framework where the scale is not used. + $result = $this->update_competency_framework($f1->get_id(), 3, true); + + $f1 = new \core_competency\competency_framework($f1->get_id()); + $this->assertEquals(3, $f1->get_scaleid()); + + // Changing the framework where the scale is used. + try { + $result = $this->update_competency_framework($f2->get_id(), 4, true); + $this->fail('The scale cannot be changed once used.'); + } catch (\core_competency\invalid_persistent_exception $e) { + $this->assertRegexp('/scaleid/', $e->getMessage()); + } + } + + /** + * Test we can update a competency framework with read permissions. + */ + public function test_update_competency_frameworks_with_read_permissions() { + $this->setExpectedException('required_capability_exception'); + $this->setUser($this->creator); + $result = $this->create_competency_framework(1, true); + + $this->setUser($this->user); + $result = $this->update_competency_framework($result->id, 2, true); + } + + /** + * Test we can list and count competency frameworks with manage permissions. + */ + public function test_list_and_count_competency_frameworks_with_manage_permissions() { + $this->setUser($this->creator); + $result = $this->create_competency_framework(1, true); + $result = $this->create_competency_framework(2, true); + $result = $this->create_competency_framework(3, true); + $result = $this->create_competency_framework(4, false); + + $result = external::count_competency_frameworks(array('contextid' => context_system::instance()->id), 'self'); + $result = external_api::clean_returnvalue(external::count_competency_frameworks_returns(), $result); + + $this->assertEquals($result, 3); + + $result = external::list_competency_frameworks('shortname', 'ASC', 0, 10, + array('contextid' => context_system::instance()->id), 'self', false); + $result = external_api::clean_returnvalue(external::list_competency_frameworks_returns(), $result); + + $this->assertEquals(count($result), 3); + $result = (object) $result[0]; + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname1', $result->shortname); + $this->assertEquals('idnumber1', $result->idnumber); + $this->assertEquals('description1', $result->description); + $this->assertEquals(FORMAT_HTML, $result->descriptionformat); + $this->assertEquals(1, $result->scaleid); + $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration); + $this->assertEquals(true, $result->visible); + } + + public function test_list_competency_frameworks_with_query() { + $this->setUser($this->creator); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $framework1 = $lpg->create_framework(array( + 'shortname' => 'shortname_beetroot', + 'idnumber' => 'idnumber_cinnamon', + 'description' => 'description', + 'descriptionformat' => FORMAT_HTML, + 'visible' => true, + 'contextid' => context_system::instance()->id + )); + $framework2 = $lpg->create_framework(array( + 'shortname' => 'shortname_citrus', + 'idnumber' => 'idnumber_beer', + 'description' => 'description', + 'descriptionformat' => FORMAT_HTML, + 'visible' => true, + 'contextid' => context_system::instance()->id + )); + + // Search on both ID number and shortname. + $result = external::list_competency_frameworks('shortname', 'ASC', 0, 10, + array('contextid' => context_system::instance()->id), 'self', false, 'bee'); + $result = external_api::clean_returnvalue(external::list_competency_frameworks_returns(), $result); + $this->assertCount(2, $result); + $f = (object) array_shift($result); + $this->assertEquals($framework1->get_id(), $f->id); + $f = (object) array_shift($result); + $this->assertEquals($framework2->get_id(), $f->id); + + // Search on ID number. + $result = external::list_competency_frameworks('shortname', 'ASC', 0, 10, + array('contextid' => context_system::instance()->id), 'self', false, 'beer'); + $result = external_api::clean_returnvalue(external::list_competency_frameworks_returns(), $result); + $this->assertCount(1, $result); + $f = (object) array_shift($result); + $this->assertEquals($framework2->get_id(), $f->id); + + // Search on shortname. + $result = external::list_competency_frameworks('shortname', 'ASC', 0, 10, + array('contextid' => context_system::instance()->id), 'self', false, 'cinnamon'); + $result = external_api::clean_returnvalue(external::list_competency_frameworks_returns(), $result); + $this->assertCount(1, $result); + $f = (object) array_shift($result); + $this->assertEquals($framework1->get_id(), $f->id); + + // No match. + $result = external::list_competency_frameworks('shortname', 'ASC', 0, 10, + array('contextid' => context_system::instance()->id), 'self', false, 'pwnd!'); + $result = external_api::clean_returnvalue(external::list_competency_frameworks_returns(), $result); + $this->assertCount(0, $result); + } + + /** + * Test we can list and count competency frameworks with read permissions. + */ + public function test_list_and_count_competency_frameworks_with_read_permissions() { + $this->setUser($this->creator); + $result = $this->create_competency_framework(1, true); + $result = $this->create_competency_framework(2, true); + $result = $this->create_competency_framework(3, true); + $result = $this->create_competency_framework(4, false); + + $this->setUser($this->user); + $result = external::count_competency_frameworks(array('contextid' => context_system::instance()->id), 'self'); + $result = external_api::clean_returnvalue(external::count_competency_frameworks_returns(), $result); + $this->assertEquals($result, 3); + + $result = external::list_competency_frameworks('shortname', 'ASC', 0, 10, + array('contextid' => context_system::instance()->id), 'self', false); + $result = external_api::clean_returnvalue(external::list_competency_frameworks_returns(), $result); + + $this->assertEquals(count($result), 3); + $result = (object) $result[0]; + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname1', $result->shortname); + $this->assertEquals('idnumber1', $result->idnumber); + $this->assertEquals('description1', $result->description); + $this->assertEquals(FORMAT_HTML, $result->descriptionformat); + $this->assertEquals(1, $result->scaleid); + $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration); + $this->assertEquals(true, $result->visible); + } + + /** + * Test we can't create a competency with only read permissions. + */ + public function test_create_competency_with_read_permissions() { + $this->setExpectedException('required_capability_exception'); + $framework = $this->getDataGenerator()->get_plugin_generator('core_competency')->create_framework(); + $this->setUser($this->user); + $competency = $this->create_competency(1, $framework->get_id()); + } + + /** + * Test we can create a competency with manage permissions. + */ + public function test_create_competency_with_manage_permissions() { + $this->setUser($this->creator); + $framework = $this->create_competency_framework(1, true); + $competency = $this->create_competency(1, $framework->id); + + $this->assertGreaterThan(0, $competency->timecreated); + $this->assertGreaterThan(0, $competency->timemodified); + $this->assertEquals($this->creator->id, $competency->usermodified); + $this->assertEquals('shortname1', $competency->shortname); + $this->assertEquals('idnumber1', $competency->idnumber); + $this->assertEquals('description1', $competency->description); + $this->assertEquals(FORMAT_HTML, $competency->descriptionformat); + $this->assertEquals(0, $competency->parentid); + $this->assertEquals($framework->id, $competency->competencyframeworkid); + } + + + /** + * Test we can create a competency with manage permissions. + */ + public function test_create_competency_with_manage_permissions_in_category() { + $this->setUser($this->creator); + + $insystem = $this->create_competency_framework(1, true); + $incat = $this->create_competency_framework(2, false); + + $this->setUser($this->catcreator); + + $competency = $this->create_competency(1, $incat->id); + + $this->assertGreaterThan(0, $competency->timecreated); + $this->assertGreaterThan(0, $competency->timemodified); + $this->assertEquals($this->catcreator->id, $competency->usermodified); + $this->assertEquals('shortname1', $competency->shortname); + $this->assertEquals('idnumber1', $competency->idnumber); + $this->assertEquals('description1', $competency->description); + $this->assertEquals(FORMAT_HTML, $competency->descriptionformat); + $this->assertEquals(0, $competency->parentid); + $this->assertEquals($incat->id, $competency->competencyframeworkid); + + try { + $competency = $this->create_competency(2, $insystem->id); + $this->fail('User should not be able to create a competency in system context.'); + } catch (required_capability_exception $e) { + // All good. + } + } + + /** + * Test we cannot create a competency with nasty data. + */ + public function test_create_competency_with_nasty_data() { + $this->setUser($this->creator); + $framework = $this->create_competency_framework(1, true); + $this->setExpectedException('invalid_parameter_exception'); + $competency = array( + 'shortname' => 'shortname', + 'idnumber' => 'id;"number', + 'description' => 'de<>\\..scription', + 'descriptionformat' => FORMAT_HTML, + 'competencyframeworkid' => $framework->id, + 'sortorder' => 0 + ); + $result = external::create_competency($competency); + $result = (object) external_api::clean_returnvalue(external::create_competency_returns(), $result); + } + + /** + * Test we can read a competency with manage permissions. + */ + public function test_read_competencies_with_manage_permissions() { + $this->setUser($this->creator); + $framework = $this->create_competency_framework(1, true); + $competency = $this->create_competency(1, $framework->id); + + $id = $competency->id; + $result = external::read_competency($id); + $result = (object) external_api::clean_returnvalue(external::read_competency_returns(), $result); + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname1', $result->shortname); + $this->assertEquals('idnumber1', $result->idnumber); + $this->assertEquals('description1', $result->description); + $this->assertEquals(FORMAT_HTML, $result->descriptionformat); + $this->assertEquals(0, $result->parentid); + $this->assertEquals($framework->id, $result->competencyframeworkid); + } + + /** + * Test we can read a competency with manage permissions. + */ + public function test_read_competencies_with_manage_permissions_in_category() { + $this->setUser($this->creator); + + $sysframework = $this->create_competency_framework(1, true); + $insystem = $this->create_competency(1, $sysframework->id); + + $catframework = $this->create_competency_framework(2, false); + $incat = $this->create_competency(2, $catframework->id); + + $this->setUser($this->catcreator); + $id = $incat->id; + $result = external::read_competency($id); + $result = (object) external_api::clean_returnvalue(external::read_competency_returns(), $result); + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname2', $result->shortname); + $this->assertEquals('idnumber2', $result->idnumber); + $this->assertEquals('description2', $result->description); + $this->assertEquals(FORMAT_HTML, $result->descriptionformat); + $this->assertEquals(0, $result->parentid); + $this->assertEquals($catframework->id, $result->competencyframeworkid); + + try { + external::read_competency($insystem->id); + $this->fail('User should not be able to read a competency in system context.'); + } catch (required_capability_exception $e) { + // All good. + } + } + + /** + * Test we can read a competency with read permissions. + */ + public function test_read_competencies_with_read_permissions() { + $this->setUser($this->creator); + $framework = $this->create_competency_framework(1, true); + $competency = $this->create_competency(1, $framework->id); + + // Switch users to someone with less permissions. + $this->setUser($this->user); + $id = $competency->id; + $result = external::read_competency($id); + $result = (object) external_api::clean_returnvalue(external::read_competency_returns(), $result); + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname1', $result->shortname); + $this->assertEquals('idnumber1', $result->idnumber); + $this->assertEquals('description1', $result->description); + $this->assertEquals(FORMAT_HTML, $result->descriptionformat); + $this->assertEquals(0, $result->parentid); + $this->assertEquals($framework->id, $result->competencyframeworkid); + } + + /** + * Test we can read a competency with read permissions. + */ + public function test_read_competencies_with_read_permissions_in_category() { + $this->setUser($this->creator); + $sysframework = $this->create_competency_framework(1, true); + $insystem = $this->create_competency(1, $sysframework->id); + $catframework = $this->create_competency_framework(2, false); + $incat = $this->create_competency(2, $catframework->id); + + // Switch users to someone with less permissions. + $this->setUser($this->catuser); + $id = $incat->id; + $result = external::read_competency($id); + $result = (object) external_api::clean_returnvalue(external::read_competency_returns(), $result); + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname2', $result->shortname); + $this->assertEquals('idnumber2', $result->idnumber); + $this->assertEquals('description2', $result->description); + $this->assertEquals(FORMAT_HTML, $result->descriptionformat); + $this->assertEquals(0, $result->parentid); + $this->assertEquals($catframework->id, $result->competencyframeworkid); + + try { + external::read_competency($insystem->id); + $this->fail('User should not be able to read a competency in system context.'); + } catch (required_capability_exception $e) { + // All good. + } + } + + /** + * Test we can delete a competency with manage permissions. + */ + public function test_delete_competency_with_manage_permissions() { + $this->setUser($this->creator); + $framework = $this->create_competency_framework(1, true); + $result = $this->create_competency(1, $framework->id); + + $id = $result->id; + $result = external::delete_competency($id); + $result = external_api::clean_returnvalue(external::delete_competency_returns(), $result); + + $this->assertTrue($result); + } + + /** + * Test we can delete a competency with manage permissions. + */ + public function test_delete_competency_with_manage_permissions_in_category() { + $this->setUser($this->creator); + + $sysframework = $this->create_competency_framework(1, true); + $insystem = $this->create_competency(1, $sysframework->id); + $catframework = $this->create_competency_framework(2, false); + $incat = $this->create_competency(2, $catframework->id); + + $this->setUser($this->catcreator); + $id = $incat->id; + $result = external::delete_competency($id); + $result = external_api::clean_returnvalue(external::delete_competency_returns(), $result); + + $this->assertTrue($result); + + try { + $result = external::delete_competency($insystem->id); + $this->fail('User should not be able to delete a competency in system context.'); + } catch (required_capability_exception $e) { + // All good. + } + } + + /** + * Test we can delete a competency with read permissions. + */ + public function test_delete_competency_with_read_permissions() { + $this->setExpectedException('required_capability_exception'); + $this->setUser($this->creator); + $framework = $this->create_competency_framework(1, true); + $result = $this->create_competency(1, $framework->id); + + $id = $result->id; + // Switch users to someone with less permissions. + $this->setUser($this->user); + $result = external::delete_competency($id); + } + + /** + * Test we can update a competency with manage permissions. + */ + public function test_update_competency_with_manage_permissions() { + $this->setUser($this->creator); + $framework = $this->create_competency_framework(1, true); + $result = $this->create_competency(1, $framework->id); + + $result = $this->update_competency($result->id, 2); + + $this->assertTrue($result); + } + + /** + * Test we can update a competency with manage permissions. + */ + public function test_update_competency_with_manage_permissions_in_category() { + $this->setUser($this->creator); + + $sysframework = $this->create_competency_framework(1, true); + $insystem = $this->create_competency(1, $sysframework->id); + $catframework = $this->create_competency_framework(2, false); + $incat = $this->create_competency(2, $catframework->id); + + $this->setUser($this->catcreator); + + $result = $this->update_competency($incat->id, 2); + + $this->assertTrue($result); + + try { + $result = $this->update_competency($insystem->id, 3); + $this->fail('User should not be able to update a competency in system context.'); + } catch (required_capability_exception $e) { + // All good. + } + } + + /** + * Test we can update a competency with read permissions. + */ + public function test_update_competency_with_read_permissions() { + $this->setExpectedException('required_capability_exception'); + $this->setUser($this->creator); + $framework = $this->create_competency_framework(1, true); + $result = $this->create_competency(1, $framework->id); + + $this->setUser($this->user); + $result = $this->update_competency($result->id, 2); + } + + /** + * Test count competencies with filters. + */ + public function test_count_competencies_with_filters() { + $this->setUser($this->creator); + + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $f1 = $lpg->create_framework(); + $f2 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id(), 'shortname' => 'A')); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c4 = $lpg->create_competency(array('competencyframeworkid' => $f2->get_id())); + $c5 = $lpg->create_competency(array('competencyframeworkid' => $f2->get_id())); + + $result = external::count_competencies(array(array('column' => 'competencyframeworkid', 'value' => $f2->get_id()))); + $result = external_api::clean_returnvalue(external::count_competencies_returns(), $result); + $this->assertEquals(2, $result); + + $result = external::count_competencies(array(array('column' => 'competencyframeworkid', 'value' => $f1->get_id()))); + $result = external_api::clean_returnvalue(external::count_competencies_returns(), $result); + $this->assertEquals(3, $result); + + $result = external::count_competencies(array(array('column' => 'shortname', 'value' => 'A'))); + $result = external_api::clean_returnvalue(external::count_competencies_returns(), $result); + $this->assertEquals(1, $result); + } + + /** + * Test we can list and count competencies with manage permissions. + */ + public function test_list_and_count_competencies_with_manage_permissions() { + $this->setUser($this->creator); + $framework = $this->create_competency_framework(1, true); + $result = $this->create_competency(1, $framework->id); + $result = $this->create_competency(2, $framework->id); + $result = $this->create_competency(3, $framework->id); + + $result = external::count_competencies(array()); + $result = external_api::clean_returnvalue(external::count_competencies_returns(), $result); + + $this->assertEquals($result, 3); + + array('id' => $result = external::list_competencies(array(), 'shortname', 'ASC', 0, 10, context_system::instance()->id)); + $result = external_api::clean_returnvalue(external::list_competencies_returns(), $result); + + $this->assertEquals(count($result), 3); + $result = (object) $result[0]; + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname1', $result->shortname); + $this->assertEquals('idnumber1', $result->idnumber); + $this->assertEquals('description1', $result->description); + } + + /** + * Test we can list and count competencies with read permissions. + */ + public function test_list_and_count_competencies_with_read_permissions() { + $this->setUser($this->creator); + $framework = $this->create_competency_framework(1, true); + $result = $this->create_competency(1, $framework->id); + $result = $this->create_competency(2, $framework->id); + $result = $this->create_competency(3, $framework->id); + + $this->setUser($this->user); + + $result = external::count_competencies(array()); + $result = external_api::clean_returnvalue(external::count_competencies_returns(), $result); + + $this->assertEquals($result, 3); + + array('id' => $result = external::list_competencies(array(), 'shortname', 'ASC', 0, 10, context_system::instance()->id)); + $result = external_api::clean_returnvalue(external::list_competencies_returns(), $result); + + $this->assertEquals(count($result), 3); + $result = (object) $result[0]; + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname1', $result->shortname); + $this->assertEquals('idnumber1', $result->idnumber); + $this->assertEquals('description1', $result->description); + } + + /** + * Test we can search for competencies. + */ + public function test_search_competencies_with_read_permissions() { + $this->setUser($this->creator); + $framework = $this->create_competency_framework(1, true); + $result = $this->create_competency(1, $framework->id); + $result = $this->create_competency(2, $framework->id); + $result = $this->create_competency(3, $framework->id); + + $this->setUser($this->user); + + $result = external::search_competencies('short', $framework->id); + $result = external_api::clean_returnvalue(external::search_competencies_returns(), $result); + + $this->assertEquals(count($result), 3); + $result = (object) $result[0]; + + $this->assertGreaterThan(0, $result->timecreated); + $this->assertGreaterThan(0, $result->timemodified); + $this->assertEquals($this->creator->id, $result->usermodified); + $this->assertEquals('shortname1', $result->shortname); + $this->assertEquals('idnumber1', $result->idnumber); + $this->assertEquals('description1', $result->description); + } + + /** + * Test plans creation and updates. + */ + public function test_create_and_update_plans() { + $syscontext = context_system::instance(); + + $this->setUser($this->creator); + $plan0 = $this->create_plan(1, $this->creator->id, 0, plan::STATUS_ACTIVE, 0); + + $this->setUser($this->user); + + try { + $plan1 = $this->create_plan(2, $this->user->id, 0, plan::STATUS_DRAFT, 0); + $this->fail('Exception expected due to not permissions to create draft plans'); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $this->userrole, $syscontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + $this->setUser($this->user); + + $plan2 = $this->create_plan(3, $this->user->id, 0, plan::STATUS_DRAFT, 0); + + try { + $plan3 = $this->create_plan(4, $this->user->id, 0, plan::STATUS_ACTIVE, 0); + $this->fail('Exception expected due to not permissions to create active plans'); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + try { + $plan3 = $this->update_plan($plan2->id, 4, $this->user->id, 0, plan::STATUS_COMPLETE, 0); + $this->fail('We cannot complete a plan using api::update_plan().'); + } catch (coding_exception $e) { + $this->assertTrue(true); + } + + assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->userrole, $syscontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + $plan3 = $this->create_plan(4, $this->user->id, 0, plan::STATUS_ACTIVE, 0); + try { + $plan4 = $this->create_plan(6, $this->creator->id, 0, plan::STATUS_COMPLETE, 0); + $this->fail('Plans cannot be created as complete.'); + } catch (coding_exception $e) { + $this->assertRegexp('/A plan cannot be created as complete./', $e->getMessage()); + } + + try { + $plan0 = $this->update_plan($plan0->id, 1, $this->user->id, 0, plan::STATUS_ACTIVE, 0); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + unassign_capability('moodle/competency:planmanageown', $this->userrole, $syscontext->id); + unassign_capability('moodle/competency:planmanageowndraft', $this->userrole, $syscontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + try { + // Cannot be updated even if they created it. + $this->update_plan($plan2->id, 1, $this->user->id, 0, plan::STATUS_ACTIVE, 0); + $this->fail('The user can not update their own plan without permissions.'); + } catch (required_capability_exception $e) { + $this->assertRegexp('/Manage learning plans./', $e->getMessage()); + } + } + + /** + * Test complete plan. + */ + public function test_complete_plan() { + $syscontext = context_system::instance(); + + $this->setUser($this->creator); + + $this->setUser($this->user); + + assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $this->userrole, $syscontext->id); + assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->userrole, $syscontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + $this->setUser($this->user); + + $plan = $this->create_plan(1, $this->user->id, 0, plan::STATUS_ACTIVE, 0); + + $result = external::complete_plan($plan->id); + $this->assertTrue($result); + } + + /** + * Test reopen plan. + */ + public function test_reopen_plan() { + $syscontext = context_system::instance(); + + $this->setUser($this->creator); + + $this->setUser($this->user); + + assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $this->userrole, $syscontext->id); + assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->userrole, $syscontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + $this->setUser($this->user); + + $plan = $this->create_plan(1, $this->user->id, 0, plan::STATUS_ACTIVE, 0); + external::complete_plan($plan->id); + + $result = external::reopen_plan($plan->id); + $this->assertTrue($result); + } + + /** + * Test that we can read plans. + */ + public function test_read_plans() { + global $OUTPUT; + $this->setUser($this->creator); + + $syscontext = context_system::instance(); + + $plan1 = $this->create_plan(1, $this->user->id, 0, plan::STATUS_DRAFT, 0); + $plan2 = $this->create_plan(2, $this->user->id, 0, plan::STATUS_ACTIVE, 0); + $plan3 = $this->create_plan(3, $this->user->id, 0, plan::STATUS_ACTIVE, 0); + external::complete_plan($plan3->id); + $plan3 = (object) external::read_plan($plan3->id); + + $data = external::read_plan($plan1->id); + $this->assertEquals((array)$plan1, external::read_plan($plan1->id)); + $data = external::read_plan($plan2->id); + $this->assertEquals((array)$plan2, external::read_plan($plan2->id)); + $data = external::read_plan($plan3->id); + $this->assertEquals((array)$plan3, external::read_plan($plan3->id)); + + $this->setUser($this->user); + + // The normal user can not edit these plans. + $plan1->canmanage = false; + $plan2->canmanage = false; + $plan3->canmanage = false; + $plan1->canbeedited = false; + $plan2->canbeedited = false; + $plan3->canbeedited = false; + $plan1->canrequestreview = true; + $plan2->canrequestreview = true; + $plan3->canrequestreview = true; + $plan1->canreview = false; + $plan2->canreview = false; + $plan3->canreview = false; + $plan1->iscompleteallowed = false; + $plan2->iscompleteallowed = false; + $plan3->iscompleteallowed = false; + $plan1->isrequestreviewallowed = true; + $plan2->isrequestreviewallowed = true; + $plan3->isrequestreviewallowed = true; + $plan1->isapproveallowed = false; + $plan2->isapproveallowed = false; + $plan3->isapproveallowed = false; + $plan1->isunapproveallowed = false; + $plan2->isunapproveallowed = false; + $plan3->isunapproveallowed = false; + $plan3->isreopenallowed = false; + $plan1->commentarea['canpost'] = false; + $plan1->commentarea['canview'] = true; + + // Prevent the user from seeing their own non-draft plans. + assign_capability('moodle/competency:plancommentown', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); + assign_capability('moodle/competency:planviewown', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); + assign_capability('moodle/competency:planviewowndraft', CAP_ALLOW, $this->userrole, $syscontext->id, true); + accesslib_clear_all_caches_for_unit_testing(); + + $this->assertEquals((array)$plan1, external::read_plan($plan1->id)); + + try { + external::read_plan($plan2->id); + $this->fail('Exception expected due to not permissions to read plan'); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + try { + external::read_plan($plan3->id); + $this->fail('Exception expected due to not permissions to read plan'); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Allow user to see their plan. + assign_capability('moodle/competency:plancommentown', CAP_ALLOW, $this->userrole, $syscontext->id, true); + assign_capability('moodle/competency:planviewown', CAP_ALLOW, $this->userrole, $syscontext->id, true); + assign_capability('moodle/competency:planmanageowndraft', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); + accesslib_clear_all_caches_for_unit_testing(); + + $plan1->commentarea['canpost'] = true; + $plan1->commentarea['canview'] = true; + $plan2->commentarea['canpost'] = true; + $plan2->isrequestreviewallowed = false; + $plan3->commentarea['canpost'] = true; + $plan3->isrequestreviewallowed = false; + $plan1->commentarea['canpostorhascomments'] = true; + $plan2->commentarea['canpostorhascomments'] = true; + $plan3->commentarea['canpostorhascomments'] = true; + + $this->assertEquals((array)$plan1, external::read_plan($plan1->id)); + $this->assertEquals((array)$plan2, external::read_plan($plan2->id)); + $this->assertEquals((array)$plan3, external::read_plan($plan3->id)); + + // Allow use to manage their own draft plan. + assign_capability('moodle/competency:planviewown', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); + assign_capability('moodle/competency:planmanageown', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); + assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $this->userrole, $syscontext->id, true); + accesslib_clear_all_caches_for_unit_testing(); + + $plan1->canmanage = true; + $plan1->canbeedited = true; + $plan1->canrequestreview = true; + $plan1->isrequestreviewallowed = true; + $this->assertEquals((array)$plan1, external::read_plan($plan1->id)); + try { + external::read_plan($plan2->id); + $this->fail('Exception expected due to not permissions to read plan'); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + try { + external::read_plan($plan3->id); + $this->fail('Exception expected due to not permissions to read plan'); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + // Allow use to manage their plan. + assign_capability('moodle/competency:planviewown', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); + assign_capability('moodle/competency:planmanageowndraft', CAP_PROHIBIT, $this->userrole, $syscontext->id, true); + assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->userrole, $syscontext->id, true); + accesslib_clear_all_caches_for_unit_testing(); + + $plan1->canmanage = false; + $plan1->canbeedited = false; + $plan1->canrequestreview = true; + $plan1->canreview = true; + $plan1->isrequestreviewallowed = true; + $plan1->isapproveallowed = true; + $plan1->iscompleteallowed = false; + + $plan2->canmanage = true; + $plan2->canbeedited = true; + $plan2->canreview = true; + $plan2->iscompleteallowed = true; + $plan2->isunapproveallowed = true; + + $plan3->canmanage = true; + $plan3->canreview = true; + $plan3->isreopenallowed = true; + + $this->assertEquals((array)$plan1, external::read_plan($plan1->id)); + $this->assertEquals((array)$plan2, external::read_plan($plan2->id)); + $this->assertEquals((array)$plan3, external::read_plan($plan3->id)); + } + + public function test_delete_plans() { + $this->setUser($this->creator); + + $syscontext = context_system::instance(); + + $plan1 = $this->create_plan(1, $this->user->id, 0, plan::STATUS_ACTIVE, 0); + $plan2 = $this->create_plan(2, $this->user->id, 0, plan::STATUS_ACTIVE, 0); + $plan3 = $this->create_plan(3, $this->creator->id, 0, plan::STATUS_ACTIVE, 0); + + $this->assertTrue(external::delete_plan($plan1->id)); + + unassign_capability('moodle/competency:planmanage', $this->creatorrole, $syscontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + try { + external::delete_plan($plan2->id); + $this->fail('Exception expected due to not permissions to manage plans'); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + $this->setUser($this->user); + + // Can not delete plans created by other users. + try { + external::delete_plan($plan2->id); + $this->fail('Exception expected due to not permissions to manage plans'); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->userrole, $syscontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + $this->assertTrue(external::delete_plan($plan2->id)); + + // Can not delete plans created for other users. + try { + external::delete_plan($plan3->id); + $this->fail('Exception expected due to not permissions to manage plans'); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + $plan4 = $this->create_plan(4, $this->user->id, 0, plan::STATUS_ACTIVE, 0); + $this->assertTrue(external::delete_plan($plan4->id)); + } + + public function test_delete_plan_removes_relations() { + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $user = $dg->create_user(); + $plan = $lpg->create_plan(array('userid' => $user->id)); + $framework = $lpg->create_framework(); + $comp1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $comp3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $pc1 = $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $comp1->get_id())); + $pc2 = $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $comp2->get_id())); + $pc3 = $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $comp3->get_id())); + + // Complete the plan to generate user_competency_plan entries. + api::complete_plan($plan); + + // Confirm the data we have. + $this->assertEquals(3, plan_competency::count_records(array('planid' => $plan->get_id()))); + $this->assertEquals(3, user_competency_plan::count_records(array('planid' => $plan->get_id(), 'userid' => $user->id))); + + // Delete the plan now. + api::delete_plan($plan->get_id()); + $this->assertEquals(0, plan_competency::count_records(array('planid' => $plan->get_id()))); + $this->assertEquals(0, user_competency_plan::count_records(array('planid' => $plan->get_id(), 'userid' => $user->id))); + } + + public function test_list_plan_competencies() { + $this->setUser($this->creator); + + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $f1 = $lpg->create_framework(); + $f2 = $lpg->create_framework(); + + $c1a = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c1c = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2a = $lpg->create_competency(array('competencyframeworkid' => $f2->get_id())); + $c2b = $lpg->create_competency(array('competencyframeworkid' => $f2->get_id())); + + $tpl = $lpg->create_template(); + $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $c1a->get_id())); + $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $c1c->get_id())); + $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $c2b->get_id())); + + $plan = $lpg->create_plan(array('userid' => $this->user->id, 'templateid' => $tpl->get_id())); + + $uc1a = $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c1a->get_id(), + 'status' => user_competency::STATUS_IN_REVIEW, 'reviewerid' => $this->creator->id)); + $uc1b = $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c1b->get_id())); + $uc2b = $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c2b->get_id(), + 'grade' => 2, 'proficiency' => 1)); + $ux1a = $lpg->create_user_competency(array('userid' => $this->creator->id, 'competencyid' => $c1a->get_id())); + + $result = external::list_plan_competencies($plan->get_id()); + $result = external::clean_returnvalue(external::list_plan_competencies_returns(), $result); + + $this->assertCount(3, $result); + $this->assertEquals($c1a->get_id(), $result[0]['competency']['id']); + $this->assertEquals($this->user->id, $result[0]['usercompetency']['userid']); + $this->assertArrayNotHasKey('usercompetencyplan', $result[0]); + $this->assertEquals($c1c->get_id(), $result[1]['competency']['id']); + $this->assertEquals($this->user->id, $result[1]['usercompetency']['userid']); + $this->assertArrayNotHasKey('usercompetencyplan', $result[1]); + $this->assertEquals($c2b->get_id(), $result[2]['competency']['id']); + $this->assertEquals($this->user->id, $result[2]['usercompetency']['userid']); + $this->assertArrayNotHasKey('usercompetencyplan', $result[2]); + $this->assertEquals(user_competency::STATUS_IN_REVIEW, $result[0]['usercompetency']['status']); + $this->assertEquals(null, $result[1]['usercompetency']['grade']); + $this->assertEquals(2, $result[2]['usercompetency']['grade']); + $this->assertEquals(1, $result[2]['usercompetency']['proficiency']); + + // Check the return values when the plan status is complete. + $completedplan = $lpg->create_plan(array('userid' => $this->user->id, 'templateid' => $tpl->get_id(), + 'status' => plan::STATUS_COMPLETE)); + + $uc1a = $lpg->create_user_competency_plan(array('userid' => $this->user->id, 'competencyid' => $c1a->get_id(), + 'planid' => $completedplan->get_id())); + $uc1b = $lpg->create_user_competency_plan(array('userid' => $this->user->id, 'competencyid' => $c1c->get_id(), + 'planid' => $completedplan->get_id())); + $uc2b = $lpg->create_user_competency_plan(array('userid' => $this->user->id, 'competencyid' => $c2b->get_id(), + 'planid' => $completedplan->get_id(), 'grade' => 2, 'proficiency' => 1)); + $ux1a = $lpg->create_user_competency_plan(array('userid' => $this->creator->id, 'competencyid' => $c1a->get_id(), + 'planid' => $completedplan->get_id())); + + $result = external::list_plan_competencies($completedplan->get_id()); + $result = external::clean_returnvalue(external::list_plan_competencies_returns(), $result); + + $this->assertCount(3, $result); + $this->assertEquals($c1a->get_id(), $result[0]['competency']['id']); + $this->assertEquals($this->user->id, $result[0]['usercompetencyplan']['userid']); + $this->assertArrayNotHasKey('usercompetency', $result[0]); + $this->assertEquals($c1c->get_id(), $result[1]['competency']['id']); + $this->assertEquals($this->user->id, $result[1]['usercompetencyplan']['userid']); + $this->assertArrayNotHasKey('usercompetency', $result[1]); + $this->assertEquals($c2b->get_id(), $result[2]['competency']['id']); + $this->assertEquals($this->user->id, $result[2]['usercompetencyplan']['userid']); + $this->assertArrayNotHasKey('usercompetency', $result[2]); + $this->assertEquals(null, $result[1]['usercompetencyplan']['grade']); + $this->assertEquals(2, $result[2]['usercompetencyplan']['grade']); + $this->assertEquals(1, $result[2]['usercompetencyplan']['proficiency']); + } + + public function test_add_competency_to_template() { + $this->setUser($this->creator); + + $syscontext = context_system::instance(); + + // Create a template. + $template = $this->create_template(1, true); + + // Create a competency. + $framework = $this->create_competency_framework(1, true); + $competency = $this->create_competency(1, $framework->id); + + // Add the competency. + external::add_competency_to_template($template->id, $competency->id); + + // Check that it was added. + $this->assertEquals(1, external::count_competencies_in_template($template->id)); + + // Unassign capability. + unassign_capability('moodle/competency:templatemanage', $this->creatorrole, $syscontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + // Check we can not add the competency now. + try { + external::add_competency_to_template($template->id, $competency->id); + $this->fail('Exception expected due to not permissions to manage template competencies'); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + } + + public function test_remove_competency_from_template() { + $syscontext = context_system::instance(); + $this->setUser($this->creator); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + // Create a template. + $template = $this->create_template(1, true); + + // Create a competency. + $framework = $lpg->create_framework(); + $competency = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // Add the competency. + external::add_competency_to_template($template->id, $competency->get_id()); + + // Check that it was added. + $this->assertEquals(1, external::count_competencies_in_template($template->id)); + + // Check that we can remove the competency. + external::remove_competency_from_template($template->id, $competency->get_id()); + + // Check that it was removed. + $this->assertEquals(0, external::count_competencies_in_template($template->id)); + + // Unassign capability. + unassign_capability('moodle/competency:templatemanage', $this->creatorrole, $syscontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + // Check we can not remove the competency now. + try { + external::add_competency_to_template($template->id, $competency->get_id()); + $this->fail('Exception expected due to not permissions to manage template competencies'); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + } + + /** + * Test we can re-order competency frameworks. + */ + public function test_reorder_template_competencies() { + $this->setUser($this->creator); + + $syscontext = context_system::instance(); + $onehour = time() + 60 * 60; + + // Create a template. + $template = $this->create_template(1, true); + + // Create a competency framework. + $framework = $this->create_competency_framework(1, true); + + // Create multiple competencies. + $competency1 = $this->create_competency(1, $framework->id); + $competency2 = $this->create_competency(2, $framework->id); + $competency3 = $this->create_competency(3, $framework->id); + $competency4 = $this->create_competency(4, $framework->id); + + // Add the competencies. + external::add_competency_to_template($template->id, $competency1->id); + external::add_competency_to_template($template->id, $competency2->id); + external::add_competency_to_template($template->id, $competency3->id); + external::add_competency_to_template($template->id, $competency4->id); + + // Test if removing competency from template don't create sortorder holes. + external::remove_competency_from_template($template->id, $competency3->id); + $templcomp4 = template_competency::get_record(array( + 'templateid' => $template->id, + 'competencyid' => $competency4->id + )); + + $this->assertEquals(2, $templcomp4->get_sortorder()); + + // This is a move up. + external::reorder_template_competency($template->id, $competency4->id, $competency2->id); + $result = external::list_competencies_in_template($template->id); + $result = external_api::clean_returnvalue(external::list_competencies_in_template_returns(), $result); + + $r1 = (object) $result[0]; + $r2 = (object) $result[1]; + $r3 = (object) $result[2]; + + $this->assertEquals($competency1->id, $r1->id); + $this->assertEquals($competency4->id, $r2->id); + $this->assertEquals($competency2->id, $r3->id); + + // This is a move down. + external::reorder_template_competency($template->id, $competency1->id, $competency4->id); + $result = external::list_competencies_in_template($template->id); + $result = external_api::clean_returnvalue(external::list_competencies_in_template_returns(), $result); + + $r1 = (object) $result[0]; + $r2 = (object) $result[1]; + $r3 = (object) $result[2]; + + $this->assertEquals($competency4->id, $r1->id); + $this->assertEquals($competency1->id, $r2->id); + $this->assertEquals($competency2->id, $r3->id); + + $this->setExpectedException('required_capability_exception'); + $this->setUser($this->user); + external::reorder_template_competency($template->id, $competency1->id, $competency2->id); + } + + /** + * Test we can duplicate learning plan template. + */ + public function test_duplicate_learning_plan_template() { + $this->setUser($this->creator); + + $syscontext = context_system::instance(); + $onehour = time() + 60 * 60; + + // Create a template. + $template = $this->create_template(1, true); + + // Create a competency framework. + $framework = $this->create_competency_framework(1, true); + + // Create multiple competencies. + $competency1 = $this->create_competency(1, $framework->id); + $competency2 = $this->create_competency(2, $framework->id); + $competency3 = $this->create_competency(3, $framework->id); + + // Add the competencies. + external::add_competency_to_template($template->id, $competency1->id); + external::add_competency_to_template($template->id, $competency2->id); + external::add_competency_to_template($template->id, $competency3->id); + + // Duplicate the learning plan template. + $duplicatedtemplate = external::duplicate_template($template->id); + + $result = external::list_competencies_in_template($template->id); + $resultduplicated = external::list_competencies_in_template($duplicatedtemplate->id); + + $this->assertEquals(count($result), count($resultduplicated)); + $this->assertContains($template->shortname, $duplicatedtemplate->shortname); + $this->assertEquals($duplicatedtemplate->description, $template->description); + $this->assertEquals($duplicatedtemplate->descriptionformat, $template->descriptionformat); + $this->assertEquals($duplicatedtemplate->visible, $template->visible); + } + + /** + * Test that we can return scale values for a scale with the scale ID. + */ + public function test_get_scale_values() { + global $DB; + + $this->setUser($this->creator); + + // Create a scale. + $record = new stdClass(); + $record->courseid = 0; + $record->userid = $this->creator->id; + $record->name = 'Test scale'; + $record->scale = 'Poor, Not good, Okay, Fine, Excellent'; + $record->description = '

    Test scale description.

    '; + $record->descriptionformat = 1; + $record->timemodified = time(); + $scaleid = $DB->insert_record('scale', $record); + // Expected return value. + $expected = array(array( + 'id' => 1, + 'name' => 'Poor' + ), array( + 'id' => 2, + 'name' => 'Not good' + ), array( + 'id' => 3, + 'name' => 'Okay' + ), array( + 'id' => 4, + 'name' => 'Fine' + ), array( + 'id' => 5, + 'name' => 'Excellent' + ) + ); + // Call the webservice. + $result = external::get_scale_values($scaleid); + $this->assertEquals($expected, $result); + } + + /** + * Create a template. + */ + public function test_create_template() { + $syscontextid = context_system::instance()->id; + $catcontextid = context_coursecat::instance($this->category->id)->id; + + // A user without permission. + $this->setUser($this->user); + try { + $result = $this->create_template(1, true); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + + // A user without permission in a category. + $this->setUser($this->catuser); + try { + $result = $this->create_template(1, false); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + + // A user with permissions in the system. + $this->setUser($this->creator); + $result = $this->create_template(1, true); + $this->assertEquals('shortname1', $result->shortname); + $this->assertEquals($syscontextid, $result->contextid); + $this->assertNotEmpty($result->id); + + $result = $this->create_template(2, false); + $this->assertEquals('shortname2', $result->shortname); + $this->assertEquals($catcontextid, $result->contextid); + $this->assertNotEmpty($result->id); + + // A user with permissions in the category. + $this->setUser($this->catcreator); + try { + $result = $this->create_template(3, true); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + + $result = $this->create_template(3, false); + $this->assertEquals('shortname3', $result->shortname); + $this->assertEquals($catcontextid, $result->contextid); + $this->assertNotEmpty($result->id); + } + + /** + * Read a template. + */ + public function test_read_template() { + $syscontextid = context_system::instance()->id; + $catcontextid = context_coursecat::instance($this->category->id)->id; + + // Set a due date for the next year. + $date = new DateTime('now'); + $date->modify('+1 year'); + $duedate = $date->getTimestamp(); + + // Creating two templates. + $this->setUser($this->creator); + $systemplate = $this->create_template(1, true); + $cattemplate = $this->create_template(2, false); + + // User without permissions to read in system. + assign_capability('moodle/competency:templateview', CAP_PROHIBIT, $this->userrole, $syscontextid, true); + accesslib_clear_all_caches_for_unit_testing(); + $this->setUser($this->user); + $this->assertFalse(has_capability('moodle/competency:templateview', context_system::instance())); + try { + external::read_template($systemplate->id); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + try { + external::read_template($cattemplate->id); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + + // User with permissions to read in a category. + assign_capability('moodle/competency:templateview', CAP_PREVENT, $this->userrole, $syscontextid, true); + assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $catcontextid, true); + accesslib_clear_all_caches_for_unit_testing(); + $this->assertFalse(has_capability('moodle/competency:templateview', context_system::instance())); + $this->assertTrue(has_capability('moodle/competency:templateview', context_coursecat::instance($this->category->id))); + try { + external::read_template($systemplate->id); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + + $result = external::read_template($cattemplate->id); + $result = external_api::clean_returnvalue(external::read_template_returns(), $result); + $this->assertEquals($cattemplate->id, $result['id']); + $this->assertEquals('shortname2', $result['shortname']); + $this->assertEquals('description2', $result['description']); + $this->assertEquals(FORMAT_HTML, $result['descriptionformat']); + $this->assertEquals(1, $result['visible']); + $this->assertEquals(0, $result['duedate']); + $this->assertEquals(userdate(0), $result['duedateformatted']); + + // User with permissions to read in the system. + assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $syscontextid, true); + accesslib_clear_all_caches_for_unit_testing(); + $this->assertTrue(has_capability('moodle/competency:templateview', context_system::instance())); + $result = external::read_template($systemplate->id); + $result = external_api::clean_returnvalue(external::read_template_returns(), $result); + $this->assertEquals($systemplate->id, $result['id']); + $this->assertEquals('shortname1', $result['shortname']); + $this->assertEquals('description1', $result['description']); + $this->assertEquals(FORMAT_HTML, $result['descriptionformat']); + $this->assertEquals(true, $result['visible']); + $this->assertEquals(0, $result['duedate']); + $this->assertEquals(userdate(0), $result['duedateformatted']); + + $result = external::read_template($cattemplate->id); + $result = external_api::clean_returnvalue(external::read_template_returns(), $result); + $this->assertEquals($cattemplate->id, $result['id']); + $this->assertEquals('shortname2', $result['shortname']); + $this->assertEquals('description2', $result['description']); + $this->assertEquals(FORMAT_HTML, $result['descriptionformat']); + $this->assertEquals(true, $result['visible']); + $this->assertEquals(0, $result['duedate']); + $this->assertEquals(userdate(0), $result['duedateformatted']); + } + + /** + * Update a template. + */ + public function test_update_template() { + $syscontextid = context_system::instance()->id; + $catcontextid = context_coursecat::instance($this->category->id)->id; + + // Set a due date for the next year. + $date = new DateTime('now'); + $date->modify('+1 year'); + $duedate = $date->getTimestamp(); + + // Creating two templates. + $this->setUser($this->creator); + $systemplate = $this->create_template(1, true); + $cattemplate = $this->create_template(2, false); + + // Trying to update in a without permissions. + $this->setUser($this->user); + try { + $this->update_template($systemplate->id, 3); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + + try { + $this->update_template($cattemplate->id, 3); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + + // User with permissions to update in category. + $this->setUser($this->catcreator); + try { + $this->update_template($systemplate->id, 3); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + + $result = $this->update_template($cattemplate->id, 3); + $this->assertTrue($result); + $result = external::read_template($cattemplate->id); + $result = external_api::clean_returnvalue(external::read_template_returns(), $result); + $this->assertEquals($cattemplate->id, $result['id']); + $this->assertEquals('shortname3', $result['shortname']); + $this->assertEquals("description3", $result['description']); + $this->assertEquals(FORMAT_HTML, $result['descriptionformat']); + $this->assertEquals(true, $result['visible']); + $this->assertEquals(0, $result['duedate']); + $this->assertEquals(userdate(0), $result['duedateformatted']); + + // User with permissions to update in the system. + $this->setUser($this->creator); + $result = $this->update_template($systemplate->id, 4); + $this->assertTrue($result); + $result = external::read_template($systemplate->id); + $result = external_api::clean_returnvalue(external::read_template_returns(), $result); + $this->assertEquals($systemplate->id, $result['id']); + $this->assertEquals('shortname4', $result['shortname']); + $this->assertEquals('description4', $result['description']); + $this->assertEquals(FORMAT_HTML, $result['descriptionformat']); + $this->assertEquals(true, $result['visible']); + $this->assertEquals(0, $result['duedate']); + $this->assertEquals(userdate(0), $result['duedateformatted']); + + $result = $this->update_template($cattemplate->id, 5); + $this->assertTrue($result); + $result = external::read_template($cattemplate->id); + $result = external_api::clean_returnvalue(external::read_template_returns(), $result); + $this->assertEquals($cattemplate->id, $result['id']); + $this->assertEquals('shortname5', $result['shortname']); + $this->assertEquals('description5', $result['description']); + $this->assertEquals(FORMAT_HTML, $result['descriptionformat']); + $this->assertEquals(1, $result['visible']); + $this->assertEquals(0, $result['duedate']); + $this->assertEquals(userdate(0), $result['duedateformatted']); + } + + /** + * Delete a template. + */ + public function test_delete_template() { + global $DB; + $syscontextid = context_system::instance()->id; + $catcontextid = context_coursecat::instance($this->category->id)->id; + + // Creating a few templates. + $this->setUser($this->creator); + $sys1 = $this->create_template(1, true); + $cat1 = $this->create_template(2, false); + $cat2 = $this->create_template(3, false); + $this->assertTrue($DB->record_exists(template::TABLE, array('id' => $sys1->id))); + $this->assertTrue($DB->record_exists(template::TABLE, array('id' => $cat1->id))); + $this->assertTrue($DB->record_exists(template::TABLE, array('id' => $cat2->id))); + + // User without permissions. + $this->setUser($this->user); + try { + external::delete_template($sys1->id); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + try { + external::delete_template($cat1->id); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + + // User with category permissions. + $this->setUser($this->catcreator); + try { + external::delete_template($sys1->id); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + + $result = external::delete_template($cat1->id); + $result = external_api::clean_returnvalue(external::delete_template_returns(), $result); + $this->assertTrue($result); + $this->assertFalse($DB->record_exists(template::TABLE, array('id' => $cat1->id))); + + // User with system permissions. + $this->setUser($this->creator); + $result = external::delete_template($sys1->id); + $result = external_api::clean_returnvalue(external::delete_template_returns(), $result); + $this->assertTrue($result); + $result = external::delete_template($cat2->id); + $result = external_api::clean_returnvalue(external::delete_template_returns(), $result); + $this->assertTrue($result); + $this->assertFalse($DB->record_exists(template::TABLE, array('id' => $sys1->id))); + $this->assertFalse($DB->record_exists(template::TABLE, array('id' => $cat2->id))); + } + + /** + * List templates. + */ + public function test_list_templates() { + $syscontextid = context_system::instance()->id; + $catcontextid = context_coursecat::instance($this->category->id)->id; + + // Creating a few templates. + $this->setUser($this->creator); + $sys1 = $this->create_template(1, true); + $sys2 = $this->create_template(2, true); + $cat1 = $this->create_template(3, false); + $cat2 = $this->create_template(4, false); + + // User without permission. + $this->setUser($this->user); + assign_capability('moodle/competency:templateview', CAP_PROHIBIT, $this->userrole, $syscontextid, true); + accesslib_clear_all_caches_for_unit_testing(); + try { + external::list_templates('id', 'ASC', 0, 10, array('contextid' => $syscontextid), 'children', false); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + + // User with category permissions. + assign_capability('moodle/competency:templateview', CAP_PREVENT, $this->userrole, $syscontextid, true); + assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $catcontextid, true); + accesslib_clear_all_caches_for_unit_testing(); + $result = external::list_templates('id', 'ASC', 0, 10, array('contextid' => $syscontextid), 'children', false); + $result = external_api::clean_returnvalue(external::list_templates_returns(), $result); + $this->assertCount(2, $result); + $this->assertEquals($cat1->id, $result[0]['id']); + $this->assertEquals($cat2->id, $result[1]['id']); + + // User with system permissions. + assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $syscontextid, true); + accesslib_clear_all_caches_for_unit_testing(); + $result = external::list_templates('id', 'DESC', 0, 3, array('contextid' => $catcontextid), 'parents', false); + $result = external_api::clean_returnvalue(external::list_templates_returns(), $result); + $this->assertCount(3, $result); + $this->assertEquals($cat2->id, $result[0]['id']); + $this->assertEquals($cat1->id, $result[1]['id']); + $this->assertEquals($sys2->id, $result[2]['id']); + } + + /** + * List templates using competency. + */ + public function test_list_templates_using_competency() { + $this->setUser($this->creator); + + // Create a template. + $template1 = $this->create_template(1, true); + $template2 = $this->create_template(2, true); + $template3 = $this->create_template(3, true); + $template4 = $this->create_template(4, true); + + // Create a competency. + $framework = $this->create_competency_framework(1, true); + $competency1 = $this->create_competency(1, $framework->id); + $competency2 = $this->create_competency(2, $framework->id); + + // Add the competency. + external::add_competency_to_template($template1->id, $competency1->id); + external::add_competency_to_template($template2->id, $competency1->id); + external::add_competency_to_template($template3->id, $competency1->id); + + external::add_competency_to_template($template4->id, $competency2->id); + + $listcomp1 = external::list_templates_using_competency($competency1->id); + $listcomp2 = external::list_templates_using_competency($competency2->id); + + // Test count_templates_using_competency. + $counttempcomp1 = external::count_templates_using_competency($competency1->id); + $counttempcomp2 = external::count_templates_using_competency($competency2->id); + + $comptemp1 = $listcomp1[0]; + $comptemp2 = $listcomp1[1]; + $comptemp3 = $listcomp1[2]; + + $comptemp4 = $listcomp2[0]; + + $this->assertCount(3, $listcomp1); + $this->assertCount(1, $listcomp2); + $this->assertEquals(3, $counttempcomp1); + $this->assertEquals(1, $counttempcomp2); + $this->assertEquals($template1->id, $comptemp1->id); + $this->assertEquals($template2->id, $comptemp2->id); + $this->assertEquals($template3->id, $comptemp3->id); + $this->assertEquals($template4->id, $comptemp4->id); + } + + public function test_count_templates() { + $syscontextid = context_system::instance()->id; + $catcontextid = context_coursecat::instance($this->category->id)->id; + + // Creating a few templates. + $this->setUser($this->creator); + $sys1 = $this->create_template(1, true); + $sys2 = $this->create_template(2, true); + $cat1 = $this->create_template(3, false); + $cat2 = $this->create_template(4, false); + $cat3 = $this->create_template(5, false); + + // User without permission. + $this->setUser($this->user); + assign_capability('moodle/competency:templateview', CAP_PROHIBIT, $this->userrole, $syscontextid, true); + accesslib_clear_all_caches_for_unit_testing(); + try { + external::count_templates(array('contextid' => $syscontextid), 'children'); + $this->fail('Invalid permissions'); + } catch (required_capability_exception $e) { + // All good. + } + + // User with category permissions. + assign_capability('moodle/competency:templateview', CAP_PREVENT, $this->userrole, $syscontextid, true); + assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $catcontextid, true); + accesslib_clear_all_caches_for_unit_testing(); + $result = external::count_templates(array('contextid' => $syscontextid), 'children'); + $result = external_api::clean_returnvalue(external::count_templates_returns(), $result); + $this->assertEquals(3, $result); + + // User with system permissions. + assign_capability('moodle/competency:templateview', CAP_ALLOW, $this->userrole, $syscontextid, true); + accesslib_clear_all_caches_for_unit_testing(); + $result = external::count_templates(array('contextid' => $catcontextid), 'parents'); + $result = external_api::clean_returnvalue(external::count_templates_returns(), $result); + $this->assertEquals(5, $result); + } + + /** + * Test that we can add related competencies. + * + * @return void + */ + public function test_add_related_competency() { + global $DB; + $this->setUser($this->creator); + + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $framework2 = $lpg->create_framework(); + $competency1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $competency2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $competency3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $competency4 = $lpg->create_competency(array('competencyframeworkid' => $framework2->get_id())); + + // The lower one always as competencyid. + $result = external::add_related_competency($competency1->get_id(), $competency2->get_id()); + $result = external_api::clean_returnvalue(external::add_related_competency_returns(), $result); + $this->assertTrue($result); + $this->assertTrue($DB->record_exists_select( + related_competency::TABLE, 'competencyid = :cid AND relatedcompetencyid = :rid', + array( + 'cid' => $competency1->get_id(), + 'rid' => $competency2->get_id() + ) + )); + $this->assertFalse($DB->record_exists_select( + related_competency::TABLE, 'competencyid = :cid AND relatedcompetencyid = :rid', + array( + 'cid' => $competency2->get_id(), + 'rid' => $competency1->get_id() + ) + )); + + $result = external::add_related_competency($competency3->get_id(), $competency1->get_id()); + $result = external_api::clean_returnvalue(external::add_related_competency_returns(), $result); + $this->assertTrue($result); + $this->assertTrue($DB->record_exists_select( + related_competency::TABLE, 'competencyid = :cid AND relatedcompetencyid = :rid', + array( + 'cid' => $competency1->get_id(), + 'rid' => $competency3->get_id() + ) + )); + $this->assertFalse($DB->record_exists_select( + related_competency::TABLE, 'competencyid = :cid AND relatedcompetencyid = :rid', + array( + 'cid' => $competency3->get_id(), + 'rid' => $competency1->get_id() + ) + )); + + // We can not allow a duplicate relation, not even in the other direction. + $this->assertEquals(1, $DB->count_records_select(related_competency::TABLE, + 'competencyid = :cid AND relatedcompetencyid = :rid', + array('cid' => $competency1->get_id(), 'rid' => $competency2->get_id()))); + $this->assertEquals(0, $DB->count_records_select(related_competency::TABLE, + 'competencyid = :cid AND relatedcompetencyid = :rid', + array('rid' => $competency1->get_id(), 'cid' => $competency2->get_id()))); + $result = external::add_related_competency($competency2->get_id(), $competency1->get_id()); + $result = external_api::clean_returnvalue(external::add_related_competency_returns(), $result); + $this->assertTrue($result); + $this->assertEquals(1, $DB->count_records_select(related_competency::TABLE, + 'competencyid = :cid AND relatedcompetencyid = :rid', + array('cid' => $competency1->get_id(), 'rid' => $competency2->get_id()))); + $this->assertEquals(0, $DB->count_records_select(related_competency::TABLE, + 'competencyid = :cid AND relatedcompetencyid = :rid', + array('rid' => $competency1->get_id(), 'cid' => $competency2->get_id()))); + + // Check that we cannot create links across frameworks. + try { + external::add_related_competency($competency1->get_id(), $competency4->get_id()); + $this->fail('Exception expected due mis-use of shared competencies'); + } catch (invalid_persistent_exception $e) { + // Yay! + } + + // User without permission. + $this->setUser($this->user); + + // Check we can not add the related competency now. + try { + external::add_related_competency($competency1->get_id(), $competency3->get_id()); + $this->fail('Exception expected due to not permissions to manage template competencies'); + } catch (moodle_exception $e) { + $this->assertEquals('nopermissions', $e->errorcode); + } + + } + + /** + * Test that we can remove related competencies. + * + * @return void + */ + public function test_remove_related_competency() { + $this->setUser($this->creator); + + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $rc1 = $lpg->create_related_competency(array('competencyid' => $c1->get_id(), 'relatedcompetencyid' => $c2->get_id())); + $rc2 = $lpg->create_related_competency(array('competencyid' => $c2->get_id(), 'relatedcompetencyid' => $c3->get_id())); + + $this->assertEquals(2, related_competency::count_records()); + + // Returns false when the relation does not exist. + $result = external::remove_related_competency($c1->get_id(), $c3->get_id()); + $result = external_api::clean_returnvalue(external::remove_related_competency_returns(), $result); + $this->assertFalse($result); + + // Returns true on success. + $result = external::remove_related_competency($c2->get_id(), $c3->get_id()); + $result = external_api::clean_returnvalue(external::remove_related_competency_returns(), $result); + $this->assertTrue($result); + $this->assertEquals(1, related_competency::count_records()); + + // We don't need to specify competencyid and relatedcompetencyid in the right order. + $result = external::remove_related_competency($c2->get_id(), $c1->get_id()); + $result = external_api::clean_returnvalue(external::remove_related_competency_returns(), $result); + $this->assertTrue($result); + $this->assertEquals(0, related_competency::count_records()); + } + + /** + * Test that we can search and include related competencies. + * + * @return void + */ + public function test_search_competencies_including_related() { + $this->setUser($this->creator); + + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c5 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + // We have 1-2, 1-3, 2-4, and no relation between 2-3 nor 1-4 nor 5. + $rc12 = $lpg->create_related_competency(array('competencyid' => $c1->get_id(), 'relatedcompetencyid' => $c2->get_id())); + $rc13 = $lpg->create_related_competency(array('competencyid' => $c1->get_id(), 'relatedcompetencyid' => $c3->get_id())); + $rc24 = $lpg->create_related_competency(array('competencyid' => $c2->get_id(), 'relatedcompetencyid' => $c4->get_id())); + + $result = external::search_competencies('comp', $framework->get_id(), true); + $result = external_api::clean_returnvalue(external::search_competencies_returns(), $result); + + $this->assertCount(5, $result); + + } + + /** + * Test that we can add competency to plan if we have the right capability. + * + * @return void + */ + public function test_add_competency_to_plan() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $usermanage = $dg->create_user(); + $user = $dg->create_user(); + + $syscontext = context_system::instance(); + + // Creating specific roles. + $managerole = $dg->create_role(array( + 'name' => 'User manage', + 'shortname' => 'manage' + )); + + assign_capability('moodle/competency:planmanage', CAP_ALLOW, $managerole, $syscontext->id); + assign_capability('moodle/competency:planview', CAP_ALLOW, $managerole, $syscontext->id); + + $dg->role_assign($managerole, $usermanage->id, $syscontext->id); + + $this->setUser($usermanage); + $plan = array ( + 'userid' => $usermanage->id, + 'status' => \core_competency\plan::STATUS_ACTIVE + ); + $pl1 = $lpg->create_plan($plan); + $framework = $lpg->create_framework(); + $competency = $lpg->create_competency( + array('competencyframeworkid' => $framework->get_id()) + ); + $this->assertTrue(external::add_competency_to_plan($pl1->get_id(), $competency->get_id())); + + // A competency cannot be added to plan based on template. + $template = $lpg->create_template(); + $plan = array ( + 'userid' => $usermanage->id, + 'status' => \core_competency\plan::STATUS_ACTIVE, + 'templateid' => $template->get_id() + ); + $pl2 = $lpg->create_plan($plan); + try { + external::add_competency_to_plan($pl2->get_id(), $competency->get_id()); + $this->fail('A competency cannot be added to plan based on template'); + } catch (coding_exception $ex) { + $this->assertTrue(true); + } + + // User without capability cannot add competency to a plan. + $this->setUser($user); + try { + external::add_competency_to_plan($pl1->get_id(), $competency->get_id()); + $this->fail('User without capability cannot add competency to a plan'); + } catch (required_capability_exception $ex) { + $this->assertTrue(true); + } + } + + /** + * Test that we can add competency to plan if we have the right capability. + * + * @return void + */ + public function test_remove_competency_from_plan() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $usermanage = $dg->create_user(); + $user = $dg->create_user(); + + $syscontext = context_system::instance(); + + // Creating specific roles. + $managerole = $dg->create_role(array( + 'name' => 'User manage', + 'shortname' => 'manage' + )); + + assign_capability('moodle/competency:planmanage', CAP_ALLOW, $managerole, $syscontext->id); + assign_capability('moodle/competency:planview', CAP_ALLOW, $managerole, $syscontext->id); + + $dg->role_assign($managerole, $usermanage->id, $syscontext->id); + + $this->setUser($usermanage); + $plan = array ( + 'userid' => $usermanage->id, + 'status' => \core_competency\plan::STATUS_ACTIVE + ); + $pl1 = $lpg->create_plan($plan); + $framework = $lpg->create_framework(); + $competency = $lpg->create_competency( + array('competencyframeworkid' => $framework->get_id()) + ); + $lpg->create_plan_competency( + array( + 'planid' => $pl1->get_id(), + 'competencyid' => $competency->get_id() + ) + ); + $this->assertTrue(external::remove_competency_from_plan($pl1->get_id(), $competency->get_id())); + $this->assertCount(0, $pl1->get_competencies()); + } + + /** + * Test that we can add competency to plan if we have the right capability. + * + * @return void + */ + public function test_reorder_plan_competency() { + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $usermanage = $dg->create_user(); + $user = $dg->create_user(); + + $syscontext = context_system::instance(); + + // Creating specific roles. + $managerole = $dg->create_role(array( + 'name' => 'User manage', + 'shortname' => 'manage' + )); + + assign_capability('moodle/competency:planmanage', CAP_ALLOW, $managerole, $syscontext->id); + assign_capability('moodle/competency:planview', CAP_ALLOW, $managerole, $syscontext->id); + + $dg->role_assign($managerole, $usermanage->id, $syscontext->id); + + $this->setUser($usermanage); + $plan = array ( + 'userid' => $usermanage->id, + 'status' => \core_competency\plan::STATUS_ACTIVE + ); + $pl1 = $lpg->create_plan($plan); + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c4 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c5 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + $lpg->create_plan_competency(array('planid' => $pl1->get_id(), 'competencyid' => $c1->get_id(), 'sortorder' => 1)); + $lpg->create_plan_competency(array('planid' => $pl1->get_id(), 'competencyid' => $c2->get_id(), 'sortorder' => 2)); + $lpg->create_plan_competency(array('planid' => $pl1->get_id(), 'competencyid' => $c3->get_id(), 'sortorder' => 3)); + $lpg->create_plan_competency(array('planid' => $pl1->get_id(), 'competencyid' => $c4->get_id(), 'sortorder' => 4)); + $lpg->create_plan_competency(array('planid' => $pl1->get_id(), 'competencyid' => $c5->get_id(), 'sortorder' => 5)); + + // Test if removing competency from plan don't create sortorder holes. + external::remove_competency_from_plan($pl1->get_id(), $c4->get_id()); + $plancomp5 = plan_competency::get_record(array( + 'planid' => $pl1->get_id(), + 'competencyid' => $c5->get_id() + )); + + $this->assertEquals(3, $plancomp5->get_sortorder()); + + $this->assertTrue(external::reorder_plan_competency($pl1->get_id(), $c2->get_id(), $c5->get_id())); + $this->assertTrue(external::reorder_plan_competency($pl1->get_id(), $c3->get_id(), $c1->get_id())); + $plancompetencies = plan_competency::get_records(array('planid' => $pl1->get_id()), 'sortorder', 'ASC'); + $plcmp1 = $plancompetencies[0]; + $plcmp2 = $plancompetencies[1]; + $plcmp3 = $plancompetencies[2]; + $plcmp4 = $plancompetencies[3]; + + $this->assertEquals($plcmp1->get_competencyid(), $c3->get_id()); + $this->assertEquals($plcmp2->get_competencyid(), $c1->get_id()); + $this->assertEquals($plcmp3->get_competencyid(), $c5->get_id()); + $this->assertEquals($plcmp4->get_competencyid(), $c2->get_id()); + } + + /** + * Test resolving sortorder when we creating competency. + */ + public function test_fix_sortorder_when_creating_competency() { + $this->resetAfterTest(true); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'sortorder' => 20)); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'sortorder' => 1)); + + $this->assertEquals(0, $c1->get_sortorder()); + $this->assertEquals(1, $c2->get_sortorder()); + $this->assertEquals(2, $c3->get_sortorder()); + } + + /** + * Test resolving sortorder when we delete competency. + */ + public function test_fix_sortorder_when_delete_competency() { + $this->resetAfterTest(true); + $this->setUser($this->creator); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $framework = $lpg->create_framework(); + + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2a = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c2->get_id())); + $c2b = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c2->get_id())); + $c2c = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c2->get_id())); + $c2d = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c2->get_id())); + + $this->assertEquals(0, $c1->get_sortorder()); + $this->assertEquals(1, $c2->get_sortorder()); + $this->assertEquals(0, $c2a->get_sortorder()); + $this->assertEquals(1, $c2b->get_sortorder()); + $this->assertEquals(2, $c2c->get_sortorder()); + $this->assertEquals(3, $c2d->get_sortorder()); + + $result = external::delete_competency($c1->get_id()); + $result = external_api::clean_returnvalue(external::delete_competency_returns(), $result); + + $c2->read(); + $c2a->read(); + $c2b->read(); + $c2c->read(); + $c2d->read(); + + $this->assertEquals(0, $c2->get_sortorder()); + $this->assertEquals(0, $c2a->get_sortorder()); + $this->assertEquals(1, $c2b->get_sortorder()); + $this->assertEquals(2, $c2c->get_sortorder()); + $this->assertEquals(3, $c2d->get_sortorder()); + + $result = external::delete_competency($c2b->get_id()); + $result = external_api::clean_returnvalue(external::delete_competency_returns(), $result); + + $c2->read(); + $c2a->read(); + $c2c->read(); + $c2d->read(); + + $this->assertEquals(0, $c2->get_sortorder()); + $this->assertEquals(0, $c2a->get_sortorder()); + $this->assertEquals(1, $c2c->get_sortorder()); + $this->assertEquals(2, $c2d->get_sortorder()); + } + + /** + * Test resolving sortorder when moving a competency. + */ + public function test_fix_sortorder_when_moving_competency() { + $this->resetAfterTest(true); + $this->setUser($this->creator); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $framework = $lpg->create_framework(); + + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c1a = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + $c1b = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2a = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c2->get_id())); + $c2b = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(), 'parentid' => $c2->get_id())); + + $this->assertEquals(0, $c1->get_sortorder()); + $this->assertEquals(0, $c1a->get_sortorder()); + $this->assertEquals(1, $c1b->get_sortorder()); + $this->assertEquals(1, $c2->get_sortorder()); + $this->assertEquals(0, $c2a->get_sortorder()); + $this->assertEquals(1, $c2b->get_sortorder()); + + $result = external::set_parent_competency($c2a->get_id(), $c1->get_id()); + $result = external_api::clean_returnvalue(external::set_parent_competency_returns(), $result); + + $c1->read(); + $c1a->read(); + $c1b->read(); + $c2->read(); + $c2a->read(); + $c2b->read(); + + $this->assertEquals(0, $c1->get_sortorder()); + $this->assertEquals(0, $c1a->get_sortorder()); + $this->assertEquals(1, $c1b->get_sortorder()); + $this->assertEquals(2, $c2a->get_sortorder()); + $this->assertEquals(1, $c2->get_sortorder()); + $this->assertEquals(0, $c2b->get_sortorder()); + + // Move a root node. + $result = external::set_parent_competency($c2->get_id(), $c1b->get_id()); + $result = external_api::clean_returnvalue(external::set_parent_competency_returns(), $result); + + $c1->read(); + $c1a->read(); + $c1b->read(); + $c2->read(); + $c2a->read(); + $c2b->read(); + + $this->assertEquals(0, $c1->get_sortorder()); + $this->assertEquals(0, $c1a->get_sortorder()); + $this->assertEquals(1, $c1b->get_sortorder()); + $this->assertEquals(0, $c2->get_sortorder()); + $this->assertEquals(0, $c2b->get_sortorder()); + $this->assertEquals(2, $c2a->get_sortorder()); + } + + public function test_grade_competency_in_plan() { + global $CFG; + + $this->setUser($this->creator); + + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $f1 = $lpg->create_framework(); + + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + + $tpl = $lpg->create_template(); + $lpg->create_template_competency(array('templateid' => $tpl->get_id(), 'competencyid' => $c1->get_id())); + + $plan = $lpg->create_plan(array('userid' => $this->user->id, 'templateid' => $tpl->get_id(), 'name' => 'Evil')); + + $uc = $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c1->get_id())); + + $evidence = external::grade_competency_in_plan($plan->get_id(), $c1->get_id(), 1, 'Evil note'); + + $this->assertEquals('The competency grade was manually set in the plan \'Evil\'.', $evidence->description); + $this->assertEquals('A', $evidence->gradename); + $this->assertEquals('Evil note', $evidence->note); + + $this->setUser($this->user); + + $this->setExpectedException('required_capability_exception'); + $evidence = external::grade_competency_in_plan($plan->get_id(), $c1->get_id(), 1); + } + + /** + * Test update course competency settings. + */ + public function test_update_course_competency_settings() { + $this->resetAfterTest(true); + + $dg = $this->getDataGenerator(); + + $course = $dg->create_course(); + $roleid = $dg->create_role(); + $noobroleid = $dg->create_role(); + $context = context_course::instance($course->id); + $compmanager = $this->getDataGenerator()->create_user(); + $compnoob = $this->getDataGenerator()->create_user(); + + assign_capability('moodle/competency:coursecompetencyconfigure', CAP_ALLOW, $roleid, $context->id, true); + assign_capability('moodle/competency:coursecompetencyview', CAP_ALLOW, $roleid, $context->id, true); + assign_capability('moodle/competency:coursecompetencyview', CAP_ALLOW, $noobroleid, $context->id, true); + + role_assign($roleid, $compmanager->id, $context->id); + role_assign($noobroleid, $compnoob->id, $context->id); + $dg->enrol_user($compmanager->id, $course->id, $roleid); + $dg->enrol_user($compnoob->id, $course->id, $noobroleid); + + $this->setUser($compmanager); + + // Start the test. + $result = external::update_course_competency_settings($course->id, array('pushratingstouserplans' => true)); + + $settings = course_competency_settings::get_by_courseid($course->id); + + $this->assertTrue((bool)$settings->get_pushratingstouserplans()); + + $result = external::update_course_competency_settings($course->id, array('pushratingstouserplans' => false)); + + $settings = course_competency_settings::get_by_courseid($course->id); + + $this->assertFalse((bool)$settings->get_pushratingstouserplans()); + $this->setUser($compnoob); + + $this->setExpectedException('required_capability_exception'); + $result = external::update_course_competency_settings($course->id, array('pushratingstouserplans' => true)); + } + +} diff --git a/competency/tests/generator/lib.php b/competency/tests/generator/lib.php new file mode 100644 index 00000000000..40068251201 --- /dev/null +++ b/competency/tests/generator/lib.php @@ -0,0 +1,563 @@ +. + +/** + * Competency data generator. + * + * @package core_competency + * @category test + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_competency\competency; +use core_competency\competency_framework; +use core_competency\course_competency; +use core_competency\course_module_competency; +use core_competency\evidence; +use core_competency\external; +use core_competency\plan; +use core_competency\plan_competency; +use core_competency\related_competency; +use core_competency\template; +use core_competency\template_cohort; +use core_competency\template_competency; +use core_competency\user_competency; +use core_competency\user_competency_course; +use core_competency\user_competency_plan; +use core_competency\user_evidence; +use core_competency\user_evidence_competency; + + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/grade/grade_scale.php'); + +/** + * Competency data generator class. + * + * @package core_competency + * @category test + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_generator extends component_generator_base { + + /** @var int Number of created competencies. */ + protected $competencycount = 0; + + /** @var int Number of created frameworks. */ + protected $frameworkcount = 0; + + /** @var int Number of created plans. */ + protected $plancount = 0; + + /** @var int Number of created templates. */ + protected $templatecount = 0; + + /** @var int Number of created user_evidence. */ + protected $userevidencecount = 0; + + /** @var stdClass Scale that we might need. */ + protected $scale; + + /** + * Reset process. + * + * Do not call directly. + * + * @return void + */ + public function reset() { + $this->competencycount = 0; + $this->frameworkcount = 0; + $this->scale = null; + } + + /** + * Create a new competency. + * + * @param array|stdClass $record + * @return competency + */ + public function create_competency($record = null) { + $this->frameworkcount++; + $i = $this->frameworkcount; + $record = (object) $record; + + if (!isset($record->competencyframeworkid)) { + throw new coding_exception('The competencyframeworkid value is required.'); + } + if (!isset($record->shortname)) { + $record->shortname = "Competency shortname $i"; + } + if (!isset($record->idnumber)) { + $record->idnumber = "cmp{$i}"; + } + if (!isset($record->description)) { + $record->description = "Competency $i description "; + } + if (!isset($record->descriptionformat)) { + $record->descriptionformat = FORMAT_HTML; + } + if (isset($record->scaleconfiguration) + && (is_array($record->scaleconfiguration) || is_object($record->scaleconfiguration))) { + // Conveniently encode the config. + $record->scaleconfiguration = json_encode($record->scaleconfiguration); + } + + $competency = new competency(0, $record); + $competency->create(); + + return $competency; + } + + /** + * Create a new framework. + * + * @param array|stdClass $record + * @return competency_framework + */ + public function create_framework($record = null) { + $generator = phpunit_util::get_data_generator(); + $this->frameworkcount++; + $i = $this->frameworkcount; + $record = (object) $record; + + if (!isset($record->shortname)) { + $record->shortname = "Framework shortname $i"; + } + if (!isset($record->idnumber)) { + $record->idnumber = "frm{$i}"; + } + if (!isset($record->description)) { + $record->description = "Framework $i description "; + } + if (!isset($record->descriptionformat)) { + $record->descriptionformat = FORMAT_HTML; + } + if (!isset($record->visible)) { + $record->visible = 1; + } + if (!isset($record->scaleid)) { + if (isset($record->scaleconfiguration)) { + throw new coding_exception('Scale configuration must be provided with a scale.'); + } + if (!$this->scale) { + $this->scale = $generator->create_scale(array('scale' => 'A,B,C,D')); + } + $record->scaleid = $this->scale->id; + } + if (!isset($record->scaleconfiguration)) { + $scale = grade_scale::fetch(array('id' => $record->scaleid)); + $values = $scale->load_items(); + foreach ($values as $key => $value) { + // Add a key (make the first value 1). + $values[$key] = array('id' => $key + 1, 'name' => $value); + } + if (count($values) < 2) { + throw new coding_exception('Please provide the scale configuration for one-item scales.'); + } + $scaleconfig = array(); + // Last item is proficient. + $item = array_pop($values); + array_unshift($scaleconfig, array( + 'id' => $item['id'], + 'proficient' => 1 + )); + // Second-last item is default and proficient. + $item = array_pop($values); + array_unshift($scaleconfig, array( + 'id' => $item['id'], + 'scaledefault' => 1, + 'proficient' => 1 + )); + array_unshift($scaleconfig, array('scaleid' => $record->scaleid)); + $record->scaleconfiguration = json_encode($scaleconfig); + } + if (is_array($record->scaleconfiguration) || is_object($record->scaleconfiguration)) { + // Conveniently encode the config. + $record->scaleconfiguration = json_encode($record->scaleconfiguration); + } + if (!isset($record->contextid)) { + $record->contextid = context_system::instance()->id; + } + + $framework = new competency_framework(0, $record); + $framework->create(); + + return $framework; + } + + /** + * Create a related competency. + * + * @param array|stdClass $record + * @return related_competency + */ + public function create_related_competency($record = null) { + $record = (object) $record; + + if (!isset($record->competencyid)) { + throw new coding_exception('Property competencyid is required.'); + } + if (!isset($record->relatedcompetencyid)) { + throw new coding_exception('Property relatedcompetencyid is required.'); + } + + $relation = related_competency::get_relation($record->competencyid, $record->relatedcompetencyid); + if ($relation->get_id()) { + throw new coding_exception('Relation already exists'); + } + $relation->create(); + + return $relation; + } + + /** + * Create a template. + * + * @param array|stdClass $record + * @return template + */ + public function create_template($record = null) { + $this->templatecount++; + $i = $this->templatecount; + $record = (object) $record; + + if (!isset($record->shortname)) { + $record->shortname = "Template shortname $i"; + } + if (!isset($record->description)) { + $record->description = "Template $i description "; + } + if (!isset($record->contextid)) { + $record->contextid = context_system::instance()->id; + } + + $template = new template(0, $record); + $template->create(); + + return $template; + } + + /** + * Create a template competency. + * + * @param array|stdClass $record + * @return template_competency + */ + public function create_template_competency($record = null) { + $record = (object) $record; + + if (!isset($record->competencyid)) { + throw new coding_exception('Property competencyid is required.'); + } + if (!isset($record->templateid)) { + throw new coding_exception('Property templateid is required.'); + } + + $relation = new template_competency(0, $record); + $relation->create(); + + return $relation; + } + + /** + * Create a new user competency. + * + * @param array|stdClass $record + * @return user_competency + */ + public function create_user_competency($record = null) { + $record = (object) $record; + + if (!isset($record->userid)) { + throw new coding_exception('The userid value is required.'); + } + if (!isset($record->competencyid)) { + throw new coding_exception('The competencyid value is required.'); + } + + $usercompetency = new user_competency(0, $record); + $usercompetency->create(); + + return $usercompetency; + } + + /** + * Create a new plan. + * + * @param array|stdClass $record + * @return plan + */ + public function create_plan($record = null) { + $this->plancount++; + $i = $this->plancount; + $record = (object) $record; + + if (!isset($record->name)) { + $record->name = "Plan shortname $i"; + } + if (!isset($record->description)) { + $record->description = "Plan $i description"; + } + if (!isset($record->descriptionformat)) { + $record->descriptionformat = FORMAT_HTML; + } + if (!isset($record->userid)) { + throw new coding_exception('The userid value is required.'); + } + + $plan = new plan(0, $record); + $plan->create(); + + return $plan; + } + + /** + * Create a new user competency course. + * + * @param array|stdClass $record + * @return user_competency_course + */ + public function create_user_competency_course($record = null) { + $record = (object) $record; + + if (!isset($record->userid)) { + throw new coding_exception('The userid value is required.'); + } + if (!isset($record->competencyid)) { + throw new coding_exception('The competencyid value is required.'); + } + + if (!isset($record->courseid)) { + throw new coding_exception('The courseid value is required.'); + } + + $usercompetencycourse = new user_competency_course(0, $record); + $usercompetencycourse->create(); + + return $usercompetencycourse; + } + + /** + * Create a new user competency plan. + * + * @param array|stdClass $record + * @return user_competency_plan + */ + public function create_user_competency_plan($record = null) { + $record = (object) $record; + + if (!isset($record->userid)) { + throw new coding_exception('The userid value is required.'); + } + if (!isset($record->competencyid)) { + throw new coding_exception('The competencyid value is required.'); + } + + if (!isset($record->planid)) { + throw new coding_exception('The planid value is required.'); + } + + if (!isset($record->sortorder)) { + $record->sortorder = 0; + } + + $usercompetencyplan = new user_competency_plan(0, $record); + $usercompetencyplan->create(); + + return $usercompetencyplan; + } + + /** + * Create a new plan competency. + * + * @param array|stdClass $record + * @return plan_competency + */ + public function create_plan_competency($record = null) { + $record = (object) $record; + + if (!isset($record->planid)) { + throw new coding_exception('The planid value is required.'); + } + if (!isset($record->competencyid)) { + throw new coding_exception('The competencyid value is required.'); + } + + $plancompetency = new plan_competency(0, $record); + $plancompetency->create(); + + return $plancompetency; + } + + /** + * Create a new template cohort. + * + * @param array|stdClass $record + * @return template_cohort + */ + public function create_template_cohort($record = null) { + $record = (object) $record; + + if (!isset($record->templateid)) { + throw new coding_exception('The templateid value is required.'); + } + if (!isset($record->cohortid)) { + throw new coding_exception('The cohortid value is required.'); + } + + $tplcohort = new template_cohort(0, $record); + $tplcohort->create(); + + return $tplcohort; + } + + /** + * Create a new evidence. + * + * @param array|stdClass $record + * @return evidence + */ + public function create_evidence($record = null) { + $record = (object) $record; + + if (!isset($record->usercompetencyid)) { + throw new coding_exception('The usercompetencyid value is required.'); + } + if (!isset($record->action) && !isset($record->grade)) { + $record->action = evidence::ACTION_LOG; + } + if (!isset($record->action)) { + throw new coding_exception('The action value is required with a grade.'); + } + + if (!isset($record->contextid)) { + $record->contextid = context_system::instance()->id; + } + if (!isset($record->descidentifier)) { + $record->descidentifier = 'invalidevidencedesc'; + } + if (!isset($record->desccomponent)) { + $record->desccomponent = 'core_competency'; + } + $evidence = new evidence(0, $record); + $evidence->create(); + + return $evidence; + } + + /** + * Create a new course competency. + * + * @param array|stdClass $record + * @return user_competency + */ + public function create_course_competency($record = null) { + $record = (object) $record; + + if (!isset($record->courseid)) { + throw new coding_exception('The courseid value is required.'); + } + if (!isset($record->competencyid)) { + throw new coding_exception('The competencyid value is required.'); + } + + $cc = new course_competency(0, $record); + $cc->create(); + + return $cc; + } + + /** + * Create a new course module competency. + * + * @param array|stdClass $record + * @return course_module_competency + */ + public function create_course_module_competency($record = null) { + $record = (object) $record; + + if (!isset($record->cmid)) { + throw new coding_exception('The cmid value is required.'); + } + if (!isset($record->competencyid)) { + throw new coding_exception('The competencyid value is required.'); + } + + $cc = new course_module_competency(0, $record); + $cc->create(); + + return $cc; + } + + /** + * Create a new user_evidence. + * + * @param array|stdClass $record + * @return evidence + */ + public function create_user_evidence($record = null) { + $this->userevidencecount++; + $i = $this->userevidencecount; + $record = (object) $record; + + if (!isset($record->userid)) { + throw new coding_exception('The userid value is required.'); + } + if (!isset($record->name)) { + $record->name = "Evidence $i name"; + } + if (!isset($record->description)) { + $record->description = "Evidence $i description"; + } + if (!isset($record->descriptionformat)) { + $record->descriptionformat = FORMAT_HTML; + } + + $ue = new user_evidence(0, $record); + $ue->create(); + + return $ue; + } + + /** + * Create a new user_evidence_comp. + * + * @param array|stdClass $record + * @return evidence + */ + public function create_user_evidence_competency($record = null) { + $record = (object) $record; + + if (!isset($record->userevidenceid)) { + throw new coding_exception('The userevidenceid value is required.'); + } + if (!isset($record->competencyid)) { + throw new coding_exception('The competencyid value is required.'); + } + + $uec = new user_evidence_competency(0, $record); + $uec->create(); + + return $uec; + } + +} + diff --git a/competency/tests/generator_test.php b/competency/tests/generator_test.php new file mode 100644 index 00000000000..6f20314ec31 --- /dev/null +++ b/competency/tests/generator_test.php @@ -0,0 +1,260 @@ +. + +/** + * Tool LP data generator tests. + * + * @package core_competency + * @category test + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_competency\competency; +use core_competency\competency_framework; +use core_competency\course_competency; +use core_competency\course_module_competency; +use core_competency\plan; +use core_competency\related_competency; +use core_competency\template; +use core_competency\template_cohort; +use core_competency\template_competency; +use core_competency\user_competency; +use core_competency\user_competency_plan; +use core_competency\plan_competency; +use core_competency\evidence; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Tool LP data generator testcase. + * + * @package core_competency + * @category test + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_generator_testcase extends advanced_testcase { + + public function test_create_framework() { + $this->resetAfterTest(true); + + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $this->assertEquals(0, competency_framework::count_records()); + $framework = $lpg->create_framework(); + $framework = $lpg->create_framework(); + $this->assertEquals(2, competency_framework::count_records()); + $this->assertInstanceOf('\core_competency\competency_framework', $framework); + } + + public function test_create_competency() { + $this->resetAfterTest(true); + + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $this->assertEquals(0, competency::count_records()); + $competency = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $competency = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $this->assertEquals(2, competency::count_records()); + $this->assertInstanceOf('\core_competency\competency', $competency); + } + + public function test_create_related_competency() { + $this->resetAfterTest(true); + + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $this->assertEquals(0, related_competency::count_records()); + $rc = $lpg->create_related_competency(array('competencyid' => $c1->get_id(), 'relatedcompetencyid' => $c2->get_id())); + $rc = $lpg->create_related_competency(array('competencyid' => $c2->get_id(), 'relatedcompetencyid' => $c3->get_id())); + $this->assertEquals(2, related_competency::count_records()); + $this->assertInstanceOf('\core_competency\related_competency', $rc); + } + + public function test_create_plan() { + $this->resetAfterTest(true); + + $user = $this->getDataGenerator()->create_user(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $this->assertEquals(0, plan::count_records()); + $plan = $lpg->create_plan(array('userid' => $user->id)); + $this->assertEquals(1, plan::count_records()); + $this->assertInstanceOf('\core_competency\plan', $plan); + } + + public function test_create_user_competency() { + $this->resetAfterTest(true); + + $user = $this->getDataGenerator()->create_user(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $this->assertEquals(0, user_competency::count_records()); + $rc = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c1->get_id())); + $rc = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c2->get_id())); + $this->assertEquals(2, user_competency::count_records()); + $this->assertInstanceOf('\core_competency\user_competency', $rc); + } + + public function test_create_user_competency_plan() { + $this->resetAfterTest(true); + + $user = $this->getDataGenerator()->create_user(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $plan = $lpg->create_plan(array('userid' => $user->id)); + $this->assertEquals(0, user_competency_plan::count_records()); + $ucp = $lpg->create_user_competency_plan(array( + 'userid' => $user->id, + 'competencyid' => $c1->get_id(), + 'planid' => $plan->get_id() + )); + $ucp = $lpg->create_user_competency_plan(array( + 'userid' => $user->id, + 'competencyid' => $c2->get_id(), + 'planid' => $plan->get_id() + )); + $this->assertEquals(2, user_competency_plan::count_records()); + $this->assertInstanceOf('\core_competency\user_competency_plan', $ucp); + } + + public function test_create_template() { + $this->resetAfterTest(true); + + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $this->assertEquals(0, template::count_records()); + $template = $lpg->create_template(); + $template = $lpg->create_template(); + $this->assertEquals(2, template::count_records()); + $this->assertInstanceOf('\core_competency\template', $template); + } + + public function test_create_template_competency() { + $this->resetAfterTest(true); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $this->assertEquals(0, template_competency::count_records()); + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $template = $lpg->create_template(); + $relation = $lpg->create_template_competency(array('competencyid' => $c1->get_id(), 'templateid' => $template->get_id())); + $relation = $lpg->create_template_competency(array('competencyid' => $c2->get_id(), 'templateid' => $template->get_id())); + $this->assertEquals(2, template_competency::count_records()); + $this->assertInstanceOf('\core_competency\template_competency', $relation); + } + + public function test_create_plan_competency() { + $this->resetAfterTest(true); + $user = $this->getDataGenerator()->create_user(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + + $plan = $lpg->create_plan(array('userid' => $user->id)); + + $pc1 = $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c1->get_id())); + $pc2 = $lpg->create_plan_competency(array('planid' => $plan->get_id(), 'competencyid' => $c2->get_id())); + + $this->assertEquals(2, plan_competency::count_records()); + $this->assertInstanceOf('\core_competency\plan_competency', $pc1); + $this->assertInstanceOf('\core_competency\plan_competency', $pc2); + $this->assertEquals($plan->get_id(), $pc1->get_planid()); + } + + public function test_create_template_cohort() { + $this->resetAfterTest(true); + + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $c1 = $this->getDataGenerator()->create_cohort(); + $c2 = $this->getDataGenerator()->create_cohort(); + $t1 = $lpg->create_template(); + $this->assertEquals(0, template_cohort::count_records()); + $tc = $lpg->create_template_cohort(array('templateid' => $t1->get_id(), 'cohortid' => $c1->id)); + $this->assertEquals(1, template_cohort::count_records()); + $tc = $lpg->create_template_cohort(array('templateid' => $t1->get_id(), 'cohortid' => $c2->id)); + $this->assertEquals(2, template_cohort::count_records()); + $this->assertInstanceOf('\core_competency\template_cohort', $tc); + } + + public function test_create_evidence() { + $this->resetAfterTest(true); + + $user = $this->getDataGenerator()->create_user(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $rc1 = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c1->get_id())); + $rc2 = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c2->get_id())); + $e = $lpg->create_evidence(array('usercompetencyid' => $rc1->get_id())); + $e = $lpg->create_evidence(array('usercompetencyid' => $rc2->get_id())); + $this->assertEquals(2, evidence::count_records()); + $this->assertInstanceOf('\core_competency\evidence', $e); + } + + public function test_create_course_competency() { + $this->resetAfterTest(true); + + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $this->assertEquals(0, course_competency::count_records()); + $rc = $lpg->create_course_competency(array('competencyid' => $c1->get_id(), 'courseid' => $course1->id)); + $rc = $lpg->create_course_competency(array('competencyid' => $c2->get_id(), 'courseid' => $course1->id)); + $this->assertEquals(2, course_competency::count_records(array('courseid' => $course1->id))); + $this->assertEquals(0, course_competency::count_records(array('courseid' => $course2->id))); + $rc = $lpg->create_course_competency(array('competencyid' => $c3->get_id(), 'courseid' => $course2->id)); + $this->assertEquals(1, course_competency::count_records(array('courseid' => $course2->id))); + $this->assertInstanceOf('\core_competency\course_competency', $rc); + } + + public function test_create_course_module_competency() { + $this->resetAfterTest(true); + + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $course1 = $this->getDataGenerator()->create_course(); + $cm1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id)); + $cm2 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id)); + $framework = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id())); + $this->assertEquals(0, course_module_competency::count_records()); + $rc = $lpg->create_course_module_competency(array('competencyid' => $c1->get_id(), 'cmid' => $cm1->cmid)); + $rc = $lpg->create_course_module_competency(array('competencyid' => $c2->get_id(), 'cmid' => $cm1->cmid)); + $this->assertEquals(2, course_module_competency::count_records(array('cmid' => $cm1->cmid))); + $this->assertEquals(0, course_module_competency::count_records(array('cmid' => $cm2->cmid))); + $rc = $lpg->create_course_module_competency(array('competencyid' => $c3->get_id(), 'cmid' => $cm2->cmid)); + $this->assertEquals(1, course_module_competency::count_records(array('cmid' => $cm2->cmid))); + $this->assertInstanceOf('\core_competency\course_module_competency', $rc); + } + +} + diff --git a/competency/tests/hooks_test.php b/competency/tests/hooks_test.php new file mode 100644 index 00000000000..568b1897e98 --- /dev/null +++ b/competency/tests/hooks_test.php @@ -0,0 +1,182 @@ +. + +/** + * Hook tests. + * + * @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(); + +use core_competency\course_competency; +use core_competency\course_module_competency; +use core_competency\user_competency_course; + +/** + * Hook tests. + * + * @package core_competency + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_hooks_testcase extends advanced_testcase { + + public function test_hook_course_deleted() { + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $ccg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + + $framework = $ccg->create_framework(); + $comp1 = $ccg->create_competency(['competencyframeworkid' => $framework->get_id()]); + $comp2 = $ccg->create_competency(['competencyframeworkid' => $framework->get_id()]); + + $c1 = $dg->create_course(); + $cc1a = $ccg->create_course_competency(['competencyid' => $comp1->get_id(), 'courseid' => $c1->id]); + $cc1b = $ccg->create_course_competency(['competencyid' => $comp2->get_id(), 'courseid' => $c1->id]); + $assign1a = $dg->create_module('assign', ['course' => $c1]); + $assign1b = $dg->create_module('assign', ['course' => $c1]); + $cmc1a = $ccg->create_course_module_competency(['competencyid' => $comp1->get_id(), 'cmid' => $assign1a->cmid]); + $cmc1b = $ccg->create_course_module_competency(['competencyid' => $comp1->get_id(), 'cmid' => $assign1b->cmid]); + $ucc1a = $ccg->create_user_competency_course(['competencyid' => $comp1->get_id(), 'courseid' => $c1->id, + 'userid' => $u1->id]); + $ucc1b = $ccg->create_user_competency_course(['competencyid' => $comp2->get_id(), 'courseid' => $c1->id, + 'userid' => $u1->id]); + + $c2 = $dg->create_course(); + $cc2a = $ccg->create_course_competency(['competencyid' => $comp1->get_id(), 'courseid' => $c2->id]); + $cc2b = $ccg->create_course_competency(['competencyid' => $comp2->get_id(), 'courseid' => $c2->id]); + $assign2a = $dg->create_module('assign', ['course' => $c2]); + $assign2b = $dg->create_module('assign', ['course' => $c2]); + $cmc2a = $ccg->create_course_module_competency(['competencyid' => $comp1->get_id(), 'cmid' => $assign2a->cmid]); + $cmc2b = $ccg->create_course_module_competency(['competencyid' => $comp1->get_id(), 'cmid' => $assign2b->cmid]); + $ucc2a = $ccg->create_user_competency_course(['competencyid' => $comp1->get_id(), 'courseid' => $c2->id, + 'userid' => $u1->id]); + $ucc2b = $ccg->create_user_competency_course(['competencyid' => $comp2->get_id(), 'courseid' => $c2->id, + 'userid' => $u1->id]); + + delete_course($c1, false); + + $this->assertEquals(0, course_competency::count_records(['courseid' => $c1->id])); + $this->assertEquals(2, course_competency::count_records(['courseid' => $c2->id])); + $this->assertEquals(0, course_module_competency::count_records(['cmid' => $assign1a->cmid])); + $this->assertEquals(0, course_module_competency::count_records(['cmid' => $assign1b->cmid])); + $this->assertEquals(1, course_module_competency::count_records(['cmid' => $assign2a->cmid])); + $this->assertEquals(1, course_module_competency::count_records(['cmid' => $assign2b->cmid])); + $this->assertEquals(0, user_competency_course::count_records(['courseid' => $c1->id, 'userid' => $u1->id])); + $this->assertEquals(2, user_competency_course::count_records(['courseid' => $c2->id, 'userid' => $u1->id])); + } + + public function test_hook_course_module_deleted() { + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $ccg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + + $framework = $ccg->create_framework(); + $comp1 = $ccg->create_competency(['competencyframeworkid' => $framework->get_id()]); + $comp2 = $ccg->create_competency(['competencyframeworkid' => $framework->get_id()]); + + $c1 = $dg->create_course(); + $cc1a = $ccg->create_course_competency(['competencyid' => $comp1->get_id(), 'courseid' => $c1->id]); + $cc1b = $ccg->create_course_competency(['competencyid' => $comp2->get_id(), 'courseid' => $c1->id]); + $assign1a = $dg->create_module('assign', ['course' => $c1]); + $assign1b = $dg->create_module('assign', ['course' => $c1]); + $cmc1a = $ccg->create_course_module_competency(['competencyid' => $comp1->get_id(), 'cmid' => $assign1a->cmid]); + $cmc1b = $ccg->create_course_module_competency(['competencyid' => $comp1->get_id(), 'cmid' => $assign1b->cmid]); + $ucc1a = $ccg->create_user_competency_course(['competencyid' => $comp1->get_id(), 'courseid' => $c1->id, + 'userid' => $u1->id]); + $ucc1b = $ccg->create_user_competency_course(['competencyid' => $comp2->get_id(), 'courseid' => $c1->id, + 'userid' => $u1->id]); + + $c2 = $dg->create_course(); + $cc2a = $ccg->create_course_competency(['competencyid' => $comp1->get_id(), 'courseid' => $c2->id]); + $cc2b = $ccg->create_course_competency(['competencyid' => $comp2->get_id(), 'courseid' => $c2->id]); + $assign2a = $dg->create_module('assign', ['course' => $c2]); + $assign2b = $dg->create_module('assign', ['course' => $c2]); + $cmc2a = $ccg->create_course_module_competency(['competencyid' => $comp1->get_id(), 'cmid' => $assign2a->cmid]); + $cmc2b = $ccg->create_course_module_competency(['competencyid' => $comp1->get_id(), 'cmid' => $assign2b->cmid]); + $ucc2a = $ccg->create_user_competency_course(['competencyid' => $comp1->get_id(), 'courseid' => $c2->id, + 'userid' => $u1->id]); + $ucc2b = $ccg->create_user_competency_course(['competencyid' => $comp2->get_id(), 'courseid' => $c2->id, + 'userid' => $u1->id]); + + course_delete_module($assign1b->cmid); + + $this->assertEquals(2, course_competency::count_records(['courseid' => $c1->id])); + $this->assertEquals(1, course_module_competency::count_records(['cmid' => $assign1a->cmid])); + $this->assertEquals(0, course_module_competency::count_records(['cmid' => $assign1b->cmid])); + $this->assertEquals(2, user_competency_course::count_records(['courseid' => $c1->id])); + + $this->assertEquals(2, course_competency::count_records(['courseid' => $c2->id])); + $this->assertEquals(1, course_module_competency::count_records(['cmid' => $assign2a->cmid])); + $this->assertEquals(1, course_module_competency::count_records(['cmid' => $assign2b->cmid])); + $this->assertEquals(2, user_competency_course::count_records(['courseid' => $c2->id, 'userid' => $u1->id])); + } + + public function test_hook_course_reset_competency_ratings() { + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $ccg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + + $framework = $ccg->create_framework(); + $comp1 = $ccg->create_competency(['competencyframeworkid' => $framework->get_id()]); + $comp2 = $ccg->create_competency(['competencyframeworkid' => $framework->get_id()]); + + $c1 = $dg->create_course(); + $cc1a = $ccg->create_course_competency(['competencyid' => $comp1->get_id(), 'courseid' => $c1->id]); + $cc1b = $ccg->create_course_competency(['competencyid' => $comp2->get_id(), 'courseid' => $c1->id]); + $assign1a = $dg->create_module('assign', ['course' => $c1]); + $assign1b = $dg->create_module('assign', ['course' => $c1]); + $cmc1a = $ccg->create_course_module_competency(['competencyid' => $comp1->get_id(), 'cmid' => $assign1a->cmid]); + $cmc1b = $ccg->create_course_module_competency(['competencyid' => $comp1->get_id(), 'cmid' => $assign1b->cmid]); + $ucc1a = $ccg->create_user_competency_course(['competencyid' => $comp1->get_id(), 'courseid' => $c1->id, + 'userid' => $u1->id]); + $ucc1b = $ccg->create_user_competency_course(['competencyid' => $comp2->get_id(), 'courseid' => $c1->id, + 'userid' => $u1->id]); + + $c2 = $dg->create_course(); + $cc2a = $ccg->create_course_competency(['competencyid' => $comp1->get_id(), 'courseid' => $c2->id]); + $cc2b = $ccg->create_course_competency(['competencyid' => $comp2->get_id(), 'courseid' => $c2->id]); + $assign2a = $dg->create_module('assign', ['course' => $c2]); + $assign2b = $dg->create_module('assign', ['course' => $c2]); + $cmc2a = $ccg->create_course_module_competency(['competencyid' => $comp1->get_id(), 'cmid' => $assign2a->cmid]); + $cmc2b = $ccg->create_course_module_competency(['competencyid' => $comp1->get_id(), 'cmid' => $assign2b->cmid]); + $ucc2a = $ccg->create_user_competency_course(['competencyid' => $comp1->get_id(), 'courseid' => $c2->id, + 'userid' => $u1->id]); + $ucc2b = $ccg->create_user_competency_course(['competencyid' => $comp2->get_id(), 'courseid' => $c2->id, + 'userid' => $u1->id]); + + reset_course_userdata((object) ['id' => $c1->id, 'reset_competency_ratings' => true]); + + $this->assertEquals(2, course_competency::count_records(['courseid' => $c1->id])); + $this->assertEquals(2, course_competency::count_records(['courseid' => $c2->id])); + $this->assertEquals(1, course_module_competency::count_records(['cmid' => $assign1a->cmid])); + $this->assertEquals(1, course_module_competency::count_records(['cmid' => $assign1b->cmid])); + $this->assertEquals(1, course_module_competency::count_records(['cmid' => $assign2a->cmid])); + $this->assertEquals(1, course_module_competency::count_records(['cmid' => $assign2b->cmid])); + $this->assertEquals(0, user_competency_course::count_records(['courseid' => $c1->id, 'userid' => $u1->id])); + $this->assertEquals(2, user_competency_course::count_records(['courseid' => $c2->id, 'userid' => $u1->id])); + } + +} diff --git a/competency/tests/lib_test.php b/competency/tests/lib_test.php new file mode 100644 index 00000000000..4163b286bb2 --- /dev/null +++ b/competency/tests/lib_test.php @@ -0,0 +1,307 @@ +. + +/** + * Lib tests. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_competency\plan; +use core_competency\url; +use core_competency\user_competency; + +global $CFG; + +/** + * Lib testcase. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_lib_testcase extends advanced_testcase { + + public function test_comment_add_user_competency() { + global $DB; + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + $reviewerroleid = $dg->create_role(); + assign_capability('moodle/competency:planview', CAP_ALLOW, $reviewerroleid, context_system::instance()->id, true); + assign_capability('moodle/competency:usercompetencycomment', CAP_ALLOW, $reviewerroleid, + context_system::instance()->id, true); + $dg->role_assign($reviewerroleid, $u2->id, context_user::instance($u1->id)); + $dg->role_assign($reviewerroleid, $u3->id, context_user::instance($u1->id)); + accesslib_clear_all_caches_for_unit_testing(); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); // In 1 plan. + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); // In 2 plans. + $c3 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); // Orphan. + + $p1 = $lpg->create_plan(array('userid' => $u1->id)); + $lpg->create_plan_competency(array('planid' => $p1->get_id(), 'competencyid' => $c1->get_id())); + $lpg->create_plan_competency(array('planid' => $p1->get_id(), 'competencyid' => $c2->get_id())); + $p2 = $lpg->create_plan(array('userid' => $u1->id)); + $lpg->create_plan_competency(array('planid' => $p2->get_id(), 'competencyid' => $c2->get_id())); + + $DB->set_field(plan::TABLE, 'timemodified', 1, array('id' => $p1->get_id())); // Make plan 1 appear as old. + $p1->read(); + + $uc1 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c1->get_id(), + 'status' => user_competency::STATUS_IN_REVIEW, 'reviewerid' => $u2->id)); + $uc2 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c2->get_id(), + 'status' => user_competency::STATUS_IN_REVIEW, 'reviewerid' => $u2->id)); + $uc3 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c3->get_id(), + 'status' => user_competency::STATUS_IN_REVIEW, 'reviewerid' => $u2->id)); + + // Post a comment for the user competency being in one plan. The reviewer is messaged. + $this->setUser($u1); + $comment = $uc1->get_comment_object(); + $sink = $this->redirectMessages(); + $comment->add('Hello world!'); + $messages = $sink->get_messages(); + $sink->close(); + $this->assertCount(1, $messages); + $message = array_pop($messages); + + $expectedurlname = $c1->get_shortname(); + $expectedurl = url::user_competency_in_plan($u1->id, $c1->get_id(), $p1->get_id()); + $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom); + $this->assertEquals($u2->id, $message->useridto); + $this->assertTrue(strpos($message->fullmessage, 'Hello world!') !== false); + $this->assertTrue(strpos($message->fullmessagehtml, 'Hello world!') !== false); + $this->assertEquals(FORMAT_MOODLE, $message->fullmessageformat); + $this->assertEquals($expectedurl->out(false), $message->contexturl); + $this->assertEquals($expectedurlname, $message->contexturlname); + + // Reviewer posts a comment for the user competency being in two plans. Owner is messaged. + $this->setUser($u2); + $comment = $uc2->get_comment_object(); + $sink = $this->redirectMessages(); + $comment->add('Hello world!'); + $messages = $sink->get_messages(); + $sink->close(); + $this->assertCount(1, $messages); + $message = array_pop($messages); + + $expectedurlname = $c2->get_shortname(); + $expectedurl = url::user_competency_in_plan($u1->id, $c2->get_id(), $p2->get_id()); + $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom); + $this->assertEquals($u1->id, $message->useridto); + $this->assertTrue(strpos($message->fullmessage, 'Hello world!') !== false); + $this->assertTrue(strpos($message->fullmessagehtml, 'Hello world!') !== false); + $this->assertEquals(FORMAT_MOODLE, $message->fullmessageformat); + $this->assertEquals($expectedurl->out(false), $message->contexturl); + $this->assertEquals($expectedurlname, $message->contexturlname); + + // Reviewer posts a comment for the user competency being in no plans. User is messaged. + $this->setUser($u2); + $comment = $uc3->get_comment_object(); + $sink = $this->redirectMessages(); + $comment->add('Hello world!'); + $messages = $sink->get_messages(); + $sink->close(); + $this->assertCount(1, $messages); + $message = array_pop($messages); + + $expectedurlname = get_string('userplans', 'core_competency'); + $expectedurl = url::plans($u1->id); + $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom); + $this->assertEquals($u1->id, $message->useridto); + $this->assertTrue(strpos($message->fullmessage, 'Hello world!') !== false); + $this->assertTrue(strpos($message->fullmessagehtml, 'Hello world!') !== false); + $this->assertEquals(FORMAT_MOODLE, $message->fullmessageformat); + $this->assertEquals($expectedurl->out(false), $message->contexturl); + $this->assertEquals($expectedurlname, $message->contexturlname); + + // A comment is posted by another user, reviewer and owner are messaged. + $this->setUser($u3); + $comment = $uc3->get_comment_object(); + $sink = $this->redirectMessages(); + $comment->add('Hello world!'); + $messages = $sink->get_messages(); + $sink->close(); + $this->assertCount(2, $messages); + $message1 = array_shift($messages); + $message2 = array_shift($messages); + $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom); + $this->assertEquals($u1->id, $message1->useridto); + $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom); + $this->assertEquals($u2->id, $message2->useridto); + + // A comment is posted in HTML. + $this->setUser($u2); + $comment = $uc3->get_comment_object(); + $sink = $this->redirectMessages(); + $comment->add('Hello world!', FORMAT_HTML); + $messages = $sink->get_messages(); + $sink->close(); + $this->assertCount(1, $messages); + $message = array_pop($messages); + + $expectedurlname = get_string('userplans', 'core_competency'); + $expectedurl = url::plans($u1->id); + $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom); + $this->assertEquals($u1->id, $message->useridto); + $this->assertTrue(strpos($message->fullmessage, 'Hello world!') !== false); + $this->assertTrue(strpos($message->fullmessagehtml, 'Hello world!') !== false); + $this->assertEquals(FORMAT_HTML, $message->fullmessageformat); + $this->assertEquals($expectedurl->out(false), $message->contexturl); + $this->assertEquals($expectedurlname, $message->contexturlname); + } + + /** + * Commenting on a plan. + */ + public function test_comment_add_plan() { + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + $userroleid = $dg->create_role(); + $reviewerroleid = $dg->create_role(); + assign_capability('moodle/competency:planviewowndraft', CAP_ALLOW, $userroleid, context_system::instance()->id, true); + assign_capability('moodle/competency:planviewown', CAP_ALLOW, $userroleid, context_system::instance()->id, true); + assign_capability('moodle/competency:planviewdraft', CAP_ALLOW, $reviewerroleid, context_system::instance()->id, true); + assign_capability('moodle/competency:planmanage', CAP_ALLOW, $reviewerroleid, context_system::instance()->id, true); + assign_capability('moodle/competency:plancomment', CAP_ALLOW, $reviewerroleid, context_system::instance()->id, true); + $dg->role_assign($userroleid, $u1->id, context_user::instance($u1->id)); + $dg->role_assign($reviewerroleid, $u2->id, context_user::instance($u1->id)); + $dg->role_assign($reviewerroleid, $u3->id, context_system::instance()); + accesslib_clear_all_caches_for_unit_testing(); + + $p1 = $lpg->create_plan(array('userid' => $u1->id)); + + // Post a comment in own plan, no reviewer. Nobody is messaged. + $this->setUser($u1); + $comment = $p1->get_comment_object(); + $sink = $this->redirectMessages(); + $comment->add('Hello world!'); + $messages = $sink->get_messages(); + $sink->close(); + $this->assertCount(0, $messages); + + // Post a comment in plan as someone else, no reviewer. The owner is messages. + $this->setUser($u3); + $comment = $p1->get_comment_object(); + $sink = $this->redirectMessages(); + $comment->add('Hello world!'); + $messages = $sink->get_messages(); + $sink->close(); + $this->assertCount(1, $messages); + $message = array_pop($messages); + $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom); + $this->assertEquals($u1->id, $message->useridto); + + // Post a comment in a plan with reviewer. The reviewer is messaged. + $p1->set_reviewerid($u2->id); + $p1->update(); + $this->setUser($u1); + $comment = $p1->get_comment_object(); + $sink = $this->redirectMessages(); + $comment->add('Hello world!'); + $messages = $sink->get_messages(); + $sink->close(); + $this->assertCount(1, $messages); + $message = array_pop($messages); + $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom); + $this->assertEquals($u2->id, $message->useridto); + + // Post a comment as reviewer in a plan being reviewed. The owner is messaged. + $p1->set_reviewerid($u2->id); + $p1->update(); + $this->setUser($u2); + $comment = $p1->get_comment_object(); + $sink = $this->redirectMessages(); + $comment->add('Hello world!'); + $messages = $sink->get_messages(); + $sink->close(); + $this->assertCount(1, $messages); + $message = array_pop($messages); + $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom); + $this->assertEquals($u1->id, $message->useridto); + + // Post a comment as someone else in a plan being reviewed. The owner and reviewer are messaged. + $p1->set_reviewerid($u2->id); + $p1->update(); + $this->setUser($u3); + $comment = $p1->get_comment_object(); + $sink = $this->redirectMessages(); + $comment->add('Hello world!'); + $messages = $sink->get_messages(); + $sink->close(); + $this->assertCount(2, $messages); + $message1 = array_shift($messages); + $message2 = array_shift($messages); + $this->assertEquals(core_user::get_noreply_user()->id, $message1->useridfrom); + $this->assertEquals($u1->id, $message1->useridto); + $this->assertEquals(core_user::get_noreply_user()->id, $message2->useridfrom); + $this->assertEquals($u2->id, $message2->useridto); + + $p1->set_reviewerid(null); + $p1->update(); + + // Test message content. + $this->setUser($u3); + $comment = $p1->get_comment_object(); + $sink = $this->redirectMessages(); + $comment->add('Hello world!'); + $messages = $sink->get_messages(); + $sink->close(); + $this->assertCount(1, $messages); + $message = array_pop($messages); + + $expectedurlname = $p1->get_name(); + $expectedurl = url::plan($p1->get_id()); + $this->assertTrue(strpos($message->fullmessage, 'Hello world!') !== false); + $this->assertTrue(strpos($message->fullmessagehtml, 'Hello world!') !== false); + $this->assertEquals(FORMAT_MOODLE, $message->fullmessageformat); + $this->assertEquals($expectedurl->out(false), $message->contexturl); + $this->assertEquals($expectedurlname, $message->contexturlname); + + // Test message content as HTML. + $this->setUser($u3); + $comment = $p1->get_comment_object(); + $sink = $this->redirectMessages(); + $comment->add('Hello world!', FORMAT_HTML); + $messages = $sink->get_messages(); + $sink->close(); + $this->assertCount(1, $messages); + $message = array_pop($messages); + + $expectedurlname = $p1->get_name(); + $expectedurl = url::plan($p1->get_id()); + $this->assertTrue(strpos($message->fullmessage, 'Hello world!') !== false); + $this->assertTrue(strpos($message->fullmessagehtml, 'Hello world!') !== false); + $this->assertEquals(FORMAT_HTML, $message->fullmessageformat); + $this->assertEquals($expectedurl->out(false), $message->contexturl); + $this->assertEquals($expectedurlname, $message->contexturlname); + } + +} diff --git a/competency/tests/persistent_test.php b/competency/tests/persistent_test.php new file mode 100644 index 00000000000..c37f06d9c83 --- /dev/null +++ b/competency/tests/persistent_test.php @@ -0,0 +1,520 @@ +. + +/** + * Persistent class tests. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +/** + * Persistent testcase. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_persistent_testcase extends advanced_testcase { + + public function setUp() { + $this->resetAfterTest(); + } + + public function test_properties_definition() { + $expected = array( + 'shortname' => array( + 'type' => PARAM_TEXT, + 'default' => '', + 'null' => NULL_NOT_ALLOWED + ), + 'idnumber' => array( + 'type' => PARAM_TEXT, + 'null' => NULL_NOT_ALLOWED + ), + 'description' => array( + 'type' => PARAM_TEXT, + 'default' => '', + 'null' => NULL_NOT_ALLOWED + ), + 'descriptionformat' => array( + 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), + 'type' => PARAM_INT, + 'default' => FORMAT_HTML, + 'null' => NULL_NOT_ALLOWED + ), + 'parentid' => array( + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED + ), + 'path' => array( + 'type' => PARAM_RAW, + 'default' => '', + 'null' => NULL_NOT_ALLOWED + ), + 'sortorder' => array( + 'type' => PARAM_INT, + 'message' => new lang_string('invalidrequest', 'error'), + 'null' => NULL_NOT_ALLOWED + ), + 'competencyframeworkid' => array( + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_ALLOWED + ), + 'id' => array( + 'default' => 0, + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED + ), + 'timecreated' => array( + 'default' => 0, + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED + ), + 'timemodified' => array( + 'default' => 0, + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED + ), + 'usermodified' => array( + 'default' => 0, + 'type' => PARAM_INT, + 'null' => NULL_NOT_ALLOWED + ), + 'ruletype' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'ruleconfig' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'ruleoutcome' => array( + 'type' => PARAM_RAW, + 'default' => 0, + 'null' => NULL_NOT_ALLOWED + ), + 'scaleid' => array( + 'default' => null, + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED + ), + 'scaleconfiguration' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED + ) + ); + $this->assertEquals($expected, core_competency_testable_persistent::properties_definition()); + } + + public function test_to_record() { + $p = new core_competency_testable_persistent(); + $expected = (object) array( + 'shortname' => '', + 'idnumber' => null, + 'description' => '', + 'descriptionformat' => FORMAT_HTML, + 'parentid' => 0, + 'path' => '', + 'sortorder' => null, + 'competencyframeworkid' => null, + 'id' => 0, + 'timecreated' => 0, + 'timemodified' => 0, + 'usermodified' => 0, + 'ruletype' => null, + 'ruleconfig' => null, + 'ruleoutcome' => 0, + 'scaleid' => null, + 'scaleconfiguration' => null, + ); + $this->assertEquals($expected, $p->to_record()); + } + + public function test_from_record() { + $p = new core_competency_testable_persistent(); + $data = (object) array( + 'shortname' => 'ddd', + 'idnumber' => 'abc', + 'description' => 'xyz', + 'descriptionformat' => FORMAT_PLAIN, + 'parentid' => 999, + 'path' => '/a/b/c', + 'sortorder' => 12, + 'competencyframeworkid' => 5, + 'id' => 1, + 'timecreated' => 2, + 'timemodified' => 3, + 'usermodified' => 4, + 'ruletype' => null, + 'ruleconfig' => null, + 'ruleoutcome' => 0, + 'scaleid' => null, + 'scaleconfiguration' => null, + ); + $p->from_record($data); + $this->assertEquals($data, $p->to_record()); + } + + public function test_from_record_invalid_param() { + $p = new core_competency_testable_persistent(); + $data = (object) array( + 'invalidparam' => 'abc' + ); + $this->setExpectedException('coding_exception'); + $p->from_record($data); + } + + public function test_validate() { + $data = (object) array( + 'idnumber' => 'abc', + 'sortorder' => 0 + ); + $p = new core_competency_testable_persistent(0, $data); + $this->assertFalse(isset($p->beforevalidate)); + $this->assertTrue($p->validate()); + $this->assertTrue(isset($p->beforevalidate)); + $this->assertTrue($p->is_valid()); + $this->assertEquals(array(), $p->get_errors()); + $p->set_descriptionformat(-100); + + $expected = array( + 'descriptionformat' => new lang_string('invaliddata', 'error'), + ); + $this->assertEquals($expected, $p->validate()); + $this->assertFalse($p->is_valid()); + $this->assertEquals($expected, $p->get_errors()); + } + + public function test_validation_required() { + $data = (object) array( + 'idnumber' => 'abc' + ); + $p = new core_competency_testable_persistent(0, $data); + $expected = array( + 'sortorder' => new lang_string('requiredelement', 'form'), + ); + $this->assertFalse($p->is_valid()); + $this->assertEquals($expected, $p->get_errors()); + } + + public function test_validation_custom() { + $data = (object) array( + 'idnumber' => 'abc', + 'sortorder' => 10, + ); + $p = new core_competency_testable_persistent(0, $data); + $expected = array( + 'sortorder' => new lang_string('invalidkey', 'error'), + ); + $this->assertFalse($p->is_valid()); + $this->assertEquals($expected, $p->get_errors()); + } + + public function test_validation_custom_message() { + $data = (object) array( + 'idnumber' => 'abc', + 'sortorder' => 'abc', + ); + $p = new core_competency_testable_persistent(0, $data); + $expected = array( + 'sortorder' => new lang_string('invalidrequest', 'error'), + ); + $this->assertFalse($p->is_valid()); + $this->assertEquals($expected, $p->get_errors()); + } + + public function test_validation_choices() { + $data = (object) array( + 'idnumber' => 'abc', + 'sortorder' => 0, + 'descriptionformat' => -100 + ); + $p = new core_competency_testable_persistent(0, $data); + $expected = array( + 'descriptionformat' => new lang_string('invaliddata', 'error'), + ); + $this->assertFalse($p->is_valid()); + $this->assertEquals($expected, $p->get_errors()); + } + + public function test_validation_type() { + $data = (object) array( + 'idnumber' => 'abc', + 'sortorder' => 'NaN' + ); + $p = new core_competency_testable_persistent(0, $data); + $this->assertFalse($p->is_valid()); + $this->assertArrayHasKey('sortorder', $p->get_errors()); + } + + public function test_validation_null() { + $data = (object) array( + 'idnumber' => null, + 'sortorder' => 0, + 'competencyframeworkid' => 'bad!' + ); + $p = new core_competency_testable_persistent(0, $data); + $this->assertFalse($p->is_valid()); + $this->assertArrayHasKey('idnumber', $p->get_errors()); + $this->assertArrayHasKey('competencyframeworkid', $p->get_errors()); + $p->set_idnumber('abc'); + $this->assertFalse($p->is_valid()); + $this->assertArrayNotHasKey('idnumber', $p->get_errors()); + $this->assertArrayHasKey('competencyframeworkid', $p->get_errors()); + $p->set_competencyframeworkid(null); + $this->assertTrue($p->is_valid()); + $this->assertArrayNotHasKey('competencyframeworkid', $p->get_errors()); + } + + public function test_create() { + global $DB; + $p = new core_competency_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); + $this->assertFalse(isset($p->beforecreate)); + $this->assertFalse(isset($p->aftercreate)); + $p->create(); + $record = $DB->get_record(core_competency_testable_persistent::TABLE, array('id' => $p->get_id()), '*', MUST_EXIST); + $expected = $p->to_record(); + $this->assertTrue(isset($p->beforecreate)); + $this->assertTrue(isset($p->aftercreate)); + $this->assertEquals($expected->sortorder, $record->sortorder); + $this->assertEquals($expected->idnumber, $record->idnumber); + $this->assertEquals($expected->id, $record->id); + $this->assertTrue($p->is_valid()); // Should always be valid after a create. + } + + public function test_update() { + global $DB; + $p = new core_competency_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); + $p->create(); + $id = $p->get_id(); + $p->set_sortorder(456); + $p->from_record((object) array('idnumber' => 'def')); + $this->assertFalse(isset($p->beforeupdate)); + $this->assertFalse(isset($p->afterupdate)); + $p->update(); + + $expected = $p->to_record(); + $record = $DB->get_record(core_competency_testable_persistent::TABLE, array('id' => $p->get_id()), '*', MUST_EXIST); + $this->assertTrue(isset($p->beforeupdate)); + $this->assertTrue(isset($p->afterupdate)); + $this->assertEquals($id, $record->id); + $this->assertEquals(456, $record->sortorder); + $this->assertEquals('def', $record->idnumber); + $this->assertTrue($p->is_valid()); // Should always be valid after an update. + } + + public function test_read() { + $p = new core_competency_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); + $p->create(); + unset($p->beforevalidate); + unset($p->beforecreate); + unset($p->aftercreate); + + $p2 = new core_competency_testable_persistent($p->get_id()); + $this->assertEquals($p, $p2); + + $p3 = new core_competency_testable_persistent(); + $p3->set_id($p->get_id()); + $p3->read(); + $this->assertEquals($p, $p3); + } + + public function test_delete() { + global $DB; + + $p = new core_competency_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); + $p->create(); + $this->assertNotEquals(0, $p->get_id()); + $this->assertTrue($DB->record_exists_select(core_competency_testable_persistent::TABLE, 'id = ?', array($p->get_id()))); + $this->assertFalse(isset($p->beforedelete)); + $this->assertFalse(isset($p->afterdelete)); + + $p->delete(); + $this->assertFalse($DB->record_exists_select(core_competency_testable_persistent::TABLE, 'id = ?', array($p->get_id()))); + $this->assertEquals(0, $p->get_id()); + $this->assertEquals(true, $p->beforedelete); + $this->assertEquals(true, $p->afterdelete); + } + + public function test_has_property() { + $this->assertFalse(core_competency_testable_persistent::has_property('unknown')); + $this->assertTrue(core_competency_testable_persistent::has_property('idnumber')); + } + + public function test_custom_setter_getter() { + global $DB; + + $path = array(1, 2, 3); + $json = json_encode($path); + + $p = new core_competency_testable_persistent(0, (object) array('sortorder' => 0, 'idnumber' => 'abc')); + $p->set_path($path); + $this->assertEquals($path, $p->get_path()); + $this->assertEquals($json, $p->to_record()->path); + + $p->create(); + $record = $DB->get_record(core_competency_testable_persistent::TABLE, array('id' => $p->get_id()), 'id, path', MUST_EXIST); + $this->assertEquals($json, $record->path); + } + + public function test_record_exists() { + global $DB; + $this->assertFalse($DB->record_exists(core_competency_testable_persistent::TABLE, array('idnumber' => 'abc'))); + $p = new core_competency_testable_persistent(0, (object) array('sortorder' => 123, 'idnumber' => 'abc')); + $p->create(); + $id = $p->get_id(); + $this->assertTrue(core_competency_testable_persistent::record_exists($id)); + $this->assertTrue($DB->record_exists(core_competency_testable_persistent::TABLE, array('idnumber' => 'abc'))); + $p->delete(); + $this->assertFalse(core_competency_testable_persistent::record_exists($id)); + } + +} + +/** + * Example persistent class. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_testable_persistent extends \core_competency\persistent { + + const TABLE = 'competency'; + + protected static function define_properties() { + return array( + 'shortname' => array( + 'type' => PARAM_TEXT, + 'default' => '' + ), + 'idnumber' => array( + 'type' => PARAM_TEXT, + ), + 'description' => array( + 'type' => PARAM_TEXT, + 'default' => '' + ), + 'descriptionformat' => array( + 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), + 'type' => PARAM_INT, + 'default' => FORMAT_HTML + ), + 'parentid' => array( + 'type' => PARAM_INT, + 'default' => 0 + ), + 'path' => array( + 'type' => PARAM_RAW, + 'default' => '' + ), + 'sortorder' => array( + 'type' => PARAM_INT, + 'message' => new lang_string('invalidrequest', 'error') + ), + 'competencyframeworkid' => array( + 'type' => PARAM_INT, + 'default' => 0, + 'null' => NULL_ALLOWED + ), + 'ruletype' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'ruleconfig' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'ruleoutcome' => array( + 'type' => PARAM_RAW, + 'default' => 0 + ), + 'scaleid' => array( + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED + ), + 'scaleconfiguration' => array( + 'type' => PARAM_RAW, + 'default' => null, + 'null' => NULL_ALLOWED + ) + ); + } + + protected function before_validate() { + $this->beforevalidate = true; + } + + protected function before_create() { + $this->beforecreate = true; + } + + protected function before_update() { + $this->beforeupdate = true; + } + + protected function before_delete() { + $this->beforedelete = true; + } + + protected function after_create() { + $this->aftercreate = true; + } + + protected function after_update($result) { + $this->afterupdate = true; + } + + protected function after_delete($result) { + $this->afterdelete = true; + } + + public function get_path() { + $value = $this->get('path'); + if (!empty($value)) { + $value = json_decode($value); + } + return $value; + } + + public function set_path($value) { + if (!empty($value)) { + $value = json_encode($value); + } + $this->set('path', $value); + } + + protected function validate_sortorder($value) { + if ($value == 10) { + return new lang_string('invalidkey', 'error'); + } + return true; + } + +} \ No newline at end of file diff --git a/competency/tests/plan_test.php b/competency/tests/plan_test.php new file mode 100644 index 00000000000..7391ada0c82 --- /dev/null +++ b/competency/tests/plan_test.php @@ -0,0 +1,572 @@ +. + +/** + * Plan persistent class tests. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +use core_competency\api; +use core_competency\plan; + +/** + * Plan persistent testcase. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_plan_testcase extends advanced_testcase { + + public function test_can_manage_user() { + $this->resetAfterTest(true); + + $manage = create_role('Manage', 'manage', 'Plan manager'); + $manageown = create_role('Manageown', 'manageown', 'Own plan manager'); + + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + $u3 = $this->getDataGenerator()->create_user(); + + $syscontext = context_system::instance(); + $u1context = context_user::instance($u1->id); + $u2context = context_user::instance($u2->id); + $u3context = context_user::instance($u3->id); + + assign_capability('moodle/competency:planmanage', CAP_ALLOW, $manage, $syscontext->id); + assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $manageown, $u2context->id); + + role_assign($manage, $u1->id, $syscontext->id); + role_assign($manageown, $u2->id, $syscontext->id); + role_assign($manage, $u3->id, $u2context->id); + accesslib_clear_all_caches_for_unit_testing(); + + $this->setUser($u1); + $this->assertTrue(plan::can_manage_user($u1->id)); + $this->assertTrue(plan::can_manage_user($u2->id)); + $this->assertTrue(plan::can_manage_user($u3->id)); + + $this->setUser($u2); + $this->assertFalse(plan::can_manage_user($u1->id)); + $this->assertTrue(plan::can_manage_user($u2->id)); + $this->assertFalse(plan::can_manage_user($u3->id)); + + $this->setUser($u3); + $this->assertFalse(plan::can_manage_user($u1->id)); + $this->assertTrue(plan::can_manage_user($u2->id)); + $this->assertFalse(plan::can_manage_user($u3->id)); + } + + public function test_can_manage_user_draft() { + $this->resetAfterTest(true); + + $manage = create_role('Manage', 'manage', 'Plan manager'); + $manageown = create_role('Manageown', 'manageown', 'Own plan manager'); + $managedraft = create_role('Managedraft', 'managedraft', 'Draft plan manager'); + $manageowndraft = create_role('Manageowndraft', 'manageowndraft', 'Own draft plan manager'); + + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + $u3 = $this->getDataGenerator()->create_user(); + $u4 = $this->getDataGenerator()->create_user(); + $u5 = $this->getDataGenerator()->create_user(); + + $syscontext = context_system::instance(); + $u1context = context_user::instance($u1->id); + $u2context = context_user::instance($u2->id); + $u3context = context_user::instance($u3->id); + $u4context = context_user::instance($u4->id); + $u5context = context_user::instance($u5->id); + + assign_capability('moodle/competency:planmanage', CAP_ALLOW, $manage, $syscontext->id); + assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $manageown, $syscontext->id); + assign_capability('moodle/competency:planmanagedraft', CAP_ALLOW, $managedraft, $syscontext->id); + assign_capability('moodle/competency:planmanageowndraft', CAP_ALLOW, $manageowndraft, $syscontext->id); + + role_assign($manage, $u1->id, $syscontext->id); + role_assign($manageown, $u2->id, $syscontext->id); + role_assign($managedraft, $u3->id, $syscontext->id); + role_assign($managedraft, $u4->id, $u2context->id); + role_assign($manageowndraft, $u5->id, $syscontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + $this->setUser($u1); + $this->assertFalse(plan::can_manage_user_draft($u1->id)); + $this->assertFalse(plan::can_manage_user_draft($u2->id)); + $this->assertFalse(plan::can_manage_user_draft($u3->id)); + $this->assertFalse(plan::can_manage_user_draft($u4->id)); + $this->assertFalse(plan::can_manage_user_draft($u5->id)); + + $this->setUser($u2); + $this->assertFalse(plan::can_manage_user_draft($u1->id)); + $this->assertFalse(plan::can_manage_user_draft($u2->id)); + $this->assertFalse(plan::can_manage_user_draft($u3->id)); + $this->assertFalse(plan::can_manage_user_draft($u4->id)); + $this->assertFalse(plan::can_manage_user_draft($u5->id)); + + $this->setUser($u3); + $this->assertTrue(plan::can_manage_user_draft($u1->id)); + $this->assertTrue(plan::can_manage_user_draft($u2->id)); + $this->assertTrue(plan::can_manage_user_draft($u3->id)); + $this->assertTrue(plan::can_manage_user_draft($u4->id)); + $this->assertTrue(plan::can_manage_user_draft($u5->id)); + + $this->setUser($u4); + $this->assertFalse(plan::can_manage_user_draft($u1->id)); + $this->assertTrue(plan::can_manage_user_draft($u2->id)); + $this->assertFalse(plan::can_manage_user_draft($u3->id)); + $this->assertFalse(plan::can_manage_user_draft($u4->id)); + $this->assertFalse(plan::can_manage_user_draft($u5->id)); + + $this->setUser($u5); + $this->assertFalse(plan::can_manage_user_draft($u1->id)); + $this->assertFalse(plan::can_manage_user_draft($u2->id)); + $this->assertFalse(plan::can_manage_user_draft($u3->id)); + $this->assertFalse(plan::can_manage_user_draft($u4->id)); + $this->assertTrue(plan::can_manage_user_draft($u5->id)); + } + + public function test_can_read_user() { + $this->resetAfterTest(true); + + $read = create_role('Read', 'read', 'Plan reader'); + $readown = create_role('Readown', 'readown', 'Own plan reader'); + + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + $u3 = $this->getDataGenerator()->create_user(); + + $syscontext = context_system::instance(); + $u1context = context_user::instance($u1->id); + $u2context = context_user::instance($u2->id); + $u3context = context_user::instance($u3->id); + + assign_capability('moodle/competency:planview', CAP_ALLOW, $read, $syscontext->id); + assign_capability('moodle/competency:planviewown', CAP_ALLOW, $readown, $u2context->id); + + role_assign($read, $u1->id, $syscontext->id); + role_assign($readown, $u2->id, $syscontext->id); + role_assign($read, $u3->id, $u2context->id); + accesslib_clear_all_caches_for_unit_testing(); + + $this->setUser($u1); + $this->assertTrue(plan::can_read_user($u1->id)); + $this->assertTrue(plan::can_read_user($u2->id)); + $this->assertTrue(plan::can_read_user($u3->id)); + + $this->setUser($u2); + $this->assertFalse(plan::can_read_user($u1->id)); + $this->assertTrue(plan::can_read_user($u2->id)); + $this->assertFalse(plan::can_read_user($u3->id)); + + $this->setUser($u3); + $this->assertFalse(plan::can_read_user($u1->id)); + $this->assertTrue(plan::can_read_user($u2->id)); + $this->assertTrue(plan::can_read_user($u3->id)); // Due to the default capability. + } + + public function test_can_read_user_draft() { + $this->resetAfterTest(true); + + $read = create_role('Read', 'read', 'Plan readr'); + $readown = create_role('Readown', 'readown', 'Own plan readr'); + $readdraft = create_role('Readdraft', 'readdraft', 'Draft plan readr'); + $readowndraft = create_role('Readowndraft', 'readowndraft', 'Own draft plan readr'); + + $u1 = $this->getDataGenerator()->create_user(); + $u2 = $this->getDataGenerator()->create_user(); + $u3 = $this->getDataGenerator()->create_user(); + $u4 = $this->getDataGenerator()->create_user(); + $u5 = $this->getDataGenerator()->create_user(); + + $syscontext = context_system::instance(); + $u1context = context_user::instance($u1->id); + $u2context = context_user::instance($u2->id); + $u3context = context_user::instance($u3->id); + $u4context = context_user::instance($u4->id); + $u5context = context_user::instance($u5->id); + + assign_capability('moodle/competency:planview', CAP_ALLOW, $read, $syscontext->id); + assign_capability('moodle/competency:planviewown', CAP_ALLOW, $readown, $syscontext->id); + assign_capability('moodle/competency:planviewdraft', CAP_ALLOW, $readdraft, $syscontext->id); + assign_capability('moodle/competency:planviewowndraft', CAP_ALLOW, $readowndraft, $syscontext->id); + assign_capability('moodle/competency:planviewown', CAP_PROHIBIT, $readowndraft, $syscontext->id); + + role_assign($read, $u1->id, $syscontext->id); + role_assign($readown, $u2->id, $syscontext->id); + role_assign($readdraft, $u3->id, $syscontext->id); + role_assign($readdraft, $u4->id, $u2context->id); + role_assign($readowndraft, $u5->id, $syscontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + $this->setUser($u1); + $this->assertFalse(plan::can_read_user_draft($u1->id)); + $this->assertFalse(plan::can_read_user_draft($u2->id)); + $this->assertFalse(plan::can_read_user_draft($u3->id)); + $this->assertFalse(plan::can_read_user_draft($u4->id)); + $this->assertFalse(plan::can_read_user_draft($u5->id)); + + $this->setUser($u2); + $this->assertFalse(plan::can_read_user_draft($u1->id)); + $this->assertFalse(plan::can_read_user_draft($u2->id)); + $this->assertFalse(plan::can_read_user_draft($u3->id)); + $this->assertFalse(plan::can_read_user_draft($u4->id)); + $this->assertFalse(plan::can_read_user_draft($u5->id)); + + $this->setUser($u3); + $this->assertTrue(plan::can_read_user_draft($u1->id)); + $this->assertTrue(plan::can_read_user_draft($u2->id)); + $this->assertTrue(plan::can_read_user_draft($u3->id)); + $this->assertTrue(plan::can_read_user_draft($u4->id)); + $this->assertTrue(plan::can_read_user_draft($u5->id)); + + $this->setUser($u4); + $this->assertFalse(plan::can_read_user_draft($u1->id)); + $this->assertTrue(plan::can_read_user_draft($u2->id)); + $this->assertFalse(plan::can_read_user_draft($u3->id)); + $this->assertFalse(plan::can_read_user_draft($u4->id)); + $this->assertFalse(plan::can_read_user_draft($u5->id)); + + $this->setUser($u5); + $this->assertFalse(plan::can_read_user_draft($u1->id)); + $this->assertFalse(plan::can_read_user_draft($u2->id)); + $this->assertFalse(plan::can_read_user_draft($u3->id)); + $this->assertFalse(plan::can_read_user_draft($u4->id)); + $this->assertTrue(plan::can_read_user_draft($u5->id)); + } + + public function test_validate_duedate() { + global $DB; + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency'); + $user = $dg->create_user(); + + $record = array('userid' => $user->id, + 'status' => plan::STATUS_DRAFT, + 'duedate' => time() - 8000); + + // Ignore duedate validation on create/update draft plan. + $plan = $lpg->create_plan($record); + $this->assertInstanceOf('core_competency\plan', $plan); + + // Passing from draft to active. + $plan->set_status(plan::STATUS_ACTIVE); + + // Draft to active with duedate in the past. + $expected = array( + 'duedate' => new lang_string('errorcannotsetduedateinthepast', 'core_competency'), + ); + $this->assertEquals($expected, $plan->validate()); + + // Draft to active: past date => past date(fail). + $plan->set_duedate(time() - 100); + $expected = array( + 'duedate' => new lang_string('errorcannotsetduedateinthepast', 'core_competency'), + ); + $this->assertEquals($expected, $plan->validate()); + + // Draft to active: past date => too soon (fail). + $plan->set_duedate(time() + 100); + $expected = array( + 'duedate' => new lang_string('errorcannotsetduedatetoosoon', 'core_competency'), + ); + $this->assertEquals($expected, $plan->validate()); + + // Draft to active: past date => future date (pass). + $plan->set_duedate(time() + plan::DUEDATE_THRESHOLD + 10); + $this->assertEquals(true, $plan->validate()); + + // Draft to active: past date => unset date (pass). + $plan->set_duedate(0); + $this->assertEquals(true, $plan->validate()); + + // Updating active plan. + $plan->update(); + + // Active to active: past => same past (pass). + $record = $plan->to_record(); + $record->duedate = 1; + $DB->update_record(plan::TABLE, $record); + $plan->read(); + $plan->set_description(uniqid()); // Force revalidation. + $this->assertTrue($plan->is_valid()); + + // Active to active: past => unset (pass). + $plan->set_duedate(0); + $this->assertTrue($plan->is_valid()); + $plan->update(); + + // Active to active: unset => unset (pass). + $plan->set_description(uniqid()); // Force revalidation. + $this->assertTrue($plan->is_valid()); + + // Active to active: unset date => past date(fail). + $plan->set_duedate(time() - 100); + $expected = array( + 'duedate' => new lang_string('errorcannotsetduedateinthepast', 'core_competency'), + ); + $this->assertEquals($expected, $plan->validate()); + + // Active to active: unset date => too soon (fail). + $plan->set_duedate(time() + 100); + $expected = array( + 'duedate' => new lang_string('errorcannotsetduedatetoosoon', 'core_competency'), + ); + $this->assertEquals($expected, $plan->validate()); + + // Active to active: unset date => future date (pass). + $plan->set_duedate(time() + plan::DUEDATE_THRESHOLD + 10); + $this->assertEquals(true, $plan->validate()); + + // Updating active plan with future date. + $plan->update(); + + // Active to active: future => same future (pass). + $plan->set_description(uniqid()); // Force revalidation. + $this->assertTrue($plan->is_valid()); + + // Active to active: future date => unset date (pass). + $plan->set_duedate(0); + $this->assertEquals(true, $plan->validate()); + + // Active to active: future date => past date(fail). + $plan->set_duedate(time() - 100); + $expected = array( + 'duedate' => new lang_string('errorcannotsetduedateinthepast', 'core_competency'), + ); + $this->assertEquals($expected, $plan->validate()); + + // Active to active: future date => too soon (fail). + $plan->set_duedate(time() + 100); + $expected = array( + 'duedate' => new lang_string('errorcannotsetduedatetoosoon', 'core_competency'), + ); + $this->assertEquals($expected, $plan->validate()); + + // Active to active: future date => future date (pass). + $plan->set_duedate(time() + plan::DUEDATE_THRESHOLD + 10); + $this->assertEquals(true, $plan->validate()); + + // Completing plan: with due date in the past. + $record = $plan->to_record(); + $record->status = plan::STATUS_ACTIVE; + $record->duedate = time() - 200; + $DB->update_record(plan::TABLE, $record); + + $success = core_competency\api::complete_plan($plan->get_id()); + $this->assertTrue($success); + + // Completing plan: with due date too soon (pass). + $record = $plan->to_record(); + $record->status = plan::STATUS_ACTIVE; + $record->duedate = time() + 200; + $DB->update_record(plan::TABLE, $record); + + $success = core_competency\api::complete_plan($plan->get_id()); + $this->assertTrue($success); + + // Completing plan: with due date in the future (pass). + $record = $plan->to_record(); + $record->status = plan::STATUS_ACTIVE; + $record->duedate = time() + plan::DUEDATE_THRESHOLD + 10; + $DB->update_record(plan::TABLE, $record); + + $success = core_competency\api::complete_plan($plan->get_id()); + $this->assertTrue($success); + + // Completing plan: with due date unset (pass). + $record = $plan->to_record(); + $record->status = plan::STATUS_ACTIVE; + $record->duedate = 0; + $DB->update_record(plan::TABLE, $record); + + $success = core_competency\api::complete_plan($plan->get_id()); + $this->assertTrue($success); + + // Reopening plan: with due date in the past => duedate unset. + $record = $plan->to_record(); + $record->status = plan::STATUS_COMPLETE; + $record->duedate = time() - 200; + $DB->update_record(plan::TABLE, $record); + + $success = core_competency\api::reopen_plan($plan->get_id()); + $this->assertTrue($success); + $plan->read(); + $this->assertEquals(0, $plan->get_duedate()); + + // Reopening plan: with due date too soon => duedate unset. + $record = $plan->to_record(); + $record->status = plan::STATUS_COMPLETE; + $record->duedate = time() + 100; + $DB->update_record(plan::TABLE, $record); + + $success = core_competency\api::reopen_plan($plan->get_id()); + $this->assertTrue($success); + $plan->read(); + $this->assertEquals(0, $plan->get_duedate()); + + // Reopening plan: with due date in the future => duedate unchanged. + $record = $plan->to_record(); + $record->status = plan::STATUS_COMPLETE; + $record->duedate = time() + plan::DUEDATE_THRESHOLD + 10; + $DB->update_record(plan::TABLE, $record); + + $success = core_competency\api::reopen_plan($plan->get_id()); + $this->assertTrue($success); + $plan->read(); + + // Check that the due date has not changed, but allow for PHP Unit latency. + $this->assertTrue($plan->get_duedate() >= time() + plan::DUEDATE_THRESHOLD + 10); + $this->assertTrue($plan->get_duedate() <= time() + plan::DUEDATE_THRESHOLD + 15); + } + + public function test_get_by_user_and_competency() { + $this->resetAfterTest(); + $this->setAdminUser(); + + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + $u4 = $dg->create_user(); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + + $tpl1 = $lpg->create_template(); + $lpg->create_template_competency(array('competencyid' => $c1->get_id(), 'templateid' => $tpl1->get_id())); + + $p1 = $lpg->create_plan(array('userid' => $u1->id)); + $lpg->create_plan_competency(array('planid' => $p1->get_id(), 'competencyid' => $c1->get_id())); + $p2 = $lpg->create_plan(array('userid' => $u2->id)); + $lpg->create_plan_competency(array('planid' => $p2->get_id(), 'competencyid' => $c1->get_id())); + $p3 = $lpg->create_plan(array('userid' => $u3->id, 'templateid' => $tpl1->get_id())); + $p4 = $lpg->create_plan(array('userid' => $u4->id, 'templateid' => $tpl1->get_id())); + api::complete_plan($p2); + api::complete_plan($p4); + + // Finding a plan, not completed. + $plans = plan::get_by_user_and_competency($u1->id, $c1->get_id()); + $this->assertCount(1, $plans); + $plan = array_shift($plans); + $this->assertEquals($p1->get_id(), $plan->get_id()); + $this->assertNotEquals(plan::STATUS_COMPLETE, $plan->get_status()); + + // Finding a completed plan. + $plans = plan::get_by_user_and_competency($u2->id, $c1->get_id()); + $this->assertCount(1, $plans); + $plan = array_shift($plans); + $this->assertEquals($p2->get_id(), $plan->get_id()); + $this->assertEquals(plan::STATUS_COMPLETE, $plan->get_status()); + + // Finding a plan based on a template, not completed. + $plans = plan::get_by_user_and_competency($u3->id, $c1->get_id()); + $this->assertCount(1, $plans); + $plan = array_shift($plans); + $this->assertEquals($p3->get_id(), $plan->get_id()); + $this->assertTrue($plan->is_based_on_template()); + $this->assertNotEquals(plan::STATUS_COMPLETE, $plan->get_status()); + + // Finding a plan based on a template. + $plans = plan::get_by_user_and_competency($u4->id, $c1->get_id()); + $this->assertCount(1, $plans); + $plan = array_shift($plans); + $this->assertEquals($p4->get_id(), $plan->get_id()); + $this->assertTrue($plan->is_based_on_template()); + $this->assertEquals(plan::STATUS_COMPLETE, $plan->get_status()); + + // Finding more than one plan, no template. + $p5 = $lpg->create_plan(array('userid' => $u1->id)); + $lpg->create_plan_competency(array('planid' => $p5->get_id(), 'competencyid' => $c1->get_id())); + $plans = plan::get_by_user_and_competency($u1->id, $c1->get_id()); + $this->assertCount(2, $plans); + $plan = array_shift($plans); + $this->assertEquals($p1->get_id(), $plan->get_id()); + $plan = array_shift($plans); + $this->assertEquals($p5->get_id(), $plan->get_id()); + + // Finding more than one plan, with template. + $p6 = $lpg->create_plan(array('userid' => $u1->id, 'templateid' => $tpl1->get_id())); + $plans = plan::get_by_user_and_competency($u1->id, $c1->get_id()); + $this->assertCount(3, $plans); + $plan = array_shift($plans); + $this->assertEquals($p1->get_id(), $plan->get_id()); + $plan = array_shift($plans); + $this->assertEquals($p5->get_id(), $plan->get_id()); + $plan = array_shift($plans); + $this->assertEquals($p6->get_id(), $plan->get_id()); + + // Finding no plans. + $plans = plan::get_by_user_and_competency($u1->id, $c2->get_id()); + $this->assertCount(0, $plans); + } + + public function test_get_competency() { + $this->resetAfterTest(); + $this->setAdminUser(); + + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + $u2 = $dg->create_user(); + $u3 = $dg->create_user(); + $u4 = $dg->create_user(); + + $f1 = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + $c4 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id())); + + $tpl1 = $lpg->create_template(); + $p1 = $lpg->create_plan(array('userid' => $u1->id)); + $p2 = $lpg->create_plan(array('userid' => $u2->id)); + $p3 = $lpg->create_plan(array('userid' => $u3->id, 'templateid' => $tpl1->get_id())); + $p4 = $lpg->create_plan(array('userid' => $u4->id, 'templateid' => $tpl1->get_id())); + + $lpg->create_plan_competency(array('planid' => $p1->get_id(), 'competencyid' => $c1->get_id())); + $lpg->create_plan_competency(array('planid' => $p2->get_id(), 'competencyid' => $c2->get_id())); + $lpg->create_template_competency(array('templateid' => $tpl1->get_id(), 'competencyid' => $c3->get_id())); + $lpg->create_template_competency(array('templateid' => $tpl1->get_id(), 'competencyid' => $c4->get_id())); + + // Completing the plans and removing a competency from the template. + api::complete_plan($p2); + api::complete_plan($p4); + api::remove_competency_from_template($tpl1->get_id(), $c4->get_id()); + + // We can find all competencies. + $this->assertEquals($c1->to_record(), $p1->get_competency($c1->get_id())->to_record()); + $this->assertEquals($c2->to_record(), $p2->get_competency($c2->get_id())->to_record()); + $this->assertEquals($c3->to_record(), $p3->get_competency($c3->get_id())->to_record()); + $this->assertEquals($c4->to_record(), $p4->get_competency($c4->get_id())->to_record()); + + // Getting the competency 4 from the non-completed plan based on a template p4, will throw an exception. + $this->setExpectedException('coding_exception', 'The competency does not belong to this template: '); + $p3->get_competency($c4->get_id()); + } +} diff --git a/competency/tests/task_test.php b/competency/tests/task_test.php new file mode 100644 index 00000000000..311a7efff58 --- /dev/null +++ b/competency/tests/task_test.php @@ -0,0 +1,314 @@ +. + +/** + * Task tests. + * + * @package core_competency + * @copyright 2015 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_competency\api; +use core_competency\plan; + +/** + * Task tests. + * + * @package core_competency + * @copyright 2015 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_task_testcase extends advanced_testcase { + + public function test_sync_plans_from_cohorts_task() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + // Sql to simulate the execution in time. + $cmsql = "UPDATE {cohort_members} SET timeadded = :currenttime WHERE cohortid = :cohortid AND userid = :userid"; + $tplsql = "UPDATE {" . \core_competency\template::TABLE . "} SET timemodified = :currenttime WHERE id = :templateid"; + $plansql = "UPDATE {" . \core_competency\plan::TABLE . "} SET timemodified = :currenttime WHERE id = :planid"; + + $currenttime = time(); + + $user1 = $dg->create_user(); + $user2 = $dg->create_user(); + $user3 = $dg->create_user(); + $user4 = $dg->create_user(); + $user5 = $dg->create_user(); + + $cohort = $dg->create_cohort(); + $tpl = $lpg->create_template(); + + // Add 2 users to the cohort. + cohort_add_member($cohort->id, $user1->id); + cohort_add_member($cohort->id, $user2->id); + + // Creating plans from template cohort. + $templatecohort = api::create_template_cohort($tpl->get_id(), $cohort->id); + $created = api::create_plans_from_template_cohort($tpl->get_id(), $cohort->id); + + $this->assertEquals(2, $created); + + $task = \core\task\manager::get_scheduled_task('\\core\\task\\sync_plans_from_template_cohorts_task'); + $this->assertInstanceOf('\core\task\sync_plans_from_template_cohorts_task', $task); + + // Add two more users to the cohort. + cohort_add_member($cohort->id, $user3->id); + cohort_add_member($cohort->id, $user4->id); + + $currenttime = $currenttime + 1; + $task->execute(); + $task->set_last_run_time($currenttime); + + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + + // Test if remove user from cohort will affect plans. + cohort_remove_member($cohort->id, $user3->id); + cohort_remove_member($cohort->id, $user4->id); + + $currenttime = $currenttime + 1; + $task->execute(); + $task->set_last_run_time($currenttime); + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + + // The template is now hidden, and I've added a user with a missing plan. Nothing should happen. + $currenttime = $currenttime + 1; + $tpl->set_visible(false); + $tpl->update(); + $DB->execute($tplsql, array('currenttime' => $currenttime, 'templateid' => $tpl->get_id())); + $currenttime = $currenttime + 1; + cohort_add_member($cohort->id, $user5->id); + $DB->execute($cmsql, array('currenttime' => $currenttime, 'cohortid' => $cohort->id, 'userid' => $user5->id)); + $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get_id()))); + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + $currenttime = $currenttime + 1; + $task->execute(); + $task->set_last_run_time($currenttime); + $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get_id()))); + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + + // Now I set the template as visible again, the plan is created. + $currenttime = $currenttime + 1; + $tpl->set_visible(true); + $tpl->update(); + $DB->execute($tplsql, array('currenttime' => $currenttime, 'templateid' => $tpl->get_id())); + $currenttime = $currenttime + 1; + $task->execute(); + $task->set_last_run_time($currenttime); + $this->assertTrue(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get_id()))); + $this->assertEquals(5, plan::count_records(array('templateid' => $tpl->get_id()))); + + // Let's unlink the plan and run the task again, it should not be recreated. + $currenttime = $currenttime + 1; + $plan = plan::get_record(array('userid' => $user5->id, 'templateid' => $tpl->get_id())); + \core_competency\api::unlink_plan_from_template($plan); + $DB->execute($plansql, array('currenttime' => $currenttime, 'planid' => $plan->get_id())); + $this->assertTrue(plan::record_exists_select('userid = ?', array($user5->id))); + $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get_id()))); + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + $currenttime = $currenttime + 1; + $task->execute(); + $task->set_last_run_time($currenttime); + $this->assertTrue(plan::record_exists_select('userid = ?', array($user5->id))); + $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get_id()))); + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + + // Adding users to cohort that already exist in plans. + $currenttime = $currenttime + 1; + cohort_add_member($cohort->id, $user3->id); + cohort_add_member($cohort->id, $user4->id); + $DB->execute($cmsql, array('currenttime' => $currenttime, 'cohortid' => $cohort->id, 'userid' => $user3->id)); + $DB->execute($cmsql, array('currenttime' => $currenttime, 'cohortid' => $cohort->id, 'userid' => $user3->id)); + + $currenttime = $currenttime + 1; + $task->execute(); + $task->set_last_run_time($currenttime); + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + + // Test a user plan deleted will not be recreated. + $currenttime = $currenttime + 1; + $plan = plan::get_record(array('userid' => $user4->id, 'templateid' => $tpl->get_id())); + \core_competency\api::delete_plan($plan->get_id()); + $currenttime = $currenttime + 1; + $task->execute(); + $task->set_last_run_time($currenttime); + $this->assertEquals(3, plan::count_records(array('templateid' => $tpl->get_id()))); + } + + public function test_sync_plans_from_cohorts_with_templateduedate_task() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $user1 = $dg->create_user(); + $user2 = $dg->create_user(); + $user3 = $dg->create_user(); + $user4 = $dg->create_user(); + $user5 = $dg->create_user(); + + $cohort = $dg->create_cohort(); + $tpl = $lpg->create_template(array('duedate' => time() + 400)); + + // Add 2 users to the cohort. + cohort_add_member($cohort->id, $user1->id); + cohort_add_member($cohort->id, $user2->id); + + // Creating plans from template cohort. + $templatecohort = api::create_template_cohort($tpl->get_id(), $cohort->id); + $created = api::create_plans_from_template_cohort($tpl->get_id(), $cohort->id); + + $this->assertEquals(2, $created); + + $task = \core\task\manager::get_scheduled_task('\\core\\task\\sync_plans_from_template_cohorts_task'); + $this->assertInstanceOf('\core\task\sync_plans_from_template_cohorts_task', $task); + + // Add two more users to the cohort. + cohort_add_member($cohort->id, $user3->id); + cohort_add_member($cohort->id, $user4->id); + + $task->execute(); + + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + + // Test if remove user from cohort will affect plans. + cohort_remove_member($cohort->id, $user3->id); + cohort_remove_member($cohort->id, $user4->id); + + $task->execute(); + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + + // The template is now hidden, and I've added a user with a missing plan. Nothing should happen. + $tpl->set_visible(false); + $tpl->update(); + cohort_add_member($cohort->id, $user5->id); + $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get_id()))); + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + $task->execute(); + $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get_id()))); + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + + // Now I set the template as visible again, the plan is created. + $tpl->set_visible(true); + $tpl->update(); + $task->execute(); + $this->assertTrue(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get_id()))); + $this->assertEquals(5, plan::count_records(array('templateid' => $tpl->get_id()))); + + // Let's unlink the plan and run the task again, it should not be recreated. + $plan = plan::get_record(array('userid' => $user5->id, 'templateid' => $tpl->get_id())); + \core_competency\api::unlink_plan_from_template($plan); + $this->assertTrue(plan::record_exists_select('userid = ?', array($user5->id))); + $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get_id()))); + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + $task->execute(); + $this->assertTrue(plan::record_exists_select('userid = ?', array($user5->id))); + $this->assertFalse(plan::record_exists_select('userid = ? AND templateid = ?', array($user5->id, $tpl->get_id()))); + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + + // Adding users to cohort that already exist in plans. + cohort_add_member($cohort->id, $user3->id); + cohort_add_member($cohort->id, $user4->id); + + $task->execute(); + $this->assertEquals(4, plan::count_records(array('templateid' => $tpl->get_id()))); + } + + public function test_sync_plans_from_cohorts_with_passed_duedate() { + global $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $user1 = $dg->create_user(); + $user2 = $dg->create_user(); + $cohort = $dg->create_cohort(); + $tpl = $lpg->create_template(array('duedate' => time() + 1000)); + $templatecohort = api::create_template_cohort($tpl->get_id(), $cohort->id); + $task = \core\task\manager::get_scheduled_task('\\core\\task\\sync_plans_from_template_cohorts_task'); + + // Add 1 user to the cohort. + cohort_add_member($cohort->id, $user1->id); + + // Creating plans from template cohort. + $task->execute(); + $this->assertEquals(1, \core_competency\plan::count_records()); + + // Now add another user, but this time the template will be expired. + cohort_add_member($cohort->id, $user2->id); + $record = $tpl->to_record(); + $record->duedate = time() - 10000; + $DB->update_record(\core_competency\template::TABLE, $record); + $tpl->read(); + $task->execute(); + $this->assertEquals(1, \core_competency\plan::count_records()); // Still only one plan. + + // Pretend it wasn't expired. + $tpl->set_duedate(time() + 100); + $tpl->update(); + $task->execute(); + $this->assertEquals(2, \core_competency\plan::count_records()); // Now there is two. + } + + public function test_complete_plans_task() { + global $DB; + $this->resetAfterTest(true); + $this->setAdminUser(); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $user = $dg->create_user(); + + $up1 = $lpg->create_plan(array('userid' => $user->id, + 'status' => \core_competency\plan::STATUS_DRAFT)); + $up2 = $lpg->create_plan(array('userid' => $user->id, + 'status' => \core_competency\plan::STATUS_ACTIVE)); + // Set duedate in the past. + $date = new \DateTime('yesterday'); + $record1 = $up1->to_record(); + $record2 = $up2->to_record(); + + $record1->duedate = $date->getTimestamp(); + $record2->duedate = $date->getTimestamp(); + $DB->update_record(plan::TABLE, $record1); + $DB->update_record(plan::TABLE, $record2); + + $task = \core\task\manager::get_scheduled_task('\\core\\task\\complete_plans_task'); + $this->assertInstanceOf('\\core\\task\\complete_plans_task', $task); + + // Test that draft plan can not be completed on running task. + $task->execute(); + + $plandraft = api::read_plan($up1->get_id()); + $this->assertEquals(\core_competency\plan::STATUS_DRAFT, $plandraft->get_status()); + + // Test that active plan can be completed on running task. + $task->execute(); + + $planactive = api::read_plan($up2->get_id()); + $this->assertEquals(\core_competency\plan::STATUS_COMPLETE, $planactive->get_status()); + } +} diff --git a/competency/tests/template_test.php b/competency/tests/template_test.php new file mode 100644 index 00000000000..217a68d45a6 --- /dev/null +++ b/competency/tests/template_test.php @@ -0,0 +1,115 @@ +. + +/** + * Template persistent class tests. + * + * @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(); +global $CFG; + +use core_competency\template; + +/** + * Template persistent testcase. + * + * @package core_competency + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_template_testcase extends advanced_testcase { + + public function test_validate_duedate() { + global $DB; + + $this->resetAfterTest(); + $tpl = $this->getDataGenerator()->get_plugin_generator('core_competency')->create_template(); + + // No due date -> pass. + $tpl->set_duedate(0); + $this->assertTrue($tpl->is_valid()); + + // Setting new due date in the past -> fail. + $tpl->set_duedate(1); + $errors = $tpl->get_errors(); + $this->assertCount(1, $errors); + $this->assertArrayHasKey('duedate', $errors); + + // Setting new due date in very close past -> pass. + $tpl->set_duedate(time() - 10); + $this->assertTrue($tpl->is_valid()); + + // Setting new due date in future -> pass. + $tpl->set_duedate(time() + 600); + $this->assertTrue($tpl->is_valid()); + + // Save due date in the future. + $tpl->update(); + + // Going from future date to past -> fail. + $tpl->set_duedate(1); + $errors = $tpl->get_errors(); + $this->assertCount(1, $errors); + $this->assertArrayHasKey('duedate', $errors); + + // Going from future date to none -> pass. + $tpl->set_duedate(0); + $this->assertTrue($tpl->is_valid()); + + // Going from future date to other future -> pass. + $tpl->set_duedate(time() + 6000); + $this->assertTrue($tpl->is_valid()); + + // Going from future date to close past -> pass. + $tpl->set_duedate(time() - 10); + $this->assertTrue($tpl->is_valid()); + + // Mocking past due date. + $record = $tpl->to_record(); + $record->duedate = 1; + $DB->update_record(template::TABLE, $record); + $tpl->read(); + $this->assertEquals(1, $tpl->get_duedate()); + + // Not changing the past due date -> pass. + // Note: changing visibility to force validation. + $tpl->set_visible(0); + $tpl->set_visible(1); + $this->assertTrue($tpl->is_valid()); + + // Changing past due date to other past -> fail. + $tpl->set_duedate(10); + $errors = $tpl->get_errors(); + $this->assertCount(1, $errors); + $this->assertArrayHasKey('duedate', $errors); + + // Changing past due date close past -> pass. + $tpl->set_duedate(time() + 10); + $this->assertTrue($tpl->is_valid()); + + // Changing past due date to future -> pass. + $tpl->set_duedate(time() + 1000); + $this->assertTrue($tpl->is_valid()); + + // Changing past due date to none -> pass. + $tpl->set_duedate(0); + $this->assertTrue($tpl->is_valid()); + } +} diff --git a/competency/tests/user_evidence_competency_test.php b/competency/tests/user_evidence_competency_test.php new file mode 100644 index 00000000000..fe892ed9808 --- /dev/null +++ b/competency/tests/user_evidence_competency_test.php @@ -0,0 +1,92 @@ +. + +/** + * User evidence competency persistent class tests. + * + * @package core_competency + * @copyright 2016 Serge Gauthier - + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +use core_competency\user_evidence_competency; + +/** + * User evidence competency persistent testcase. + * + * @package core_competency + * @copyright 2016 Serge Gauthier - + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_competency_user_evidence_competency_testcase extends advanced_testcase { + + public function test_get_user_competencies_by_userevidenceid() { + global $CFG, $DB; + + $this->resetAfterTest(true); + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $u1 = $dg->create_user(); + + // Create framework with competencies. + $fw = $lpg->create_framework(); + $c1 = $lpg->create_competency(array('competencyframeworkid' => $fw->get_id())); + $c2 = $lpg->create_competency(array('competencyframeworkid' => $fw->get_id())); + $c3 = $lpg->create_competency(array('competencyframeworkid' => $fw->get_id())); + $c4 = $lpg->create_competency(array('competencyframeworkid' => $fw->get_id())); + + // Create a plan with competencies. + $p1 = $lpg->create_plan(array('userid' => $u1->id)); + $lpg->create_plan_competency(array('planid' => $p1->get_id(), 'competencyid' => $c1->get_id())); + $lpg->create_plan_competency(array('planid' => $p1->get_id(), 'competencyid' => $c2->get_id())); + $lpg->create_plan_competency(array('planid' => $p1->get_id(), 'competencyid' => $c3->get_id())); + $lpg->create_plan_competency(array('planid' => $p1->get_id(), 'competencyid' => $c4->get_id())); + + // Create a prior learning evidence and link competencies. + $ue1 = $lpg->create_user_evidence(array('userid' => $u1->id)); + $uec11 = $lpg->create_user_evidence_competency(array('userevidenceid' => $ue1->get_id(), 'competencyid' => $c1->get_id())); + $uec12 = $lpg->create_user_evidence_competency(array('userevidenceid' => $ue1->get_id(), 'competencyid' => $c2->get_id())); + $uec13 = $lpg->create_user_evidence_competency(array('userevidenceid' => $ue1->get_id(), 'competencyid' => $c3->get_id())); + $uc11 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c1->get_id())); + $uc12 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c2->get_id())); + $uc13 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c3->get_id())); + + // Create an other prior learning evidence and link competencies. + $ue2 = $lpg->create_user_evidence(array('userid' => $u1->id)); + $uec22 = $lpg->create_user_evidence_competency(array('userevidenceid' => $ue2->get_id(), 'competencyid' => $c4->get_id())); + $uc22 = $lpg->create_user_competency(array('userid' => $u1->id, 'competencyid' => $c4->get_id())); + + // Check the user competencies associated to the first prior learning evidence. + $ucs = user_evidence_competency::get_user_competencies_by_userevidenceid($ue1->get_id()); + $this->assertCount(3, $ucs); + $uc = array_shift($ucs); + $this->assertEquals($uc->get_id(), $uc11->get_id()); + $uc = array_shift($ucs); + $this->assertEquals($uc->get_id(), $uc12->get_id()); + $uc = array_shift($ucs); + $this->assertEquals($uc->get_id(), $uc13->get_id()); + + // Check the user competencies associated to the second prior learning evidence. + $ucs = user_evidence_competency::get_user_competencies_by_userevidenceid($ue2->get_id()); + $this->assertCount(1, $ucs); + $uc = array_shift($ucs); + $this->assertEquals($uc->get_id(), $uc22->get_id()); + } +} diff --git a/course/lib.php b/course/lib.php index 9307e3c60ee..5cf46bb4bab 100644 --- a/course/lib.php +++ b/course/lib.php @@ -1770,6 +1770,9 @@ function course_delete_module($cmid) { core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id); core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id); + // Notify the competency subsystem. + \core_competency\api::hook_course_module_deleted($cm); + // Delete the context. context_helper::delete_instance(CONTEXT_MODULE, $cm->id); diff --git a/course/modlib.php b/course/modlib.php index 56814ad910a..ab0c7a6c091 100644 --- a/course/modlib.php +++ b/course/modlib.php @@ -174,6 +174,23 @@ function add_moduleinfo($moduleinfo, $course, $mform = null) { return $moduleinfo; } +/** + * Hook for plugins to take action when a module is created or updated. + * + * @param stdClass $moduleinfo the module info + * @param stdClass $course the course of the module + * + * @return stdClass moduleinfo updated by plugins. + */ +function plugin_extend_coursemodule_edit_post_actions($moduleinfo, $course) { + $callbacks = get_plugins_with_function('coursemodule_edit_post_actions', 'lib.php'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $moduleinfo = $pluginfunction($moduleinfo, $course); + } + } + return $moduleinfo; +} /** * Common create/update module module actions that need to be processed as soon as a module is created/updaded. @@ -328,6 +345,9 @@ function edit_module_post_actions($moduleinfo, $course) { require_once($CFG->libdir.'/plagiarismlib.php'); plagiarism_save_form_elements($moduleinfo); + // Allow plugins to extend the course module form. + $moduleinfo = plugin_extend_coursemodule_edit_post_actions($moduleinfo, $course); + return $moduleinfo; } diff --git a/course/moodleform_mod.php b/course/moodleform_mod.php index 3ff050d6147..5e101e6b7f4 100644 --- a/course/moodleform_mod.php +++ b/course/moodleform_mod.php @@ -31,6 +31,14 @@ abstract class moodleform_mod extends moodleform { * @var mixed */ protected $_cm; + + /** + * Current course. + * + * @var mixed + */ + protected $_course; + /** * List of modform features */ @@ -65,6 +73,7 @@ abstract class moodleform_mod extends moodleform { $this->_instance = $current->instance; $this->_section = $section; $this->_cm = $cm; + $this->_course = $course; if ($this->_cm) { $this->context = context_module::instance($this->_cm->id); } else { @@ -96,6 +105,63 @@ abstract class moodleform_mod extends moodleform { self::__construct($current, $section, $cm, $course); } + /** + * Get the current data for the form. + * @return stdClass|null + */ + public function get_current() { + return $this->current; + } + + /** + * Get the DB record for the current instance. + * @return stdClass|null + */ + public function get_instance() { + return $this->_instance; + } + + /** + * Get the course section number (relative). + * @return int + */ + public function get_section() { + return $this->_section; + } + + /** + * Get the course id. + * @return int + */ + public function get_course() { + return $this->_course; + } + + /** + * Get the course module object. + * @return stdClass|null + */ + public function get_coursemodule() { + return $this->_cm; + } + + /** + * Return the course context for new modules, or the module context for existing modules. + * @return context + */ + public function get_context() { + return $this->context; + } + + /** + * Return the features this module supports. + * @return stdClass + */ + public function get_features() { + return $this->_features; + } + + protected function init_features() { global $CFG; @@ -359,6 +425,33 @@ abstract class moodleform_mod extends moodleform { \core_availability\frontend::report_validation_errors($data, $errors); } + $pluginerrors = $this->plugin_extend_coursemodule_validation($data); + if (!empty($pluginerrors)) { + $errors = array_merge($errors, $pluginerrors); + } + + return $errors; + } + + /** + * Extend the validation function from any other plugin. + * + * @param stdClass $data The form data. + * @return array $errors The list of errors keyed by element name. + */ + protected function plugin_extend_coursemodule_validation($data) { + $errors = array(); + + $callbacks = get_plugins_with_function('coursemodule_validation', 'lib.php'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + // We have exposed all the important properties with public getters - the errors array should be pass by reference. + $pluginerrors = $pluginfunction($this, $data); + if (!empty($pluginerrors)) { + $errors = array_merge($errors, $pluginerrors); + } + } + } return $errors; } @@ -608,6 +701,22 @@ abstract class moodleform_mod extends moodleform { } $this->standard_hidden_coursemodule_elements(); + + $this->plugin_extend_coursemodule_standard_elements(); + } + + /** + * Plugins can extend the coursemodule settings form. + */ + protected function plugin_extend_coursemodule_standard_elements() { + $callbacks = get_plugins_with_function('coursemodule_standard_elements', 'lib.php'); + foreach ($callbacks as $type => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + // We have exposed all the important properties with public getters - and the callback can manipulate the mform + // directly. + $pluginfunction($this, $this->_form); + } + } } /** diff --git a/course/reset_form.php b/course/reset_form.php index 1662307f1d2..4fe8ddb30b4 100644 --- a/course/reset_form.php +++ b/course/reset_form.php @@ -48,7 +48,7 @@ class course_reset_form extends moodleform { $mform->addElement('checkbox', 'reset_completion', get_string('deletecompletiondata', 'completion')); $mform->addElement('checkbox', 'delete_blog_associations', get_string('deleteblogassociations', 'blog')); $mform->addHelpButton('delete_blog_associations', 'deleteblogassociations', 'blog'); - + $mform->addElement('checkbox', 'reset_competency_ratings', get_string('deletecompetencyratings', 'core_competency')); $mform->addElement('header', 'rolesheader', get_string('roles')); diff --git a/lang/en/competency.php b/lang/en/competency.php new file mode 100644 index 00000000000..56aaecd5d9f --- /dev/null +++ b/lang/en/competency.php @@ -0,0 +1,152 @@ +. + +/** + * Competency language 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(); + +$string['allchildrenarecomplete'] = 'All children are complete'; +$string['competencies'] = 'Competencies'; +$string['competenciesarenotenabled'] = 'The competencies module is not enabled.'; +$string['competenciessettings'] = 'Competencies settings'; +$string['completeplanstask'] = 'Complete plans which are due'; +$string['coursecompetencyoutcome_complete'] = 'Complete the competency'; +$string['coursecompetencyoutcome_evidence'] = 'Attach an evidence'; +$string['coursecompetencyoutcome_none'] = 'Do nothing'; +$string['coursecompetencyoutcome_recommend'] = 'Send for review'; +$string['coursemodulecompetencyoutcome_complete'] = 'Complete the competency'; +$string['coursemodulecompetencyoutcome_evidence'] = 'Attach an evidence'; +$string['coursemodulecompetencyoutcome_none'] = 'Do nothing'; +$string['coursemodulecompetencyoutcome_recommend'] = 'Send for review'; +$string['deletecompetencyratings'] = 'Delete competency ratings'; +$string['duplicateditemname'] = '{$a} (copy)'; +$string['enablecompetencies'] = 'Enable competencies'; +$string['enablecompetencies_desc'] = 'When unticked the competencies module will be disabled.'; +$string['errorcannotchangeapastduedate'] = 'The due date has passed, it cannot be changed.'; +$string['errorcannotsetduedateinthepast'] = 'The due date cannot be set in the past.'; +$string['errorcannotsetduedatetoosoon'] = 'The due date is too soon.'; +$string['errorcompetencyrule'] = 'Course competency rule \'{$a}\' unknown'; +$string['errorcoursecompetencyrule'] = 'Course competency rule \'{$a}\' unknown'; +$string['errorinvalidcourse'] = 'Invalid course.'; +$string['errornocompetency'] = '{$a} competency can not be found'; +$string['errorplanstatus'] = 'Learning plans \'{$a}\' status unknown'; +$string['errorscalealreadyused'] = 'The scale cannot be changed, it is already being used.'; +$string['errorscaleconfiguration'] = 'You must configure the scale by selecting default and proficient values.'; +$string['errorusercomptencystatus'] = 'User competency status \'{$a}\' unknown'; +$string['eventcompetencycreated'] = 'Competency created.'; +$string['eventcompetencydeleted'] = 'Competency deleted.'; +$string['eventcompetencyframeworkcreated'] = 'Competency framework created.'; +$string['eventcompetencyframeworkdeleted'] = 'Competency framework deleted.'; +$string['eventcompetencyframeworkupdated'] = 'Competency framework updated.'; +$string['eventcompetencyframeworkviewed'] = 'Competency framework viewed.'; +$string['eventcompetencyupdated'] = 'Competency updated.'; +$string['eventcompetencyviewed'] = 'Competency viewed.'; +$string['eventevidencecreated'] = 'Evidence created.'; +$string['eventplanapproved'] = 'Plan approved.'; +$string['eventplancompleted'] = 'Plan completed.'; +$string['eventplancreated'] = 'Plan created.'; +$string['eventplandeleted'] = 'Plan deleted.'; +$string['eventplanreopened'] = 'Plan reopened.'; +$string['eventplanreviewrequestcancelled'] = 'Plan review request cancelled.'; +$string['eventplanreviewrequested'] = 'Plan review requested.'; +$string['eventplanreviewstarted'] = 'Plan review started.'; +$string['eventplanreviewstopped'] = 'Plan review stopped.'; +$string['eventplanunapproved'] = 'Plan unapproved.'; +$string['eventplanunlinked'] = 'Plan unlinked.'; +$string['eventplanupdated'] = 'Plan updated.'; +$string['eventplanviewed'] = 'Plan viewed.'; +$string['eventtemplatecreated'] = 'Template created.'; +$string['eventtemplatedeleted'] = 'Template deleted.'; +$string['eventtemplateupdated'] = 'Template updated.'; +$string['eventtemplateviewed'] = 'Template viewed.'; +$string['eventusercompetencygraderated'] = 'User competency grade rated.'; +$string['eventusercompetencygraderatedincourse'] = 'User competency grade rated in course.'; +$string['eventusercompetencygraderatedinplan'] = 'User competency grade rated in plan.'; +$string['eventusercompetencyplanviewed'] = 'User competency plan viewed.'; +$string['eventusercompetencyreviewrequestcancelled'] = 'User competency review request cancelled.'; +$string['eventusercompetencyreviewrequested'] = 'User competency review requested.'; +$string['eventusercompetencyreviewstarted'] = 'User competency review started.'; +$string['eventusercompetencyreviewstopped'] = 'User competency review stopped.'; +$string['eventusercompetencyviewed'] = 'User competency viewed.'; +$string['eventusercompetencyviewedincourse'] = 'User competency viewed in a course.'; +$string['eventusercompetencyviewedinplan'] = 'User competency viewed in a plan.'; +$string['eventuserevidencecreated'] = 'Evidence of prior learning created.'; +$string['eventuserevidencedeleted'] = 'Evidence of prior learning deleted.'; +$string['eventuserevidenceupdated'] = 'Evidence of prior learning updated.'; +$string['evidence_competencyrule'] = 'The rule of the competency was met.'; +$string['evidence_coursecompleted'] = 'The course \'{$a}\' was completed.'; +$string['evidence_coursemodulecompleted'] = 'The activity \'{$a}\' was completed.'; +$string['evidence_evidenceofpriorlearninglinked'] = 'The evidence of prior learning \'{$a}\' was linked.'; +$string['evidence_evidenceofpriorlearningunlinked'] = 'The evidence of prior learning \'{$a}\' was unlinked.'; +$string['evidence_manualoverride'] = 'The competency rating was manually set.'; +$string['evidence_manualoverrideincourse'] = 'The competency grade was manually set in the course \'{$a}\'.'; +$string['evidence_manualoverrideinplan'] = 'The competency grade was manually set in the plan \'{$a}\'.'; +$string['invalidevidencedesc'] = 'Invalid evidence description'; +$string['invalidgrade'] = 'Invalid grade'; +$string['invalidpersistent'] = 'Invalid persistent'; +$string['invalidplan'] = 'Invalid learning plan'; +$string['invalidtaxonomy'] = 'Invalid taxonomy: {$a}'; +$string['invalidurl'] = 'The URL is not valid.'; +$string['planstatusactive'] = 'Active'; +$string['planstatuscomplete'] = 'Complete'; +$string['planstatusdraft'] = 'Draft'; +$string['planstatusinreview'] = 'In review'; +$string['planstatuswaitingforreview'] = 'Waiting for review'; +$string['pointsrequiredaremet'] = 'Points required are met'; +$string['pushcourseratingstouserplans'] = 'Push course ratings to individual learning plans'; +$string['pushcourseratingstouserplans_desc'] = 'Default value for the course setting to update individual learning plans when course competencies are rated.'; +$string['syncplanscohorts'] = 'Sync plans from template cohorts'; +$string['taxonomy_behaviour'] = 'Behaviour'; +$string['taxonomy_competency'] = 'Competency'; +$string['taxonomy_concept'] = 'Concept'; +$string['taxonomy_domain'] = 'Domain'; +$string['taxonomy_indicator'] = 'Indicator'; +$string['taxonomy_level'] = 'Level'; +$string['taxonomy_outcome'] = 'Outcome'; +$string['taxonomy_practice'] = 'Practice'; +$string['taxonomy_proficiency'] = 'Proficiency'; +$string['taxonomy_skill'] = 'Skill'; +$string['taxonomy_value'] = 'Value'; +$string['usercommentedonacompetency'] = '{$a->fullname} commented on the competency "{$a->competency}": + +{$a->comment} + +See: {$a->url}'; +$string['usercommentedonacompetencyhtml'] = '

    {$a->fullname} commented on the competency "{$a->competency}":

    +
    {$a->comment}
    +

    See: {$a->urlname}.

    '; +$string['usercommentedonacompetencysmall'] = '{$a->fullname} commented on the competency "{$a->competency}".'; +$string['usercommentedonacompetencysubject'] = '{$a} commented on a competency.'; +$string['usercommentedonaplan'] = '{$a->fullname} commented on the plan "{$a->plan}": + +{$a->comment} + +See: {$a->url}'; +$string['usercommentedonaplanhtml'] = '

    {$a->fullname} commented on the plan "{$a->plan}":

    +
    {$a->comment}
    +

    See: {$a->urlname}.

    '; +$string['usercommentedonaplansmall'] = '{$a->fullname} commented on the plan "{$a->plan}".'; +$string['usercommentedonaplansubject'] = '{$a} commented on a plan.'; +$string['usercompetencystatus_idle'] = 'Idle'; +$string['usercompetencystatus_inreview'] = 'In review'; +$string['usercompetencystatus_waitingforreview'] = 'Waiting for review'; +$string['userplans'] = 'User plans'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index a5407741470..d9cac66c9c8 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1122,6 +1122,8 @@ $string['messageprovider:availableupdate'] = 'Available update notifications'; $string['messageprovider:backup'] = 'Backup notifications'; $string['messageprovider:badgecreatornotice'] = 'Badge creator notifications'; $string['messageprovider:badgerecipientnotice'] = 'Badge recipient notifications'; +$string['messageprovider:competencyplancomment'] = 'Comment posted on a learning plan'; +$string['messageprovider:competencyusercompcomment'] = 'Comment posted on a competency'; $string['messageprovider:courserequestapproved'] = 'Course creation request approval notification'; $string['messageprovider:courserequested'] = 'Course creation request notification'; $string['messageprovider:courserequestrejected'] = 'Course creation request rejection notification'; diff --git a/lang/en/role.php b/lang/en/role.php index bb2868dbd87..d42a3e0c3a2 100644 --- a/lang/en/role.php +++ b/lang/en/role.php @@ -106,6 +106,38 @@ $string['comment:post'] = 'Post comments'; $string['comment:view'] = 'View comments'; $string['community:add'] = 'Use the community block to search hubs and find courses'; $string['community:download'] = 'Download a course from the community block'; +$string['competency:competencymanage'] = 'Manage competency frameworks'; +$string['competency:competencygrade'] = 'Set competency grade'; +$string['competency:competencyview'] = 'View competency frameworks'; +$string['competency:coursecompetencygradable'] = 'Receive competency marks'; +$string['competency:coursecompetencyconfigure'] = 'Configure course competency settings'; +$string['competency:coursecompetencymanage'] = 'Manage course competencies'; +$string['competency:coursecompetencyview'] = 'View course competencies'; +$string['competency:evidencedelete'] = 'Delete evidence'; +$string['competency:plancomment'] = 'Comment on a learning plan'; +$string['competency:plancommentown'] = 'Comment on own learning plan'; +$string['competency:planmanage'] = 'Manage learning plans'; +$string['competency:planmanagedraft'] = 'Manage draft learning plans'; +$string['competency:planmanageown'] = 'Manage own learning plans'; +$string['competency:planmanageowndraft'] = 'Manage own draft learning plans'; +$string['competency:planrequestreview'] = 'Request the review of a learning plan'; +$string['competency:planrequestreviewown'] = 'Request the review of own learning plan'; +$string['competency:planreview'] = 'Review a learning plan'; +$string['competency:planview'] = 'View all learning plans'; +$string['competency:planviewdraft'] = 'View draft learning plans'; +$string['competency:planviewown'] = 'View own learning plans'; +$string['competency:planviewowndraft'] = 'View own draft learning plans'; +$string['competency:templatemanage'] = 'Manage templates'; +$string['competency:templateview'] = 'View template'; +$string['competency:usercompetencycomment'] = 'Comment on a user competency'; +$string['competency:usercompetencycommentown'] = 'Comment on own user competency'; +$string['competency:usercompetencyrequestreview'] = 'Request the review of a user competency'; +$string['competency:usercompetencyrequestreviewown'] = 'Request the review of own user competency'; +$string['competency:usercompetencyreview'] = 'Review a user competency'; +$string['competency:usercompetencyview'] = 'View a user competency'; +$string['competency:userevidencemanage'] = 'Manage evidence of prior learning'; +$string['competency:userevidencemanageown'] = 'Manage own evidence of prior learning'; +$string['competency:userevidenceview'] = 'View evidence of prior learning of a user'; $string['confirmaddadmin'] = 'Do you really want to add user {$a} as new site administrator?'; $string['confirmdeladmin'] = 'Do you really want to remove user {$a} from the list of site administrators?'; $string['confirmroleprevent'] = 'Do you really want to remove "{$a->role}" from the list of allowed roles for capability "{$a->cap}" in context "{$a->context}"?'; diff --git a/lib/blocklib.php b/lib/blocklib.php index a111b823b3e..90cee6f62ed 100644 --- a/lib/blocklib.php +++ b/lib/blocklib.php @@ -2269,6 +2269,6 @@ function blocks_add_default_system_blocks() { } $newblocks = array('private_files', 'online_users', 'badges', 'calendar_month', 'calendar_upcoming'); - $newcontent = array('course_overview'); + $newcontent = array('lp', 'course_overview'); $page->blocks->add_blocks(array(BLOCK_POS_RIGHT => $newblocks, 'content' => $newcontent), 'my-index', $subpagepattern); } diff --git a/lib/classes/component.php b/lib/classes/component.php index 8bd659ddffd..f740f22113e 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -353,6 +353,7 @@ $cache = '.var_export($cache, true).'; 'calendar' => $CFG->dirroot.'/calendar', 'cohort' => $CFG->dirroot.'/cohort', 'comment' => $CFG->dirroot.'/comment', + 'competency' => $CFG->dirroot.'/competency', 'completion' => $CFG->dirroot.'/completion', 'countries' => null, 'course' => $CFG->dirroot.'/course', diff --git a/lib/classes/event/competency_comment_created.php b/lib/classes/event/competency_comment_created.php new file mode 100644 index 00000000000..d496b407bef --- /dev/null +++ b/lib/classes/event/competency_comment_created.php @@ -0,0 +1,57 @@ +. + +/** + * Comment created event for core_competency areas. + * + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Comment created event class for core_competency areas. + * + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_comment_created extends \core\event\comment_created { + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' added the comment with id '$this->objectid' to the '$this->component'"; + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + return null; + } +} diff --git a/lib/classes/event/competency_comment_deleted.php b/lib/classes/event/competency_comment_deleted.php new file mode 100644 index 00000000000..4af2f3ecc00 --- /dev/null +++ b/lib/classes/event/competency_comment_deleted.php @@ -0,0 +1,48 @@ +. + +/** + * Comment deleted event for core_competency areas. + * + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Comment deleted event class for core_competency areas. + * + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_comment_deleted extends \core\event\comment_deleted { + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' deleted the comment with id '$this->objectid' from '$this->component'"; + } +} diff --git a/lib/classes/event/competency_created.php b/lib/classes/event/competency_created.php new file mode 100644 index 00000000000..0ada2d4225c --- /dev/null +++ b/lib/classes/event/competency_created.php @@ -0,0 +1,108 @@ +. + +/** + * Competency created event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\competency; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Competency created event class. + * + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_created extends base { + + /** + * Convenience method to instantiate the event. + * + * @param competency $competency The competency. + * @return self + */ + public static function create_from_competency(competency $competency) { + if (!$competency->get_id()) { + throw new \coding_exception('The competency ID must be set.'); + } + $event = static::create(array( + 'contextid' => $competency->get_context()->id, + 'objectid' => $competency->get_id() + )); + $event->add_record_snapshot(competency::TABLE, $competency->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' created the competency with id '$this->objectid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventcompetencycreated', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::competency($this->objectid, $this->contextid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = competency::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_deleted.php b/lib/classes/event/competency_deleted.php new file mode 100644 index 00000000000..032a8fb61de --- /dev/null +++ b/lib/classes/event/competency_deleted.php @@ -0,0 +1,116 @@ +. + +/** + * Competency deleted event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\competency; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Competency deleted event class. + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_deleted extends base { + + /** + * Convenience method to instantiate the event. + * + * @param competency $competency The competency. + * @return self + */ + public static function create_from_competency(competency $competency) { + if (!$competency->get_id()) { + throw new \coding_exception('The competency ID must be set.'); + } + $event = static::create(array( + 'contextid' => $competency->get_context()->id, + 'objectid' => $competency->get_id(), + )); + $event->add_record_snapshot(competency::TABLE, $competency->to_record()); + return $event; + } + + /** + * Instantiate events from competency ids. + * + * @param array $competencyids Array of competency ids. + * @param int $contextid The context id. + * @return self[] Array of events. + */ + public static function create_multiple_from_competencyids($competencyids, $contextid) { + $events = array(); + foreach ($competencyids as $id) { + $events[$id] = static::create(array( + 'contextid' => $contextid, + 'objectid' => $id + )); + } + return $events; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' deleted the competency with id '$this->objectid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventcompetencydeleted', 'core_competency'); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = competency::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_evidence_created.php b/lib/classes/event/competency_evidence_created.php new file mode 100644 index 00000000000..605754cb26b --- /dev/null +++ b/lib/classes/event/competency_evidence_created.php @@ -0,0 +1,171 @@ +. + +/** + * Evidence created event. + * + * @package core_competency + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +use core\event\base; +use core_competency\evidence; +use core_competency\user_competency; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Evidence created event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int usercompetencyid: The user_competency ID linked to the evidence. + * - int competencyid: The competency ID linked to the evidence from user_competency. + * - int action: The action constant. + * - bool recommend: The recommend flag. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_evidence_created extends base { + + /** + * Convenience method to instantiate the event. + * + * @param evidence $evidence The evidence. + * @param user_competency $usercompetency The user competency object linked to the evidence. + * @param bool $recommend The recommend flag. + * @return evidence_created + * @throws \coding_exception + */ + public static final function create_from_evidence(evidence $evidence, user_competency $usercompetency, $recommend) { + // Make sure we have a valid evidence. + if (!$evidence->get_id()) { + throw new \coding_exception('The evidence ID must be set.'); + } + + // Make sure we have a valid user competency. + if (!$usercompetency->get_id()) { + throw new \coding_exception('The user competency ID must be set.'); + } + + // Make sure that the a proper user competecy is linked to the evidence. + if ($evidence->get_usercompetencyid() != $usercompetency->get_id()) { + throw new \coding_exception('The user competency linked with this evidence is invalid.'); + } + + $event = static::create([ + 'contextid' => $evidence->get_contextid(), + 'objectid' => $evidence->get_id(), + 'userid' => $evidence->get_actionuserid(), + 'relateduserid' => $usercompetency->get_userid(), + 'other' => [ + 'usercompetencyid' => $usercompetency->get_id(), + 'competencyid' => $usercompetency->get_competencyid(), + 'action' => $evidence->get_action(), + 'recommend' => $recommend + ] + ]); + + // Add record snapshot for the evidence. + $event->add_record_snapshot(evidence::TABLE, $evidence->to_record()); + + // Add record snapshot for the user competency. + $event->add_record_snapshot(user_competency::TABLE, $usercompetency->to_record()); + + return $event; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventevidencecreated', 'core_competency'); + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' created an evidence with id '$this->objectid'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_competency($this->other['usercompetencyid']); + } + + /** + * Initialise the event data. + */ + protected function init() { + $this->data['objecttable'] = evidence::TABLE; + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + + /** + * Validate the data. + * + * @throws \coding_exception + */ + protected function validate_data() { + parent::validate_data(); + + if (!isset($this->relateduserid)) { + throw new \coding_exception('The \'relateduserid\' must be set.'); + } + + if (!isset($this->other['usercompetencyid'])) { + throw new \coding_exception('The \'usercompetencyid\' data in \'other\' must be set.'); + } + + if (!isset($this->other['competencyid'])) { + throw new \coding_exception('The \'competencyid\' data in \'other\' must be set.'); + } + + if (!isset($this->other['action'])) { + throw new \coding_exception('The \'action\' data in \'other\' must be set.'); + } + + if (!isset($this->other['recommend'])) { + throw new \coding_exception('The \'recommend\' data in \'other\' must be set.'); + } + } +} diff --git a/lib/classes/event/competency_framework_created.php b/lib/classes/event/competency_framework_created.php new file mode 100644 index 00000000000..66ea6a8a0cb --- /dev/null +++ b/lib/classes/event/competency_framework_created.php @@ -0,0 +1,108 @@ +. + +/** + * Competency framework created event. + * + * @package core_competency + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +use core\event\base; +use core_competency\competency_framework; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Competency framework created event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_framework_created extends base { + + /** + * Convenience method to instantiate the event. + * + * @param competency_framework $framework The framework. + * @return self + */ + public static final function create_from_framework(competency_framework $framework) { + if (!$framework->get_id()) { + throw new \coding_exception('The competency framework ID must be set.'); + } + $event = static::create(array( + 'contextid' => $framework->get_contextid(), + 'objectid' => $framework->get_id() + )); + $event->add_record_snapshot(competency_framework::TABLE, $framework->to_record()); + return $event; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventcompetencyframeworkcreated', 'core_competency'); + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' created the competency framework with id '$this->objectid'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::framework($this->objectid, $this->contextid); + } + + /** + * Initialise the event data. + */ + protected function init() { + $this->data['objecttable'] = competency_framework::TABLE; + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_framework_deleted.php b/lib/classes/event/competency_framework_deleted.php new file mode 100644 index 00000000000..473b1a93a4e --- /dev/null +++ b/lib/classes/event/competency_framework_deleted.php @@ -0,0 +1,102 @@ +. + +/** + * Competency framework deleted event. + * + * @package core_competency + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\competency_framework; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Competency framework deleted event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_framework_deleted extends base { + + /** + * Convenience method to instantiate the event. + * + * @param competency_framework $framework The framework. + * @return self + */ + public static final function create_from_framework(competency_framework $framework) { + if (!$framework->get_id()) { + throw new \coding_exception('The competency framework ID must be set.'); + } + $event = static::create(array( + 'contextid' => $framework->get_contextid(), + 'objectid' => $framework->get_id() + )); + $event->add_record_snapshot(competency_framework::TABLE, $framework->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' deleted the competency framework with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventcompetencyframeworkdeleted', 'core_competency'); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = competency_framework::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_framework_updated.php b/lib/classes/event/competency_framework_updated.php new file mode 100644 index 00000000000..8b81ac1b500 --- /dev/null +++ b/lib/classes/event/competency_framework_updated.php @@ -0,0 +1,111 @@ +. + +/** + * Competency framework updated event. + * + * @package core_competency + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\competency_framework; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Competency framework updated event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_framework_updated extends base { + + /** + * Convenience method to instantiate the event. + * + * @param competency_framework $framework The framework. + * @return self + */ + public static final function create_from_framework(competency_framework $framework) { + if (!$framework->get_id()) { + throw new \coding_exception('The competency framework ID must be set.'); + } + $event = static::create(array( + 'contextid' => $framework->get_contextid(), + 'objectid' => $framework->get_id() + )); + $event->add_record_snapshot(competency_framework::TABLE, $framework->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' updated the competency framework with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventcompetencyframeworkupdated', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::framework($this->objectid, $this->contextid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = competency_framework::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_framework_viewed.php b/lib/classes/event/competency_framework_viewed.php new file mode 100644 index 00000000000..3034de163ba --- /dev/null +++ b/lib/classes/event/competency_framework_viewed.php @@ -0,0 +1,107 @@ +. + +/** + * Competency framework viewed event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\competency_framework; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Competency framework viewed event class. + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_framework_viewed extends base { + + /** + * Convenience method to instantiate the event. + * + * @param competency_framework $framework The framework. + * @return self + */ + public static function create_from_framework(competency_framework $framework) { + if (!$framework->get_id()) { + throw new \coding_exception('The competency framework ID must be set.'); + } + $event = static::create(array( + 'contextid' => $framework->get_contextid(), + 'objectid' => $framework->get_id() + )); + $event->add_record_snapshot(competency_framework::TABLE, $framework->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' viewed the competency framework with id '$this->objectid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventcompetencyframeworkviewed', 'core_competency'); + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::framework($this->objectid, $this->contextid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = competency_framework::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_approved.php b/lib/classes/event/competency_plan_approved.php new file mode 100644 index 00000000000..1c7ab025d54 --- /dev/null +++ b/lib/classes/event/competency_plan_approved.php @@ -0,0 +1,113 @@ +. + +/** + * Plan approved event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan approved event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_approved extends base { + + /** + * Convenience method to instantiate the plan approved event. + * + * + * @param plan $plan The plan. + * @return self + */ + public static final function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id(), + 'relateduserid' => $plan->get_userid(), + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' approved the learning plan with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplanapproved', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::plan($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = plan::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_completed.php b/lib/classes/event/competency_plan_completed.php new file mode 100644 index 00000000000..4de68a2760e --- /dev/null +++ b/lib/classes/event/competency_plan_completed.php @@ -0,0 +1,113 @@ +. + +/** + * Plan completed event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan completed event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_completed extends base { + + /** + * Convenience method to instantiate the plan completed event. + * + * + * @param plan $plan The plan. + * @return self + */ + public static final function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id(), + 'relateduserid' => $plan->get_userid(), + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' completed the learning plan with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplancompleted', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::plan($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = plan::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_created.php b/lib/classes/event/competency_plan_created.php new file mode 100644 index 00000000000..762bd158028 --- /dev/null +++ b/lib/classes/event/competency_plan_created.php @@ -0,0 +1,108 @@ +. + +/** + * Plan created event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan created event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_created extends base { + + /** + * Convenience method to instantiate the event. + * + * @param plan $plan The plan. + * @return self + */ + public static final function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id() + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplancreated', 'core_competency'); + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' created the learning plan with id '$this->objectid'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::plan($this->objectid); + } + + /** + * Initialise the event data. + */ + protected function init() { + $this->data['objecttable'] = plan::TABLE; + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_deleted.php b/lib/classes/event/competency_plan_deleted.php new file mode 100644 index 00000000000..0814ec208ab --- /dev/null +++ b/lib/classes/event/competency_plan_deleted.php @@ -0,0 +1,102 @@ +. + +/** + * Plan deleted event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan deleted event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_deleted extends base { + + /** + * Convenience method to instantiate the event. + * + * @param plan $plan The plan. + * @return self + */ + public static final function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id() + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' deleted the learning plan with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplandeleted', 'core_competency'); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = plan::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_reopened.php b/lib/classes/event/competency_plan_reopened.php new file mode 100644 index 00000000000..016965ba9b5 --- /dev/null +++ b/lib/classes/event/competency_plan_reopened.php @@ -0,0 +1,113 @@ +. + +/** + * Plan reopened event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan reopened event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_reopened extends base { + + /** + * Convenience method to instantiate the plan reopened event. + * + * + * @param plan $plan The plan. + * @return self + */ + public static final function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id(), + 'relateduserid' => $plan->get_userid(), + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' reopened the learning plan with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplanreopened', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::plan($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = plan::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_review_request_cancelled.php b/lib/classes/event/competency_plan_review_request_cancelled.php new file mode 100644 index 00000000000..212c447b6a1 --- /dev/null +++ b/lib/classes/event/competency_plan_review_request_cancelled.php @@ -0,0 +1,113 @@ +. + +/** + * Plan review request cancelled event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan review request cancelled event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_review_request_cancelled extends base { + + /** + * Convenience method to instantiate the plan review request cancelled event. + * + * + * @param plan $plan The plan. + * @return self + */ + public static final function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id(), + 'relateduserid' => $plan->get_userid(), + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' cancelled a review request for the learning plan with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplanreviewrequestcancelled', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::plan($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = plan::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_review_requested.php b/lib/classes/event/competency_plan_review_requested.php new file mode 100644 index 00000000000..537d85dd23d --- /dev/null +++ b/lib/classes/event/competency_plan_review_requested.php @@ -0,0 +1,113 @@ +. + +/** + * Plan review requested event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan review requested event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_review_requested extends base { + + /** + * Convenience method to instantiate the plan review requested event. + * + * + * @param plan $plan The plan. + * @return self + */ + public static final function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id(), + 'relateduserid' => $plan->get_userid(), + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' sent a review request for the learning plan with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplanreviewrequested', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::plan($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = plan::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_review_started.php b/lib/classes/event/competency_plan_review_started.php new file mode 100644 index 00000000000..a0cee7a18c9 --- /dev/null +++ b/lib/classes/event/competency_plan_review_started.php @@ -0,0 +1,113 @@ +. + +/** + * Plan review started event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan review started event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_review_started extends base { + + /** + * Convenience method to instantiate the plan review started event. + * + * + * @param plan $plan The plan. + * @return self + */ + public static final function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id(), + 'relateduserid' => $plan->get_userid(), + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' started a review for the learning plan with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplanreviewstarted', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::plan($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = plan::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_review_stopped.php b/lib/classes/event/competency_plan_review_stopped.php new file mode 100644 index 00000000000..da3f8740da8 --- /dev/null +++ b/lib/classes/event/competency_plan_review_stopped.php @@ -0,0 +1,113 @@ +. + +/** + * Plan review stopped event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan review stopped event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_review_stopped extends base { + + /** + * Convenience method to instantiate the plan review stopped event. + * + * + * @param plan $plan The plan. + * @return self + */ + public static final function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id(), + 'relateduserid' => $plan->get_userid(), + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' stopped a review for the learning plan with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplanreviewstopped', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::plan($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = plan::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_unapproved.php b/lib/classes/event/competency_plan_unapproved.php new file mode 100644 index 00000000000..51be01d1ea6 --- /dev/null +++ b/lib/classes/event/competency_plan_unapproved.php @@ -0,0 +1,113 @@ +. + +/** + * Plan unapproved event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan unapproved event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_unapproved extends base { + + /** + * Convenience method to instantiate the plan unapproved event. + * + * + * @param plan $plan The plan. + * @return self + */ + public static final function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id(), + 'relateduserid' => $plan->get_userid(), + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' unapproved the learning plan with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplanunapproved', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::plan($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = plan::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_unlinked.php b/lib/classes/event/competency_plan_unlinked.php new file mode 100644 index 00000000000..58469cd5a85 --- /dev/null +++ b/lib/classes/event/competency_plan_unlinked.php @@ -0,0 +1,110 @@ +. + +/** + * Plan unlinked event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan unlinked event class. + * + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_unlinked extends base { + + /** + * Convenience method to instantiate the plan unlinked event. + * + * + * @param plan $plan The plan. + * @return self + */ + public static final function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id(), + 'relateduserid' => $plan->get_userid(), + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' unlinked the learning plan with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplanunlinked', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::plan($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = plan::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_updated.php b/lib/classes/event/competency_plan_updated.php new file mode 100644 index 00000000000..5844adb4587 --- /dev/null +++ b/lib/classes/event/competency_plan_updated.php @@ -0,0 +1,113 @@ +. + +/** + * Plan updated event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan updated event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_updated extends base { + + /** + * Convenience method to instantiate the event. + * + * Actions triggering this event will NOT trigger the underlying plan_updated for related plans. + * + * @param plan $plan The plan. + * @return self + */ + public static final function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id() + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' updated the learning plan with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplanupdated', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::plan($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = plan::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_plan_viewed.php b/lib/classes/event/competency_plan_viewed.php new file mode 100644 index 00000000000..35d607744e2 --- /dev/null +++ b/lib/classes/event/competency_plan_viewed.php @@ -0,0 +1,108 @@ +. + +/** + * Plan viewed event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +use core\event\base; +use core_competency\plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plan viewed event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_plan_viewed extends base { + + /** + * Convenience method to instantiate the event. + * + * @param plan $plan The plan. + * @return self + */ + public static function create_from_plan(plan $plan) { + if (!$plan->get_id()) { + throw new \coding_exception('The plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $plan->get_context()->id, + 'objectid' => $plan->get_id() + )); + $event->add_record_snapshot(plan::TABLE, $plan->to_record()); + return $event; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventplanviewed', 'core_competency'); + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' viewed the learning plan with id '$this->objectid'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::plan($this->objectid); + } + + /** + * Initialise the event data. + */ + protected function init() { + $this->data['objecttable'] = plan::TABLE; + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_template_created.php b/lib/classes/event/competency_template_created.php new file mode 100644 index 00000000000..471bcb64d57 --- /dev/null +++ b/lib/classes/event/competency_template_created.php @@ -0,0 +1,108 @@ +. + +/** + * Template created event. + * + * @package core_competency + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +use core\event\base; +use core_competency\template; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Template created event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_template_created extends base { + + /** + * Convenience method to instantiate the event. + * + * @param template $template The template. + * @return self + */ + public static final function create_from_template(template $template) { + if (!$template->get_id()) { + throw new \coding_exception('The template ID must be set.'); + } + $event = static::create(array( + 'contextid' => $template->get_contextid(), + 'objectid' => $template->get_id() + )); + $event->add_record_snapshot(template::TABLE, $template->to_record()); + return $event; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventtemplatecreated', 'core_competency'); + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' created the template with id '$this->objectid'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::template($this->objectid, $this->contextid); + } + + /** + * Initialise the event data. + */ + protected function init() { + $this->data['objecttable'] = template::TABLE; + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_template_deleted.php b/lib/classes/event/competency_template_deleted.php new file mode 100644 index 00000000000..614f176786d --- /dev/null +++ b/lib/classes/event/competency_template_deleted.php @@ -0,0 +1,102 @@ +. + +/** + * Template deleted event. + * + * @package core_competency + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\template; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Template deleted event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_template_deleted extends base { + + /** + * Convenience method to instantiate the event. + * + * @param template $template The template. + * @return self + */ + public static final function create_from_template(template $template) { + if (!$template->get_id()) { + throw new \coding_exception('The template ID must be set.'); + } + $event = static::create(array( + 'contextid' => $template->get_contextid(), + 'objectid' => $template->get_id() + )); + $event->add_record_snapshot(template::TABLE, $template->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' deleted the template with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventtemplatedeleted', 'core_competency'); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = template::TABLE;; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_template_updated.php b/lib/classes/event/competency_template_updated.php new file mode 100644 index 00000000000..209c0b06668 --- /dev/null +++ b/lib/classes/event/competency_template_updated.php @@ -0,0 +1,113 @@ +. + +/** + * Template updated event. + * + * @package core_competency + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\template; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Template updated event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_template_updated extends base { + + /** + * Convenience method to instantiate the event. + * + * Actions triggering this event will NOT trigger the underlying plan_updated for related plans. + * + * @param template $template The template. + * @return self + */ + public static final function create_from_template(template $template) { + if (!$template->get_id()) { + throw new \coding_exception('The template ID must be set.'); + } + $event = static::create(array( + 'contextid' => $template->get_contextid(), + 'objectid' => $template->get_id() + )); + $event->add_record_snapshot(template::TABLE, $template->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' updated the template with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventtemplateupdated', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::template($this->objectid, $this->contextid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = template::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_template_viewed.php b/lib/classes/event/competency_template_viewed.php new file mode 100644 index 00000000000..f444d1c5bef --- /dev/null +++ b/lib/classes/event/competency_template_viewed.php @@ -0,0 +1,108 @@ +. + +/** + * Template viewed event. + * + * @package core_competency + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +use core\event\base; +use core_competency\template; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Template viewed event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_template_viewed extends base { + + /** + * Convenience method to instantiate the event. + * + * @param template $template The template. + * @return self + */ + public static function create_from_template(template $template) { + if (!$template->get_id()) { + throw new \coding_exception('The template ID must be set.'); + } + $event = static::create(array( + 'contextid' => $template->get_contextid(), + 'objectid' => $template->get_id() + )); + $event->add_record_snapshot(template::TABLE, $template->to_record()); + return $event; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventtemplateviewed', 'core_competency'); + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' viewed the template with id '$this->objectid'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::template($this->objectid, $this->contextid); + } + + /** + * Initialise the event data. + */ + protected function init() { + $this->data['objecttable'] = template::TABLE; + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_updated.php b/lib/classes/event/competency_updated.php new file mode 100644 index 00000000000..d1478892409 --- /dev/null +++ b/lib/classes/event/competency_updated.php @@ -0,0 +1,108 @@ +. + +/** + * Competency updated event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\competency; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Competency updated event class. + * + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_updated extends base { + + /** + * Convenience method to instantiate the event. + * + * @param competency $competency The competency. + * @return self + */ + public static function create_from_competency(competency $competency) { + if (!$competency->get_id()) { + throw new \coding_exception('The competency ID must be set.'); + } + $event = static::create(array( + 'contextid' => $competency->get_context()->id, + 'objectid' => $competency->get_id() + )); + $event->add_record_snapshot(competency::TABLE, $competency->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' updated the competency with id '$this->objectid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventcompetencyupdated', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::competency($this->objectid, $this->contextid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = competency::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_user_competency_plan_viewed.php b/lib/classes/event/competency_user_competency_plan_viewed.php new file mode 100644 index 00000000000..94fa3419804 --- /dev/null +++ b/lib/classes/event/competency_user_competency_plan_viewed.php @@ -0,0 +1,139 @@ +. + +/** + * User competency plan viewed event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\user_competency_plan; + +defined('MOODLE_INTERNAL') || die(); + +/** + * User competency plan viewed event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int planid: id of plan for which competency is associated. + * - int competencyid: id of the competency. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_competency_plan_viewed extends base { + + /** + * Convenience method to instantiate the event. + * + * @param user_competency_plan $usercompetencyplan The user competency plan. + * @return self + */ + public static function create_from_user_competency_plan(user_competency_plan $usercompetencyplan) { + if (!$usercompetencyplan->get_id()) { + throw new \coding_exception('The user competency plan ID must be set.'); + } + $event = static::create(array( + 'contextid' => $usercompetencyplan->get_context()->id, + 'objectid' => $usercompetencyplan->get_id(), + 'relateduserid' => $usercompetencyplan->get_userid(), + 'other' => array( + 'planid' => $usercompetencyplan->get_planid(), + 'competencyid' => $usercompetencyplan->get_competencyid() + ) + )); + $event->add_record_snapshot(user_competency_plan::TABLE, $usercompetencyplan->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' viewed the user competency plan with id '$this->objectid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventusercompetencyplanviewed', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_competency_in_plan($this->relateduserid, $this->other['competencyid'], + $this->other['planid']); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = user_competency_plan::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + + /** + * Custom validation. + * + * Throw \coding_exception notice in case of any problems. + */ + protected function validate_data() { + if ($this->other === null) { + throw new \coding_exception('The \'competencyid\' and \'planid\' values must be set.'); + } + + if (!isset($this->other['competencyid'])) { + throw new \coding_exception('The \'competencyid\' value must be set.'); + } + + if (!isset($this->other['planid'])) { + throw new \coding_exception('The \'planid\' value must be set.'); + } + } + +} diff --git a/lib/classes/event/competency_user_competency_rated.php b/lib/classes/event/competency_user_competency_rated.php new file mode 100644 index 00000000000..bb2793d1ec5 --- /dev/null +++ b/lib/classes/event/competency_user_competency_rated.php @@ -0,0 +1,135 @@ +. + +/** + * User competency grade rated event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\user_competency; +defined('MOODLE_INTERNAL') || die(); + +/** + * User competency grade rated event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int grade: grade name of the user competency + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_competency_rated extends base { + + /** + * Convenience method to instantiate the event. + * + * @param user_competency $usercompetency The user competency. + * @return self + */ + public static function create_from_user_competency(user_competency $usercompetency) { + if (!$usercompetency->get_id()) { + throw new \coding_exception('The user competency ID must be set.'); + } + + $params = array( + 'contextid' => $usercompetency->get_context()->id, + 'objectid' => $usercompetency->get_id(), + 'relateduserid' => $usercompetency->get_userid(), + 'other' => array( + 'grade' => $usercompetency->get_grade() + ) + ); + + $event = static::create($params); + $event->add_record_snapshot(user_competency::TABLE, $usercompetency->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' rated the user competency with id '$this->objectid' " + . "with '" . $this->other['grade'] . "' grade"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventusercompetencygraderated', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_competency($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = user_competency::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + + /** + * Custom validation. + * + * Throw \coding_exception notice in case of any problems. + */ + protected function validate_data() { + if (!$this->relateduserid) { + throw new \coding_exception('The \'relateduserid\' value must be set.'); + } + + if (!isset($this->other) || !isset($this->other['grade'])) { + throw new \coding_exception('The \'grade\' value must be set.'); + } + } + +} diff --git a/lib/classes/event/competency_user_competency_rated_in_course.php b/lib/classes/event/competency_user_competency_rated_in_course.php new file mode 100644 index 00000000000..2051eaf529d --- /dev/null +++ b/lib/classes/event/competency_user_competency_rated_in_course.php @@ -0,0 +1,150 @@ +. + +/** + * User competency grade rated in course event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\user_competency_course; +use context_course; +defined('MOODLE_INTERNAL') || die(); + +/** + * User competency grade rated in course event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int competencyid: id of competency. + * - int grade: grade name of the user competency + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_competency_rated_in_course extends base { + + /** + * Convenience method to instantiate the event. + * + * @param user_competency_course $usercompetencycourse The user competency course. + * @return self + */ + public static function create_from_user_competency_course(user_competency_course $usercompetencycourse) { + if (!$usercompetencycourse->get_id()) { + throw new \coding_exception('The user competency course ID must be set.'); + } + + $params = array( + 'objectid' => $usercompetencycourse->get_id(), + 'relateduserid' => $usercompetencycourse->get_userid(), + 'other' => array( + 'competencyid' => $usercompetencycourse->get_competencyid(), + 'grade' => $usercompetencycourse->get_grade() + ) + ); + $coursecontext = context_course::instance($usercompetencycourse->get_courseid()); + $params['contextid'] = $coursecontext->id; + $params['courseid'] = $usercompetencycourse->get_courseid(); + + $event = static::create($params); + $event->add_record_snapshot(user_competency_course::TABLE, $usercompetencycourse->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' rated the user competency with id '$this->objectid' with " + . "'" . $this->other['grade'] . "' grade " + . "in course with id '$this->courseid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventusercompetencygraderatedincourse', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_competency_in_course($this->relateduserid, $this->other['competencyid'], + $this->courseid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = user_competency_course::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + + /** + * Custom validation. + * + * Throw \coding_exception notice in case of any problems. + */ + protected function validate_data() { + if (!isset($this->other) || !isset($this->other['competencyid'])) { + throw new \coding_exception('The \'competencyid\' value must be set.'); + } + + if (!$this->courseid) { + throw new \coding_exception('The \'courseid\' value must be set.'); + } + + if (!$this->relateduserid) { + throw new \coding_exception('The \'relateduserid\' value must be set.'); + } + + if (!isset($this->other['grade'])) { + throw new \coding_exception('The \'grade\' value must be set.'); + } + } + +} diff --git a/lib/classes/event/competency_user_competency_rated_in_plan.php b/lib/classes/event/competency_user_competency_rated_in_plan.php new file mode 100644 index 00000000000..41e0eed3079 --- /dev/null +++ b/lib/classes/event/competency_user_competency_rated_in_plan.php @@ -0,0 +1,150 @@ +. + +/** + * User competency grade rated in course event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\user_competency; +defined('MOODLE_INTERNAL') || die(); + +/** + * User competency grade rated in plan event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int competencyid: id of competency. + * - int grade: grade name of the user competency + * - int planid: the plan id + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_competency_rated_in_plan extends base { + + /** + * Convenience method to instantiate the event. + * + * @param user_competency $usercompetency The user competency. + * @param int $planid The plan ID + * @return self + */ + public static function create_from_user_competency(user_competency $usercompetency, $planid) { + if (!$usercompetency->get_id()) { + throw new \coding_exception('The user competency ID must be set.'); + } + + $params = array( + 'contextid' => $usercompetency->get_context()->id, + 'objectid' => $usercompetency->get_id(), + 'relateduserid' => $usercompetency->get_userid(), + 'other' => array( + 'competencyid' => $usercompetency->get_competencyid(), + 'grade' => $usercompetency->get_grade(), + 'planid' => $planid + ) + ); + + $event = static::create($params); + $event->add_record_snapshot(user_competency::TABLE, $usercompetency->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' rated the user competency with id '$this->objectid' with " + . "'" . $this->other['grade'] . "' grade " + . "in plan with id '" . $this->other['grade'] . "'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventusercompetencygraderatedinplan', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_competency_in_plan($this->relateduserid, $this->other['competencyid'], + $this->other['planid']); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = user_competency::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + + /** + * Custom validation. + * + * Throw \coding_exception notice in case of any problems. + */ + protected function validate_data() { + if (!isset($this->other) || !isset($this->other['competencyid'])) { + throw new \coding_exception('The \'competencyid\' value must be set.'); + } + + if (!isset($this->other['planid'])) { + throw new \coding_exception('The \'planid\' value must be set.'); + } + + if (!$this->relateduserid) { + throw new \coding_exception('The \'relateduserid\' value must be set.'); + } + + if (!isset($this->other['grade'])) { + throw new \coding_exception('The \'grade\' value must be set.'); + } + } + +} diff --git a/lib/classes/event/competency_user_competency_review_request_cancelled.php b/lib/classes/event/competency_user_competency_review_request_cancelled.php new file mode 100644 index 00000000000..a35f962e9d1 --- /dev/null +++ b/lib/classes/event/competency_user_competency_review_request_cancelled.php @@ -0,0 +1,121 @@ +. + +/** + * User competency review requested event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\user_competency; +defined('MOODLE_INTERNAL') || die(); + +/** + * User competency review requested event class. + * + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_competency_review_request_cancelled extends base { + + /** + * Convenience method to instantiate the event. + * + * @param user_competency $usercompetency The user competency. + * @return self + */ + public static function create_from_user_competency(user_competency $usercompetency) { + if (!$usercompetency->get_id()) { + throw new \coding_exception('The user competency ID must be set.'); + } + $params = array( + 'contextid' => $usercompetency->get_context()->id, + 'objectid' => $usercompetency->get_id(), + 'relateduserid' => $usercompetency->get_userid() + ); + + $event = static::create($params); + $event->add_record_snapshot(user_competency::TABLE, $usercompetency->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' cancelled a review request for the user competency with id '$this->objectid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventusercompetencyreviewrequestcancelled', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_competency($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = user_competency::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + + /** + * Custom validation. + * + * Throw \coding_exception notice in case of any problems. + */ + protected function validate_data() { + if (!$this->relateduserid) { + throw new \coding_exception('The \'relateduserid\' value must be set.'); + } + } + +} diff --git a/lib/classes/event/competency_user_competency_review_requested.php b/lib/classes/event/competency_user_competency_review_requested.php new file mode 100644 index 00000000000..42710426987 --- /dev/null +++ b/lib/classes/event/competency_user_competency_review_requested.php @@ -0,0 +1,121 @@ +. + +/** + * User competency review requested event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\user_competency; +defined('MOODLE_INTERNAL') || die(); + +/** + * User competency review requested event class. + * + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_competency_review_requested extends base { + + /** + * Convenience method to instantiate the event. + * + * @param user_competency $usercompetency The user competency. + * @return self + */ + public static function create_from_user_competency(user_competency $usercompetency) { + if (!$usercompetency->get_id()) { + throw new \coding_exception('The user competency ID must be set.'); + } + $params = array( + 'contextid' => $usercompetency->get_context()->id, + 'objectid' => $usercompetency->get_id(), + 'relateduserid' => $usercompetency->get_userid() + ); + + $event = static::create($params); + $event->add_record_snapshot(user_competency::TABLE, $usercompetency->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' sent a review request for the user competency with id '$this->objectid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventusercompetencyreviewrequested', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_competency($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = user_competency::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + + /** + * Custom validation. + * + * Throw \coding_exception notice in case of any problems. + */ + protected function validate_data() { + if (!$this->relateduserid) { + throw new \coding_exception('The \'relateduserid\' value must be set.'); + } + } + +} diff --git a/lib/classes/event/competency_user_competency_review_started.php b/lib/classes/event/competency_user_competency_review_started.php new file mode 100644 index 00000000000..b887d6ae828 --- /dev/null +++ b/lib/classes/event/competency_user_competency_review_started.php @@ -0,0 +1,121 @@ +. + +/** + * User competency review started event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\user_competency; +defined('MOODLE_INTERNAL') || die(); + +/** + * User competency review started event class. + * + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_competency_review_started extends base { + + /** + * Convenience method to instantiate the event. + * + * @param user_competency $usercompetency The user competency. + * @return self + */ + public static function create_from_user_competency(user_competency $usercompetency) { + if (!$usercompetency->get_id()) { + throw new \coding_exception('The user competency ID must be set.'); + } + $params = array( + 'contextid' => $usercompetency->get_context()->id, + 'objectid' => $usercompetency->get_id(), + 'relateduserid' => $usercompetency->get_userid() + ); + + $event = static::create($params); + $event->add_record_snapshot(user_competency::TABLE, $usercompetency->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' started a review for the user competency with id '$this->objectid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventusercompetencyreviewstarted', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_competency($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = user_competency::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + + /** + * Custom validation. + * + * Throw \coding_exception notice in case of any problems. + */ + protected function validate_data() { + if (!$this->relateduserid) { + throw new \coding_exception('The \'relateduserid\' value must be set.'); + } + } + +} diff --git a/lib/classes/event/competency_user_competency_review_stopped.php b/lib/classes/event/competency_user_competency_review_stopped.php new file mode 100644 index 00000000000..560523facb6 --- /dev/null +++ b/lib/classes/event/competency_user_competency_review_stopped.php @@ -0,0 +1,121 @@ +. + +/** + * User competency review stopped event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\user_competency; +defined('MOODLE_INTERNAL') || die(); + +/** + * User competency review stopped event class. + * + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_competency_review_stopped extends base { + + /** + * Convenience method to instantiate the event. + * + * @param user_competency $usercompetency The user competency. + * @return self + */ + public static function create_from_user_competency(user_competency $usercompetency) { + if (!$usercompetency->get_id()) { + throw new \coding_exception('The user competency ID must be set.'); + } + $params = array( + 'contextid' => $usercompetency->get_context()->id, + 'objectid' => $usercompetency->get_id(), + 'relateduserid' => $usercompetency->get_userid() + ); + + $event = static::create($params); + $event->add_record_snapshot(user_competency::TABLE, $usercompetency->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' stopped a review for the user competency with id '$this->objectid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventusercompetencyreviewstopped', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_competency($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + $this->data['objecttable'] = user_competency::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + + /** + * Custom validation. + * + * Throw \coding_exception notice in case of any problems. + */ + protected function validate_data() { + if (!$this->relateduserid) { + throw new \coding_exception('The \'relateduserid\' value must be set.'); + } + } + +} diff --git a/lib/classes/event/competency_user_competency_viewed.php b/lib/classes/event/competency_user_competency_viewed.php new file mode 100644 index 00000000000..c285e759b99 --- /dev/null +++ b/lib/classes/event/competency_user_competency_viewed.php @@ -0,0 +1,130 @@ +. + +/** + * User competency viewed event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\user_competency; +use context_course; +defined('MOODLE_INTERNAL') || die(); + +/** + * User competency viewed event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int competencyid: id of competency. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_competency_viewed extends base { + + /** + * Convenience method to instantiate the event. + * + * @param user_competency $usercompetency The user competency. + * @return self + */ + public static function create_from_user_competency_viewed(user_competency $usercompetency) { + if (!$usercompetency->get_id()) { + throw new \coding_exception('The user competency ID must be set.'); + } + $params = array( + 'contextid' => $usercompetency->get_context()->id, + 'objectid' => $usercompetency->get_id(), + 'relateduserid' => $usercompetency->get_userid(), + 'other' => array( + 'competencyid' => $usercompetency->get_competencyid() + ) + ); + + $event = static::create($params); + $event->add_record_snapshot(user_competency::TABLE, $usercompetency->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' viewed the user competency with id '$this->objectid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventusercompetencyviewed', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_competency($this->objectid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = user_competency::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + + /** + * Custom validation. + * + * Throw \coding_exception notice in case of any problems. + */ + protected function validate_data() { + if (!isset($this->other) || !isset($this->other['competencyid'])) { + throw new \coding_exception('The \'competencyid\' value must be set.'); + } + } + +} diff --git a/lib/classes/event/competency_user_competency_viewed_in_course.php b/lib/classes/event/competency_user_competency_viewed_in_course.php new file mode 100644 index 00000000000..1081ab16655 --- /dev/null +++ b/lib/classes/event/competency_user_competency_viewed_in_course.php @@ -0,0 +1,138 @@ +. + +/** + * User competency viewed event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\user_competency_course; +use context_course; +defined('MOODLE_INTERNAL') || die(); + +/** + * User competency viewed in course event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int competencyid: id of competency. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_competency_viewed_in_course extends base { + + /** + * Convenience method to instantiate the event in course. + * + * @param user_competency_course $usercompetencycourse The user competency for the course. + * @return self + */ + public static function create_from_user_competency_viewed_in_course(user_competency_course $usercompetencycourse) { + if (!$usercompetencycourse->get_id()) { + throw new \coding_exception('The user competency course ID must be set.'); + } + $params = array( + 'objectid' => $usercompetencycourse->get_id(), + 'relateduserid' => $usercompetencycourse->get_userid(), + 'other' => array( + 'competencyid' => $usercompetencycourse->get_competencyid() + ) + ); + $coursecontext = context_course::instance($usercompetencycourse->get_courseid()); + $params['contextid'] = $coursecontext->id; + $params['courseid'] = $usercompetencycourse->get_courseid(); + + $event = static::create($params); + $event->add_record_snapshot(user_competency_course::TABLE, $usercompetencycourse->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' viewed the user course competency with id '$this->objectid' " + . "in course with id '$this->courseid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventusercompetencyviewedincourse', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_competency_in_course($this->relateduserid, $this->other['competencyid'], + $this->courseid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = user_competency_course::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + + /** + * Custom validation. + * + * Throw \coding_exception notice in case of any problems. + */ + protected function validate_data() { + if (!$this->courseid) { + throw new \coding_exception('The \'courseid\' value must be set.'); + } + + if (!isset($this->other) || !isset($this->other['competencyid'])) { + throw new \coding_exception('The \'competencyid\' value must be set.'); + } + } + +} diff --git a/lib/classes/event/competency_user_competency_viewed_in_plan.php b/lib/classes/event/competency_user_competency_viewed_in_plan.php new file mode 100644 index 00000000000..de23838bd78 --- /dev/null +++ b/lib/classes/event/competency_user_competency_viewed_in_plan.php @@ -0,0 +1,143 @@ +. + +/** + * User competency viewed event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\user_competency; +use context_course; +defined('MOODLE_INTERNAL') || die(); + +/** + * User competency viewed in plan event class. + * + * @property-read array $other { + * Extra information about event. + * + * - int planid: id of plan for which competency is associated. + * - int competencyid: id of competency. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_competency_viewed_in_plan extends base { + + /** + * Convenience method to instantiate the event in plan. + * + * @param user_competency $usercompetency The user competency. + * @param int $planid The pland ID + * @return self + */ + public static function create_from_user_competency_viewed_in_plan(user_competency $usercompetency, $planid) { + if (!$usercompetency->get_id()) { + throw new \coding_exception('The user competency ID must be set.'); + } + $params = array( + 'contextid' => $usercompetency->get_context()->id, + 'objectid' => $usercompetency->get_id(), + 'relateduserid' => $usercompetency->get_userid(), + 'other' => array( + 'competencyid' => $usercompetency->get_competencyid(), + 'planid' => $planid + ) + ); + + $event = static::create($params); + $event->add_record_snapshot(user_competency::TABLE, $usercompetency->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' viewed the user competency with id '$this->objectid' " + . "in plan with id '" . $this->other['planid'] . "'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventusercompetencyviewedinplan', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_competency_in_plan($this->relateduserid, $this->other['competencyid'], + $this->other['planid']); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = user_competency::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + + /** + * Custom validation. + * + * Throw \coding_exception notice in case of any problems. + */ + protected function validate_data() { + if ($this->other === null) { + throw new \coding_exception('The \'competencyid\' and \'planid\' values must be set.'); + } + + if (!isset($this->other['competencyid'])) { + throw new \coding_exception('The \'competencyid\' value must be set.'); + } + + if (!isset($this->other['planid'])) { + throw new \coding_exception('The \'planid\' value must be set.'); + } + } + +} diff --git a/lib/classes/event/competency_user_evidence_created.php b/lib/classes/event/competency_user_evidence_created.php new file mode 100644 index 00000000000..e9f5f7d0c03 --- /dev/null +++ b/lib/classes/event/competency_user_evidence_created.php @@ -0,0 +1,109 @@ +. + +/** + * Evidence of prior learning created event. + * + * @package core_competency + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +use core\event\base; +use core_competency\user_evidence; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Evidence of prior learning created event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_evidence_created extends base { + + /** + * Convenience method to instantiate the event. + * + * @param user_evidence $userevidence The evidence of prior learning. + * @return self + */ + public static final function create_from_user_evidence(user_evidence $userevidence) { + if (!$userevidence->get_id()) { + throw new \coding_exception('The evidence of prior learning ID must be set.'); + } + $event = static::create(array( + 'contextid' => $userevidence->get_context()->id, + 'objectid' => $userevidence->get_id(), + 'relateduserid' => $userevidence->get_userid() + )); + $event->add_record_snapshot(user_evidence::TABLE, $userevidence->to_record()); + return $event; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventuserevidencecreated', 'core_competency'); + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' created the evidence of prior learning with id '$this->objectid'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_evidence($this->objectid); + } + + /** + * Initialise the event data. + */ + protected function init() { + $this->data['objecttable'] = user_evidence::TABLE; + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_user_evidence_deleted.php b/lib/classes/event/competency_user_evidence_deleted.php new file mode 100644 index 00000000000..84c047c8086 --- /dev/null +++ b/lib/classes/event/competency_user_evidence_deleted.php @@ -0,0 +1,100 @@ +. + +/** + * Evidence of prior learning deleted event. + * + * @package core_competency + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +use core\event\base; +use core_competency\user_evidence; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Evidence of prior learning deleted event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_evidence_deleted extends base { + + /** + * Convenience method to instantiate the event. + * + * @param user_evidence $userevidence The evidence of prior learning. + * @return self + */ + public static final function create_from_user_evidence(user_evidence $userevidence) { + if (!$userevidence->get_id()) { + throw new \coding_exception('The evidence of prior learning ID must be set.'); + } + $event = static::create(array( + 'contextid' => $userevidence->get_context()->id, + 'objectid' => $userevidence->get_id(), + 'relateduserid' => $userevidence->get_userid() + )); + $event->add_record_snapshot(user_evidence::TABLE, $userevidence->to_record()); + return $event; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventuserevidencedeleted', 'core_competency'); + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' deleted the evidence of prior learning with id '$this->objectid'."; + } + + /** + * Initialise the event data. + */ + protected function init() { + $this->data['objecttable'] = user_evidence::TABLE; + $this->data['crud'] = 'd'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_user_evidence_updated.php b/lib/classes/event/competency_user_evidence_updated.php new file mode 100644 index 00000000000..d35f3fea643 --- /dev/null +++ b/lib/classes/event/competency_user_evidence_updated.php @@ -0,0 +1,109 @@ +. + +/** + * Evidence of prior learning updated event. + * + * @package core_competency + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\event; + +use core\event\base; +use core_competency\user_evidence; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Evidence of prior learning updated event class. + * + * @property-read array $other { + * Extra information about event. + * } + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Serge Gauthier + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_user_evidence_updated extends base { + + /** + * Convenience method to instantiate the event. + * + * @param user_evidence $userevidence The evidence of prior learning. + * @return self + */ + public static final function create_from_user_evidence(user_evidence $userevidence) { + if (!$userevidence->get_id()) { + throw new \coding_exception('The evidence of prior learning ID must be set.'); + } + $event = static::create(array( + 'contextid' => $userevidence->get_context()->id, + 'objectid' => $userevidence->get_id(), + 'relateduserid' => $userevidence->get_userid() + )); + $event->add_record_snapshot(user_evidence::TABLE, $userevidence->to_record()); + return $event; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventuserevidenceupdated', 'core_competency'); + } + + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' updated the evidence of prior learning with id '$this->objectid'."; + } + + /** + * Returns relevant URL. + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::user_evidence($this->objectid); + } + + /** + * Initialise the event data. + */ + protected function init() { + $this->data['objecttable'] = user_evidence::TABLE; + $this->data['crud'] = 'u'; + $this->data['edulevel'] = self::LEVEL_OTHER; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/event/competency_viewed.php b/lib/classes/event/competency_viewed.php new file mode 100644 index 00000000000..da899a53215 --- /dev/null +++ b/lib/classes/event/competency_viewed.php @@ -0,0 +1,108 @@ +. + +/** + * Competency viewed event. + * + * @package core_competency + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\event; + +use core\event\base; +use core_competency\competency; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Competency viewed event class. + * + * + * @package core_competency + * @since Moodle 3.1 + * @copyright 2016 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class competency_viewed extends base { + + /** + * Convenience method to instantiate the event. + * + * @param competency $competency The competency. + * @return self + */ + public static function create_from_competency(competency $competency) { + if (!$competency->get_id()) { + throw new \coding_exception('The competency ID must be set.'); + } + $event = static::create(array( + 'contextid' => $competency->get_context()->id, + 'objectid' => $competency->get_id() + )); + $event->add_record_snapshot(competency::TABLE, $competency->to_record()); + return $event; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' viewed the competency with id '$this->objectid'"; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventcompetencyviewed', 'core_competency'); + } + + /** + * Get URL related to the action + * + * @return \moodle_url + */ + public function get_url() { + return \core_competency\url::competency($this->objectid, $this->contextid); + } + + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_OTHER; + $this->data['objecttable'] = competency::TABLE; + } + + /** + * Get_objectid_mapping method. + * + * @return string the name of the restore mapping the objectid links to + */ + public static function get_objectid_mapping() { + return base::NOT_MAPPED; + } + +} diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index d454849be47..89efac92955 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1725,7 +1725,7 @@ class core_plugin_manager { 'calendar_upcoming', 'comments', 'community', 'completionstatus', 'course_list', 'course_overview', 'course_summary', 'feedback', 'globalsearch', 'glossary_random', 'html', - 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile', + 'login', 'lp', 'mentees', 'messages', 'mnet_hosts', 'myprofile', 'navigation', 'news_items', 'online_users', 'participants', 'private_files', 'quiz_results', 'recent_activity', 'rss_client', 'search_forums', 'section_links', @@ -1865,7 +1865,7 @@ class core_plugin_manager { ), 'report' => array( - 'backups', 'completion', 'configlog', 'courseoverview', 'eventlist', + 'backups', 'competency', 'completion', 'configlog', 'courseoverview', 'eventlist', 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'search', 'security', 'stats', 'performance', 'usersessions' ), @@ -1898,10 +1898,10 @@ class core_plugin_manager { ), 'tool' => array( - 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'customlang', + 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'cohortroles', 'customlang', 'dbtransfer', 'filetypes', 'generator', 'health', 'innodb', 'installaddon', - 'langimport', 'log', 'messageinbound', 'multilangupgrade', 'monitor', 'phpunit', 'profiling', - 'recyclebin', 'replace', 'spamcleaner', 'task', 'templatelibrary', + 'langimport', 'log', 'lp', 'lpmigrate', 'messageinbound', 'multilangupgrade', 'monitor', + 'phpunit', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task', 'templatelibrary', 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb' ), diff --git a/lib/classes/task/complete_plans_task.php b/lib/classes/task/complete_plans_task.php new file mode 100644 index 00000000000..f22cefb5229 --- /dev/null +++ b/lib/classes/task/complete_plans_task.php @@ -0,0 +1,68 @@ +. + +/** + * Complete plans task. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\task; +defined('MOODLE_INTERNAL') || die(); + +use core_competency\api; +use core_competency\plan; + +/** + * Complete plans task class. + * + * This task should run relatively often because the plans due dates can be set at + * any time of the day in any timezone. + * + * @package core_competency + * @copyright 2015 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class complete_plans_task extends \core\task\scheduled_task { + + /** + * Get a descriptive name for this task. + * + * @return string + */ + public function get_name() { + return get_string('completeplanstask', 'core_competency'); + } + + /** + * Do the job. + */ + public function execute() { + if (!api::is_enabled()) { + return; + } + + $records = plan::get_recordset_for_due_and_incomplete(); + foreach ($records as $record) { + $plan = new plan(0, $record); + api::complete_plan($plan); + } + $records->close(); + } + +} diff --git a/lib/classes/task/sync_plans_from_template_cohorts_task.php b/lib/classes/task/sync_plans_from_template_cohorts_task.php new file mode 100644 index 00000000000..8373c672b64 --- /dev/null +++ b/lib/classes/task/sync_plans_from_template_cohorts_task.php @@ -0,0 +1,71 @@ +. + +/** + * Synchronise plans from template cohorts. + * + * @package core_competency + * @copyright 2015 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\task; +defined('MOODLE_INTERNAL') || die(); + +use core_competency\api; +use core_competency\template_cohort; + +/** + * Synchronise plans from template cohorts. + * + * + * @package core_competency + * @copyright 2015 Issam Taboubi + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sync_plans_from_template_cohorts_task extends \core\task\scheduled_task { + + /** + * Get a descriptive name for this task. + * + * @return string + */ + public function get_name() { + return get_string('syncplanscohorts', 'core_competency'); + } + + /** + * Do the job. + */ + public function execute() { + if (!api::is_enabled()) { + return; + } + + $missingplans = template_cohort::get_all_missing_plans(self::get_last_run_time()); + + foreach ($missingplans as $missingplan) { + foreach ($missingplan['userids'] as $userid) { + try { + api::create_plan_from_template($missingplan['template'], $userid); + } catch (\Exception $e) { + debugging(sprintf('Exception caught while creating plan for user %d from template %d. Message: %s', + $userid, $missingplan['template']->get_id(), $e->getMessage()), DEBUG_DEVELOPER); + } + } + } + } +} diff --git a/lib/db/access.php b/lib/db/access.php index 47ee5ea827d..a5692a7847a 100644 --- a/lib/db/access.php +++ b/lib/db/access.php @@ -2056,6 +2056,247 @@ $capabilities = array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) - ) + ), + + // Competencies. + 'moodle/competency:competencymanage' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSECAT, + 'archetypes' => array( + 'manager' => CAP_ALLOW + ) + ), + 'moodle/competency:competencyview' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSECAT, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/block:view' + ), + 'moodle/competency:competencygrade' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, // And CONTEXT_USER. + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + ), + // Course competencies. + 'moodle/competency:coursecompetencymanage' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/site:backup' + ), + 'moodle/competency:coursecompetencyconfigure' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/site:backup' + ), + 'moodle/competency:coursecompetencygradable' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'student' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/course:isincompletionreports' + ), + 'moodle/competency:coursecompetencyview' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/block:view' + ), + // Evidence. + 'moodle/competency:evidencedelete' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + // User plans. + 'moodle/competency:planmanage' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:planmanagedraft' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:planmanageown' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:planmanageowndraft' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:planview' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:planviewdraft' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:planviewown' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/block:view' + ), + 'moodle/competency:planviewowndraft' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:planrequestreview' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ) + ), + 'moodle/competency:planrequestreviewown' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + 'user' => CAP_ALLOW + ) + ), + 'moodle/competency:planreview' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:plancomment' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:plancommentown' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + ), + // User competencies. + 'moodle/competency:usercompetencyview' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_USER, // And CONTEXT_COURSE. + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'teacher' => CAP_ALLOW + ) + ), + 'moodle/competency:usercompetencyrequestreview' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ) + ), + 'moodle/competency:usercompetencyrequestreviewown' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + 'user' => CAP_ALLOW + ) + ), + 'moodle/competency:usercompetencyreview' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:usercompetencycomment' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:usercompetencycommentown' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + ), + // Template. + 'moodle/competency:templatemanage' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSECAT, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:templateview' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSECAT, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + // User evidence. + 'moodle/competency:userevidencemanage' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), + 'moodle/competency:userevidencemanageown' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/block:view' + ), + 'moodle/competency:userevidenceview' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + ), + 'clonepermissionsfrom' => 'moodle/site:config' + ), ); diff --git a/lib/db/events.php b/lib/db/events.php index a9ccfc08bd9..8045f8e04eb 100644 --- a/lib/db/events.php +++ b/lib/db/events.php @@ -54,6 +54,16 @@ $observers = array( 'eventname' => '\core\event\user_updated', 'callback' => 'core_badges_observer::profile_criteria_review', ), + + // Competencies. + array( + 'eventname' => '\core\event\course_completed', + 'callback' => 'core_competency\api::observe_course_completed', + ), + array( + 'eventname' => '\core\event\course_module_completion_updated', + 'callback' => 'core_competency\api::observe_course_module_completion_updated', + ), ); // List of all events triggered by Moodle can be found using Events list report. diff --git a/lib/db/install.xml b/lib/db/install.xml index ff92a464eaa..47fc98a0049 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3102,5 +3102,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    -
    + \ No newline at end of file diff --git a/lib/db/messages.php b/lib/db/messages.php index 75afbb7370a..d9aa3739ca1 100644 --- a/lib/db/messages.php +++ b/lib/db/messages.php @@ -94,5 +94,11 @@ $messageproviders = array ( 'defaults' => array( 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF, ) - ) + ), + + // A comment was left on a plan. + 'competencyplancomment' => array(), + + // A comment was left on a user competency. + 'competencyusercompcomment' => array(), ); diff --git a/lib/db/services.php b/lib/db/services.php index ead08a207c2..f9ae967ad0f 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1205,6 +1205,736 @@ $functions = array( 'capabilities' => 'moodle/badges:viewotherbadges' ), + // Competencies functions. + 'core_competency_create_competency_framework' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'create_competency_framework', + 'classpath' => '', + 'description' => 'Creates new competency frameworks.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_read_competency_framework' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'read_competency_framework', + 'classpath' => '', + 'description' => 'Load a summary of a competency framework.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'core_competency_duplicate_competency_framework' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'duplicate_competency_framework', + 'classpath' => '', + 'description' => 'Duplicate a competency framework.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_delete_competency_framework' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'delete_competency_framework', + 'classpath' => '', + 'description' => 'Delete a competency framework.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_update_competency_framework' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'update_competency_framework', + 'classpath' => '', + 'description' => 'Update a competency framework.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_list_competency_frameworks' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'list_competency_frameworks', + 'classpath' => '', + 'description' => 'Load a list of a competency frameworks.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'core_competency_count_competency_frameworks' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'count_competency_frameworks', + 'classpath' => '', + 'description' => 'Count a list of a competency frameworks.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'core_competency_competency_framework_viewed' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'competency_framework_viewed', + 'classpath' => '', + 'description' => 'Log event competency framework viewed', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'core_competency_create_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'create_competency', + 'classpath' => '', + 'description' => 'Creates new competencies.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_read_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'read_competency', + 'classpath' => '', + 'description' => 'Load a summary of a competency.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'core_competency_competency_viewed' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'competency_viewed', + 'classpath' => '', + 'description' => 'Log event competency viewed', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'core_competency_delete_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'delete_competency', + 'classpath' => '', + 'description' => 'Delete a competency.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_update_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'update_competency', + 'classpath' => '', + 'description' => 'Update a competency.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_list_competencies' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'list_competencies', + 'classpath' => '', + 'description' => 'Load a list of a competencies.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'core_competency_list_competencies_in_template' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'list_competencies_in_template', + 'classpath' => '', + 'description' => 'Load a list of a competencies for a given template.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'core_competency_count_competencies' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'count_competencies', + 'classpath' => '', + 'description' => 'Count a list of a competencies.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'core_competency_count_competencies_in_template' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'count_competencies_in_template', + 'classpath' => '', + 'description' => 'Count a list of a competencies for a given template.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'core_competency_search_competencies' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'search_competencies', + 'classpath' => '', + 'description' => 'Search a list of a competencies.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencyview', + 'ajax' => true, + ), + 'core_competency_set_parent_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'set_parent_competency', + 'classpath' => '', + 'description' => 'Set a new parent for a competency.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_move_up_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'move_up_competency', + 'classpath' => '', + 'description' => 'Re-order a competency.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_move_down_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'move_down_competency', + 'classpath' => '', + 'description' => 'Re-order a competency.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_list_course_competencies' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'list_course_competencies', + 'classpath' => '', + 'description' => 'List the competencies in a course', + 'type' => 'read', + 'capabilities' => 'moodle/competency:coursecompetencyview', + 'ajax' => true, + ), + 'core_competency_count_competencies_in_course' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'count_competencies_in_course', + 'classpath' => '', + 'description' => 'List the competencies in a course', + 'type' => 'read', + 'capabilities' => 'moodle/competency:coursecompetencyview', + 'ajax' => true, + ), + 'core_competency_count_courses_using_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'count_courses_using_competency', + 'classpath' => '', + 'description' => 'List the courses using a competency', + 'type' => 'read', + 'capabilities' => 'moodle/competency:coursecompetencyview', + 'ajax' => true, + ), + 'core_competency_add_competency_to_course' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'add_competency_to_course', + 'classpath' => '', + 'description' => 'Add the competency to a course', + 'type' => 'write', + 'capabilities' => 'moodle/competency:coursecompetencymanage', + 'ajax' => true, + ), + 'core_competency_add_competency_to_template' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'add_competency_to_template', + 'classpath' => '', + 'description' => 'Add the competency to a template', + 'type' => 'write', + 'capabilities' => 'moodle/competency:templatemanage', + 'ajax' => true, + ), + 'core_competency_remove_competency_from_course' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'remove_competency_from_course', + 'classpath' => '', + 'description' => 'Remove a competency from a course', + 'type' => 'write', + 'capabilities' => 'moodle/competency:coursecompetencymanage', + 'ajax' => true, + ), + 'core_competency_set_course_competency_ruleoutcome' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'set_course_competency_ruleoutcome', + 'classpath' => '', + 'description' => 'Modify the ruleoutcome value for course competency', + 'type' => 'write', + 'capabilities' => 'moodle/competency:coursecompetencymanage', + 'ajax' => true, + ), + 'core_competency_remove_competency_from_template' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'remove_competency_from_template', + 'classpath' => '', + 'description' => 'Remove a competency from a template', + 'type' => 'write', + 'capabilities' => 'moodle/competency:templatemanage', + 'ajax' => true, + ), + 'core_competency_reorder_course_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'reorder_course_competency', + 'classpath' => '', + 'description' => 'Move a course competency to a new relative sort order.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:coursecompetencymanage', + 'ajax' => true, + ), + 'core_competency_reorder_template_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'reorder_template_competency', + 'classpath' => '', + 'description' => 'Move a template competency to a new relative sort order.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:templatemanage', + 'ajax' => true, + ), + 'core_competency_create_template' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'create_template', + 'classpath' => '', + 'description' => 'Creates new learning plan templates.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:templatemanage', + 'ajax' => true, + ), + 'core_competency_duplicate_template' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'duplicate_template', + 'classpath' => '', + 'description' => 'Duplicate learning plan template.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:templatemanage', + 'ajax' => true, + ), + 'core_competency_read_template' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'read_template', + 'classpath' => '', + 'description' => 'Load a summary of a learning plan template.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:templateview', + 'ajax' => true, + ), + 'core_competency_delete_template' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'delete_template', + 'classpath' => '', + 'description' => 'Delete a learning plan template.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:templatemanage', + 'ajax' => true, + ), + 'core_competency_update_template' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'update_template', + 'classpath' => '', + 'description' => 'Update a learning plan template.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:templatemanage', + 'ajax' => true, + ), + 'core_competency_list_templates' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'list_templates', + 'classpath' => '', + 'description' => 'Load a list of a learning plan templates.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:templateview', + 'ajax' => true, + ), + 'core_competency_list_templates_using_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'list_templates_using_competency', + 'classpath' => '', + 'description' => 'Load a list of a learning plan templates for a given competency.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:templateview', + 'ajax' => true, + ), + 'core_competency_count_templates' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'count_templates', + 'classpath' => '', + 'description' => 'Count a list of a learning plan templates.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:templateview', + 'ajax' => true, + ), + 'core_competency_count_templates_using_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'count_templates_using_competency', + 'classpath' => '', + 'description' => 'Count a list of a learning plan templates for a given competency.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:templateview', + 'ajax' => true, + ), + 'core_competency_create_plan' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'create_plan', + 'classpath' => '', + 'description' => 'Creates a learning plan.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_update_plan' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'update_plan', + 'classpath' => '', + 'description' => 'Updates a learning plan.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_complete_plan' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'complete_plan', + 'classpath' => '', + 'description' => 'Complete learning plan.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_reopen_plan' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'reopen_plan', + 'classpath' => '', + 'description' => 'Reopen learning plan.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_read_plan' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'read_plan', + 'classpath' => '', + 'description' => 'Load a learning plan.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:planviewown', + 'ajax' => true, + ), + 'core_competency_delete_plan' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'delete_plan', + 'classpath' => '', + 'description' => 'Delete a learning plan.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_list_user_plans' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'list_user_plans', + 'classpath' => '', + 'description' => 'List a user\'s learning plans.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:planviewown', + 'ajax' => true, + ), + 'core_competency_list_plan_competencies' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'list_plan_competencies', + 'classpath' => '', + 'description' => 'List the competencies in a plan', + 'type' => 'read', + 'capabilities' => 'moodle/competency:planviewown', + 'ajax' => true, + ), + 'core_competency_add_competency_to_plan' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'add_competency_to_plan', + 'classpath' => '', + 'description' => 'Add the competency to a learning plan', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_remove_competency_from_plan' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'remove_competency_from_plan', + 'classpath' => '', + 'description' => 'Remove the competency from a learning plan', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_reorder_plan_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'reorder_plan_competency', + 'classpath' => '', + 'description' => 'Move a plan competency to a new relative sort order.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_plan_request_review' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'plan_request_review', + 'classpath' => '', + 'description' => 'Request for a plan to be reviewed.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanagedraft', + 'ajax' => true, + ), + 'core_competency_plan_start_review' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'plan_start_review', + 'classpath' => '', + 'description' => 'Start the review of a plan.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_plan_stop_review' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'plan_stop_review', + 'classpath' => '', + 'description' => 'Stop the review of a plan.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_plan_cancel_review_request' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'plan_cancel_review_request', + 'classpath' => '', + 'description' => 'Cancel the review of a plan.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanagedraft', + 'ajax' => true, + ), + 'core_competency_approve_plan' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'approve_plan', + 'classpath' => '', + 'description' => 'Approve a plan.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_unapprove_plan' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'unapprove_plan', + 'classpath' => '', + 'description' => 'Unapprove a plan.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_template_has_related_data' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'template_has_related_data', + 'classpath' => '', + 'description' => 'Check if a template has related data', + 'type' => 'read', + 'capabilities' => 'moodle/competency:templateview', + 'ajax' => true, + ), + 'core_competency_get_scale_values' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'get_scale_values', + 'classpath' => '', + 'description' => 'Fetch the values for a specific scale', + 'type' => 'read', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_add_related_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'add_related_competency', + 'classpath' => '', + 'description' => 'Adds a related competency', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_remove_related_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'remove_related_competency', + 'classpath' => '', + 'description' => 'Remove a related competency', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencymanage', + 'ajax' => true, + ), + 'core_competency_read_user_evidence' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'read_user_evidence', + 'classpath' => '', + 'description' => 'Read an evidence of prior learning.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:userevidenceview', + 'ajax' => true, + ), + 'core_competency_delete_user_evidence' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'delete_user_evidence', + 'classpath' => '', + 'description' => 'Delete an evidence of prior learning.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:userevidencemanageown', + 'ajax' => true, + ), + 'core_competency_create_user_evidence_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'create_user_evidence_competency', + 'classpath' => '', + 'description' => 'Create an evidence of prior learning relationship with a competency.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:userevidencemanageown, moodle/competency:competencyview', + 'ajax' => true, + ), + 'core_competency_delete_user_evidence_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'delete_user_evidence_competency', + 'classpath' => '', + 'description' => 'Delete an evidence of prior learning relationship with a competency.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:userevidencemanageown', + 'ajax' => true, + ), + 'core_competency_user_competency_cancel_review_request' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'user_competency_cancel_review_request', + 'classpath' => '', + 'description' => 'Cancel a review request.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:userevidencemanageown', + 'ajax' => true, + ), + 'core_competency_user_competency_request_review' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'user_competency_request_review', + 'classpath' => '', + 'description' => 'Request a review.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:userevidencemanageown', + 'ajax' => true, + ), + 'core_competency_user_competency_start_review' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'user_competency_start_review', + 'classpath' => '', + 'description' => 'Start a review.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencygrade', + 'ajax' => true, + ), + 'core_competency_user_competency_stop_review' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'user_competency_stop_review', + 'classpath' => '', + 'description' => 'Stop a review.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencygrade', + 'ajax' => true, + ), + 'core_competency_user_competency_viewed' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'user_competency_viewed', + 'classpath' => '', + 'description' => 'Log the user competency viewed event.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:usercompetencyview', + 'ajax' => true, + ), + 'core_competency_user_competency_viewed_in_plan' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'user_competency_viewed_in_plan', + 'classpath' => '', + 'description' => 'Log the user competency viewed in plan event.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:usercompetencyview', + 'ajax' => true, + ), + 'core_competency_user_competency_viewed_in_course' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'user_competency_viewed_in_course', + 'classpath' => '', + 'description' => 'Log the user competency viewed in course event', + 'type' => 'read', + 'capabilities' => 'moodle/competency:usercompetencyview', + 'ajax' => true, + ), + 'core_competency_user_competency_plan_viewed' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'user_competency_plan_viewed', + 'classpath' => '', + 'description' => 'Log the user competency plan viewed event.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:usercompetencyview', + 'ajax' => true, + ), + 'core_competency_grade_competency' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'grade_competency', + 'classpath' => '', + 'description' => 'Grade a competency.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencygrade', + 'ajax' => true, + ), + 'core_competency_grade_competency_in_plan' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'grade_competency_in_plan', + 'classpath' => '', + 'description' => 'Grade a competency from the user plan page.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencygrade', + 'ajax' => true, + ), + 'core_competency_grade_competency_in_course' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'grade_competency_in_course', + 'classpath' => '', + 'description' => 'Grade a competency from the course page.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:competencygrade', + 'ajax' => true, + ), + 'core_competency_unlink_plan_from_template' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'unlink_plan_from_template', + 'classpath' => '', + 'description' => 'Unlink a plan form it template.', + 'type' => 'write', + 'capabilities' => 'moodle/competency:planmanage', + 'ajax' => true, + ), + 'core_competency_template_viewed' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'template_viewed', + 'classpath' => '', + 'description' => 'Log event template viewed', + 'type' => 'read', + 'capabilities' => 'moodle/competency:templateview', + 'ajax' => true, + ), + 'core_competency_request_review_of_user_evidence_linked_competencies' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'request_review_of_user_evidence_linked_competencies', + 'classpath' => '', + 'description' => 'Send user evidence competencies in review', + 'type' => 'write', + 'capabilities' => 'moodle/competency:userevidencemanageown', + 'ajax' => true, + ), + 'core_competency_update_course_competency_settings' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'update_course_competency_settings', + 'classpath' => '', + 'description' => 'Update the course competency settings', + 'type' => 'write', + 'capabilities' => 'moodle/competency:coursecompetencyconfigure', + 'ajax' => true, + ), + 'core_competency_delete_evidence' => array( + 'classname' => 'core_competency\external', + 'methodname' => 'delete_evidence', + 'classpath' => '', + 'description' => 'Delete an evidence', + 'type' => 'write', + 'capabilities' => 'moodle/competency:evidencedelete', + 'ajax' => true, + ), ); $services = array( diff --git a/lib/db/tasks.php b/lib/db/tasks.php index 1a564930462..49863b894aa 100644 --- a/lib/db/tasks.php +++ b/lib/db/tasks.php @@ -319,5 +319,23 @@ $tasks = array( 'day' => '*', 'dayofweek' => '*', 'month' => '*' - ) + ), + array( + 'classname' => 'core\task\complete_plans_task', + 'blocking' => 0, + 'minute' => 'R', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\sync_plans_from_template_cohorts_task', + 'blocking' => 0, + 'minute' => 'R', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), ); diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index fc8ba43a926..072b3db812f 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1464,5 +1464,540 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2016030400.01); } + if ($oldversion < 2016041500.50) { + + // Define table competency to be created. + $table = new xmldb_table('competency'); + + // Adding fields to table competency. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('shortname', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('description', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('descriptionformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('idnumber', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('competencyframeworkid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('parentid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('path', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('ruletype', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('ruleoutcome', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('ruleconfig', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('scaleid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('scaleconfiguration', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + + // Adding keys to table competency. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table competency. + $table->add_index('idnumberframework', XMLDB_INDEX_UNIQUE, array('competencyframeworkid', 'idnumber')); + $table->add_index('ruleoutcome', XMLDB_INDEX_NOTUNIQUE, array('ruleoutcome')); + + // Conditionally launch create table for competency. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.50); + } + + if ($oldversion < 2016041500.51) { + + // Define table competency_coursecompsetting to be created. + $table = new xmldb_table('competency_coursecompsetting'); + + // Adding fields to table competency_coursecompsetting. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('pushratingstouserplans', XMLDB_TYPE_INTEGER, '2', null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + + // Adding keys to table competency_coursecompsetting. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('courseidlink', XMLDB_KEY_FOREIGN_UNIQUE, array('courseid'), 'course', array('id')); + + // Conditionally launch create table for competency_coursecompsetting. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.51); + } + + if ($oldversion < 2016041500.52) { + + // Define table competency_framework to be created. + $table = new xmldb_table('competency_framework'); + + // Adding fields to table competency_framework. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('shortname', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('idnumber', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('description', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('descriptionformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('scaleid', XMLDB_TYPE_INTEGER, '11', null, null, null, null); + $table->add_field('scaleconfiguration', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('visible', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1'); + $table->add_field('taxonomies', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + + // Adding keys to table competency_framework. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table competency_framework. + $table->add_index('idnumber', XMLDB_INDEX_UNIQUE, array('idnumber')); + + // Conditionally launch create table for competency_framework. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.52); + } + + if ($oldversion < 2016041500.53) { + + // Define table competency_coursecomp to be created. + $table = new xmldb_table('competency_coursecomp'); + + // Adding fields to table competency_coursecomp. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('competencyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('ruleoutcome', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table competency_coursecomp. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('courseidlink', XMLDB_KEY_FOREIGN, array('courseid'), 'course', array('id')); + $table->add_key('competencyid', XMLDB_KEY_FOREIGN, array('competencyid'), 'competency_competency', array('id')); + + // Adding indexes to table competency_coursecomp. + $table->add_index('courseidruleoutcome', XMLDB_INDEX_NOTUNIQUE, array('courseid', 'ruleoutcome')); + $table->add_index('courseidcompetencyid', XMLDB_INDEX_UNIQUE, array('courseid', 'competencyid')); + + // Conditionally launch create table for competency_coursecomp. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.53); + } + + if ($oldversion < 2016041500.54) { + + // Define table competency_plan to be created. + $table = new xmldb_table('competency_plan'); + + // Adding fields to table competency_plan. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('name', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); + $table->add_field('description', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('descriptionformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('templateid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('origtemplateid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('status', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null); + $table->add_field('duedate', XMLDB_TYPE_INTEGER, '10', null, null, null, '0'); + $table->add_field('reviewerid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table competency_plan. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table competency_plan. + $table->add_index('useridstatus', XMLDB_INDEX_NOTUNIQUE, array('userid', 'status')); + $table->add_index('templateid', XMLDB_INDEX_NOTUNIQUE, array('templateid')); + $table->add_index('statusduedate', XMLDB_INDEX_NOTUNIQUE, array('status', 'duedate')); + + // Conditionally launch create table for competency_plan. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.54); + } + + if ($oldversion < 2016041500.55) { + + // Define table competency_template to be created. + $table = new xmldb_table('competency_template'); + + // Adding fields to table competency_template. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('shortname', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('description', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('descriptionformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('visible', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1'); + $table->add_field('duedate', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + + // Adding keys to table competency_template. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Conditionally launch create table for competency_template. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.55); + } + + if ($oldversion < 2016041500.56) { + + // Define table competency_templatecomp to be created. + $table = new xmldb_table('competency_templatecomp'); + + // Adding fields to table competency_templatecomp. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('templateid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('competencyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + + // Adding keys to table competency_templatecomp. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('templateidlink', XMLDB_KEY_FOREIGN, array('templateid'), 'competency_template', array('id')); + $table->add_key('competencyid', XMLDB_KEY_FOREIGN, array('competencyid'), 'competency_competency', array('id')); + + // Conditionally launch create table for competency_templatecomp. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.56); + } + + if ($oldversion < 2016041500.57) { + + // Define table competency_templatecohort to be created. + $table = new xmldb_table('competency_templatecohort'); + + // Adding fields to table competency_templatecohort. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('templateid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('cohortid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table competency_templatecohort. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table competency_templatecohort. + $table->add_index('templateid', XMLDB_INDEX_NOTUNIQUE, array('templateid')); + $table->add_index('templatecohortids', XMLDB_INDEX_UNIQUE, array('templateid', 'cohortid')); + + // Conditionally launch create table for competency_templatecohort. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.57); + } + + if ($oldversion < 2016041500.58) { + + // Define table competency_relatedcomp to be created. + $table = new xmldb_table('competency_relatedcomp'); + + // Adding fields to table competency_relatedcomp. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('competencyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('relatedcompetencyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table competency_relatedcomp. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Conditionally launch create table for competency_relatedcomp. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.58); + } + + if ($oldversion < 2016041500.59) { + + // Define table competency_usercomp to be created. + $table = new xmldb_table('competency_usercomp'); + + // Adding fields to table competency_usercomp. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('competencyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('status', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('reviewerid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('proficiency', XMLDB_TYPE_INTEGER, '2', null, null, null, null); + $table->add_field('grade', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table competency_usercomp. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table competency_usercomp. + $table->add_index('useridcompetency', XMLDB_INDEX_UNIQUE, array('userid', 'competencyid')); + + // Conditionally launch create table for competency_usercomp. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.59); + } + + if ($oldversion < 2016041500.60) { + + // Define table competency_usercompcourse to be created. + $table = new xmldb_table('competency_usercompcourse'); + + // Adding fields to table competency_usercompcourse. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('competencyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('proficiency', XMLDB_TYPE_INTEGER, '2', null, null, null, null); + $table->add_field('grade', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table competency_usercompcourse. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table competency_usercompcourse. + $table->add_index('useridcoursecomp', XMLDB_INDEX_UNIQUE, array('userid', 'courseid', 'competencyid')); + + // Conditionally launch create table for competency_usercompcourse. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.60); + } + + if ($oldversion < 2016041500.61) { + + // Define table competency_usercompplan to be created. + $table = new xmldb_table('competency_usercompplan'); + + // Adding fields to table competency_usercompplan. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('competencyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('planid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('proficiency', XMLDB_TYPE_INTEGER, '2', null, null, null, null); + $table->add_field('grade', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table competency_usercompplan. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table competency_usercompplan. + $table->add_index('usercompetencyplan', XMLDB_INDEX_UNIQUE, array('userid', 'competencyid', 'planid')); + + // Conditionally launch create table for competency_usercompplan. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.61); + } + + if ($oldversion < 2016041500.62) { + + // Define table competency_plancomp to be created. + $table = new xmldb_table('competency_plancomp'); + + // Adding fields to table competency_plancomp. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('planid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('competencyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table competency_plancomp. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table competency_plancomp. + $table->add_index('planidcompetencyid', XMLDB_INDEX_UNIQUE, array('planid', 'competencyid')); + + // Conditionally launch create table for competency_plancomp. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.62); + } + + if ($oldversion < 2016041500.63) { + + // Define table competency_evidence to be created. + $table = new xmldb_table('competency_evidence'); + + // Adding fields to table competency_evidence. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('usercompetencyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('action', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, null); + $table->add_field('actionuserid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('descidentifier', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('desccomponent', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('desca', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('url', XMLDB_TYPE_CHAR, '255', null, null, null, null); + $table->add_field('grade', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('note', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table competency_evidence. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table competency_evidence. + $table->add_index('usercompetencyid', XMLDB_INDEX_NOTUNIQUE, array('usercompetencyid')); + + // Conditionally launch create table for competency_evidence. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.63); + } + + if ($oldversion < 2016041500.64) { + + // Define table competency_userevidence to be created. + $table = new xmldb_table('competency_userevidence'); + + // Adding fields to table competency_userevidence. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('name', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); + $table->add_field('description', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('descriptionformat', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null); + $table->add_field('url', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table competency_userevidence. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table competency_userevidence. + $table->add_index('userid', XMLDB_INDEX_NOTUNIQUE, array('userid')); + + // Conditionally launch create table for competency_userevidence. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.64); + } + + if ($oldversion < 2016041500.65) { + + // Define table competency_userevidencecomp to be created. + $table = new xmldb_table('competency_userevidencecomp'); + + // Adding fields to table competency_userevidencecomp. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('userevidenceid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('competencyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table competency_userevidencecomp. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table competency_userevidencecomp. + $table->add_index('userevidenceid', XMLDB_INDEX_NOTUNIQUE, array('userevidenceid')); + $table->add_index('userevidencecompids', XMLDB_INDEX_UNIQUE, array('userevidenceid', 'competencyid')); + + // Conditionally launch create table for competency_userevidencecomp. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.65); + } + + if ($oldversion < 2016041500.66) { + + // Define table competency_modulecomp to be created. + $table = new xmldb_table('competency_modulecomp'); + + // Adding fields to table competency_modulecomp. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('cmid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('competencyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('ruleoutcome', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table competency_modulecomp. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('cmidkey', XMLDB_KEY_FOREIGN, array('cmid'), 'course_modules', array('id')); + $table->add_key('competencyidkey', XMLDB_KEY_FOREIGN, array('competencyid'), 'competency_competency', array('id')); + + // Adding indexes to table competency_modulecomp. + $table->add_index('cmidruleoutcome', XMLDB_INDEX_NOTUNIQUE, array('cmid', 'ruleoutcome')); + $table->add_index('cmidcompetencyid', XMLDB_INDEX_UNIQUE, array('cmid', 'competencyid')); + + // Conditionally launch create table for competency_modulecomp. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2016041500.66); + } + return true; } diff --git a/lib/grade/grade_scale.php b/lib/grade/grade_scale.php index cd1cb2345d5..190c344cefe 100644 --- a/lib/grade/grade_scale.php +++ b/lib/grade/grade_scale.php @@ -289,6 +289,11 @@ class grade_scale extends grade_object { return true; } + // Ask the competency subsystem. + if (\core_competency\api::is_scale_used_anywhere($scaleid)) { + return true; + } + // Ask all plugins if the scale is used anywhere. $pluginsfunction = get_plugins_with_function('scale_used_anywhere'); foreach ($pluginsfunction as $plugintype => $plugins) { diff --git a/lib/moodlelib.php b/lib/moodlelib.php index ff73eb368d4..b6c2998c696 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -4815,6 +4815,9 @@ function remove_course_contents($courseid, $showfeedback = true, array $options if ($cm = get_coursemodule_from_instance($modname, $instance->id, $course->id)) { // Delete activity context questions and question categories. question_delete_activity($cm, $showfeedback); + + // Notify the competency subsystem. + \core_competency\api::hook_course_module_deleted($cm); } if (function_exists($moddelete)) { // This purges all module data in related tables, extra user prefs, settings, etc. @@ -4932,6 +4935,9 @@ function remove_course_contents($courseid, $showfeedback = true, array $options // Delete course tags. core_tag_tag::remove_all_item_tags('core', 'course', $course->id); + // Notify the competency subsystem. + \core_competency\api::hook_course_deleted($course); + // Delete calendar events. $DB->delete_records('event', array('courseid' => $course->id)); $fs->delete_area_files($coursecontext->id, 'calendar'); @@ -5118,6 +5124,12 @@ function reset_course_userdata($data) { 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false); } + if (!empty($data->reset_competency_ratings)) { + \core_competency\api::hook_course_reset_competency_ratings($data->courseid); + $status[] = array('component' => $componentstr, + 'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false); + } + $componentstr = get_string('roles'); if (!empty($data->reset_roles_overrides)) { diff --git a/lib/outputrequirementslib.php b/lib/outputrequirementslib.php index b9a25384ce8..5bd654bdec9 100644 --- a/lib/outputrequirementslib.php +++ b/lib/outputrequirementslib.php @@ -296,6 +296,7 @@ class page_requirements_manager { // Every page should include definition of following modules. $this->js_module($this->find_module('core_filepicker')); + $this->js_module($this->find_module('core_comment')); } /** diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php index d570e27f887..b3294117219 100644 --- a/lib/tests/component_test.php +++ b/lib/tests/component_test.php @@ -34,7 +34,7 @@ class core_component_testcase extends advanced_testcase { // To be changed if number of subsystems increases/decreases, // this is defined here to annoy devs that try to add more without any thinking, // always verify that it does not collide with any existing add-on modules and subplugins!!! - const SUBSYSTEMCOUNT = 64; + const SUBSYSTEMCOUNT = 65; public function test_get_core_subsystems() { global $CFG; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bd5149de853..9e305afadf6 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -140,6 +140,9 @@ search/tests + + competency/tests + diff --git a/pix/t/editinline.png b/pix/t/editinline.png new file mode 100644 index 00000000000..f69dd624701 Binary files /dev/null and b/pix/t/editinline.png differ diff --git a/pix/t/editinline.svg b/pix/t/editinline.svg new file mode 100644 index 00000000000..5cba2c5d753 --- /dev/null +++ b/pix/t/editinline.svg @@ -0,0 +1,14 @@ + + + +]> + + + + + diff --git a/report/competency/amd/build/grading_popup.min.js b/report/competency/amd/build/grading_popup.min.js new file mode 100644 index 00000000000..0bb5cc4dc69 --- /dev/null +++ b/report/competency/amd/build/grading_popup.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/str","core/ajax","core/log","core/templates","tool_lp/dialogue"],function(a,b,c,d,e,f,g){var h=function(b,c){this._regionSelector=b,this._userCompetencySelector=c,a(this._regionSelector).on("click",this._userCompetencySelector,this._handleClick.bind(this))};return h.prototype._handleClick=function(c){var f=a(c.target).closest(this._userCompetencySelector),g=a(f).data("competencyid"),h=a(f).data("courseid"),i=a(f).data("userid");e.debug("Clicked on cell: competencyId="+g+", courseId="+h+", userId="+i);var j=d.call([{methodname:"tool_lp_data_for_user_competency_summary_in_course",args:{userid:i,competencyid:g,courseid:h},done:this._contextLoaded.bind(this),fail:b.exception}]);j[0].then(function(){d.call([{methodname:"core_competency_user_competency_viewed_in_course",args:{userid:i,competencyid:g,courseid:h},fail:b.exception}])})},h.prototype._contextLoaded=function(a){var d=this;a.displayuser=!0,f.render("tool_lp/user_competency_summary_in_course",a).done(function(a,e){c.get_string("usercompetencysummary","report_competency").done(function(b){new g(b,a,f.runTemplateJS.bind(f,e),d._refresh.bind(d),!0)}).fail(b.exception)}).fail(b.exception)},h.prototype._refresh=function(){var c=a(this._regionSelector),e=c.data("courseid"),f=c.data("userid");d.call([{methodname:"report_competency_data_for_report",args:{courseid:e,userid:f},done:this._pageContextLoaded.bind(this),fail:b.exception}])},h.prototype._pageContextLoaded=function(a){var c=this;f.render("report_competency/report",a).done(function(a,b){f.replaceNode(c._regionSelector,a,b)}).fail(b.exception)},h.prototype._regionSelector=null,h.prototype._userCompetencySelector=null,h}); \ No newline at end of file diff --git a/report/competency/amd/build/user_course_navigation.min.js b/report/competency/amd/build/user_course_navigation.min.js new file mode 100644 index 00000000000..5cfef53bbfe --- /dev/null +++ b/report/competency/amd/build/user_course_navigation.min.js @@ -0,0 +1 @@ +define(["jquery"],function(a){var b=function(b,c,d,e){this._baseUrl=c,this._userId=d+"",this._courseId=e,this._ignoreFirstUser=!0,a(b).on("change",this._userChanged.bind(this))};return b.prototype._userChanged=function(b){if(this._ignoreFirstUser)return void(this._ignoreFirstUser=!1);var c=a(b.target).val(),d="?user="+c+"&id="+this._courseId;document.location=this._baseUrl+d},b.prototype._userId=null,b.prototype._courseId=null,b.prototype._baseUrl=null,b.prototype._ignoreFirstUser=null,b}); \ No newline at end of file diff --git a/report/competency/amd/src/grading_popup.js b/report/competency/amd/src/grading_popup.js new file mode 100644 index 00000000000..38fcce8bb25 --- /dev/null +++ b/report/competency/amd/src/grading_popup.js @@ -0,0 +1,126 @@ +// 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 . + +/** + * Module to enable inline editing of a comptency grade. + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', 'core/notification', 'core/str', 'core/ajax', 'core/log', 'core/templates', 'tool_lp/dialogue'], + function($, notification, str, ajax, log, templates, Dialogue) { + + /** + * GradingPopup + * + * @param {String} The regionSelector + * @param {String} The userCompetencySelector + */ + var GradingPopup = function(regionSelector, userCompetencySelector) { + this._regionSelector = regionSelector; + this._userCompetencySelector = userCompetencySelector; + + $(this._regionSelector).on('click', this._userCompetencySelector, this._handleClick.bind(this)); + }; + + /** + * Get the data from the clicked cell and open the popup. + * + * @method _handleClick + * @param {Event} e + */ + GradingPopup.prototype._handleClick = function(e) { + var cell = $(e.target).closest(this._userCompetencySelector); + var competencyId = $(cell).data('competencyid'); + var courseId = $(cell).data('courseid'); + var userId = $(cell).data('userid'); + + log.debug('Clicked on cell: competencyId=' + competencyId + ', courseId=' + courseId + ', userId=' + userId); + + var requests = ajax.call([{ + methodname : 'tool_lp_data_for_user_competency_summary_in_course', + args: { userid: userId, competencyid: competencyId, courseid: courseId }, + done: this._contextLoaded.bind(this), + fail: notification.exception + }]); + + // Log the user competency viewed in course event. + requests[0].then(function(){ + ajax.call([{ + methodname : 'core_competency_user_competency_viewed_in_course', + args: { userid: userId, competencyid: competencyId, courseid: courseId }, + fail: notification.exception + }]); + }); + }; + + /** + * We loaded the context, now render the template. + * + * @method _contextLoaded + * @param {Object} context + */ + GradingPopup.prototype._contextLoaded = function(context) { + var self = this; + // We have to display user info in popup. + context.displayuser = true; + templates.render('tool_lp/user_competency_summary_in_course', context).done(function(html, js) { + str.get_string('usercompetencysummary', 'report_competency').done(function(title) { + (new Dialogue(title, html, templates.runTemplateJS.bind(templates, js), self._refresh.bind(self), true)); + }).fail(notification.exception); + }).fail(notification.exception); + }; + + /** + * Refresh the page. + * + * @method _refresh + */ + GradingPopup.prototype._refresh = function() { + var region = $(this._regionSelector); + var courseId = region.data('courseid'); + var userId = region.data('userid'); + + ajax.call([{ + methodname : 'report_competency_data_for_report', + args: { courseid: courseId, userid: userId }, + done: this._pageContextLoaded.bind(this), + fail: notification.exception + }]); + }; + + /** + * We loaded the context, now render the template. + * + * @method _pageContextLoaded + * @param {Object} context + */ + GradingPopup.prototype._pageContextLoaded = function(context) { + var self = this; + templates.render('report_competency/report', context).done(function(html, js) { + templates.replaceNode(self._regionSelector, html, js); + }).fail(notification.exception); + }; + + /** @type {String} The selector for the region with the user competencies */ + GradingPopup.prototype._regionSelector = null; + /** @type {String} The selector for the region with a single user competencies */ + GradingPopup.prototype._userCompetencySelector = null; + + return /** @alias module:report_competency/grading_popup */ GradingPopup; + +}); diff --git a/report/competency/amd/src/user_course_navigation.js b/report/competency/amd/src/user_course_navigation.js new file mode 100644 index 00000000000..07a1595096b --- /dev/null +++ b/report/competency/amd/src/user_course_navigation.js @@ -0,0 +1,71 @@ +// 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 . + +/** + * Module to navigation between users in a course. + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery'], function($) { + + /** + * UserCourseNavigation + * + * @param {String} The selector of the user element. + * @param {String} The base url for the page (no params). + * @param {Number} The course id + * @param {Number} The user id + */ + var UserCourseNavigation = function(userSelector, baseUrl, userId, courseId) { + this._baseUrl = baseUrl; + this._userId = userId + ''; + this._courseId = courseId; + this._ignoreFirstUser = true; + + $(userSelector).on('change', this._userChanged.bind(this)); + }; + + /** + * The user was changed in the select list. + * + * @method _userChanged + * @param {Event} e + */ + UserCourseNavigation.prototype._userChanged = function(e) { + if (this._ignoreFirstUser) { + this._ignoreFirstUser = false; + return; + } + + var newUserId = $(e.target).val(); + var queryStr = '?user=' + newUserId + '&id=' + this._courseId; + document.location = this._baseUrl + queryStr; + }; + + /** @type {Number} The id of the user. */ + UserCourseNavigation.prototype._userId = null; + /** @type {Number} The id of the course. */ + UserCourseNavigation.prototype._courseId = null; + /** @type {String} Plugin base url. */ + UserCourseNavigation.prototype._baseUrl = null; + /** @type {Boolean} Ignore the first change event for users. */ + UserCourseNavigation.prototype._ignoreFirstUser = null; + + return /** @alias module:report_competency/user_course_navigation */ UserCourseNavigation; + +}); diff --git a/report/competency/classes/external.php b/report/competency/classes/external.php new file mode 100644 index 00000000000..6d0bde16009 --- /dev/null +++ b/report/competency/classes/external.php @@ -0,0 +1,123 @@ +. + +/** + * This is the external API for this report. + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace report_competency; + +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->libdir/externallib.php"); + +use context_course; +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; +use core_competency\external\user_competency_course_exporter; +use core_competency\external\user_summary_exporter; +use tool_lp\external\competency_summary_exporter; +use tool_lp\external\course_summary_exporter; + +/** + * This is the external API for this report. + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class external extends external_api { + + /** + * Returns description of data_for_competency_frameworks_manage_page() parameters. + * + * @return \external_function_parameters + */ + public static function data_for_report_parameters() { + $courseid = new external_value( + PARAM_INT, + 'The course id', + VALUE_REQUIRED + ); + $userid = new external_value( + PARAM_INT, + 'The user id', + VALUE_REQUIRED + ); + $params = array( + 'courseid' => $courseid, + 'userid' => $userid + ); + return new external_function_parameters($params); + } + + /** + * Loads the data required to render the report. + * + * @param int $courseid The course id + * @param int $userid The user id + * @return \stdClass + */ + public static function data_for_report($courseid, $userid) { + global $PAGE; + + $params = self::validate_parameters( + self::data_for_report_parameters(), + array( + 'courseid' => $courseid, + 'userid' => $userid + ) + ); + $context = context_course::instance($params['courseid']); + self::validate_context($context); + if (!is_enrolled($context, $params['userid'], 'moodle/competency:coursecompetencygradable')) { + throw new coding_exception('invaliduser'); + } + + $renderable = new output\report($params['courseid'], $params['userid']); + $renderer = $PAGE->get_renderer('report_competency'); + + $data = $renderable->export_for_template($renderer); + + return $data; + } + + /** + * Returns description of data_for_report() result value. + * + * @return \external_description + */ + public static function data_for_report_returns() { + return new external_single_structure(array ( + 'courseid' => new external_value(PARAM_INT, 'Course id'), + 'user' => user_summary_exporter::get_read_structure(), + 'course' => course_summary_exporter::get_read_structure(), + 'usercompetencies' => new external_multiple_structure( + new external_single_structure(array( + 'usercompetencycourse' => user_competency_course_exporter::get_read_structure(), + 'competency' => competency_summary_exporter::get_read_structure() + )) + ), + 'pushratingstouserplans' => new external_value(PARAM_BOOL, 'True if rating is push to user plans') + )); + } + +} diff --git a/report/competency/classes/output/renderer.php b/report/competency/classes/output/renderer.php new file mode 100644 index 00000000000..b75013c66f2 --- /dev/null +++ b/report/competency/classes/output/renderer.php @@ -0,0 +1,98 @@ +. + +/** + * Renderer class for report_competency + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace report_competency\output; + +defined('MOODLE_INTERNAL') || die; + +use plugin_renderer_base; +use renderable; + +/** + * Renderer class for competency breakdown report + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Defer to template. + * + * @param report $page + * @return string html for the page + */ + public function render_report(report $page) { + $data = $page->export_for_template($this); + return parent::render_from_template('report_competency/report', $data); + } + + /** + * Defer to template. + * + * @param user_course_navigation $nav + * @return string + */ + public function render_user_course_navigation(user_course_navigation $nav) { + $data = $nav->export_for_template($this); + return parent::render_from_template('report_competency/user_course_navigation', $data); + } + + /** + * 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); + } +} diff --git a/report/competency/classes/output/report.php b/report/competency/classes/output/report.php new file mode 100644 index 00000000000..c3e296e5635 --- /dev/null +++ b/report/competency/classes/output/report.php @@ -0,0 +1,146 @@ +. + +/** + * Class containing data for learning plan template competencies page + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace report_competency\output; + +use context_course; +use renderable; +use core_user; +use templatable; +use renderer_base; +use moodle_url; +use stdClass; +use core_competency\api; +use core_competency\external\user_competency_course_exporter; +use core_competency\external\user_summary_exporter; +use core_competency\url; +use core_competency\user_competency; +use tool_lp\external\competency_summary_exporter; +use tool_lp\external\course_summary_exporter; + +/** + * Class containing data for learning plan template competencies page + * + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report implements renderable, templatable { + + /** @var context $context */ + protected $context; + /** @var int $courseid */ + protected $courseid; + /** @var array $competencies */ + protected $competencies; + + /** + * Construct this renderable. + * + * @param int $courseid The course id + * @param int $userid The user id + */ + public function __construct($courseid, $userid) { + $this->courseid = $courseid; + $this->userid = $userid; + $this->context = context_course::instance($courseid); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + global $DB; + + $data = new stdClass(); + $data->courseid = $this->courseid; + + $course = $DB->get_record('course', array('id' => $this->courseid)); + $coursecontext = context_course::instance($course->id); + $exporter = new course_summary_exporter($course, array('context' => $coursecontext)); + $coursecompetencysettings = api::read_course_competency_settings($course->id); + $data->pushratingstouserplans = $coursecompetencysettings->get_pushratingstouserplans(); + $data->course = $exporter->export($output); + + $data->usercompetencies = array(); + $scalecache = array(); + $frameworkcache = array(); + + $user = core_user::get_user($this->userid); + + $exporter = new user_summary_exporter($user); + $data->user = $exporter->export($output); + $data->usercompetencies = array(); + $coursecompetencies = api::list_course_competencies($this->courseid); + $usercompetencycourses = api::list_user_competencies_in_course($this->courseid, $user->id); + + foreach ($usercompetencycourses as $usercompetencycourse) { + $onerow = new stdClass(); + $competency = null; + foreach ($coursecompetencies as $coursecompetency) { + if ($coursecompetency['competency']->get_id() == $usercompetencycourse->get_competencyid()) { + $competency = $coursecompetency['competency']; + break; + } + } + if (!$competency) { + continue; + } + // Fetch the framework. + if (!isset($frameworkcache[$competency->get_competencyframeworkid()])) { + $frameworkcache[$competency->get_competencyframeworkid()] = $competency->get_framework(); + } + $framework = $frameworkcache[$competency->get_competencyframeworkid()]; + + // Fetch the scale. + $scaleid = $competency->get_scaleid(); + if ($scaleid === null) { + $scaleid = $framework->get_scaleid(); + if (!isset($scalecache[$scaleid])) { + $scalecache[$competency->get_scaleid()] = $framework->get_scale(); + } + + } else if (!isset($scalecache[$scaleid])) { + $scalecache[$competency->get_scaleid()] = $competency->get_scale(); + } + $scale = $scalecache[$competency->get_scaleid()]; + + $exporter = new user_competency_course_exporter($usercompetencycourse, array('scale' => $scale)); + $record = $exporter->export($output); + $onerow->usercompetencycourse = $record; + $exporter = new competency_summary_exporter(null, array( + 'competency' => $competency, + 'framework' => $framework, + 'context' => $framework->get_context(), + 'relatedcompetencies' => array(), + 'linkedcourses' => array() + )); + $onerow->competency = $exporter->export($output); + array_push($data->usercompetencies, $onerow); + } + + return $data; + } +} diff --git a/report/competency/classes/output/user_course_navigation.php b/report/competency/classes/output/user_course_navigation.php new file mode 100644 index 00000000000..6b7a62b5cf5 --- /dev/null +++ b/report/competency/classes/output/user_course_navigation.php @@ -0,0 +1,116 @@ +. + +/** + * User navigation class. + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace report_competency\output; + +use renderable; +use renderer_base; +use templatable; +use context_course; +use core_competency\external\user_summary_exporter; +use stdClass; + +/** + * User course navigation class. + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_course_navigation implements renderable, templatable { + + /** @var userid */ + protected $userid; + + /** @var courseid */ + protected $courseid; + + /** @var baseurl */ + protected $baseurl; + + /** + * Construct. + * + * @param int $userid + * @param int $courseid + * @param string $baseurl + */ + public function __construct($userid, $courseid, $baseurl) { + $this->userid = $userid; + $this->courseid = $courseid; + $this->baseurl = $baseurl; + } + + /** + * Export the data. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + global $CFG, $DB, $SESSION, $PAGE; + + $context = context_course::instance($this->courseid); + + $data = new stdClass(); + $data->userid = $this->userid; + $data->courseid = $this->courseid; + $data->baseurl = $this->baseurl; + $data->groupselector = ''; + + if (has_capability('moodle/competency:coursecompetencymanage', $context)) { + $course = $DB->get_record('course', array('id' => $this->courseid)); + $currentgroup = groups_get_course_group($course, true); + if ($currentgroup !== false) { + $select = groups_allgroups_course_menu($course, $PAGE->url, true, $currentgroup); + $data->groupselector = $select; + } + // Fetch showactive. + $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol); + $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol); + $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $context); + + // Fetch current active group. + $groupmode = groups_get_course_groupmode($course); + + $users = get_enrolled_users($context, 'moodle/competency:coursecompetencygradable', $currentgroup, + 'u.*', null, 0, 0, $showonlyactiveenrol); + + $data->users = array(); + foreach ($users as $user) { + $exporter = new user_summary_exporter($user); + $user = $exporter->export($output); + if ($user->id == $this->userid) { + $user->selected = true; + } + $data->users[] = $user; + } + $data->hasusers = true; + } else { + $data->users = array(); + $data->hasusers = false; + } + + return $data; + } +} diff --git a/report/competency/db/services.php b/report/competency/db/services.php new file mode 100644 index 00000000000..dc134648fee --- /dev/null +++ b/report/competency/db/services.php @@ -0,0 +1,43 @@ +. + + +/** + * Competency report webservice functions + * + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = array( + + // Learning plan related functions. + + 'report_competency_data_for_report' => array( + 'classname' => 'report_competency\external', + 'methodname' => 'data_for_report', + 'classpath' => '', + 'description' => 'Load the data for the competency report in a course.', + 'type' => 'read', + 'capabilities' => 'moodle/competency:coursecompetencyview', + 'ajax' => true, + ) +); + diff --git a/report/competency/index.php b/report/competency/index.php new file mode 100644 index 00000000000..656619a179a --- /dev/null +++ b/report/competency/index.php @@ -0,0 +1,89 @@ +. + +/** + * This page lets users to manage site wide competencies. + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../config.php'); + +$id = required_param('id', PARAM_INT); + +$params = array('id' => $id); +$course = $DB->get_record('course', $params, '*', MUST_EXIST); +require_login($course); +$context = context_course::instance($course->id); +$currentuser = optional_param('user', null, PARAM_INT); + +// Fetch current active group. +$groupmode = groups_get_course_groupmode($course); +$currentgroup = groups_get_course_group($course, true); +if (empty($currentuser)) { + $gradable = get_enrolled_users($context, 'moodle/competency:coursecompetencygradable', $currentgroup, 'u.id', null, 0, 1); + if (empty($gradable)) { + $currentuser = 0; + } else { + $currentuser = array_pop($gradable)->id; + } +} else { + $gradable = get_enrolled_users($context, 'moodle/competency:coursecompetencygradable', $currentgroup, 'u.id'); + if (count($gradable) == 0) { + $currentuser = 0; + } else if (!in_array($currentuser, array_keys($gradable))) { + $currentuser = array_shift($gradable)->id; + } +} + +$urlparams = array('id' => $id, 'user' => $currentuser); + +$url = new moodle_url('/report/competency/index.php', $urlparams); +$title = get_string('pluginname', 'report_competency'); +$PAGE->set_url($url); +$PAGE->set_title($title); +$coursename = format_text($course->fullname, false, array('context' => $context)); +$PAGE->set_heading($coursename); +$PAGE->set_pagelayout('incourse'); + +$output = $PAGE->get_renderer('report_competency'); + +echo $output->header(); +$baseurl = new moodle_url('/report/competency/index.php'); +$nav = new \report_competency\output\user_course_navigation($currentuser, $course->id, $baseurl); +echo $output->render($nav); +if ($currentuser > 0) { + $user = core_user::get_user($currentuser); + $usercontext = context_user::instance($currentuser); + $userheading = array( + 'heading' => fullname($user), + 'user' => $user, + 'usercontext' => $usercontext + ); + echo $output->context_header($userheading, 3); +} +echo $output->heading($title, 3); + +if ($currentuser > 0) { + $page = new \report_competency\output\report($course->id, $currentuser); + echo $output->render($page); +} else { + echo $output->container('', 'clearfix'); + echo $output->notify_problem(get_string('noparticipants', 'tool_lp')); +} +echo $output->footer(); diff --git a/report/competency/lang/en/report_competency.php b/report/competency/lang/en/report_competency.php new file mode 100644 index 00000000000..ace8284ee62 --- /dev/null +++ b/report/competency/lang/en/report_competency.php @@ -0,0 +1,29 @@ +. + +/** + * Strings for component 'report_competency', language 'en' + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['competency'] = 'Competency'; +$string['coursecompetencybreakdownsummary'] = 'A report of all the students in the course, and their progress towards the course competencies'; +$string['pluginname'] = 'Competency breakdown'; +$string['rating'] = 'Rating'; +$string['usercompetencysummary'] = 'User competency summary'; diff --git a/report/competency/lib.php b/report/competency/lib.php new file mode 100644 index 00000000000..35bd488f875 --- /dev/null +++ b/report/competency/lib.php @@ -0,0 +1,42 @@ +. + +/** + * Public API of the competency report. + * + * Defines the APIs used by competency reports + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * This function extends the navigation with the report items + * + * @param navigation_node $navigation The navigation node to extend + * @param stdClass $course The course to object for the report + * @param stdClass $context The context of the course + */ +function report_competency_extend_navigation_course($navigation, $course, $context) { + if (has_capability('moodle/competency:coursecompetencyview', $context)) { + $url = new moodle_url('/report/competency/index.php', array('id' => $course->id)); + $name = get_string('pluginname', 'report_competency'); + $navigation->add($name, $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/report', '')); + } +} diff --git a/report/competency/styles.css b/report/competency/styles.css new file mode 100644 index 00000000000..1018db33f93 --- /dev/null +++ b/report/competency/styles.css @@ -0,0 +1,21 @@ +.dir-rtl.path-report-competency [data-region="competency-breakdown-report"] .row-fluid [class*="span"] { + float: right; +} + +/** Broader RTL changes. */ +.dir-rtl.path-report-competency .pull-left { + float: right; +} +.dir-rtl.path-report-competency .pull-right { + float: left; +} +.dir-rtl.path-report-competency dd { + margin-right: 10px; +} +.dir-rtl.path-report-competency ul.inline { + margin-right: 0; +} +.dir-rtl.path-report-competency .table th, +.dir-rtl.path-report-competency .table td { + text-align: right; +} diff --git a/report/competency/templates/report.mustache b/report/competency/templates/report.mustache new file mode 100644 index 00000000000..f0254687ff2 --- /dev/null +++ b/report/competency/templates/report.mustache @@ -0,0 +1,60 @@ +
    +
    +{{#pushratingstouserplans}} +

    + {{#str}}coursecompetencyratingsarepushedtouserplans, tool_lp{{/str}} +

    +{{/pushratingstouserplans}} +{{^pushratingstouserplans}} +

    + {{#str}}coursecompetencyratingsarenotpushedtouserplans, tool_lp{{/str}} +

    +{{/pushratingstouserplans}} +
    +
    + + + +

    {{#str}}coursecompetencybreakdownsummary, report_competency{{/str}}

    +
    + + + + + {{#usercompetencies}} + + {{#competency}} + + {{/competency}} + {{#usercompetencycourse}} + + {{/usercompetencycourse}} + + {{/usercompetencies}} +
    + {{#str}}competency, report_competency{{/str}} + + {{#str}}rating, report_competency{{/str}} +
    + + {{competency.shortname}} {{competency.idnumber}} + + + {{> report_competency/user_competency_summary}} +
    +
    +
    +
    +{{#js}} +require(['tool_lp/competencydialogue'], function(Compdialogue) { + var competencydialogue = new Compdialogue({includerelated : true}); + competencydialogue.watch('[data-region="competency-breakdown-report"]'); +}); +require(['report_competency/grading_popup'], function(Popup) { + (new Popup('[data-region=competency-breakdown-report]', '[data-user-competency=true]')); +}); +{{/js}} diff --git a/report/competency/templates/user_competency_summary.mustache b/report/competency/templates/user_competency_summary.mustache new file mode 100644 index 00000000000..055f4a9e373 --- /dev/null +++ b/report/competency/templates/user_competency_summary.mustache @@ -0,0 +1 @@ +{{#grade}}{{gradename}}{{/grade}}{{^grade}}Not rated{{/grade}} diff --git a/report/competency/templates/user_course_navigation.mustache b/report/competency/templates/user_course_navigation.mustache new file mode 100644 index 00000000000..6531653f64f --- /dev/null +++ b/report/competency/templates/user_course_navigation.mustache @@ -0,0 +1,23 @@ +
    +

    {{{groupselector}}}

    +
    +{{#hasusers}} + + + + +{{/hasusers}} +
    +
    +{{#js}} +require(['core/form-autocomplete', 'report_competency/user_course_navigation'], function(autocomplete, nav) { + (new nav('#user-nav-{{uniqid}}', '{{baseurl}}', {{userid}}, {{courseid}})); +{{#hasusers}} + autocomplete.enhance('#user-nav-{{uniqid}}', false, false, '{{#str}}jumptouser, tool_lp{{/str}}'); +{{/hasusers}} +}); +{{/js}} diff --git a/report/competency/version.php b/report/competency/version.php new file mode 100644 index 00000000000..2f634e79f57 --- /dev/null +++ b/report/competency/version.php @@ -0,0 +1,33 @@ +. + +/** + * Plugin version info + * + * @package report_competency + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +$plugin->version = 2015111020; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2014110400; // Requires this Moodle version. +$plugin->component = 'report_competency'; // Full name of the plugin (used for diagnostics). +$plugin->dependencies = array( + 'tool_lp' => ANY_VERSION +); diff --git a/user/lib.php b/user/lib.php index a063ea363a7..2948ac97911 100644 --- a/user/lib.php +++ b/user/lib.php @@ -22,7 +22,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - /** * Creates a user * diff --git a/version.php b/version.php index a7bbdee35bb..7a328f301e6 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2016041500.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2016041500.67; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.