See Release Notes
Long Term Support Release
<?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for 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 { /** @var boolean Allow api functions even if competencies are not enabled for the site. */ private static $skipenabled = false; /** * Returns whether competencies are enabled. * * This method should never do more than checking the config setting, the reason * being that some other code could be checking the config value directly * to avoid having to load this entire file into memory. * * @return boolean True when enabled. */ public static function is_enabled() { return self::$skipenabled || get_config('core_competency', 'enabled'); } /** * When competencies used to be enabled, we can show the text but do not include links. * * @return boolean True means show links. */ public static function show_links() { return isloggedin() && !isguestuser() && get_config('core_competency', 'enabled'); } /** * Allow calls to competency api functions even if competencies are not currently enabled. */ public static function skip_enabled() { self::$skipenabled = true; } /** * Restore the checking that competencies are enabled with any api function. */ public static function check_enabled() { self::$skipenabled = false; } /** * 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', 0); $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> * @return competency*/ 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; } /** * Count the competencies associated to a course module. * * @param mixed $cmorid The course module, or its ID. * @return int */ public static function count_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', ''); } return course_module_competency::count_competencies($cm->id); } /** * 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_module_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', 'uc_'); $compfields = competency::get_sql_fields('c', '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)) AND u.deleted = 0"; $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, 'uc_')), 'competency' => new competency(0, competency::extract_record($record, 'c_')), '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. * @param bool $overridegrade If true, will override existing grades in related competencies. * @return bool True on success. */ public static function set_course_module_competency_ruleoutcome($coursemodulecompetencyorid, $ruleoutcome, $overridegrade = false) { 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); $coursemodulecompetency->set('overridegrade', $overridegrade); 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; $coursecompetency = new course_competency(); $exists = course_competency::get_record(array('courseid' => $courseid, 'competencyid' => $competencyid)); if ($exists) { // Delete all course_module_competencies for this competency in this course. $cmcs = course_module_competency::get_records_by_competencyid_in_course($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(); $template = new template($templateid); // First we do a permissions check. if (!$template->can_manage()) { throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', 'nopermissions', ''); } $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', 'plan_'); $tplfields = template::get_sql_fields('t', 'tpl_'); $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; // Order by ID just to have some ordering in place. $ordersql = " ORDER BY p.id ASC"; $plans = array(); $records = $DB->get_recordset_sql($select . $sql . $ordersql, $params, $skip, $limit); foreach ($records as $record) { $plan = new plan(0, plan::extract_record($record, 'plan_')); $template = null; if ($plan->is_based_on_template()) { $template = new template(0, template::extract_record($record, 'tpl_')); } $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 plans with a competency. * * @param int $userid The user id we want the plans for. * @param int $competencyorid The competency, or its ID. * @return array[plan] Array of learning plans. */ public static function list_plans_with_competency($userid, $competencyorid) { global $USER; static::require_enabled(); $competencyid = $competencyorid; $competency = null; if (is_object($competencyid)) { $competency = $competencyid; $competencyid = $competency->get('id'); } $plans = plan::get_by_user_and_competency($userid, $competencyid); foreach ($plans as $index => $plan) { // Filter plans we cannot read. if (!$plan->can_read()) { unset($plans[$index]); } } return $plans; } /** * 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 (property_exists($data, 'userid') && $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 stdClass[] $tree - list of framework competency nodes* @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 array $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.> * @param plan $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.> * @param plan $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(); } $context = context_course::instance($courseid); return evidence::get_records_for_usercompetency($usercompetency->get('id'), $context, $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, $overridegrade = false) { 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 or the override option is enabled. if ($usercompetencycourse->get('grade') === null || $overridegrade) { // 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 or the override option is enabled. if ($usercompetency->get('grade') !== null && !$overridegrade) { $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 or if override option is enabled. // Complete is an action to use with automated systems. if ($usercompetency->get('grade') === null || $overridegrade) { $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, $overridegrade); } 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, $overridegrade = false) { // 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, null, null, null, null, $overridegrade ); } /** * 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'; $overridegrade = $coursemodulecompetency->get('overridegrade'); if ($outcome == course_module_competency::OUTCOME_NONE) { continue; } 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, null, null, null, $overridegrade ); } } } /** * 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_module_competency::OUTCOME_NONE) { continue; } 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)); } /** * Action to perform when a cohort is deleted. * * Do not call this directly, this is reserved for core use. * * @param \stdClass $cohort The cohort object. * @return void */ public static function hook_cohort_deleted(\stdClass $cohort) { global $DB; $DB->delete_records(template_cohort::TABLE, array('cohortid' => $cohort->id)); } /** * Action to perform when a user is deleted. * * @param int $userid The user id. */ public static function hook_user_deleted($userid) { global $DB; $usercompetencies = $DB->get_records(user_competency::TABLE, ['userid' => $userid], '', 'id'); foreach ($usercompetencies as $usercomp) { $DB->delete_records(evidence::TABLE, ['usercompetencyid' => $usercomp->id]); } $DB->delete_records(user_competency::TABLE, ['userid' => $userid]); $DB->delete_records(user_competency_course::TABLE, ['userid' => $userid]); $DB->delete_records(user_competency_plan::TABLE, ['userid' => $userid]); // Delete any associated files. $fs = get_file_storage(); $context = context_user::instance($userid); $userevidences = $DB->get_records(user_evidence::TABLE, ['userid' => $userid], '', 'id'); foreach ($userevidences as $userevidence) { $DB->delete_records(user_evidence_competency::TABLE, ['userevidenceid' => $userevidence->id]); $DB->delete_records(user_evidence::TABLE, ['id' => $userevidence->id]); $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $userevidence->id); } $userplans = $DB->get_records(plan::TABLE, ['userid' => $userid], '', 'id'); foreach ($userplans as $userplan) { $DB->delete_records(plan_competency::TABLE, ['planid' => $userplan->id]); $DB->delete_records(plan::TABLE, ['id' => $userplan->id]); } } /** * 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:coursecompetencyview', 'moodle/competency:coursecompetencymanage'), $coursecontext)) { throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', '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_roles_sitewide_accessdata($userid); // Build up a list of level 2 contexts (candidates to be user context). $filtercontexts = array(); // Build list of roles to check overrides. $roles = 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]]); } $roles = array_merge($roles, $role); } // Add all contexts in which a role may be overidden. $rdefs = get_role_definitions($roles); foreach ($rdefs as $roledef) { foreach ($roledef as $path => $caps) { if (!isset($caps[$capability])) { // The capability is not mentioned, we can ignore. continue; } $parts = explode('/', $path); if (count($parts) === 3) { // Only get potential user contexts, they only ever have 2 slashes /parentId/Id. $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); } }