"Fossies" - the Fresh Open Source Software Archive

Member "moodle/competency/classes/api.php" (6 Sep 2019, 204975 Bytes) of package /linux/www/moodle-3.6.6.tgz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "api.php" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 3.6.5_vs_3.6.6.

    1 <?php
    2 // This file is part of Moodle - http://moodle.org/
    3 //
    4 // Moodle is free software: you can redistribute it and/or modify
    5 // it under the terms of the GNU General Public License as published by
    6 // the Free Software Foundation, either version 3 of the License, or
    7 // (at your option) any later version.
    8 //
    9 // Moodle is distributed in the hope that it will be useful,
   10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
   11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   12 // GNU General Public License for more details.
   13 //
   14 // You should have received a copy of the GNU General Public License
   15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
   16 
   17 /**
   18  * Class for loading/storing competency frameworks from the DB.
   19  *
   20  * @package    core_competency
   21  * @copyright  2015 Damyon Wiese
   22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
   23  */
   24 namespace core_competency;
   25 defined('MOODLE_INTERNAL') || die();
   26 
   27 use stdClass;
   28 use cm_info;
   29 use context;
   30 use context_helper;
   31 use context_system;
   32 use context_course;
   33 use context_module;
   34 use context_user;
   35 use coding_exception;
   36 use require_login_exception;
   37 use moodle_exception;
   38 use moodle_url;
   39 use required_capability_exception;
   40 
   41 /**
   42  * Class for doing things with competency frameworks.
   43  *
   44  * @copyright  2015 Damyon Wiese
   45  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
   46  */
   47 class api {
   48 
   49     /**
   50      * Returns whether competencies are enabled.
   51      *
   52      * This method should never do more than checking the config setting, the reason
   53      * being that some other code could be checking the config value directly
   54      * to avoid having to load this entire file into memory.
   55      *
   56      * @return boolean True when enabled.
   57      */
   58     public static function is_enabled() {
   59         return get_config('core_competency', 'enabled');
   60     }
   61 
   62     /**
   63      * Throws an exception if competencies are not enabled.
   64      *
   65      * @return void
   66      * @throws moodle_exception
   67      */
   68     public static function require_enabled() {
   69         if (!static::is_enabled()) {
   70             throw new moodle_exception('competenciesarenotenabled', 'core_competency');
   71         }
   72     }
   73 
   74     /**
   75      * Checks whether a scale is used anywhere in the plugin.
   76      *
   77      * This public API has two exceptions:
   78      * - It MUST NOT perform any capability checks.
   79      * - It MUST ignore whether competencies are enabled or not ({@link self::is_enabled()}).
   80      *
   81      * @param int $scaleid The scale ID.
   82      * @return bool
   83      */
   84     public static function is_scale_used_anywhere($scaleid) {
   85         global $DB;
   86         $sql = "SELECT s.id
   87                   FROM {scale} s
   88              LEFT JOIN {" . competency_framework::TABLE ."} f
   89                     ON f.scaleid = :scaleid1
   90              LEFT JOIN {" . competency::TABLE ."} c
   91                     ON c.scaleid = :scaleid2
   92                  WHERE f.id IS NOT NULL
   93                     OR c.id IS NOT NULL";
   94         return $DB->record_exists_sql($sql, ['scaleid1' => $scaleid, 'scaleid2' => $scaleid]);
   95     }
   96 
   97     /**
   98      * Validate if current user have acces to the course_module if hidden.
   99      *
  100      * @param mixed $cmmixed The cm_info class, course module record or its ID.
  101      * @param bool $throwexception Throw an exception or not.
  102      * @return bool
  103      */
  104     protected static function validate_course_module($cmmixed, $throwexception = true) {
  105         $cm = $cmmixed;
  106         if (!is_object($cm)) {
  107             $cmrecord = get_coursemodule_from_id(null, $cmmixed);
  108             $modinfo = get_fast_modinfo($cmrecord->course);
  109             $cm = $modinfo->get_cm($cmmixed);
  110         } else if (!$cm instanceof cm_info) {
  111             // Assume we got a course module record.
  112             $modinfo = get_fast_modinfo($cm->course);
  113             $cm = $modinfo->get_cm($cm->id);
  114         }
  115 
  116         if (!$cm->uservisible) {
  117             if ($throwexception) {
  118                 throw new require_login_exception('Course module is hidden');
  119             } else {
  120                 return false;
  121             }
  122         }
  123 
  124         return true;
  125     }
  126 
  127     /**
  128      * Validate if current user have acces to the course if hidden.
  129      *
  130      * @param mixed $courseorid The course or it ID.
  131      * @param bool $throwexception Throw an exception or not.
  132      * @return bool
  133      */
  134     protected static function validate_course($courseorid, $throwexception = true) {
  135         $course = $courseorid;
  136         if (!is_object($course)) {
  137             $course = get_course($course);
  138         }
  139 
  140         $coursecontext = context_course::instance($course->id);
  141         if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
  142             if ($throwexception) {
  143                 throw new require_login_exception('Course is hidden');
  144             } else {
  145                 return false;
  146             }
  147         }
  148 
  149         return true;
  150     }
  151 
  152     /**
  153      * Create a competency from a record containing all the data for the class.
  154      *
  155      * Requires moodle/competency:competencymanage capability at the system context.
  156      *
  157      * @param stdClass $record Record containing all the data for an instance of the class.
  158      * @return competency
  159      */
  160     public static function create_competency(stdClass $record) {
  161         static::require_enabled();
  162         $competency = new competency(0, $record);
  163 
  164         // First we do a permissions check.
  165         require_capability('moodle/competency:competencymanage', $competency->get_context());
  166 
  167         // Reset the sortorder, use reorder instead.
  168         $competency->set('sortorder', 0);
  169         $competency->create();
  170 
  171         \core\event\competency_created::create_from_competency($competency)->trigger();
  172 
  173         // Reset the rule of the parent.
  174         $parent = $competency->get_parent();
  175         if ($parent) {
  176             $parent->reset_rule();
  177             $parent->update();
  178         }
  179 
  180         return $competency;
  181     }
  182 
  183     /**
  184      * Delete a competency by id.
  185      *
  186      * Requires moodle/competency:competencymanage capability at the system context.
  187      *
  188      * @param int $id The record to delete. This will delete alot of related data - you better be sure.
  189      * @return boolean
  190      */
  191     public static function delete_competency($id) {
  192         global $DB;
  193         static::require_enabled();
  194         $competency = new competency($id);
  195 
  196         // First we do a permissions check.
  197         require_capability('moodle/competency:competencymanage', $competency->get_context());
  198 
  199         $events = array();
  200         $competencyids = array(intval($competency->get('id')));
  201         $contextid = $competency->get_context()->id;
  202         $competencyids = array_merge(competency::get_descendants_ids($competency), $competencyids);
  203         if (!competency::can_all_be_deleted($competencyids)) {
  204             return false;
  205         }
  206         $transaction = $DB->start_delegated_transaction();
  207 
  208         try {
  209 
  210             // Reset the rule of the parent.
  211             $parent = $competency->get_parent();
  212             if ($parent) {
  213                 $parent->reset_rule();
  214                 $parent->update();
  215             }
  216 
  217             // Delete the competency separately so the after_delete event can be triggered.
  218             $competency->delete();
  219 
  220             // Delete the competencies.
  221             competency::delete_multiple($competencyids);
  222 
  223             // Delete the competencies relation.
  224             related_competency::delete_multiple_relations($competencyids);
  225 
  226             // Delete competency evidences.
  227             user_evidence_competency::delete_by_competencyids($competencyids);
  228 
  229             // Register the competencies deleted events.
  230             $events = \core\event\competency_deleted::create_multiple_from_competencyids($competencyids, $contextid);
  231 
  232         } catch (\Exception $e) {
  233             $transaction->rollback($e);
  234         }
  235 
  236         $transaction->allow_commit();
  237         // Trigger events.
  238         foreach ($events as $event) {
  239             $event->trigger();
  240         }
  241 
  242         return true;
  243     }
  244 
  245     /**
  246      * Reorder this competency.
  247      *
  248      * Requires moodle/competency:competencymanage capability at the system context.
  249      *
  250      * @param int $id The id of the competency to move.
  251      * @return boolean
  252      */
  253     public static function move_down_competency($id) {
  254         static::require_enabled();
  255         $current = new competency($id);
  256 
  257         // First we do a permissions check.
  258         require_capability('moodle/competency:competencymanage', $current->get_context());
  259 
  260         $max = self::count_competencies(array('parentid' => $current->get('parentid'),
  261                                               'competencyframeworkid' => $current->get('competencyframeworkid')));
  262         if ($max > 0) {
  263             $max--;
  264         }
  265 
  266         $sortorder = $current->get('sortorder');
  267         if ($sortorder >= $max) {
  268             return false;
  269         }
  270         $sortorder = $sortorder + 1;
  271         $current->set('sortorder', $sortorder);
  272 
  273         $filters = array('parentid' => $current->get('parentid'),
  274                          'competencyframeworkid' => $current->get('competencyframeworkid'),
  275                          'sortorder' => $sortorder);
  276         $children = self::list_competencies($filters, 'id');
  277         foreach ($children as $needtoswap) {
  278             $needtoswap->set('sortorder', $sortorder - 1);
  279             $needtoswap->update();
  280         }
  281 
  282         // OK - all set.
  283         $result = $current->update();
  284 
  285         return $result;
  286     }
  287 
  288     /**
  289      * Reorder this competency.
  290      *
  291      * Requires moodle/competency:competencymanage capability at the system context.
  292      *
  293      * @param int $id The id of the competency to move.
  294      * @return boolean
  295      */
  296     public static function move_up_competency($id) {
  297         static::require_enabled();
  298         $current = new competency($id);
  299 
  300         // First we do a permissions check.
  301         require_capability('moodle/competency:competencymanage', $current->get_context());
  302 
  303         $sortorder = $current->get('sortorder');
  304         if ($sortorder == 0) {
  305             return false;
  306         }
  307 
  308         $sortorder = $sortorder - 1;
  309         $current->set('sortorder', $sortorder);
  310 
  311         $filters = array('parentid' => $current->get('parentid'),
  312                          'competencyframeworkid' => $current->get('competencyframeworkid'),
  313                          'sortorder' => $sortorder);
  314         $children = self::list_competencies($filters, 'id');
  315         foreach ($children as $needtoswap) {
  316             $needtoswap->set('sortorder', $sortorder + 1);
  317             $needtoswap->update();
  318         }
  319 
  320         // OK - all set.
  321         $result = $current->update();
  322 
  323         return $result;
  324     }
  325 
  326     /**
  327      * Move this competency so it sits in a new parent.
  328      *
  329      * Requires moodle/competency:competencymanage capability at the system context.
  330      *
  331      * @param int $id The id of the competency to move.
  332      * @param int $newparentid The new parent id for the competency.
  333      * @return boolean
  334      */
  335     public static function set_parent_competency($id, $newparentid) {
  336         global $DB;
  337         static::require_enabled();
  338         $current = new competency($id);
  339 
  340         // First we do a permissions check.
  341         require_capability('moodle/competency:competencymanage', $current->get_context());
  342         if ($id == $newparentid) {
  343             throw new coding_exception('Can not set a competency as a parent of itself.');
  344         } if ($newparentid == $current->get('parentid')) {
  345             throw new coding_exception('Can not move a competency to the same location.');
  346         }
  347 
  348         // Some great variable assignment right here.
  349         $currentparent = $current->get_parent();
  350         $parent = !empty($newparentid) ? new competency($newparentid) : null;
  351         $parentpath = !empty($parent) ? $parent->get('path') : '/0/';
  352 
  353         // We're going to change quite a few things.
  354         $transaction = $DB->start_delegated_transaction();
  355 
  356         // If we are moving a node to a child of itself:
  357         // - promote all the child nodes by one level.
  358         // - remove the rule on self.
  359         // - re-read the parent.
  360         $newparents = explode('/', $parentpath);
  361         if (in_array($current->get('id'), $newparents)) {
  362             $children = competency::get_records(array('parentid' => $current->get('id')), 'id');
  363             foreach ($children as $child) {
  364                 $child->set('parentid', $current->get('parentid'));
  365                 $child->update();
  366             }
  367 
  368             // Reset the rule on self as our children have changed.
  369             $current->reset_rule();
  370 
  371             // The destination parent is one of our descendants, we need to re-fetch its values (path, parentid).
  372             $parent->read();
  373         }
  374 
  375         // Reset the rules of initial parent and destination.
  376         if (!empty($currentparent)) {
  377             $currentparent->reset_rule();
  378             $currentparent->update();
  379         }
  380         if (!empty($parent)) {
  381             $parent->reset_rule();
  382             $parent->update();
  383         }
  384 
  385         // Do the actual move.
  386         $current->set('parentid', $newparentid);
  387         $result = $current->update();
  388 
  389         // All right, let's commit this.
  390         $transaction->allow_commit();
  391 
  392         return $result;
  393     }
  394 
  395     /**
  396      * Update the details for a competency.
  397      *
  398      * Requires moodle/competency:competencymanage capability at the system context.
  399      *
  400      * @param stdClass $record The new details for the competency.
  401      *                         Note - must contain an id that points to the competency to update.
  402      *
  403      * @return boolean
  404      */
  405     public static function update_competency($record) {
  406         static::require_enabled();
  407         $competency = new competency($record->id);
  408 
  409         // First we do a permissions check.
  410         require_capability('moodle/competency:competencymanage', $competency->get_context());
  411 
  412         // Some things should not be changed in an update - they should use a more specific method.
  413         $record->sortorder = $competency->get('sortorder');
  414         $record->parentid = $competency->get('parentid');
  415         $record->competencyframeworkid = $competency->get('competencyframeworkid');
  416 
  417         $competency->from_record($record);
  418         require_capability('moodle/competency:competencymanage', $competency->get_context());
  419 
  420         // OK - all set.
  421         $result = $competency->update();
  422 
  423         // Trigger the update event.
  424         \core\event\competency_updated::create_from_competency($competency)->trigger();
  425 
  426         return $result;
  427     }
  428 
  429     /**
  430      * Read a the details for a single competency and return a record.
  431      *
  432      * Requires moodle/competency:competencyview capability at the system context.
  433      *
  434      * @param int $id The id of the competency to read.
  435      * @param bool $includerelated Include related tags or not.
  436      * @return stdClass
  437      */
  438     public static function read_competency($id, $includerelated = false) {
  439         static::require_enabled();
  440         $competency = new competency($id);
  441 
  442         // First we do a permissions check.
  443         $context = $competency->get_context();
  444         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
  445              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
  446         }
  447 
  448         // OK - all set.
  449         if ($includerelated) {
  450             $relatedcompetency = new related_competency();
  451             if ($related = $relatedcompetency->list_relations($id)) {
  452                 $competency->relatedcompetencies = $related;
  453             }
  454         }
  455 
  456         return $competency;
  457     }
  458 
  459     /**
  460      * Perform a text search based and return all results and their parents.
  461      *
  462      * Requires moodle/competency:competencyview capability at the framework context.
  463      *
  464      * @param string $textsearch A string to search for.
  465      * @param int $competencyframeworkid The id of the framework to limit the search.
  466      * @return array of competencies
  467      */
  468     public static function search_competencies($textsearch, $competencyframeworkid) {
  469         static::require_enabled();
  470         $framework = new competency_framework($competencyframeworkid);
  471 
  472         // First we do a permissions check.
  473         $context = $framework->get_context();
  474         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
  475              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
  476         }
  477 
  478         // OK - all set.
  479         $competencies = competency::search($textsearch, $competencyframeworkid);
  480         return $competencies;
  481     }
  482 
  483     /**
  484      * Perform a search based on the provided filters and return a paginated list of records.
  485      *
  486      * Requires moodle/competency:competencyview capability at some context.
  487      *
  488      * @param array $filters A list of filters to apply to the list.
  489      * @param string $sort The column to sort on
  490      * @param string $order ('ASC' or 'DESC')
  491      * @param int $skip Number of records to skip (pagination)
  492      * @param int $limit Max of records to return (pagination)
  493      * @return array of competencies
  494      */
  495     public static function list_competencies($filters, $sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
  496         static::require_enabled();
  497         if (!isset($filters['competencyframeworkid'])) {
  498             $context = context_system::instance();
  499         } else {
  500             $framework = new competency_framework($filters['competencyframeworkid']);
  501             $context = $framework->get_context();
  502         }
  503 
  504         // First we do a permissions check.
  505         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
  506              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
  507         }
  508 
  509         // OK - all set.
  510         return competency::get_records($filters, $sort, $order, $skip, $limit);
  511     }
  512 
  513     /**
  514      * Perform a search based on the provided filters and return a paginated list of records.
  515      *
  516      * Requires moodle/competency:competencyview capability at some context.
  517      *
  518      * @param array $filters A list of filters to apply to the list.
  519      * @return int
  520      */
  521     public static function count_competencies($filters) {
  522         static::require_enabled();
  523         if (!isset($filters['competencyframeworkid'])) {
  524             $context = context_system::instance();
  525         } else {
  526             $framework = new competency_framework($filters['competencyframeworkid']);
  527             $context = $framework->get_context();
  528         }
  529 
  530         // First we do a permissions check.
  531         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
  532              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
  533         }
  534 
  535         // OK - all set.
  536         return competency::count_records($filters);
  537     }
  538 
  539     /**
  540      * Create a competency framework from a record containing all the data for the class.
  541      *
  542      * Requires moodle/competency:competencymanage capability at the system context.
  543      *
  544      * @param stdClass $record Record containing all the data for an instance of the class.
  545      * @return competency_framework
  546      */
  547     public static function create_framework(stdClass $record) {
  548         static::require_enabled();
  549         $framework = new competency_framework(0, $record);
  550         require_capability('moodle/competency:competencymanage', $framework->get_context());
  551 
  552         // Account for different formats of taxonomies.
  553         if (isset($record->taxonomies)) {
  554             $framework->set('taxonomies', $record->taxonomies);
  555         }
  556 
  557         $framework = $framework->create();
  558 
  559         // Trigger a competency framework created event.
  560         \core\event\competency_framework_created::create_from_framework($framework)->trigger();
  561 
  562         return $framework;
  563     }
  564 
  565     /**
  566      * Duplicate a competency framework by id.
  567      *
  568      * Requires moodle/competency:competencymanage capability at the system context.
  569      *
  570      * @param int $id The record to duplicate. All competencies associated and related will be duplicated.
  571      * @return competency_framework the framework duplicated
  572      */
  573     public static function duplicate_framework($id) {
  574         global $DB;
  575         static::require_enabled();
  576 
  577         $framework = new competency_framework($id);
  578         require_capability('moodle/competency:competencymanage', $framework->get_context());
  579         // Starting transaction.
  580         $transaction = $DB->start_delegated_transaction();
  581 
  582         try {
  583             // Get a uniq idnumber based on the origin framework.
  584             $idnumber = competency_framework::get_unused_idnumber($framework->get('idnumber'));
  585             $framework->set('idnumber', $idnumber);
  586             // Adding the suffix copy to the shortname.
  587             $framework->set('shortname', get_string('duplicateditemname', 'core_competency', $framework->get('shortname')));
  588             $framework->set('id', 0);
  589             $framework = $framework->create();
  590 
  591             // Array that match the old competencies ids with the new one to use when copying related competencies.
  592             $frameworkcompetency = competency::get_framework_tree($id);
  593             $matchids = self::duplicate_competency_tree($framework->get('id'), $frameworkcompetency, 0, 0);
  594 
  595             // Copy the related competencies.
  596             $relcomps = related_competency::get_multiple_relations(array_keys($matchids));
  597 
  598             foreach ($relcomps as $relcomp) {
  599                 $compid = $relcomp->get('competencyid');
  600                 $relcompid = $relcomp->get('relatedcompetencyid');
  601                 if (isset($matchids[$compid]) && isset($matchids[$relcompid])) {
  602                     $newcompid = $matchids[$compid]->get('id');
  603                     $newrelcompid = $matchids[$relcompid]->get('id');
  604                     if ($newcompid < $newrelcompid) {
  605                         $relcomp->set('competencyid', $newcompid);
  606                         $relcomp->set('relatedcompetencyid', $newrelcompid);
  607                     } else {
  608                         $relcomp->set('competencyid', $newrelcompid);
  609                         $relcomp->set('relatedcompetencyid', $newcompid);
  610                     }
  611                     $relcomp->set('id', 0);
  612                     $relcomp->create();
  613                 } else {
  614                     // Debugging message when there is no match found.
  615                     debugging('related competency id not found');
  616                 }
  617             }
  618 
  619             // Setting rules on duplicated competencies.
  620             self::migrate_competency_tree_rules($frameworkcompetency, $matchids);
  621 
  622             $transaction->allow_commit();
  623 
  624         } catch (\Exception $e) {
  625             $transaction->rollback($e);
  626         }
  627 
  628         // Trigger a competency framework created event.
  629         \core\event\competency_framework_created::create_from_framework($framework)->trigger();
  630 
  631         return $framework;
  632     }
  633 
  634     /**
  635      * Delete a competency framework by id.
  636      *
  637      * Requires moodle/competency:competencymanage capability at the system context.
  638      *
  639      * @param int $id The record to delete. This will delete alot of related data - you better be sure.
  640      * @return boolean
  641      */
  642     public static function delete_framework($id) {
  643         global $DB;
  644         static::require_enabled();
  645         $framework = new competency_framework($id);
  646         require_capability('moodle/competency:competencymanage', $framework->get_context());
  647 
  648         $events = array();
  649         $competenciesid = competency::get_ids_by_frameworkid($id);
  650         $contextid = $framework->get('contextid');
  651         if (!competency::can_all_be_deleted($competenciesid)) {
  652             return false;
  653         }
  654         $transaction = $DB->start_delegated_transaction();
  655         try {
  656             if (!empty($competenciesid)) {
  657                 // Delete competencies.
  658                 competency::delete_by_frameworkid($id);
  659 
  660                 // Delete the related competencies.
  661                 related_competency::delete_multiple_relations($competenciesid);
  662 
  663                 // Delete the evidences for competencies.
  664                 user_evidence_competency::delete_by_competencyids($competenciesid);
  665             }
  666 
  667             // Create a competency framework deleted event.
  668             $event = \core\event\competency_framework_deleted::create_from_framework($framework);
  669             $result = $framework->delete();
  670 
  671             // Register the deleted events competencies.
  672             $events = \core\event\competency_deleted::create_multiple_from_competencyids($competenciesid, $contextid);
  673 
  674         } catch (\Exception $e) {
  675             $transaction->rollback($e);
  676         }
  677 
  678         // Commit the transaction.
  679         $transaction->allow_commit();
  680 
  681         // If all operations are successfull then trigger the delete event.
  682         $event->trigger();
  683 
  684         // Trigger deleted event competencies.
  685         foreach ($events as $event) {
  686             $event->trigger();
  687         }
  688 
  689         return $result;
  690     }
  691 
  692     /**
  693      * Update the details for a competency framework.
  694      *
  695      * Requires moodle/competency:competencymanage capability at the system context.
  696      *
  697      * @param stdClass $record The new details for the framework. Note - must contain an id that points to the framework to update.
  698      * @return boolean
  699      */
  700     public static function update_framework($record) {
  701         static::require_enabled();
  702         $framework = new competency_framework($record->id);
  703 
  704         // Check the permissions before update.
  705         require_capability('moodle/competency:competencymanage', $framework->get_context());
  706 
  707         // Account for different formats of taxonomies.
  708         $framework->from_record($record);
  709         if (isset($record->taxonomies)) {
  710             $framework->set('taxonomies', $record->taxonomies);
  711         }
  712 
  713         // Trigger a competency framework updated event.
  714         \core\event\competency_framework_updated::create_from_framework($framework)->trigger();
  715 
  716         return $framework->update();
  717     }
  718 
  719     /**
  720      * Read a the details for a single competency framework and return a record.
  721      *
  722      * Requires moodle/competency:competencyview capability at the system context.
  723      *
  724      * @param int $id The id of the framework to read.
  725      * @return competency_framework
  726      */
  727     public static function read_framework($id) {
  728         static::require_enabled();
  729         $framework = new competency_framework($id);
  730         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
  731                 $framework->get_context())) {
  732             throw new required_capability_exception($framework->get_context(), 'moodle/competency:competencyview',
  733                 'nopermissions', '');
  734         }
  735         return $framework;
  736     }
  737 
  738     /**
  739      * Logg the competency framework viewed event.
  740      *
  741      * @param competency_framework|int $frameworkorid The competency_framework object or competency framework id
  742      * @return bool
  743      */
  744     public static function competency_framework_viewed($frameworkorid) {
  745         static::require_enabled();
  746         $framework = $frameworkorid;
  747         if (!is_object($framework)) {
  748             $framework = new competency_framework($framework);
  749         }
  750         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
  751                 $framework->get_context())) {
  752             throw new required_capability_exception($framework->get_context(), 'moodle/competency:competencyview',
  753                 'nopermissions', '');
  754         }
  755         \core\event\competency_framework_viewed::create_from_framework($framework)->trigger();
  756         return true;
  757     }
  758 
  759     /**
  760      * Logg the competency viewed event.
  761      *
  762      * @param competency|int $competencyorid The competency object or competency id
  763      * @return bool
  764      */
  765     public static function competency_viewed($competencyorid) {
  766         static::require_enabled();
  767         $competency = $competencyorid;
  768         if (!is_object($competency)) {
  769             $competency = new competency($competency);
  770         }
  771 
  772         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
  773                 $competency->get_context())) {
  774             throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview',
  775                 'nopermissions', '');
  776         }
  777 
  778         \core\event\competency_viewed::create_from_competency($competency)->trigger();
  779         return true;
  780     }
  781 
  782     /**
  783      * Perform a search based on the provided filters and return a paginated list of records.
  784      *
  785      * Requires moodle/competency:competencyview capability at the system context.
  786      *
  787      * @param string $sort The column to sort on
  788      * @param string $order ('ASC' or 'DESC')
  789      * @param int $skip Number of records to skip (pagination)
  790      * @param int $limit Max of records to return (pagination)
  791      * @param context $context The parent context of the frameworks.
  792      * @param string $includes Defines what other contexts to fetch frameworks from.
  793      *                         Accepted values are:
  794      *                          - children: All descendants
  795      *                          - parents: All parents, grand parents, etc...
  796      *                          - self: Context passed only.
  797      * @param bool $onlyvisible If true return only visible frameworks
  798      * @param string $query A string to use to filter down the frameworks.
  799      * @return array of competency_framework
  800      */
  801     public static function list_frameworks($sort, $order, $skip, $limit, $context, $includes = 'children',
  802                                            $onlyvisible = false, $query = '') {
  803         global $DB;
  804         static::require_enabled();
  805 
  806         // Get all the relevant contexts.
  807         $contexts = self::get_related_contexts($context, $includes,
  808             array('moodle/competency:competencyview', 'moodle/competency:competencymanage'));
  809 
  810         if (empty($contexts)) {
  811             throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
  812         }
  813 
  814         // OK - all set.
  815         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
  816         $select = "contextid $insql";
  817         if ($onlyvisible) {
  818             $select .= " AND visible = :visible";
  819             $inparams['visible'] = 1;
  820         }
  821 
  822         if (!empty($query) || is_numeric($query)) {
  823             $sqlnamelike = $DB->sql_like('shortname', ':namelike', false);
  824             $sqlidnlike = $DB->sql_like('idnumber', ':idnlike', false);
  825 
  826             $select .= " AND ($sqlnamelike OR $sqlidnlike) ";
  827             $inparams['namelike'] = '%' . $DB->sql_like_escape($query) . '%';
  828             $inparams['idnlike'] = '%' . $DB->sql_like_escape($query) . '%';
  829         }
  830 
  831         return competency_framework::get_records_select($select, $inparams, $sort . ' ' . $order, '*', $skip, $limit);
  832     }
  833 
  834     /**
  835      * Perform a search based on the provided filters and return a paginated list of records.
  836      *
  837      * Requires moodle/competency:competencyview capability at the system context.
  838      *
  839      * @param context $context The parent context of the frameworks.
  840      * @param string $includes Defines what other contexts to fetch frameworks from.
  841      *                         Accepted values are:
  842      *                          - children: All descendants
  843      *                          - parents: All parents, grand parents, etc...
  844      *                          - self: Context passed only.
  845      * @return int
  846      */
  847     public static function count_frameworks($context, $includes) {
  848         global $DB;
  849         static::require_enabled();
  850 
  851         // Get all the relevant contexts.
  852         $contexts = self::get_related_contexts($context, $includes,
  853             array('moodle/competency:competencyview', 'moodle/competency:competencymanage'));
  854 
  855         if (empty($contexts)) {
  856             throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
  857         }
  858 
  859         // OK - all set.
  860         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
  861         return competency_framework::count_records_select("contextid $insql", $inparams);
  862     }
  863 
  864     /**
  865      * Fetches all the relevant contexts.
  866      *
  867      * Note: This currently only supports system, category and user contexts. However user contexts
  868      * behave a bit differently and will fallback on the system context. This is what makes the most
  869      * sense because a user context does not have descendants, and only has system as a parent.
  870      *
  871      * @param context $context The context to start from.
  872      * @param string $includes Defines what other contexts to find.
  873      *                         Accepted values are:
  874      *                          - children: All descendants
  875      *                          - parents: All parents, grand parents, etc...
  876      *                          - self: Context passed only.
  877      * @param array $hasanycapability Array of capabilities passed to {@link has_any_capability()} in each context.
  878      * @return context[] An array of contexts where keys are context IDs.
  879      */
  880     public static function get_related_contexts($context, $includes, array $hasanycapability = null) {
  881         global $DB;
  882         static::require_enabled();
  883 
  884         if (!in_array($includes, array('children', 'parents', 'self'))) {
  885             throw new coding_exception('Invalid parameter value for \'includes\'.');
  886         }
  887 
  888         // If context user swap it for the context_system.
  889         if ($context->contextlevel == CONTEXT_USER) {
  890             $context = context_system::instance();
  891         }
  892 
  893         $contexts = array($context->id => $context);
  894 
  895         if ($includes == 'children') {
  896             $params = array('coursecatlevel' => CONTEXT_COURSECAT, 'path' => $context->path . '/%');
  897             $pathlike = $DB->sql_like('path', ':path');
  898             $sql = "contextlevel = :coursecatlevel AND $pathlike";
  899             $rs = $DB->get_recordset_select('context', $sql, $params);
  900             foreach ($rs as $record) {
  901                 $ctxid = $record->id;
  902                 context_helper::preload_from_record($record);
  903                 $contexts[$ctxid] = context::instance_by_id($ctxid);
  904             }
  905             $rs->close();
  906 
  907         } else if ($includes == 'parents') {
  908             $children = $context->get_parent_contexts();
  909             foreach ($children as $ctx) {
  910                 $contexts[$ctx->id] = $ctx;
  911             }
  912         }
  913 
  914         // Filter according to the capabilities required.
  915         if (!empty($hasanycapability)) {
  916             foreach ($contexts as $key => $ctx) {
  917                 if (!has_any_capability($hasanycapability, $ctx)) {
  918                     unset($contexts[$key]);
  919                 }
  920             }
  921         }
  922 
  923         return $contexts;
  924     }
  925 
  926     /**
  927      * Count all the courses using a competency.
  928      *
  929      * @param int $competencyid The id of the competency to check.
  930      * @return int
  931      */
  932     public static function count_courses_using_competency($competencyid) {
  933         static::require_enabled();
  934 
  935         // OK - all set.
  936         $courses = course_competency::list_courses_min($competencyid);
  937         $count = 0;
  938 
  939         // Now check permissions on each course.
  940         foreach ($courses as $course) {
  941             if (!self::validate_course($course, false)) {
  942                 continue;
  943             }
  944 
  945             $context = context_course::instance($course->id);
  946             $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
  947             if (!has_any_capability($capabilities, $context)) {
  948                 continue;
  949             }
  950 
  951             $count++;
  952         }
  953 
  954         return $count;
  955     }
  956 
  957     /**
  958      * List all the courses modules using a competency in a course.
  959      *
  960      * @param int $competencyid The id of the competency to check.
  961      * @param int $courseid The id of the course to check.
  962      * @return array[int] Array of course modules ids.
  963      */
  964     public static function list_course_modules_using_competency($competencyid, $courseid) {
  965         static::require_enabled();
  966 
  967         $result = array();
  968         self::validate_course($courseid);
  969 
  970         $coursecontext = context_course::instance($courseid);
  971 
  972         // We will not check each module - course permissions should be enough.
  973         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
  974         if (!has_any_capability($capabilities, $coursecontext)) {
  975             throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
  976         }
  977 
  978         $cmlist = course_module_competency::list_course_modules($competencyid, $courseid);
  979         foreach ($cmlist as $cmid) {
  980             if (self::validate_course_module($cmid, false)) {
  981                 array_push($result, $cmid);
  982             }
  983         }
  984 
  985         return $result;
  986     }
  987 
  988     /**
  989      * List all the competencies linked to a course module.
  990      *
  991      * @param mixed $cmorid The course module, or its ID.
  992      * @return array[competency] Array of competency records.
  993      */
  994     public static function list_course_module_competencies_in_course_module($cmorid) {
  995         static::require_enabled();
  996         $cm = $cmorid;
  997         if (!is_object($cmorid)) {
  998             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
  999         }
 1000 
 1001         // Check the user have access to the course module.
 1002         self::validate_course_module($cm);
 1003         $context = context_module::instance($cm->id);
 1004 
 1005         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
 1006         if (!has_any_capability($capabilities, $context)) {
 1007             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
 1008         }
 1009 
 1010         $result = array();
 1011 
 1012         $cmclist = course_module_competency::list_course_module_competencies($cm->id);
 1013         foreach ($cmclist as $id => $cmc) {
 1014             array_push($result, $cmc);
 1015         }
 1016 
 1017         return $result;
 1018     }
 1019 
 1020     /**
 1021      * List all the courses using a competency.
 1022      *
 1023      * @param int $competencyid The id of the competency to check.
 1024      * @return array[stdClass] Array of stdClass containing id and shortname.
 1025      */
 1026     public static function list_courses_using_competency($competencyid) {
 1027         static::require_enabled();
 1028 
 1029         // OK - all set.
 1030         $courses = course_competency::list_courses($competencyid);
 1031         $result = array();
 1032 
 1033         // Now check permissions on each course.
 1034         foreach ($courses as $id => $course) {
 1035             $context = context_course::instance($course->id);
 1036             $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
 1037             if (!has_any_capability($capabilities, $context)) {
 1038                 unset($courses[$id]);
 1039                 continue;
 1040             }
 1041             if (!self::validate_course($course, false)) {
 1042                 unset($courses[$id]);
 1043                 continue;
 1044             }
 1045             array_push($result, $course);
 1046         }
 1047 
 1048         return $result;
 1049     }
 1050 
 1051     /**
 1052      * Count the proficient competencies in a course for one user.
 1053      *
 1054      * @param int $courseid The id of the course to check.
 1055      * @param int $userid The id of the user to check.
 1056      * @return int
 1057      */
 1058     public static function count_proficient_competencies_in_course_for_user($courseid, $userid) {
 1059         static::require_enabled();
 1060         // Check the user have access to the course.
 1061         self::validate_course($courseid);
 1062 
 1063         // First we do a permissions check.
 1064         $context = context_course::instance($courseid);
 1065 
 1066         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
 1067         if (!has_any_capability($capabilities, $context)) {
 1068              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
 1069         }
 1070 
 1071         // OK - all set.
 1072         return user_competency_course::count_proficient_competencies($courseid, $userid);
 1073     }
 1074 
 1075     /**
 1076      * Count all the competencies in a course.
 1077      *
 1078      * @param int $courseid The id of the course to check.
 1079      * @return int
 1080      */
 1081     public static function count_competencies_in_course($courseid) {
 1082         static::require_enabled();
 1083         // Check the user have access to the course.
 1084         self::validate_course($courseid);
 1085 
 1086         // First we do a permissions check.
 1087         $context = context_course::instance($courseid);
 1088 
 1089         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
 1090         if (!has_any_capability($capabilities, $context)) {
 1091              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
 1092         }
 1093 
 1094         // OK - all set.
 1095         return course_competency::count_competencies($courseid);
 1096     }
 1097 
 1098     /**
 1099      * List the competencies associated to a course.
 1100      *
 1101      * @param mixed $courseorid The course, or its ID.
 1102      * @return array( array(
 1103      *                   'competency' => \core_competency\competency,
 1104      *                   'coursecompetency' => \core_competency\course_competency
 1105      *              ))
 1106      */
 1107     public static function list_course_competencies($courseorid) {
 1108         static::require_enabled();
 1109         $course = $courseorid;
 1110         if (!is_object($courseorid)) {
 1111             $course = get_course($courseorid);
 1112         }
 1113 
 1114         // Check the user have access to the course.
 1115         self::validate_course($course);
 1116         $context = context_course::instance($course->id);
 1117 
 1118         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
 1119         if (!has_any_capability($capabilities, $context)) {
 1120             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
 1121         }
 1122 
 1123         $result = array();
 1124 
 1125         // TODO We could improve the performance of this into one single query.
 1126         $coursecompetencies = course_competency::list_course_competencies($course->id);
 1127         $competencies = course_competency::list_competencies($course->id);
 1128 
 1129         // Build the return values.
 1130         foreach ($coursecompetencies as $key => $coursecompetency) {
 1131             $result[] = array(
 1132                 'competency' => $competencies[$coursecompetency->get('competencyid')],
 1133                 'coursecompetency' => $coursecompetency
 1134             );
 1135         }
 1136 
 1137         return $result;
 1138     }
 1139 
 1140     /**
 1141      * Get a user competency.
 1142      *
 1143      * @param int $userid The user ID.
 1144      * @param int $competencyid The competency ID.
 1145      * @return user_competency
 1146      */
 1147     public static function get_user_competency($userid, $competencyid) {
 1148         static::require_enabled();
 1149         $existing = user_competency::get_multiple($userid, array($competencyid));
 1150         $uc = array_pop($existing);
 1151 
 1152         if (!$uc) {
 1153             $uc = user_competency::create_relation($userid, $competencyid);
 1154             $uc->create();
 1155         }
 1156 
 1157         if (!$uc->can_read()) {
 1158             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
 1159                 'nopermissions', '');
 1160         }
 1161         return $uc;
 1162     }
 1163 
 1164     /**
 1165      * Get a user competency by ID.
 1166      *
 1167      * @param int $usercompetencyid The user competency ID.
 1168      * @return user_competency
 1169      */
 1170     public static function get_user_competency_by_id($usercompetencyid) {
 1171         static::require_enabled();
 1172         $uc = new user_competency($usercompetencyid);
 1173         if (!$uc->can_read()) {
 1174             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
 1175                 'nopermissions', '');
 1176         }
 1177         return $uc;
 1178     }
 1179 
 1180     /**
 1181      * List the competencies associated to a course module.
 1182      *
 1183      * @param mixed $cmorid The course module, or its ID.
 1184      * @return array( array(
 1185      *                   'competency' => \core_competency\competency,
 1186      *                   'coursemodulecompetency' => \core_competency\course_module_competency
 1187      *              ))
 1188      */
 1189     public static function list_course_module_competencies($cmorid) {
 1190         static::require_enabled();
 1191         $cm = $cmorid;
 1192         if (!is_object($cmorid)) {
 1193             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
 1194         }
 1195 
 1196         // Check the user have access to the course module.
 1197         self::validate_course_module($cm);
 1198         $context = context_module::instance($cm->id);
 1199 
 1200         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
 1201         if (!has_any_capability($capabilities, $context)) {
 1202             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
 1203         }
 1204 
 1205         $result = array();
 1206 
 1207         // TODO We could improve the performance of this into one single query.
 1208         $coursemodulecompetencies = course_module_competency::list_course_module_competencies($cm->id);
 1209         $competencies = course_module_competency::list_competencies($cm->id);
 1210 
 1211         // Build the return values.
 1212         foreach ($coursemodulecompetencies as $key => $coursemodulecompetency) {
 1213             $result[] = array(
 1214                 'competency' => $competencies[$coursemodulecompetency->get('competencyid')],
 1215                 'coursemodulecompetency' => $coursemodulecompetency
 1216             );
 1217         }
 1218 
 1219         return $result;
 1220     }
 1221 
 1222     /**
 1223      * Get a user competency in a course.
 1224      *
 1225      * @param int $courseid The id of the course to check.
 1226      * @param int $userid The id of the course to check.
 1227      * @param int $competencyid The id of the competency.
 1228      * @return user_competency_course
 1229      */
 1230     public static function get_user_competency_in_course($courseid, $userid, $competencyid) {
 1231         static::require_enabled();
 1232         // First we do a permissions check.
 1233         $context = context_course::instance($courseid);
 1234 
 1235         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
 1236         if (!has_any_capability($capabilities, $context)) {
 1237             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
 1238         } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
 1239             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
 1240         }
 1241 
 1242         // This will throw an exception if the competency does not belong to the course.
 1243         $competency = course_competency::get_competency($courseid, $competencyid);
 1244 
 1245         $params = array('courseid' => $courseid, 'userid' => $userid, 'competencyid' => $competencyid);
 1246         $exists = user_competency_course::get_record($params);
 1247         // Create missing.
 1248         if ($exists) {
 1249             $ucc = $exists;
 1250         } else {
 1251             $ucc = user_competency_course::create_relation($userid, $competency->get('id'), $courseid);
 1252             $ucc->create();
 1253         }
 1254 
 1255         return $ucc;
 1256     }
 1257 
 1258     /**
 1259      * List all the user competencies in a course.
 1260      *
 1261      * @param int $courseid The id of the course to check.
 1262      * @param int $userid The id of the course to check.
 1263      * @return array of user_competency_course objects
 1264      */
 1265     public static function list_user_competencies_in_course($courseid, $userid) {
 1266         static::require_enabled();
 1267         // First we do a permissions check.
 1268         $context = context_course::instance($courseid);
 1269         $onlyvisible = 1;
 1270 
 1271         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
 1272         if (!has_any_capability($capabilities, $context)) {
 1273             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
 1274         } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
 1275             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
 1276         }
 1277 
 1278         // OK - all set.
 1279         $competencylist = course_competency::list_competencies($courseid, false);
 1280 
 1281         $existing = user_competency_course::get_multiple($userid, $courseid, $competencylist);
 1282         // Create missing.
 1283         $orderedusercompetencycourses = array();
 1284 
 1285         $somemissing = false;
 1286         foreach ($competencylist as $coursecompetency) {
 1287             $found = false;
 1288             foreach ($existing as $usercompetencycourse) {
 1289                 if ($usercompetencycourse->get('competencyid') == $coursecompetency->get('id')) {
 1290                     $found = true;
 1291                     $orderedusercompetencycourses[$usercompetencycourse->get('id')] = $usercompetencycourse;
 1292                     break;
 1293                 }
 1294             }
 1295             if (!$found) {
 1296                 $ucc = user_competency_course::create_relation($userid, $coursecompetency->get('id'), $courseid);
 1297                 $ucc->create();
 1298                 $orderedusercompetencycourses[$ucc->get('id')] = $ucc;
 1299             }
 1300         }
 1301 
 1302         return $orderedusercompetencycourses;
 1303     }
 1304 
 1305     /**
 1306      * List the user competencies to review.
 1307      *
 1308      * The method returns values in this format:
 1309      *
 1310      * array(
 1311      *     'competencies' => array(
 1312      *         (stdClass)(
 1313      *             'usercompetency' => (user_competency),
 1314      *             'competency' => (competency),
 1315      *             'user' => (user)
 1316      *         )
 1317      *     ),
 1318      *     'count' => (int)
 1319      * )
 1320      *
 1321      * @param int $skip The number of records to skip.
 1322      * @param int $limit The number of results to return.
 1323      * @param int $userid The user we're getting the competencies to review for.
 1324      * @return array Containing the keys 'count', and 'competencies'. The 'competencies' key contains an object
 1325      *               which contains 'competency', 'usercompetency' and 'user'.
 1326      */
 1327     public static function list_user_competencies_to_review($skip = 0, $limit = 50, $userid = null) {
 1328         global $DB, $USER;
 1329         static::require_enabled();
 1330         if ($userid === null) {
 1331             $userid = $USER->id;
 1332         }
 1333 
 1334         $capability = 'moodle/competency:usercompetencyreview';
 1335         $ucfields = user_competency::get_sql_fields('uc', 'uc_');
 1336         $compfields = competency::get_sql_fields('c', 'c_');
 1337         $usercols = array('id') + get_user_fieldnames();
 1338         $userfields = array();
 1339         foreach ($usercols as $field) {
 1340             $userfields[] = "u." . $field . " AS usr_" . $field;
 1341         }
 1342         $userfields = implode(',', $userfields);
 1343 
 1344         $select = "SELECT $ucfields, $compfields, $userfields";
 1345         $countselect = "SELECT COUNT('x')";
 1346         $sql = "  FROM {" . user_competency::TABLE . "} uc
 1347                   JOIN {" . competency::TABLE . "} c
 1348                     ON c.id = uc.competencyid
 1349                   JOIN {user} u
 1350                     ON u.id = uc.userid
 1351                  WHERE (uc.status = :waitingforreview
 1352                     OR (uc.status = :inreview AND uc.reviewerid = :reviewerid))
 1353                    AND u.deleted = 0";
 1354         $ordersql = " ORDER BY c.shortname ASC";
 1355         $params = array(
 1356             'inreview' => user_competency::STATUS_IN_REVIEW,
 1357             'reviewerid' => $userid,
 1358             'waitingforreview' => user_competency::STATUS_WAITING_FOR_REVIEW,
 1359         );
 1360         $countsql = $countselect . $sql;
 1361 
 1362         // Primary check to avoid the hard work of getting the users in which the user has permission.
 1363         $count = $DB->count_records_sql($countselect . $sql, $params);
 1364         if ($count < 1) {
 1365             return array('count' => 0, 'competencies' => array());
 1366         }
 1367 
 1368         // TODO MDL-52243 Use core function.
 1369         list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql(
 1370             $capability, $userid, SQL_PARAMS_NAMED);
 1371         $params += $inparams;
 1372         $countsql = $countselect . $sql . " AND uc.userid $insql";
 1373         $getsql = $select . $sql . " AND uc.userid $insql " . $ordersql;
 1374 
 1375         // Extracting the results.
 1376         $competencies = array();
 1377         $records = $DB->get_recordset_sql($getsql, $params, $skip, $limit);
 1378         foreach ($records as $record) {
 1379             $objects = (object) array(
 1380                 'usercompetency' => new user_competency(0, user_competency::extract_record($record, 'uc_')),
 1381                 'competency' => new competency(0, competency::extract_record($record, 'c_')),
 1382                 'user' => persistent::extract_record($record, 'usr_'),
 1383             );
 1384             $competencies[] = $objects;
 1385         }
 1386         $records->close();
 1387 
 1388         return array(
 1389             'count' => $DB->count_records_sql($countsql, $params),
 1390             'competencies' => $competencies
 1391         );
 1392     }
 1393 
 1394     /**
 1395      * Add a competency to this course module.
 1396      *
 1397      * @param mixed $cmorid The course module, or id of the course module
 1398      * @param int $competencyid The id of the competency
 1399      * @return bool
 1400      */
 1401     public static function add_competency_to_course_module($cmorid, $competencyid) {
 1402         static::require_enabled();
 1403         $cm = $cmorid;
 1404         if (!is_object($cmorid)) {
 1405             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
 1406         }
 1407 
 1408         // Check the user have access to the course module.
 1409         self::validate_course_module($cm);
 1410 
 1411         // First we do a permissions check.
 1412         $context = context_module::instance($cm->id);
 1413 
 1414         require_capability('moodle/competency:coursecompetencymanage', $context);
 1415 
 1416         // Check that the competency belongs to the course.
 1417         $exists = course_competency::get_records(array('courseid' => $cm->course, 'competencyid' => $competencyid));
 1418         if (!$exists) {
 1419             throw new coding_exception('Cannot add a competency to a module if it does not belong to the course');
 1420         }
 1421 
 1422         $record = new stdClass();
 1423         $record->cmid = $cm->id;
 1424         $record->competencyid = $competencyid;
 1425 
 1426         $coursemodulecompetency = new course_module_competency();
 1427         $exists = $coursemodulecompetency->get_records(array('cmid' => $cm->id, 'competencyid' => $competencyid));
 1428         if (!$exists) {
 1429             $coursemodulecompetency->from_record($record);
 1430             if ($coursemodulecompetency->create()) {
 1431                 return true;
 1432             }
 1433         }
 1434         return false;
 1435     }
 1436 
 1437     /**
 1438      * Remove a competency from this course module.
 1439      *
 1440      * @param mixed $cmorid The course module, or id of the course module
 1441      * @param int $competencyid The id of the competency
 1442      * @return bool
 1443      */
 1444     public static function remove_competency_from_course_module($cmorid, $competencyid) {
 1445         static::require_enabled();
 1446         $cm = $cmorid;
 1447         if (!is_object($cmorid)) {
 1448             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
 1449         }
 1450         // Check the user have access to the course module.
 1451         self::validate_course_module($cm);
 1452 
 1453         // First we do a permissions check.
 1454         $context = context_module::instance($cm->id);
 1455 
 1456         require_capability('moodle/competency:coursecompetencymanage', $context);
 1457 
 1458         $record = new stdClass();
 1459         $record->cmid = $cm->id;
 1460         $record->competencyid = $competencyid;
 1461 
 1462         $competency = new competency($competencyid);
 1463         $exists = course_module_competency::get_record(array('cmid' => $cm->id, 'competencyid' => $competencyid));
 1464         if ($exists) {
 1465             return $exists->delete();
 1466         }
 1467         return false;
 1468     }
 1469 
 1470     /**
 1471      * Move the course module competency up or down in the display list.
 1472      *
 1473      * Requires moodle/competency:coursecompetencymanage capability at the course module context.
 1474      *
 1475      * @param mixed $cmorid The course module, or id of the course module
 1476      * @param int $competencyidfrom The id of the competency we are moving.
 1477      * @param int $competencyidto The id of the competency we are moving to.
 1478      * @return boolean
 1479      */
 1480     public static function reorder_course_module_competency($cmorid, $competencyidfrom, $competencyidto) {
 1481         static::require_enabled();
 1482         $cm = $cmorid;
 1483         if (!is_object($cmorid)) {
 1484             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
 1485         }
 1486         // Check the user have access to the course module.
 1487         self::validate_course_module($cm);
 1488 
 1489         // First we do a permissions check.
 1490         $context = context_module::instance($cm->id);
 1491 
 1492         require_capability('moodle/competency:coursecompetencymanage', $context);
 1493 
 1494         $down = true;
 1495         $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidfrom));
 1496         if (count($matches) == 0) {
 1497              throw new coding_exception('The link does not exist');
 1498         }
 1499 
 1500         $competencyfrom = array_pop($matches);
 1501         $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidto));
 1502         if (count($matches) == 0) {
 1503              throw new coding_exception('The link does not exist');
 1504         }
 1505 
 1506         $competencyto = array_pop($matches);
 1507 
 1508         $all = course_module_competency::get_records(array('cmid' => $cm->id), 'sortorder', 'ASC', 0, 0);
 1509 
 1510         if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
 1511             // We are moving up, so put it before the "to" item.
 1512             $down = false;
 1513         }
 1514 
 1515         foreach ($all as $id => $coursemodulecompetency) {
 1516             $sort = $coursemodulecompetency->get('sortorder');
 1517             if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
 1518                 $coursemodulecompetency->set('sortorder', $coursemodulecompetency->get('sortorder') - 1);
 1519                 $coursemodulecompetency->update();
 1520             } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
 1521                 $coursemodulecompetency->set('sortorder', $coursemodulecompetency->get('sortorder') + 1);
 1522                 $coursemodulecompetency->update();
 1523             }
 1524         }
 1525         $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
 1526         return $competencyfrom->update();
 1527     }
 1528 
 1529     /**
 1530      * Update ruleoutcome value for a course module competency.
 1531      *
 1532      * @param int|course_module_competency $coursemodulecompetencyorid The course_module_competency, or its ID.
 1533      * @param int $ruleoutcome The value of ruleoutcome.
 1534      * @return bool True on success.
 1535      */
 1536     public static function set_course_module_competency_ruleoutcome($coursemodulecompetencyorid, $ruleoutcome) {
 1537         static::require_enabled();
 1538         $coursemodulecompetency = $coursemodulecompetencyorid;
 1539         if (!is_object($coursemodulecompetency)) {
 1540             $coursemodulecompetency = new course_module_competency($coursemodulecompetencyorid);
 1541         }
 1542 
 1543         $cm = get_coursemodule_from_id('', $coursemodulecompetency->get('cmid'), 0, true, MUST_EXIST);
 1544 
 1545         self::validate_course_module($cm);
 1546         $context = context_module::instance($cm->id);
 1547 
 1548         require_capability('moodle/competency:coursecompetencymanage', $context);
 1549 
 1550         $coursemodulecompetency->set('ruleoutcome', $ruleoutcome);
 1551         return $coursemodulecompetency->update();
 1552     }
 1553 
 1554     /**
 1555      * Add a competency to this course.
 1556      *
 1557      * @param int $courseid The id of the course
 1558      * @param int $competencyid The id of the competency
 1559      * @return bool
 1560      */
 1561     public static function add_competency_to_course($courseid, $competencyid) {
 1562         static::require_enabled();
 1563         // Check the user have access to the course.
 1564         self::validate_course($courseid);
 1565 
 1566         // First we do a permissions check.
 1567         $context = context_course::instance($courseid);
 1568 
 1569         require_capability('moodle/competency:coursecompetencymanage', $context);
 1570 
 1571         $record = new stdClass();
 1572         $record->courseid = $courseid;
 1573         $record->competencyid = $competencyid;
 1574 
 1575         $competency = new competency($competencyid);
 1576 
 1577         // Can not add a competency that belong to a hidden framework.
 1578         if ($competency->get_framework()->get('visible') == false) {
 1579             throw new coding_exception('A competency belonging to hidden framework can not be linked to course');
 1580         }
 1581 
 1582         $coursecompetency = new course_competency();
 1583         $exists = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyid));
 1584         if (!$exists) {
 1585             $coursecompetency->from_record($record);
 1586             if ($coursecompetency->create()) {
 1587                 return true;
 1588             }
 1589         }
 1590         return false;
 1591     }
 1592 
 1593     /**
 1594      * Remove a competency from this course.
 1595      *
 1596      * @param int $courseid The id of the course
 1597      * @param int $competencyid The id of the competency
 1598      * @return bool
 1599      */
 1600     public static function remove_competency_from_course($courseid, $competencyid) {
 1601         static::require_enabled();
 1602         // Check the user have access to the course.
 1603         self::validate_course($courseid);
 1604 
 1605         // First we do a permissions check.
 1606         $context = context_course::instance($courseid);
 1607 
 1608         require_capability('moodle/competency:coursecompetencymanage', $context);
 1609 
 1610         $record = new stdClass();
 1611         $record->courseid = $courseid;
 1612         $record->competencyid = $competencyid;
 1613 
 1614         $coursecompetency = new course_competency();
 1615         $exists = course_competency::get_record(array('courseid' => $courseid, 'competencyid' => $competencyid));
 1616         if ($exists) {
 1617             // Delete all course_module_competencies for this competency in this course.
 1618             $cmcs = course_module_competency::get_records_by_competencyid_in_course($competencyid, $courseid);
 1619             foreach ($cmcs as $cmc) {
 1620                 $cmc->delete();
 1621             }
 1622             return $exists->delete();
 1623         }
 1624         return false;
 1625     }
 1626 
 1627     /**
 1628      * Move the course competency up or down in the display list.
 1629      *
 1630      * Requires moodle/competency:coursecompetencymanage capability at the course context.
 1631      *
 1632      * @param int $courseid The course
 1633      * @param int $competencyidfrom The id of the competency we are moving.
 1634      * @param int $competencyidto The id of the competency we are moving to.
 1635      * @return boolean
 1636      */
 1637     public static function reorder_course_competency($courseid, $competencyidfrom, $competencyidto) {
 1638         static::require_enabled();
 1639         // Check the user have access to the course.
 1640         self::validate_course($courseid);
 1641 
 1642         // First we do a permissions check.
 1643         $context = context_course::instance($courseid);
 1644 
 1645         require_capability('moodle/competency:coursecompetencymanage', $context);
 1646 
 1647         $down = true;
 1648         $coursecompetency = new course_competency();
 1649         $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidfrom));
 1650         if (count($matches) == 0) {
 1651              throw new coding_exception('The link does not exist');
 1652         }
 1653 
 1654         $competencyfrom = array_pop($matches);
 1655         $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidto));
 1656         if (count($matches) == 0) {
 1657              throw new coding_exception('The link does not exist');
 1658         }
 1659 
 1660         $competencyto = array_pop($matches);
 1661 
 1662         $all = $coursecompetency->get_records(array('courseid' => $courseid), 'sortorder', 'ASC', 0, 0);
 1663 
 1664         if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
 1665             // We are moving up, so put it before the "to" item.
 1666             $down = false;
 1667         }
 1668 
 1669         foreach ($all as $id => $coursecompetency) {
 1670             $sort = $coursecompetency->get('sortorder');
 1671             if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
 1672                 $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') - 1);
 1673                 $coursecompetency->update();
 1674             } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
 1675                 $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') + 1);
 1676                 $coursecompetency->update();
 1677             }
 1678         }
 1679         $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
 1680         return $competencyfrom->update();
 1681     }
 1682 
 1683     /**
 1684      * Update ruleoutcome value for a course competency.
 1685      *
 1686      * @param int|course_competency $coursecompetencyorid The course_competency, or its ID.
 1687      * @param int $ruleoutcome The value of ruleoutcome.
 1688      * @return bool True on success.
 1689      */
 1690     public static function set_course_competency_ruleoutcome($coursecompetencyorid, $ruleoutcome) {
 1691         static::require_enabled();
 1692         $coursecompetency = $coursecompetencyorid;
 1693         if (!is_object($coursecompetency)) {
 1694             $coursecompetency = new course_competency($coursecompetencyorid);
 1695         }
 1696 
 1697         $courseid = $coursecompetency->get('courseid');
 1698         self::validate_course($courseid);
 1699         $coursecontext = context_course::instance($courseid);
 1700 
 1701         require_capability('moodle/competency:coursecompetencymanage', $coursecontext);
 1702 
 1703         $coursecompetency->set('ruleoutcome', $ruleoutcome);
 1704         return $coursecompetency->update();
 1705     }
 1706 
 1707     /**
 1708      * Create a learning plan template from a record containing all the data for the class.
 1709      *
 1710      * Requires moodle/competency:templatemanage capability.
 1711      *
 1712      * @param stdClass $record Record containing all the data for an instance of the class.
 1713      * @return template
 1714      */
 1715     public static function create_template(stdClass $record) {
 1716         static::require_enabled();
 1717         $template = new template(0, $record);
 1718 
 1719         // First we do a permissions check.
 1720         if (!$template->can_manage()) {
 1721             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
 1722                 'nopermissions', '');
 1723         }
 1724 
 1725         // OK - all set.
 1726         $template = $template->create();
 1727 
 1728         // Trigger a template created event.
 1729         \core\event\competency_template_created::create_from_template($template)->trigger();
 1730 
 1731         return $template;
 1732     }
 1733 
 1734     /**
 1735      * Duplicate a learning plan template.
 1736      *
 1737      * Requires moodle/competency:templatemanage capability at the template context.
 1738      *
 1739      * @param int $id the template id.
 1740      * @return template
 1741      */
 1742     public static function duplicate_template($id) {
 1743         static::require_enabled();
 1744         $template = new template($id);
 1745 
 1746         // First we do a permissions check.
 1747         if (!$template->can_manage()) {
 1748             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
 1749                 'nopermissions', '');
 1750         }
 1751 
 1752         // OK - all set.
 1753         $competencies = template_competency::list_competencies($id, false);
 1754 
 1755         // Adding the suffix copy.
 1756         $template->set('shortname', get_string('duplicateditemname', 'core_competency', $template->get('shortname')));
 1757         $template->set('id', 0);
 1758 
 1759         $duplicatedtemplate = $template->create();
 1760 
 1761         // Associate each competency for the duplicated template.
 1762         foreach ($competencies as $competency) {
 1763             self::add_competency_to_template($duplicatedtemplate->get('id'), $competency->get('id'));
 1764         }
 1765 
 1766         // Trigger a template created event.
 1767         \core\event\competency_template_created::create_from_template($duplicatedtemplate)->trigger();
 1768 
 1769         return $duplicatedtemplate;
 1770     }
 1771 
 1772     /**
 1773      * Delete a learning plan template by id.
 1774      * If the learning plan template has associated cohorts they will be deleted.
 1775      *
 1776      * Requires moodle/competency:templatemanage capability.
 1777      *
 1778      * @param int $id The record to delete.
 1779      * @param boolean $deleteplans True to delete plans associaated to template, false to unlink them.
 1780      * @return boolean
 1781      */
 1782     public static function delete_template($id, $deleteplans = true) {
 1783         global $DB;
 1784         static::require_enabled();
 1785         $template = new template($id);
 1786 
 1787         // First we do a permissions check.
 1788         if (!$template->can_manage()) {
 1789             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
 1790                 'nopermissions', '');
 1791         }
 1792 
 1793         $transaction = $DB->start_delegated_transaction();
 1794         $success = true;
 1795 
 1796         // Check if there are cohorts associated.
 1797         $templatecohorts = template_cohort::get_relations_by_templateid($template->get('id'));
 1798         foreach ($templatecohorts as $templatecohort) {
 1799             $success = $templatecohort->delete();
 1800             if (!$success) {
 1801                 break;
 1802             }
 1803         }
 1804 
 1805         // Still OK, delete or unlink the plans from the template.
 1806         if ($success) {
 1807             $plans = plan::get_records(array('templateid' => $template->get('id')));
 1808             foreach ($plans as $plan) {
 1809                 $success = $deleteplans ? self::delete_plan($plan->get('id')) : self::unlink_plan_from_template($plan);
 1810                 if (!$success) {
 1811                     break;
 1812                 }
 1813             }
 1814         }
 1815 
 1816         // Still OK, delete the template comptencies.
 1817         if ($success) {
 1818             $success = template_competency::delete_by_templateid($template->get('id'));
 1819         }
 1820 
 1821         // OK - all set.
 1822         if ($success) {
 1823             // Create a template deleted event.
 1824             $event = \core\event\competency_template_deleted::create_from_template($template);
 1825 
 1826             $success = $template->delete();
 1827         }
 1828 
 1829         if ($success) {
 1830             // Trigger a template deleted event.
 1831             $event->trigger();
 1832 
 1833             // Commit the transaction.
 1834             $transaction->allow_commit();
 1835         } else {
 1836             $transaction->rollback(new moodle_exception('Error while deleting the template.'));
 1837         }
 1838 
 1839         return $success;
 1840     }
 1841 
 1842     /**
 1843      * Update the details for a learning plan template.
 1844      *
 1845      * Requires moodle/competency:templatemanage capability.
 1846      *
 1847      * @param stdClass $record The new details for the template. Note - must contain an id that points to the template to update.
 1848      * @return boolean
 1849      */
 1850     public static function update_template($record) {
 1851         global $DB;
 1852         static::require_enabled();
 1853         $template = new template($record->id);
 1854 
 1855         // First we do a permissions check.
 1856         if (!$template->can_manage()) {
 1857             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
 1858                 'nopermissions', '');
 1859 
 1860         } else if (isset($record->contextid) && $record->contextid != $template->get('contextid')) {
 1861             // We can never change the context of a template.
 1862             throw new coding_exception('Changing the context of an existing tempalte is forbidden.');
 1863 
 1864         }
 1865 
 1866         $updateplans = false;
 1867         $before = $template->to_record();
 1868 
 1869         $template->from_record($record);
 1870         $after = $template->to_record();
 1871 
 1872         // Should we update the related plans?
 1873         if ($before->duedate != $after->duedate ||
 1874                 $before->shortname != $after->shortname ||
 1875                 $before->description != $after->description ||
 1876                 $before->descriptionformat != $after->descriptionformat) {
 1877             $updateplans = true;
 1878         }
 1879 
 1880         $transaction = $DB->start_delegated_transaction();
 1881         $success = $template->update();
 1882 
 1883         if (!$success) {
 1884             $transaction->rollback(new moodle_exception('Error while updating the template.'));
 1885             return $success;
 1886         }
 1887 
 1888         // Trigger a template updated event.
 1889         \core\event\competency_template_updated::create_from_template($template)->trigger();
 1890 
 1891         if ($updateplans) {
 1892             plan::update_multiple_from_template($template);
 1893         }
 1894 
 1895         $transaction->allow_commit();
 1896 
 1897         return $success;
 1898     }
 1899 
 1900     /**
 1901      * Read a the details for a single learning plan template and return a record.
 1902      *
 1903      * Requires moodle/competency:templateview capability at the system context.
 1904      *
 1905      * @param int $id The id of the template to read.
 1906      * @return template
 1907      */
 1908     public static function read_template($id) {
 1909         static::require_enabled();
 1910         $template = new template($id);
 1911         $context = $template->get_context();
 1912 
 1913         // First we do a permissions check.
 1914         if (!$template->can_read()) {
 1915              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
 1916                 'nopermissions', '');
 1917         }
 1918 
 1919         // OK - all set.
 1920         return $template;
 1921     }
 1922 
 1923     /**
 1924      * Perform a search based on the provided filters and return a paginated list of records.
 1925      *
 1926      * Requires moodle/competency:templateview capability at the system context.
 1927      *
 1928      * @param string $sort The column to sort on
 1929      * @param string $order ('ASC' or 'DESC')
 1930      * @param int $skip Number of records to skip (pagination)
 1931      * @param int $limit Max of records to return (pagination)
 1932      * @param context $context The parent context of the frameworks.
 1933      * @param string $includes Defines what other contexts to fetch frameworks from.
 1934      *                         Accepted values are:
 1935      *                          - children: All descendants
 1936      *                          - parents: All parents, grand parents, etc...
 1937      *                          - self: Context passed only.
 1938      * @param bool $onlyvisible If should list only visible templates
 1939      * @return array of competency_framework
 1940      */
 1941     public static function list_templates($sort, $order, $skip, $limit, $context, $includes = 'children', $onlyvisible = false) {
 1942         global $DB;
 1943         static::require_enabled();
 1944 
 1945         // Get all the relevant contexts.
 1946         $contexts = self::get_related_contexts($context, $includes,
 1947             array('moodle/competency:templateview', 'moodle/competency:templatemanage'));
 1948 
 1949         // First we do a permissions check.
 1950         if (empty($contexts)) {
 1951              throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
 1952         }
 1953 
 1954         // Make the order by.
 1955         $orderby = '';
 1956         if (!empty($sort)) {
 1957             $orderby = $sort . ' ' . $order;
 1958         }
 1959 
 1960         // OK - all set.
 1961         $template = new template();
 1962         list($insql, $params) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
 1963         $select = "contextid $insql";
 1964 
 1965         if ($onlyvisible) {
 1966             $select .= " AND visible = :visible";
 1967             $params['visible'] = 1;
 1968         }
 1969         return $template->get_records_select($select, $params, $orderby, '*', $skip, $limit);
 1970     }
 1971 
 1972     /**
 1973      * Perform a search based on the provided filters and return how many results there are.
 1974      *
 1975      * Requires moodle/competency:templateview capability at the system context.
 1976      *
 1977      * @param context $context The parent context of the frameworks.
 1978      * @param string $includes Defines what other contexts to fetch frameworks from.
 1979      *                         Accepted values are:
 1980      *                          - children: All descendants
 1981      *                          - parents: All parents, grand parents, etc...
 1982      *                          - self: Context passed only.
 1983      * @return int
 1984      */
 1985     public static function count_templates($context, $includes) {
 1986         global $DB;
 1987         static::require_enabled();
 1988 
 1989         // First we do a permissions check.
 1990         $contexts = self::get_related_contexts($context, $includes,
 1991             array('moodle/competency:templateview', 'moodle/competency:templatemanage'));
 1992 
 1993         if (empty($contexts)) {
 1994              throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
 1995         }
 1996 
 1997         // OK - all set.
 1998         $template = new template();
 1999         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
 2000         return $template->count_records_select("contextid $insql", $inparams);
 2001     }
 2002 
 2003     /**
 2004      * Count all the templates using a competency.
 2005      *
 2006      * @param int $competencyid The id of the competency to check.
 2007      * @return int
 2008      */
 2009     public static function count_templates_using_competency($competencyid) {
 2010         static::require_enabled();
 2011         // First we do a permissions check.
 2012         $context = context_system::instance();
 2013         $onlyvisible = 1;
 2014 
 2015         $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
 2016         if (!has_any_capability($capabilities, $context)) {
 2017              throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
 2018         }
 2019 
 2020         if (has_capability('moodle/competency:templatemanage', $context)) {
 2021             $onlyvisible = 0;
 2022         }
 2023 
 2024         // OK - all set.
 2025         return template_competency::count_templates($competencyid, $onlyvisible);
 2026     }
 2027 
 2028     /**
 2029      * List all the learning plan templatesd using a competency.
 2030      *
 2031      * @param int $competencyid The id of the competency to check.
 2032      * @return array[stdClass] Array of stdClass containing id and shortname.
 2033      */
 2034     public static function list_templates_using_competency($competencyid) {
 2035         static::require_enabled();
 2036         // First we do a permissions check.
 2037         $context = context_system::instance();
 2038         $onlyvisible = 1;
 2039 
 2040         $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
 2041         if (!has_any_capability($capabilities, $context)) {
 2042              throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
 2043         }
 2044 
 2045         if (has_capability('moodle/competency:templatemanage', $context)) {
 2046             $onlyvisible = 0;
 2047         }
 2048 
 2049         // OK - all set.
 2050         return template_competency::list_templates($competencyid, $onlyvisible);
 2051 
 2052     }
 2053 
 2054     /**
 2055      * Count all the competencies in a learning plan template.
 2056      *
 2057      * @param  template|int $templateorid The template or its ID.
 2058      * @return int
 2059      */
 2060     public static function count_competencies_in_template($templateorid) {
 2061         static::require_enabled();
 2062         // First we do a permissions check.
 2063         $template = $templateorid;
 2064         if (!is_object($template)) {
 2065             $template = new template($template);
 2066         }
 2067 
 2068         if (!$template->can_read()) {
 2069             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
 2070                 'nopermissions', '');
 2071         }
 2072 
 2073         // OK - all set.
 2074         return template_competency::count_competencies($template->get('id'));
 2075     }
 2076 
 2077     /**
 2078      * Count all the competencies in a learning plan template with no linked courses.
 2079      *
 2080      * @param  template|int $templateorid The template or its ID.
 2081      * @return int
 2082      */
 2083     public static function count_competencies_in_template_with_no_courses($templateorid) {
 2084         // First we do a permissions check.
 2085         $template = $templateorid;
 2086         if (!is_object($template)) {
 2087             $template = new template($template);
 2088         }
 2089 
 2090         if (!$template->can_read()) {
 2091             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
 2092                 'nopermissions', '');
 2093         }
 2094 
 2095         // OK - all set.
 2096         return template_competency::count_competencies_with_no_courses($template->get('id'));
 2097     }
 2098 
 2099     /**
 2100      * List all the competencies in a template.
 2101      *
 2102      * @param  template|int $templateorid The template or its ID.
 2103      * @return array of competencies
 2104      */
 2105     public static function list_competencies_in_template($templateorid) {
 2106         static::require_enabled();
 2107         // First we do a permissions check.
 2108         $template = $templateorid;
 2109         if (!is_object($template)) {
 2110             $template = new template($template);
 2111         }
 2112 
 2113         if (!$template->can_read()) {
 2114             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
 2115                 'nopermissions', '');
 2116         }
 2117 
 2118         // OK - all set.
 2119         return template_competency::list_competencies($template->get('id'));
 2120     }
 2121 
 2122     /**
 2123      * Add a competency to this template.
 2124      *
 2125      * @param int $templateid The id of the template
 2126      * @param int $competencyid The id of the competency
 2127      * @return bool
 2128      */
 2129     public static function add_competency_to_template($templateid, $competencyid) {
 2130         static::require_enabled();
 2131         // First we do a permissions check.
 2132         $template = new template($templateid);
 2133         if (!$template->can_manage()) {
 2134             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
 2135                 'nopermissions', '');
 2136         }
 2137 
 2138         $record = new stdClass();
 2139         $record->templateid = $templateid;
 2140         $record->competencyid = $competencyid;
 2141 
 2142         $competency = new competency($competencyid);
 2143 
 2144         // Can not add a competency that belong to a hidden framework.
 2145         if ($competency->get_framework()->get('visible') == false) {
 2146             throw new coding_exception('A competency belonging to hidden framework can not be added');
 2147         }
 2148 
 2149         $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
 2150         if (!$exists) {
 2151             $templatecompetency = new template_competency(0, $record);
 2152             $templatecompetency->create();
 2153             return true;
 2154         }
 2155         return false;
 2156     }
 2157 
 2158     /**
 2159      * Remove a competency from this template.
 2160      *
 2161      * @param int $templateid The id of the template
 2162      * @param int $competencyid The id of the competency
 2163      * @return bool
 2164      */
 2165     public static function remove_competency_from_template($templateid, $competencyid) {
 2166         static::require_enabled();
 2167         // First we do a permissions check.
 2168         $template = new template($templateid);
 2169         if (!$template->can_manage()) {
 2170             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
 2171                 'nopermissions', '');
 2172         }
 2173 
 2174         $record = new stdClass();
 2175         $record->templateid = $templateid;
 2176         $record->competencyid = $competencyid;
 2177 
 2178         $competency = new competency($competencyid);
 2179 
 2180         $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
 2181         if ($exists) {
 2182             $link = array_pop($exists);
 2183             return $link->delete();
 2184         }
 2185         return false;
 2186     }
 2187 
 2188     /**
 2189      * Move the template competency up or down in the display list.
 2190      *
 2191      * Requires moodle/competency:templatemanage capability at the system context.
 2192      *
 2193      * @param int $templateid The template id
 2194      * @param int $competencyidfrom The id of the competency we are moving.
 2195      * @param int $competencyidto The id of the competency we are moving to.
 2196      * @return boolean
 2197      */
 2198     public static function reorder_template_competency($templateid, $competencyidfrom, $competencyidto) {
 2199         static::require_enabled();
 2200         $template = new template($templateid);
 2201 
 2202         // First we do a permissions check.
 2203         if (!$template->can_manage()) {
 2204             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
 2205                 'nopermissions', '');
 2206         }
 2207 
 2208         $down = true;
 2209         $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidfrom));
 2210         if (count($matches) == 0) {
 2211             throw new coding_exception('The link does not exist');
 2212         }
 2213 
 2214         $competencyfrom = array_pop($matches);
 2215         $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidto));
 2216         if (count($matches) == 0) {
 2217             throw new coding_exception('The link does not exist');
 2218         }
 2219 
 2220         $competencyto = array_pop($matches);
 2221 
 2222         $all = template_competency::get_records(array('templateid' => $templateid), 'sortorder', 'ASC', 0, 0);
 2223 
 2224         if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
 2225             // We are moving up, so put it before the "to" item.
 2226             $down = false;
 2227         }
 2228 
 2229         foreach ($all as $id => $templatecompetency) {
 2230             $sort = $templatecompetency->get('sortorder');
 2231             if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
 2232                 $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') - 1);
 2233                 $templatecompetency->update();
 2234             } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
 2235                 $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') + 1);
 2236                 $templatecompetency->update();
 2237             }
 2238         }
 2239         $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
 2240         return $competencyfrom->update();
 2241     }
 2242 
 2243     /**
 2244      * Create a relation between a template and a cohort.
 2245      *
 2246      * This silently ignores when the relation already existed.
 2247      *
 2248      * @param  template|int $templateorid The template or its ID.
 2249      * @param  stdClass|int $cohortorid   The cohort ot its ID.
 2250      * @return template_cohort
 2251      */
 2252     public static function create_template_cohort($templateorid, $cohortorid) {
 2253         global $DB;
 2254         static::require_enabled();
 2255 
 2256         $template = $templateorid;
 2257         if (!is_object($template)) {
 2258             $template = new template($template);
 2259         }
 2260         require_capability('moodle/competency:templatemanage', $template->get_context());
 2261 
 2262         $cohort = $cohortorid;
 2263         if (!is_object($cohort)) {
 2264             $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
 2265         }
 2266 
 2267         // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
 2268         $cohortcontext = context::instance_by_id($cohort->contextid);
 2269         if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
 2270             throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
 2271         }
 2272 
 2273         $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id);
 2274         if (!$tplcohort->get('id')) {
 2275             $tplcohort->create();
 2276         }
 2277 
 2278         return $tplcohort;
 2279     }
 2280 
 2281     /**
 2282      * Remove a relation between a template and a cohort.
 2283      *
 2284      * @param  template|int $templateorid The template or its ID.
 2285      * @param  stdClass|int $cohortorid   The cohort ot its ID.
 2286      * @return boolean True on success or when the relation did not exist.
 2287      */
 2288     public static function delete_template_cohort($templateorid, $cohortorid) {
 2289         global $DB;
 2290         static::require_enabled();
 2291 
 2292         $template = $templateorid;
 2293         if (!is_object($template)) {
 2294             $template = new template($template);
 2295         }
 2296         require_capability('moodle/competency:templatemanage', $template->get_context());
 2297 
 2298         $cohort = $cohortorid;
 2299         if (!is_object($cohort)) {
 2300             $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
 2301         }
 2302 
 2303         $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id);
 2304         if (!$tplcohort->get('id')) {
 2305             return true;
 2306         }
 2307 
 2308         return $tplcohort->delete();
 2309     }
 2310 
 2311     /**
 2312      * Lists user plans.
 2313      *
 2314      * @param int $userid
 2315      * @return \core_competency\plan[]
 2316      */
 2317     public static function list_user_plans($userid) {
 2318         global $DB, $USER;
 2319         static::require_enabled();
 2320         $select = 'userid = :userid';
 2321         $params = array('userid' => $userid);
 2322         $context = context_user::instance($userid);
 2323 
 2324         // Check that we can read something here.
 2325         if (!plan::can_read_user($userid) && !plan::can_read_user_draft($userid)) {
 2326             throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
 2327         }
 2328 
 2329         // The user cannot view the drafts.
 2330         if (!plan::can_read_user_draft($userid)) {
 2331             list($insql, $inparams) = $DB->get_in_or_equal(plan::get_draft_statuses(), SQL_PARAMS_NAMED, 'param', false);
 2332             $select .= " AND status $insql";
 2333             $params += $inparams;
 2334         }
 2335         // The user cannot view the non-drafts.
 2336         if (!plan::can_read_user($userid)) {
 2337             list($insql, $inparams) = $DB->get_in_or_equal(array(plan::STATUS_ACTIVE, plan::STATUS_COMPLETE),
 2338                 SQL_PARAMS_NAMED, 'param', false);
 2339             $select .= " AND status $insql";
 2340             $params += $inparams;
 2341         }
 2342 
 2343         return plan::get_records_select($select, $params, 'name ASC');
 2344     }
 2345 
 2346     /**
 2347      * List the plans to review.
 2348      *
 2349      * The method returns values in this format:
 2350      *
 2351      * array(
 2352      *     'plans' => array(
 2353      *         (stdClass)(
 2354      *             'plan' => (plan),
 2355      *             'template' => (template),
 2356      *             'owner' => (stdClass)
 2357      *         )
 2358      *     ),
 2359      *     'count' => (int)
 2360      * )
 2361      *
 2362      * @param int $skip The number of records to skip.
 2363      * @param int $limit The number of results to return.
 2364      * @param int $userid The user we're getting the plans to review for.
 2365      * @return array Containing the keys 'count', and 'plans'. The 'plans' key contains an object
 2366      *               which contains 'plan', 'template' and 'owner'.
 2367      */
 2368     public static function list_plans_to_review($skip = 0, $limit = 100, $userid = null) {
 2369         global $DB, $USER;
 2370         static::require_enabled();
 2371 
 2372         if ($userid === null) {
 2373             $userid = $USER->id;
 2374         }
 2375 
 2376         $planfields = plan::get_sql_fields('p', 'plan_');
 2377         $tplfields = template::get_sql_fields('t', 'tpl_');
 2378         $usercols = array('id') + get_user_fieldnames();
 2379         $userfields = array();
 2380         foreach ($usercols as $field) {
 2381             $userfields[] = "u." . $field . " AS usr_" . $field;
 2382         }
 2383         $userfields = implode(',', $userfields);
 2384 
 2385         $select = "SELECT $planfields, $tplfields, $userfields";
 2386         $countselect = "SELECT COUNT('x')";
 2387 
 2388         $sql = "  FROM {" . plan::TABLE . "} p
 2389                   JOIN {user} u
 2390                     ON u.id = p.userid
 2391              LEFT JOIN {" . template::TABLE . "} t
 2392                     ON t.id = p.templateid
 2393                  WHERE (p.status = :waitingforreview
 2394                     OR (p.status = :inreview AND p.reviewerid = :reviewerid))
 2395                    AND p.userid != :userid";
 2396 
 2397         $params = array(
 2398             'waitingforreview' => plan::STATUS_WAITING_FOR_REVIEW,
 2399             'inreview' => plan::STATUS_IN_REVIEW,
 2400             'reviewerid' => $userid,
 2401             'userid' => $userid
 2402         );
 2403 
 2404         // Primary check to avoid the hard work of getting the users in which the user has permission.
 2405         $count = $DB->count_records_sql($countselect . $sql, $params);
 2406         if ($count < 1) {
 2407             return array('count' => 0, 'plans' => array());
 2408         }
 2409 
 2410         // TODO MDL-52243 Use core function.
 2411         list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql('moodle/competency:planreview',
 2412             $userid, SQL_PARAMS_NAMED);
 2413         $sql .= " AND p.userid $insql";
 2414         $params += $inparams;
 2415 
 2416         // Order by ID just to have some ordering in place.
 2417         $ordersql = " ORDER BY p.id ASC";
 2418 
 2419         $plans = array();
 2420         $records = $DB->get_recordset_sql($select . $sql . $ordersql, $params, $skip, $limit);
 2421         foreach ($records as $record) {
 2422             $plan = new plan(0, plan::extract_record($record, 'plan_'));
 2423             $template = null;
 2424 
 2425             if ($plan->is_based_on_template()) {
 2426                 $template = new template(0, template::extract_record($record, 'tpl_'));
 2427             }
 2428 
 2429             $plans[] = (object) array(
 2430                 'plan' => $plan,
 2431                 'template' => $template,
 2432                 'owner' => persistent::extract_record($record, 'usr_'),
 2433             );
 2434         }
 2435         $records->close();
 2436 
 2437         return array(
 2438             'count' => $DB->count_records_sql($countselect . $sql, $params),
 2439             'plans' => $plans
 2440         );
 2441     }
 2442 
 2443     /**
 2444      * Creates a learning plan based on the provided data.
 2445      *
 2446      * @param stdClass $record
 2447      * @return \core_competency\plan
 2448      */
 2449     public static function create_plan(stdClass $record) {
 2450         global $USER;
 2451         static::require_enabled();
 2452         $plan = new plan(0, $record);
 2453 
 2454         if ($plan->is_based_on_template()) {
 2455             throw new coding_exception('To create a plan from a template use api::create_plan_from_template().');
 2456         } else if ($plan->get('status') == plan::STATUS_COMPLETE) {
 2457             throw new coding_exception('A plan cannot be created as complete.');
 2458         }
 2459 
 2460         if (!$plan->can_manage()) {
 2461             $context = context_user::instance($plan->get('userid'));
 2462             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
 2463         }
 2464 
 2465         $plan->create();
 2466 
 2467         // Trigger created event.
 2468         \core\event\competency_plan_created::create_from_plan($plan)->trigger();
 2469         return $plan;
 2470     }
 2471 
 2472     /**
 2473      * Create a learning plan from a template.
 2474      *
 2475      * @param  mixed $templateorid The template object or ID.
 2476      * @param  int $userid
 2477      * @return false|\core_competency\plan Returns false when the plan already exists.
 2478      */
 2479     public static function create_plan_from_template($templateorid, $userid) {
 2480         static::require_enabled();
 2481         $template = $templateorid;
 2482         if (!is_object($template)) {
 2483             $template = new template($template);
 2484         }
 2485 
 2486         // The user must be able to view the template to use it as a base for a plan.
 2487         if (!$template->can_read()) {
 2488             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
 2489                 'nopermissions', '');
 2490         }
 2491         // Can not create plan from a hidden template.
 2492         if ($template->get('visible') == false) {
 2493             throw new coding_exception('A plan can not be created from a hidden template');
 2494         }
 2495 
 2496         // Convert the template to a plan.
 2497         $record = $template->to_record();
 2498         $record->templateid = $record->id;
 2499         $record->userid = $userid;
 2500         $record->name = $record->shortname;
 2501         $record->status = plan::STATUS_ACTIVE;
 2502 
 2503         unset($record->id);
 2504         unset($record->timecreated);
 2505         unset($record->timemodified);
 2506         unset($record->usermodified);
 2507 
 2508         // Remove extra keys.
 2509         $properties = plan::properties_definition();
 2510         foreach ($record as $key => $value) {
 2511             if (!array_key_exists($key, $properties)) {
 2512                 unset($record->$key);
 2513             }
 2514         }
 2515 
 2516         $plan = new plan(0, $record);
 2517         if (!$plan->can_manage()) {
 2518             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage',
 2519                 'nopermissions', '');
 2520         }
 2521 
 2522         // We first apply the permission checks as we wouldn't want to leak information by returning early that
 2523         // the plan already exists.
 2524         if (plan::record_exists_select('templateid = :templateid AND userid = :userid', array(
 2525                 'templateid' => $template->get('id'), 'userid' => $userid))) {
 2526             return false;
 2527         }
 2528 
 2529         $plan->create();
 2530 
 2531         // Trigger created event.
 2532         \core\event\competency_plan_created::create_from_plan($plan)->trigger();
 2533         return $plan;
 2534     }
 2535 
 2536     /**
 2537      * Create learning plans from a template and cohort.
 2538      *
 2539      * @param  mixed $templateorid The template object or ID.
 2540      * @param  int $cohortid The cohort ID.
 2541      * @param  bool $recreateunlinked When true the plans that were unlinked from this template will be re-created.
 2542      * @return int The number of plans created.
 2543      */
 2544     public static function create_plans_from_template_cohort($templateorid, $cohortid, $recreateunlinked = false) {
 2545         global $DB, $CFG;
 2546         static::require_enabled();
 2547         require_once($CFG->dirroot . '/cohort/lib.php');
 2548 
 2549         $template = $templateorid;
 2550         if (!is_object($template)) {
 2551             $template = new template($template);
 2552         }
 2553 
 2554         // The user must be able to view the template to use it as a base for a plan.
 2555         if (!$template->can_read()) {
 2556             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
 2557                 'nopermissions', '');
 2558         }
 2559 
 2560         // Can not create plan from a hidden template.
 2561         if ($template->get('visible') == false) {
 2562             throw new coding_exception('A plan can not be created from a hidden template');
 2563         }
 2564 
 2565         // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
 2566         $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
 2567         $cohortcontext = context::instance_by_id($cohort->contextid);
 2568         if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
 2569             throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
 2570         }
 2571 
 2572         // Convert the template to a plan.
 2573         $recordbase = $template->to_record();
 2574         $recordbase->templateid = $recordbase->id;
 2575         $recordbase->name = $recordbase->shortname;
 2576         $recordbase->status = plan::STATUS_ACTIVE;
 2577 
 2578         unset($recordbase->id);
 2579         unset($recordbase->timecreated);
 2580         unset($recordbase->timemodified);
 2581         unset($recordbase->usermodified);
 2582 
 2583         // Remove extra keys.
 2584         $properties = plan::properties_definition();
 2585         foreach ($recordbase as $key => $value) {
 2586             if (!array_key_exists($key, $properties)) {
 2587                 unset($recordbase->$key);
 2588             }
 2589         }
 2590 
 2591         // Create the plans.
 2592         $created = 0;
 2593         $userids = template_cohort::get_missing_plans($template->get('id'), $cohortid, $recreateunlinked);
 2594         foreach ($userids as $userid) {
 2595             $record = (object) (array) $recordbase;
 2596             $record->userid = $userid;
 2597 
 2598             $plan = new plan(0, $record);
 2599             if (!$plan->can_manage()) {
 2600                 // Silently skip members where permissions are lacking.
 2601                 continue;
 2602             }
 2603 
 2604             $plan->create();
 2605             // Trigger created event.
 2606             \core\event\competency_plan_created::create_from_plan($plan)->trigger();
 2607             $created++;
 2608         }
 2609 
 2610         return $created;
 2611     }
 2612 
 2613     /**
 2614      * Unlink a plan from its template.
 2615      *
 2616      * @param  \core_competency\plan|int $planorid The plan or its ID.
 2617      * @return bool
 2618      */
 2619     public static function unlink_plan_from_template($planorid) {
 2620         global $DB;
 2621         static::require_enabled();
 2622 
 2623         $plan = $planorid;
 2624         if (!is_object($planorid)) {
 2625             $plan = new plan($planorid);
 2626         }
 2627 
 2628         // The user must be allowed to manage the plans of the user, nothing about the template.
 2629         if (!$plan->can_manage()) {
 2630             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
 2631         }
 2632 
 2633         // Only plan with status DRAFT or ACTIVE can be unliked..
 2634         if ($plan->get('status') == plan::STATUS_COMPLETE) {
 2635             throw new coding_exception('Only draft or active plan can be unliked from a template');
 2636         }
 2637 
 2638         // Early exit, it's already done...
 2639         if (!$plan->is_based_on_template()) {
 2640             return true;
 2641         }
 2642 
 2643         // Fetch the template.
 2644         $template = new template($plan->get('templateid'));
 2645 
 2646         // Now, proceed by copying all competencies to the plan, then update the plan.
 2647         $transaction = $DB->start_delegated_transaction();
 2648         $competencies = template_competency::list_competencies($template->get('id'), false);
 2649         $i = 0;
 2650         foreach ($competencies as $competency) {
 2651             $record = (object) array(
 2652                 'planid' => $plan->get('id'),
 2653                 'competencyid' => $competency->get('id'),
 2654                 'sortorder' => $i++
 2655             );
 2656             $pc = new plan_competency(null, $record);
 2657             $pc->create();
 2658         }
 2659         $plan->set('origtemplateid', $template->get('id'));
 2660         $plan->set('templateid', null);
 2661         $success = $plan->update();
 2662         $transaction->allow_commit();
 2663 
 2664         // Trigger unlinked event.
 2665         \core\event\competency_plan_unlinked::create_from_plan($plan)->trigger();
 2666 
 2667         return $success;
 2668     }
 2669 
 2670     /**
 2671      * Updates a plan.
 2672      *
 2673      * @param stdClass $record
 2674      * @return \core_competency\plan
 2675      */
 2676     public static function update_plan(stdClass $record) {
 2677         static::require_enabled();
 2678 
 2679         $plan = new plan($record->id);
 2680 
 2681         // Validate that the plan as it is can be managed.
 2682         if (!$plan->can_manage()) {
 2683             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
 2684 
 2685         } else if ($plan->get('status') == plan::STATUS_COMPLETE) {
 2686             // A completed plan cannot be edited.
 2687             throw new coding_exception('Completed plan cannot be edited.');
 2688 
 2689         } else if ($plan->is_based_on_template()) {
 2690             // Prevent a plan based on a template to be edited.
 2691             throw new coding_exception('Cannot update a plan that is based on a template.');
 2692 
 2693         } else if (isset($record->templateid) && $plan->get('templateid') != $record->templateid) {
 2694             // Prevent a plan to be based on a template.
 2695             throw new coding_exception('Cannot base a plan on a template.');
 2696 
 2697         } else if (isset($record->userid) && $plan->get('userid') != $record->userid) {
 2698             // Prevent change of ownership as the capabilities are checked against that.
 2699             throw new coding_exception('A plan cannot be transfered to another user');
 2700 
 2701         } else if (isset($record->status) && $plan->get('status') != $record->status) {
 2702             // Prevent change of status.
 2703             throw new coding_exception('To change the status of a plan use the appropriate methods.');
 2704 
 2705         }
 2706 
 2707         $plan->from_record($record);
 2708         $plan->update();
 2709 
 2710         // Trigger updated event.
 2711         \core\event\competency_plan_updated::create_from_plan($plan)->trigger();
 2712 
 2713         return $plan;
 2714     }
 2715 
 2716     /**
 2717      * Returns a plan data.
 2718      *
 2719      * @param int $id
 2720      * @return \core_competency\plan
 2721      */
 2722     public static function read_plan($id) {
 2723         static::require_enabled();
 2724         $plan = new plan($id);
 2725 
 2726         if (!$plan->can_read()) {
 2727             $context = context_user::instance($plan->get('userid'));
 2728             throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
 2729         }
 2730 
 2731         return $plan;
 2732     }
 2733 
 2734     /**
 2735      * Plan event viewed.
 2736      *
 2737      * @param mixed $planorid The id or the plan.
 2738      * @return boolean
 2739      */
 2740     public static function plan_viewed($planorid) {
 2741         static::require_enabled();
 2742         $plan = $planorid;
 2743         if (!is_object($plan)) {
 2744             $plan = new plan($plan);
 2745         }
 2746 
 2747         // First we do a permissions check.
 2748         if (!$plan->can_read()) {
 2749             $context = context_user::instance($plan->get('userid'));
 2750             throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
 2751         }
 2752 
 2753         // Trigger a template viewed event.
 2754         \core\event\competency_plan_viewed::create_from_plan($plan)->trigger();
 2755 
 2756         return true;
 2757     }
 2758 
 2759     /**
 2760      * Deletes a plan.
 2761      *
 2762      * Plans based on a template can be removed just like any other one.
 2763      *
 2764      * @param int $id
 2765      * @return bool Success?
 2766      */
 2767     public static function delete_plan($id) {
 2768         global $DB;
 2769         static::require_enabled();
 2770 
 2771         $plan = new plan($id);
 2772 
 2773         if (!$plan->can_manage()) {
 2774             $context = context_user::instance($plan->get('userid'));
 2775             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
 2776         }
 2777 
 2778         // Wrap the suppression in a DB transaction.
 2779         $transaction = $DB->start_delegated_transaction();
 2780 
 2781         // Delete plan competencies.
 2782         $plancomps = plan_competency::get_records(array('planid' => $plan->get('id')));
 2783         foreach ($plancomps as $plancomp) {
 2784             $plancomp->delete();
 2785         }
 2786 
 2787         // Delete archive user competencies if the status of the plan is complete.
 2788         if ($plan->get('status') == plan::STATUS_COMPLETE) {
 2789             self::remove_archived_user_competencies_in_plan($plan);
 2790         }
 2791         $event = \core\event\competency_plan_deleted::create_from_plan($plan);
 2792         $success = $plan->delete();
 2793 
 2794         $transaction->allow_commit();
 2795 
 2796         // Trigger deleted event.
 2797         $event->trigger();
 2798 
 2799         return $success;
 2800     }
 2801 
 2802     /**
 2803      * Cancel the review of a plan.
 2804      *
 2805      * @param int|plan $planorid The plan, or its ID.
 2806      * @return bool
 2807      */
 2808     public static function plan_cancel_review_request($planorid) {
 2809         static::require_enabled();
 2810         $plan = $planorid;
 2811         if (!is_object($plan)) {
 2812             $plan = new plan($plan);
 2813         }
 2814 
 2815         // We need to be able to view the plan at least.
 2816         if (!$plan->can_read()) {
 2817             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
 2818         }
 2819 
 2820         if ($plan->is_based_on_template()) {
 2821             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
 2822         } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) {
 2823             throw new coding_exception('The plan review cannot be cancelled at this stage.');
 2824         } else if (!$plan->can_request_review()) {
 2825             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
 2826         }
 2827 
 2828         $plan->set('status', plan::STATUS_DRAFT);
 2829         $result = $plan->update();
 2830 
 2831         // Trigger review request cancelled event.
 2832         \core\event\competency_plan_review_request_cancelled::create_from_plan($plan)->trigger();
 2833 
 2834         return $result;
 2835     }
 2836 
 2837     /**
 2838      * Request the review of a plan.
 2839      *
 2840      * @param int|plan $planorid The plan, or its ID.
 2841      * @return bool
 2842      */
 2843     public static function plan_request_review($planorid) {
 2844         static::require_enabled();
 2845         $plan = $planorid;
 2846         if (!is_object($plan)) {
 2847             $plan = new plan($plan);
 2848         }
 2849 
 2850         // We need to be able to view the plan at least.
 2851         if (!$plan->can_read()) {
 2852             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
 2853         }
 2854 
 2855         if ($plan->is_based_on_template()) {
 2856             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
 2857         } else if ($plan->get('status') != plan::STATUS_DRAFT) {
 2858             throw new coding_exception('The plan cannot be sent for review at this stage.');
 2859         } else if (!$plan->can_request_review()) {
 2860             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
 2861         }
 2862 
 2863         $plan->set('status', plan::STATUS_WAITING_FOR_REVIEW);
 2864         $result = $plan->update();
 2865 
 2866         // Trigger review requested event.
 2867         \core\event\competency_plan_review_requested::create_from_plan($plan)->trigger();
 2868 
 2869         return $result;
 2870     }
 2871 
 2872     /**
 2873      * Start the review of a plan.
 2874      *
 2875      * @param int|plan $planorid The plan, or its ID.
 2876      * @return bool
 2877      */
 2878     public static function plan_start_review($planorid) {
 2879         global $USER;
 2880         static::require_enabled();
 2881         $plan = $planorid;
 2882         if (!is_object($plan)) {
 2883             $plan = new plan($plan);
 2884         }
 2885 
 2886         // We need to be able to view the plan at least.
 2887         if (!$plan->can_read()) {
 2888             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
 2889         }
 2890 
 2891         if ($plan->is_based_on_template()) {
 2892             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
 2893         } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) {
 2894             throw new coding_exception('The plan review cannot be started at this stage.');
 2895         } else if (!$plan->can_review()) {
 2896             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
 2897         }
 2898 
 2899         $plan->set('status', plan::STATUS_IN_REVIEW);
 2900         $plan->set('reviewerid', $USER->id);
 2901         $result = $plan->update();
 2902 
 2903         // Trigger review started event.
 2904         \core\event\competency_plan_review_started::create_from_plan($plan)->trigger();
 2905 
 2906         return $result;
 2907     }
 2908 
 2909     /**
 2910      * Stop reviewing a plan.
 2911      *
 2912      * @param  int|plan $planorid The plan, or its ID.
 2913      * @return bool
 2914      */
 2915     public static function plan_stop_review($planorid) {
 2916         static::require_enabled();
 2917         $plan = $planorid;
 2918         if (!is_object($plan)) {
 2919             $plan = new plan($plan);
 2920         }
 2921 
 2922         // We need to be able to view the plan at least.
 2923         if (!$plan->can_read()) {
 2924             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
 2925         }
 2926 
 2927         if ($plan->is_based_on_template()) {
 2928             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
 2929         } else if ($plan->get('status') != plan::STATUS_IN_REVIEW) {
 2930             throw new coding_exception('The plan review cannot be stopped at this stage.');
 2931         } else if (!$plan->can_review()) {
 2932             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
 2933         }
 2934 
 2935         $plan->set('status', plan::STATUS_DRAFT);
 2936         $plan->set('reviewerid', null);
 2937         $result = $plan->update();
 2938 
 2939         // Trigger review stopped event.
 2940         \core\event\competency_plan_review_stopped::create_from_plan($plan)->trigger();
 2941 
 2942         return $result;
 2943     }
 2944 
 2945     /**
 2946      * Approve a plan.
 2947      *
 2948      * This means making the plan active.
 2949      *
 2950      * @param  int|plan $planorid The plan, or its ID.
 2951      * @return bool
 2952      */
 2953     public static function approve_plan($planorid) {
 2954         static::require_enabled();
 2955         $plan = $planorid;
 2956         if (!is_object($plan)) {
 2957             $plan = new plan($plan);
 2958         }
 2959 
 2960         // We need to be able to view the plan at least.
 2961         if (!$plan->can_read()) {
 2962             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
 2963         }
 2964 
 2965         // We can approve a plan that is either a draft, in review, or waiting for review.
 2966         if ($plan->is_based_on_template()) {
 2967             throw new coding_exception('Template plans are already approved.');   // This should never happen.
 2968         } else if (!$plan->is_draft()) {
 2969             throw new coding_exception('The plan cannot be approved at this stage.');
 2970         } else if (!$plan->can_review()) {
 2971             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
 2972         }
 2973 
 2974         $plan->set('status', plan::STATUS_ACTIVE);
 2975         $plan->set('reviewerid', null);
 2976         $result = $plan->update();
 2977 
 2978         // Trigger approved event.
 2979         \core\event\competency_plan_approved::create_from_plan($plan)->trigger();
 2980 
 2981         return $result;
 2982     }
 2983 
 2984     /**
 2985      * Unapprove a plan.
 2986      *
 2987      * This means making the plan draft.
 2988      *
 2989      * @param  int|plan $planorid The plan, or its ID.
 2990      * @return bool
 2991      */
 2992     public static function unapprove_plan($planorid) {
 2993         static::require_enabled();
 2994         $plan = $planorid;
 2995         if (!is_object($plan)) {
 2996             $plan = new plan($plan);
 2997         }
 2998 
 2999         // We need to be able to view the plan at least.
 3000         if (!$plan->can_read()) {
 3001             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
 3002         }
 3003 
 3004         if ($plan->is_based_on_template()) {
 3005             throw new coding_exception('Template plans are always approved.');   // This should never happen.
 3006         } else if ($plan->get('status') != plan::STATUS_ACTIVE) {
 3007             throw new coding_exception('The plan cannot be sent back to draft at this stage.');
 3008         } else if (!$plan->can_review()) {
 3009             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
 3010         }
 3011 
 3012         $plan->set('status', plan::STATUS_DRAFT);
 3013         $result = $plan->update();
 3014 
 3015         // Trigger unapproved event.
 3016         \core\event\competency_plan_unapproved::create_from_plan($plan)->trigger();
 3017 
 3018         return $result;
 3019     }
 3020 
 3021     /**
 3022      * Complete a plan.
 3023      *
 3024      * @param int|plan $planorid The plan, or its ID.
 3025      * @return bool
 3026      */
 3027     public static function complete_plan($planorid) {
 3028         global $DB;
 3029         static::require_enabled();
 3030 
 3031         $plan = $planorid;
 3032         if (!is_object($planorid)) {
 3033             $plan = new plan($planorid);
 3034         }
 3035 
 3036         // Validate that the plan can be managed.
 3037         if (!$plan->can_manage()) {
 3038             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
 3039         }
 3040 
 3041         // Check if the plan was already completed.
 3042         if ($plan->get('status') == plan::STATUS_COMPLETE) {
 3043             throw new coding_exception('The plan is already completed.');
 3044         }
 3045 
 3046         $originalstatus = $plan->get('status');
 3047         $plan->set('status', plan::STATUS_COMPLETE);
 3048 
 3049         // The user should also be able to manage the plan when it's completed.
 3050         if (!$plan->can_manage()) {
 3051             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
 3052         }
 3053 
 3054         // Put back original status because archive needs it to extract competencies from the right table.
 3055         $plan->set('status', $originalstatus);
 3056 
 3057         // Do the things.
 3058         $transaction = $DB->start_delegated_transaction();
 3059         self::archive_user_competencies_in_plan($plan);
 3060         $plan->set('status', plan::STATUS_COMPLETE);
 3061         $success = $plan->update();
 3062 
 3063         if (!$success) {
 3064             $transaction->rollback(new moodle_exception('The plan could not be updated.'));
 3065             return $success;
 3066         }
 3067 
 3068         $transaction->allow_commit();
 3069 
 3070         // Trigger updated event.
 3071         \core\event\competency_plan_completed::create_from_plan($plan)->trigger();
 3072 
 3073         return $success;
 3074     }
 3075 
 3076     /**
 3077      * Reopen a plan.
 3078      *
 3079      * @param int|plan $planorid The plan, or its ID.
 3080      * @return bool
 3081      */
 3082     public static function reopen_plan($planorid) {
 3083         global $DB;
 3084         static::require_enabled();
 3085 
 3086         $plan = $planorid;
 3087         if (!is_object($planorid)) {
 3088             $plan = new plan($planorid);
 3089         }
 3090 
 3091         // Validate that the plan as it is can be managed.
 3092         if (!$plan->can_manage()) {
 3093             $context = context_user::instance($plan->get('userid'));
 3094             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
 3095         }
 3096 
 3097         $beforestatus = $plan->get('status');
 3098         $plan->set('status', plan::STATUS_ACTIVE);
 3099 
 3100         // Validate if status can be changed.
 3101         if (!$plan->can_manage()) {
 3102             $context = context_user::instance($plan->get('userid'));
 3103             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
 3104         }
 3105 
 3106         // Wrap the updates in a DB transaction.
 3107         $transaction = $DB->start_delegated_transaction();
 3108 
 3109         // Delete archived user competencies if the status of the plan is changed from complete to another status.
 3110         $mustremovearchivedcompetencies = ($beforestatus == plan::STATUS_COMPLETE && $plan->get('status') != plan::STATUS_COMPLETE);
 3111         if ($mustremovearchivedcompetencies) {
 3112             self::remove_archived_user_competencies_in_plan($plan);
 3113         }
 3114 
 3115         // If duedate less than or equal to duedate_threshold unset it.
 3116         if ($plan->get('duedate') <= time() + plan::DUEDATE_THRESHOLD) {
 3117             $plan->set('duedate', 0);
 3118         }
 3119 
 3120         $success = $plan->update();
 3121 
 3122         if (!$success) {
 3123             $transaction->rollback(new moodle_exception('The plan could not be updated.'));
 3124             return $success;
 3125         }
 3126 
 3127         $transaction->allow_commit();
 3128 
 3129         // Trigger reopened event.
 3130         \core\event\competency_plan_reopened::create_from_plan($plan)->trigger();
 3131 
 3132         return $success;
 3133     }
 3134 
 3135     /**
 3136      * Get a single competency from the user plan.
 3137      *
 3138      * @param  int $planorid The plan, or its ID.
 3139      * @param  int $competencyid The competency id.
 3140      * @return (object) array(
 3141      *                      'competency' => \core_competency\competency,
 3142      *                      'usercompetency' => \core_competency\user_competency
 3143      *                      'usercompetencyplan' => \core_competency\user_competency_plan
 3144      *                  )
 3145      *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
 3146      */
 3147     public static function get_plan_competency($planorid, $competencyid) {
 3148         static::require_enabled();
 3149         $plan = $planorid;
 3150         if (!is_object($planorid)) {
 3151             $plan = new plan($planorid);
 3152         }
 3153 
 3154         if (!user_competency::can_read_user($plan->get('userid'))) {
 3155             throw new required_capability_exception($plan->get_context(), 'moodle/competency:usercompetencyview',
 3156                 'nopermissions', '');
 3157         }
 3158 
 3159         $competency = $plan->get_competency($competencyid);
 3160 
 3161         // Get user competencies from user_competency_plan if the plan status is set to complete.
 3162         $iscompletedplan = $plan->get('status') == plan::STATUS_COMPLETE;
 3163         if ($iscompletedplan) {
 3164             $usercompetencies = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), array($competencyid));
 3165             $ucresultkey = 'usercompetencyplan';
 3166         } else {
 3167             $usercompetencies = user_competency::get_multiple($plan->get('userid'), array($competencyid));
 3168             $ucresultkey = 'usercompetency';
 3169         }
 3170 
 3171         $found = count($usercompetencies);
 3172 
 3173         if ($found) {
 3174             $uc = array_pop($usercompetencies);
 3175         } else {
 3176             if ($iscompletedplan) {
 3177                 throw new coding_exception('A user competency plan is missing');
 3178             } else {
 3179                 $uc = user_competency::create_relation($plan->get('userid'), $competency->get('id'));
 3180                 $uc->create();
 3181             }
 3182         }
 3183 
 3184         $plancompetency = (object) array(
 3185             'competency' => $competency,
 3186             'usercompetency' => null,
 3187             'usercompetencyplan' => null
 3188         );
 3189         $plancompetency->$ucresultkey = $uc;
 3190 
 3191         return $plancompetency;
 3192     }
 3193 
 3194     /**
 3195      * List the competencies in a user plan.
 3196      *
 3197      * @param  int $planorid The plan, or its ID.
 3198      * @return array((object) array(
 3199      *                            'competency' => \core_competency\competency,
 3200      *                            'usercompetency' => \core_competency\user_competency
 3201      *                            'usercompetencyplan' => \core_competency\user_competency_plan
 3202      *                        ))
 3203      *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
 3204      */
 3205     public static function list_plan_competencies($planorid) {
 3206         static::require_enabled();
 3207         $plan = $planorid;
 3208         if (!is_object($planorid)) {
 3209             $plan = new plan($planorid);
 3210         }
 3211 
 3212         if (!$plan->can_read()) {
 3213             $context = context_user::instance($plan->get('userid'));
 3214             throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
 3215         }
 3216 
 3217         $result = array();
 3218         $competencies = $plan->get_competencies();
 3219 
 3220         // Get user competencies from user_competency_plan if the plan status is set to complete.
 3221         $iscompletedplan = $plan->get('status') == plan::STATUS_COMPLETE;
 3222         if ($iscompletedplan) {
 3223             $usercompetencies = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), $competencies);
 3224             $ucresultkey = 'usercompetencyplan';
 3225         } else {
 3226             $usercompetencies = user_competency::get_multiple($plan->get('userid'), $competencies);
 3227             $ucresultkey = 'usercompetency';
 3228         }
 3229 
 3230         // Build the return values.
 3231         foreach ($competencies as $key => $competency) {
 3232             $found = false;
 3233 
 3234             foreach ($usercompetencies as $uckey => $uc) {
 3235                 if ($uc->get('competencyid') == $competency->get('id')) {
 3236                     $found = true;
 3237                     unset($usercompetencies[$uckey]);
 3238                     break;
 3239                 }
 3240             }
 3241 
 3242             if (!$found) {
 3243                 if ($iscompletedplan) {
 3244                     throw new coding_exception('A user competency plan is missing');
 3245                 } else {
 3246                     $uc = user_competency::create_relation($plan->get('userid'), $competency->get('id'));
 3247                 }
 3248             }
 3249 
 3250             $plancompetency = (object) array(
 3251                 'competency' => $competency,
 3252                 'usercompetency' => null,
 3253                 'usercompetencyplan' => null
 3254             );
 3255             $plancompetency->$ucresultkey = $uc;
 3256             $result[] = $plancompetency;
 3257         }
 3258 
 3259         return $result;
 3260     }
 3261 
 3262     /**
 3263      * Add a competency to a plan.
 3264      *
 3265      * @param int $planid The id of the plan
 3266      * @param int $competencyid The id of the competency
 3267      * @return bool
 3268      */
 3269     public static function add_competency_to_plan($planid, $competencyid) {
 3270         static::require_enabled();
 3271         $plan = new plan($planid);
 3272 
 3273         // First we do a permissions check.
 3274         if (!$plan->can_manage()) {
 3275             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
 3276 
 3277         } else if ($plan->is_based_on_template()) {
 3278             throw new coding_exception('A competency can not be added to a learning plan based on a template');
 3279         }
 3280 
 3281         if (!$plan->can_be_edited()) {
 3282             throw new coding_exception('A competency can not be added to a learning plan completed');
 3283         }
 3284 
 3285         $competency = new competency($competencyid);
 3286 
 3287         // Can not add a competency that belong to a hidden framework.
 3288         if ($competency->get_framework()->get('visible') == false) {
 3289             throw new coding_exception('A competency belonging to hidden framework can not be added');
 3290         }
 3291 
 3292         $exists = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
 3293         if (!$exists) {
 3294             $record = new stdClass();
 3295             $record->planid = $planid;
 3296             $record->competencyid = $competencyid;
 3297             $plancompetency = new plan_competency(0, $record);
 3298             $plancompetency->create();
 3299         }
 3300 
 3301         return true;
 3302     }
 3303 
 3304     /**
 3305      * Remove a competency from a plan.
 3306      *
 3307      * @param int $planid The plan id
 3308      * @param int $competencyid The id of the competency
 3309      * @return bool
 3310      */
 3311     public static function remove_competency_from_plan($planid, $competencyid) {
 3312         static::require_enabled();
 3313         $plan = new plan($planid);
 3314 
 3315         // First we do a permissions check.
 3316         if (!$plan->can_manage()) {
 3317             $context = context_user::instance($plan->get('userid'));
 3318             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
 3319 
 3320         } else if ($plan->is_based_on_template()) {
 3321             throw new coding_exception('A competency can not be removed from a learning plan based on a template');
 3322         }
 3323 
 3324         if (!$plan->can_be_edited()) {
 3325             throw new coding_exception('A competency can not be removed from a learning plan completed');
 3326         }
 3327 
 3328         $link = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
 3329         if ($link) {
 3330             return $link->delete();
 3331         }
 3332         return false;
 3333     }
 3334 
 3335     /**
 3336      * Move the plan competency up or down in the display list.
 3337      *
 3338      * Requires moodle/competency:planmanage capability at the system context.
 3339      *
 3340      * @param int $planid The plan  id
 3341      * @param int $competencyidfrom The id of the competency we are moving.
 3342      * @param int $competencyidto The id of the competency we are moving to.
 3343      * @return boolean
 3344      */
 3345     public static function reorder_plan_competency($planid, $competencyidfrom, $competencyidto) {
 3346         static::require_enabled();
 3347         $plan = new plan($planid);
 3348 
 3349         // First we do a permissions check.
 3350         if (!$plan->can_manage()) {
 3351             $context = context_user::instance($plan->get('userid'));
 3352             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
 3353 
 3354         } else if ($plan->is_based_on_template()) {
 3355             throw new coding_exception('A competency can not be reordered in a learning plan based on a template');
 3356         }
 3357 
 3358         if (!$plan->can_be_edited()) {
 3359             throw new coding_exception('A competency can not be reordered in a learning plan completed');
 3360         }
 3361 
 3362         $down = true;
 3363         $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidfrom));
 3364         if (count($matches) == 0) {
 3365             throw new coding_exception('The link does not exist');
 3366         }
 3367 
 3368         $competencyfrom = array_pop($matches);
 3369         $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidto));
 3370         if (count($matches) == 0) {
 3371             throw new coding_exception('The link does not exist');
 3372         }
 3373 
 3374         $competencyto = array_pop($matches);
 3375 
 3376         $all = plan_competency::get_records(array('planid' => $planid), 'sortorder', 'ASC', 0, 0);
 3377 
 3378         if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
 3379             // We are moving up, so put it before the "to" item.
 3380             $down = false;
 3381         }
 3382 
 3383         foreach ($all as $id => $plancompetency) {
 3384             $sort = $plancompetency->get('sortorder');
 3385             if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
 3386                 $plancompetency->set('sortorder', $plancompetency->get('sortorder') - 1);
 3387                 $plancompetency->update();
 3388             } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
 3389                 $plancompetency->set('sortorder', $plancompetency->get('sortorder') + 1);
 3390                 $plancompetency->update();
 3391             }
 3392         }
 3393         $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
 3394         return $competencyfrom->update();
 3395     }
 3396 
 3397     /**
 3398      * Cancel a user competency review request.
 3399      *
 3400      * @param  int $userid       The user ID.
 3401      * @param  int $competencyid The competency ID.
 3402      * @return bool
 3403      */
 3404     public static function user_competency_cancel_review_request($userid, $competencyid) {
 3405         static::require_enabled();
 3406         $context = context_user::instance($userid);
 3407         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
 3408         if (!$uc || !$uc->can_read()) {
 3409             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
 3410         } else if ($uc->get('status') != user_competency::STATUS_WAITING_FOR_REVIEW) {
 3411             throw new coding_exception('The competency can not be cancel review request at this stage.');
 3412         } else if (!$uc->can_request_review()) {
 3413             throw new required_capability_exception($context, 'moodle/competency:usercompetencyrequestreview', 'nopermissions', '');
 3414         }
 3415 
 3416         $uc->set('status', user_competency::STATUS_IDLE);
 3417         $result = $uc->update();
 3418         if ($result) {
 3419             \core\event\competency_user_competency_review_request_cancelled::create_from_user_competency($uc)->trigger();
 3420         }
 3421         return $result;
 3422     }
 3423 
 3424     /**
 3425      * Request a user competency review.
 3426      *
 3427      * @param  int $userid       The user ID.
 3428      * @param  int $competencyid The competency ID.
 3429      * @return bool
 3430      */
 3431     public static function user_competency_request_review($userid, $competencyid) {
 3432         static::require_enabled();
 3433         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
 3434         if (!$uc) {
 3435             $uc = user_competency::create_relation($userid, $competencyid);
 3436             $uc->create();
 3437         }
 3438 
 3439         if (!$uc->can_read()) {
 3440             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
 3441                 'nopermissions', '');
 3442         } else if ($uc->get('status') != user_competency::STATUS_IDLE) {
 3443             throw new coding_exception('The competency can not be sent for review at this stage.');
 3444         } else if (!$uc->can_request_review()) {
 3445             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyrequestreview',
 3446                 'nopermissions', '');
 3447         }
 3448 
 3449         $uc->set('status', user_competency::STATUS_WAITING_FOR_REVIEW);
 3450         $result = $uc->update();
 3451         if ($result) {
 3452             \core\event\competency_user_competency_review_requested::create_from_user_competency($uc)->trigger();
 3453         }
 3454         return $result;
 3455     }
 3456 
 3457     /**
 3458      * Start a user competency review.
 3459      *
 3460      * @param  int $userid       The user ID.
 3461      * @param  int $competencyid The competency ID.
 3462      * @return bool
 3463      */
 3464     public static function user_competency_start_review($userid, $competencyid) {
 3465         global $USER;
 3466         static::require_enabled();
 3467 
 3468         $context = context_user::instance($userid);
 3469         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
 3470         if (!$uc || !$uc->can_read()) {
 3471             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
 3472         } else if ($uc->get('status') != user_competency::STATUS_WAITING_FOR_REVIEW) {
 3473             throw new coding_exception('The competency review can not be started at this stage.');
 3474         } else if (!$uc->can_review()) {
 3475             throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', '');
 3476         }
 3477 
 3478         $uc->set('status', user_competency::STATUS_IN_REVIEW);
 3479         $uc->set('reviewerid', $USER->id);
 3480         $result = $uc->update();
 3481         if ($result) {
 3482             \core\event\competency_user_competency_review_started::create_from_user_competency($uc)->trigger();
 3483         }
 3484         return $result;
 3485     }
 3486 
 3487     /**
 3488      * Stop a user competency review.
 3489      *
 3490      * @param  int $userid       The user ID.
 3491      * @param  int $competencyid The competency ID.
 3492      * @return bool
 3493      */
 3494     public static function user_competency_stop_review($userid, $competencyid) {
 3495         static::require_enabled();
 3496         $context = context_user::instance($userid);
 3497         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
 3498         if (!$uc || !$uc->can_read()) {
 3499             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
 3500         } else if ($uc->get('status') != user_competency::STATUS_IN_REVIEW) {
 3501             throw new coding_exception('The competency review can not be stopped at this stage.');
 3502         } else if (!$uc->can_review()) {
 3503             throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', '');
 3504         }
 3505 
 3506         $uc->set('status', user_competency::STATUS_IDLE);
 3507         $result = $uc->update();
 3508         if ($result) {
 3509             \core\event\competency_user_competency_review_stopped::create_from_user_competency($uc)->trigger();
 3510         }
 3511         return $result;
 3512     }
 3513 
 3514     /**
 3515      * Log user competency viewed event.
 3516      *
 3517      * @param user_competency|int $usercompetencyorid The user competency object or user competency id
 3518      * @return bool
 3519      */
 3520     public static function user_competency_viewed($usercompetencyorid) {
 3521         static::require_enabled();
 3522         $uc = $usercompetencyorid;
 3523         if (!is_object($uc)) {
 3524             $uc = new user_competency($uc);
 3525         }
 3526 
 3527         if (!$uc || !$uc->can_read()) {
 3528             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
 3529                 'nopermissions', '');
 3530         }
 3531 
 3532         \core\event\competency_user_competency_viewed::create_from_user_competency_viewed($uc)->trigger();
 3533         return true;
 3534     }
 3535 
 3536     /**
 3537      * Log user competency viewed in plan event.
 3538      *
 3539      * @param user_competency|int $usercompetencyorid The user competency object or user competency id
 3540      * @param int $planid The plan ID
 3541      * @return bool
 3542      */
 3543     public static function user_competency_viewed_in_plan($usercompetencyorid, $planid) {
 3544         static::require_enabled();
 3545         $uc = $usercompetencyorid;
 3546         if (!is_object($uc)) {
 3547             $uc = new user_competency($uc);
 3548         }
 3549 
 3550         if (!$uc || !$uc->can_read()) {
 3551             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
 3552                 'nopermissions', '');
 3553         }
 3554         $plan = new plan($planid);
 3555         if ($plan->get('status') == plan::STATUS_COMPLETE) {
 3556             throw new coding_exception('To log the user competency in completed plan use user_competency_plan_viewed method.');
 3557         }
 3558 
 3559         \core\event\competency_user_competency_viewed_in_plan::create_from_user_competency_viewed_in_plan($uc, $planid)->trigger();
 3560         return true;
 3561     }
 3562 
 3563     /**
 3564      * Log user competency viewed in course event.
 3565      *
 3566      * @param user_competency_course|int $usercoursecompetencyorid The user competency course object or its ID.
 3567      * @param int $courseid The course ID
 3568      * @return bool
 3569      */
 3570     public static function user_competency_viewed_in_course($usercoursecompetencyorid) {
 3571         static::require_enabled();
 3572         $ucc = $usercoursecompetencyorid;
 3573         if (!is_object($ucc)) {
 3574             $ucc = new user_competency_course($ucc);
 3575         }
 3576 
 3577         if (!$ucc || !user_competency::can_read_user_in_course($ucc->get('userid'), $ucc->get('courseid'))) {
 3578             throw new required_capability_exception($ucc->get_context(), 'moodle/competency:usercompetencyview',
 3579                 'nopermissions', '');
 3580         }
 3581 
 3582         // Validate the course, this will throw an exception if not valid.
 3583         self::validate_course($ucc->get('courseid'));
 3584 
 3585         \core\event\competency_user_competency_viewed_in_course::create_from_user_competency_viewed_in_course($ucc)->trigger();
 3586         return true;
 3587     }
 3588 
 3589     /**
 3590      * Log user competency plan viewed event.
 3591      *
 3592      * @param user_competency_plan|int $usercompetencyplanorid The user competency plan object or user competency plan id
 3593      * @return bool
 3594      */
 3595     public static function user_competency_plan_viewed($usercompetencyplanorid) {
 3596         static::require_enabled();
 3597         $ucp = $usercompetencyplanorid;
 3598         if (!is_object($ucp)) {
 3599             $ucp = new user_competency_plan($ucp);
 3600         }
 3601 
 3602         if (!$ucp || !user_competency::can_read_user($ucp->get('userid'))) {
 3603             throw new required_capability_exception($ucp->get_context(), 'moodle/competency:usercompetencyview',
 3604                 'nopermissions', '');
 3605         }
 3606         $plan = new plan($ucp->get('planid'));
 3607         if ($plan->get('status') != plan::STATUS_COMPLETE) {
 3608             throw new coding_exception('To log the user competency in non-completed plan use '
 3609                 . 'user_competency_viewed_in_plan method.');
 3610         }
 3611 
 3612         \core\event\competency_user_competency_plan_viewed::create_from_user_competency_plan($ucp)->trigger();
 3613         return true;
 3614     }
 3615 
 3616     /**
 3617      * Check if template has related data.
 3618      *
 3619      * @param int $templateid The id of the template to check.
 3620      * @return boolean
 3621      */
 3622     public static function template_has_related_data($templateid) {
 3623         static::require_enabled();
 3624         // First we do a permissions check.
 3625         $template = new template($templateid);
 3626 
 3627         if (!$template->can_read()) {
 3628             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
 3629                 'nopermissions', '');
 3630         }
 3631 
 3632         // OK - all set.
 3633         return $template->has_plans();
 3634     }
 3635 
 3636     /**
 3637      * List all the related competencies.
 3638      *
 3639      * @param int $competencyid The id of the competency to check.
 3640      * @return competency[]
 3641      */
 3642     public static function list_related_competencies($competencyid) {
 3643         static::require_enabled();
 3644         $competency = new competency($competencyid);
 3645 
 3646         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
 3647                 $competency->get_context())) {
 3648             throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview',
 3649                 'nopermissions', '');
 3650         }
 3651 
 3652         return $competency->get_related_competencies();
 3653     }
 3654 
 3655     /**
 3656      * Add a related competency.
 3657      *
 3658      * @param int $competencyid The id of the competency
 3659      * @param int $relatedcompetencyid The id of the related competency.
 3660      * @return bool False when create failed, true on success, or if the relation already existed.
 3661      */
 3662     public static function add_related_competency($competencyid, $relatedcompetencyid) {
 3663         static::require_enabled();
 3664         $competency1 = new competency($competencyid);
 3665         $competency2 = new competency($relatedcompetencyid);
 3666 
 3667         require_capability('moodle/competency:competencymanage', $competency1->get_context());
 3668 
 3669         $relatedcompetency = related_competency::get_relation($competency1->get('id'), $competency2->get('id'));
 3670         if (!$relatedcompetency->get('id')) {
 3671             $relatedcompetency->create();
 3672             return true;
 3673         }
 3674 
 3675         return true;
 3676     }
 3677 
 3678     /**
 3679      * Remove a related competency.
 3680      *
 3681      * @param int $competencyid The id of the competency.
 3682      * @param int $relatedcompetencyid The id of the related competency.
 3683      * @return bool True when it was deleted, false when it wasn't or the relation doesn't exist.
 3684      */
 3685     public static function remove_related_competency($competencyid, $relatedcompetencyid) {
 3686         static::require_enabled();
 3687         $competency = new competency($competencyid);
 3688 
 3689         // This only check if we have the permission in either competency because both competencies
 3690         // should belong to the same framework.
 3691         require_capability('moodle/competency:competencymanage', $competency->get_context());
 3692 
 3693         $relatedcompetency = related_competency::get_relation($competencyid, $relatedcompetencyid);
 3694         if ($relatedcompetency->get('id')) {
 3695             return $relatedcompetency->delete();
 3696         }
 3697 
 3698         return false;
 3699     }
 3700 
 3701     /**
 3702      * Read a user evidence.
 3703      *
 3704      * @param int $id
 3705      * @return user_evidence
 3706      */
 3707     public static function read_user_evidence($id) {
 3708         static::require_enabled();
 3709         $userevidence = new user_evidence($id);
 3710 
 3711         if (!$userevidence->can_read()) {
 3712             $context = $userevidence->get_context();
 3713             throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', '');
 3714         }
 3715 
 3716         return $userevidence;
 3717     }
 3718 
 3719     /**
 3720      * Create a new user evidence.
 3721      *
 3722      * @param  object $data        The data.
 3723      * @param  int    $draftitemid The draft ID in which files have been saved.
 3724      * @return user_evidence
 3725      */
 3726     public static function create_user_evidence($data, $draftitemid = null) {
 3727         static::require_enabled();
 3728         $userevidence = new user_evidence(null, $data);
 3729         $context = $userevidence->get_context();
 3730 
 3731         if (!$userevidence->can_manage()) {
 3732             throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
 3733         }
 3734 
 3735         $userevidence->create();
 3736         if (!empty($draftitemid)) {
 3737             $fileareaoptions = array('subdirs' => true);
 3738             $itemid = $userevidence->get('id');
 3739             file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions);
 3740         }
 3741 
 3742         // Trigger an evidence of prior learning created event.
 3743         \core\event\competency_user_evidence_created::create_from_user_evidence($userevidence)->trigger();
 3744 
 3745         return $userevidence;
 3746     }
 3747 
 3748     /**
 3749      * Create a new user evidence.
 3750      *
 3751      * @param  object $data        The data.
 3752      * @param  int    $draftitemid The draft ID in which files have been saved.
 3753      * @return user_evidence
 3754      */
 3755     public static function update_user_evidence($data, $draftitemid = null) {
 3756         static::require_enabled();
 3757         $userevidence = new user_evidence($data->id);
 3758         $context = $userevidence->get_context();
 3759 
 3760         if (!$userevidence->can_manage()) {
 3761             throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
 3762 
 3763         } else if (array_key_exists('userid', $data) && $data->userid != $userevidence->get('userid')) {
 3764             throw new coding_exception('Can not change the userid of a user evidence.');
 3765         }
 3766 
 3767         $userevidence->from_record($data);
 3768         $userevidence->update();
 3769 
 3770         if (!empty($draftitemid)) {
 3771             $fileareaoptions = array('subdirs' => true);
 3772             $itemid = $userevidence->get('id');
 3773             file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions);
 3774         }
 3775 
 3776         // Trigger an evidence of prior learning updated event.
 3777         \core\event\competency_user_evidence_updated::create_from_user_evidence($userevidence)->trigger();
 3778 
 3779         return $userevidence;
 3780     }
 3781 
 3782     /**
 3783      * Delete a user evidence.
 3784      *
 3785      * @param  int $id The user evidence ID.
 3786      * @return bool
 3787      */
 3788     public static function delete_user_evidence($id) {
 3789         static::require_enabled();
 3790         $userevidence = new user_evidence($id);
 3791         $context = $userevidence->get_context();
 3792 
 3793         if (!$userevidence->can_manage()) {
 3794             throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
 3795         }
 3796 
 3797         // Delete the user evidence.
 3798         $userevidence->delete();
 3799 
 3800         // Delete associated files.
 3801         $fs = get_file_storage();
 3802         $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $id);
 3803 
 3804         // Delete relation between evidence and competencies.
 3805         $userevidence->set('id', $id);     // Restore the ID to fully mock the object.
 3806         $competencies = user_evidence_competency::get_competencies_by_userevidenceid($id);
 3807         foreach ($competencies as $competency) {
 3808             static::delete_user_evidence_competency($userevidence, $competency->get('id'));
 3809         }
 3810 
 3811         // Trigger an evidence of prior learning deleted event.
 3812         \core\event\competency_user_evidence_deleted::create_from_user_evidence($userevidence)->trigger();
 3813 
 3814         $userevidence->set('id', 0);       // Restore the object.
 3815 
 3816         return true;
 3817     }
 3818 
 3819     /**
 3820      * List the user evidence of a user.
 3821      *
 3822      * @param  int $userid The user ID.
 3823      * @return user_evidence[]
 3824      */
 3825     public static function list_user_evidence($userid) {
 3826         static::require_enabled();
 3827         if (!user_evidence::can_read_user($userid)) {
 3828             $context = context_user::instance($userid);
 3829             throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', '');
 3830         }
 3831 
 3832         $evidence = user_evidence::get_records(array('userid' => $userid), 'name');
 3833         return $evidence;
 3834     }
 3835 
 3836     /**
 3837      * Link a user evidence with a competency.
 3838      *
 3839      * @param  user_evidence|int $userevidenceorid User evidence or its ID.
 3840      * @param  int $competencyid Competency ID.
 3841      * @return user_evidence_competency
 3842      */
 3843     public static function create_user_evidence_competency($userevidenceorid, $competencyid) {
 3844         global $USER;
 3845         static::require_enabled();
 3846 
 3847         $userevidence = $userevidenceorid;
 3848         if (!is_object($userevidence)) {
 3849             $userevidence = self::read_user_evidence($userevidence);
 3850         }
 3851 
 3852         // Perform user evidence capability checks.
 3853         if (!$userevidence->can_manage()) {
 3854             $context = $userevidence->get_context();
 3855             throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
 3856         }
 3857 
 3858         // Perform competency capability checks.
 3859         $competency = self::read_competency($competencyid);
 3860 
 3861         // Get (and create) the relation.
 3862         $relation = user_evidence_competency::get_relation($userevidence->get('id'), $competency->get('id'));
 3863         if (!$relation->get('id')) {
 3864             $relation->create();
 3865 
 3866             $link = url::user_evidence($userevidence->get('id'));
 3867             self::add_evidence(
 3868                 $userevidence->get('userid'),
 3869                 $competency,
 3870                 $userevidence->get_context(),
 3871                 evidence::ACTION_LOG,
 3872                 'evidence_evidenceofpriorlearninglinked',
 3873                 'core_competency',
 3874                 $userevidence->get('name'),
 3875                 false,
 3876                 $link->out(false),
 3877                 null,
 3878                 $USER->id
 3879             );
 3880         }
 3881 
 3882         return $relation;
 3883     }
 3884 
 3885     /**
 3886      * Delete a relationship between a user evidence and a competency.
 3887      *
 3888      * @param  user_evidence|int $userevidenceorid User evidence or its ID.
 3889      * @param  int $competencyid Competency ID.
 3890      * @return bool
 3891      */
 3892     public static function delete_user_evidence_competency($userevidenceorid, $competencyid) {
 3893         global $USER;
 3894         static::require_enabled();
 3895 
 3896         $userevidence = $userevidenceorid;
 3897         if (!is_object($userevidence)) {
 3898             $userevidence = self::read_user_evidence($userevidence);
 3899         }
 3900 
 3901         // Perform user evidence capability checks.
 3902         if (!$userevidence->can_manage()) {
 3903             $context = $userevidence->get_context();
 3904             throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
 3905         }
 3906 
 3907         // Get (and delete) the relation.
 3908         $relation = user_evidence_competency::get_relation($userevidence->get('id'), $competencyid);
 3909         if (!$relation->get('id')) {
 3910             return true;
 3911         }
 3912 
 3913         $success = $relation->delete();
 3914         if ($success) {
 3915             self::add_evidence(
 3916                 $userevidence->get('userid'),
 3917                 $competencyid,
 3918                 $userevidence->get_context(),
 3919                 evidence::ACTION_LOG,
 3920                 'evidence_evidenceofpriorlearningunlinked',
 3921                 'core_competency',
 3922                 $userevidence->get('name'),
 3923                 false,
 3924                 null,
 3925                 null,
 3926                 $USER->id
 3927             );
 3928         }
 3929 
 3930         return $success;
 3931     }
 3932 
 3933     /**
 3934      * Send request review for user evidence competencies.
 3935      *
 3936      * @param  int $id The user evidence ID.
 3937      * @return bool
 3938      */
 3939     public static function request_review_of_user_evidence_linked_competencies($id) {
 3940         $userevidence = new user_evidence($id);
 3941         $context = $userevidence->get_context();
 3942         $userid = $userevidence->get('userid');
 3943 
 3944         if (!$userevidence->can_manage()) {
 3945             throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
 3946         }
 3947 
 3948         $usercompetencies = user_evidence_competency::get_user_competencies_by_userevidenceid($id);
 3949         foreach ($usercompetencies as $usercompetency) {
 3950             if ($usercompetency->get('status') == user_competency::STATUS_IDLE) {
 3951                 static::user_competency_request_review($userid, $usercompetency->get('competencyid'));
 3952             }
 3953         }
 3954 
 3955         return true;
 3956     }
 3957 
 3958     /**
 3959      * Recursively duplicate competencies from a tree, we start duplicating from parents to children to have a correct path.
 3960      * This method does not copy the related competencies.
 3961      *
 3962      * @param int $frameworkid - framework id
 3963      * @param competency[] $tree - array of competencies object
 3964      * @param int $oldparent - old parent id
 3965      * @param int $newparent - new parent id
 3966      * @return competency[] $matchids - List of old competencies ids matched with new competencies object.
 3967      */
 3968     protected static function duplicate_competency_tree($frameworkid, $tree, $oldparent = 0, $newparent = 0) {
 3969         $matchids = array();
 3970         foreach ($tree as $node) {
 3971             if ($node->competency->get('parentid') == $oldparent) {
 3972                 $parentid = $node->competency->get('id');
 3973 
 3974                 // Create the competency.
 3975                 $competency = new competency(0, $node->competency->to_record());
 3976                 $competency->set('competencyframeworkid', $frameworkid);
 3977                 $competency->set('parentid', $newparent);
 3978                 $competency->set('path', '');
 3979                 $competency->set('id', 0);
 3980                 $competency->reset_rule();
 3981                 $competency->create();
 3982 
 3983                 // Trigger the created event competency.
 3984                 \core\event\competency_created::create_from_competency($competency)->trigger();
 3985 
 3986                 // Match the old id with the new one.
 3987                 $matchids[$parentid] = $competency;
 3988 
 3989                 if (!empty($node->children)) {
 3990                     // Duplicate children competency.
 3991                     $childrenids = self::duplicate_competency_tree($frameworkid, $node->children, $parentid, $competency->get('id'));
 3992                     // Array_merge does not keep keys when merging so we use the + operator.
 3993                     $matchids = $matchids + $childrenids;
 3994                 }
 3995             }
 3996         }
 3997         return $matchids;
 3998     }
 3999 
 4000     /**
 4001      * Recursively migrate competency rules.
 4002      *
 4003      * @param competency[] $tree - array of competencies object
 4004      * @param competency[] $matchids - List of old competencies ids matched with new competencies object
 4005      */
 4006     protected static function migrate_competency_tree_rules($tree, $matchids) {
 4007 
 4008         foreach ($tree as $node) {
 4009             $oldcompid = $node->competency->get('id');
 4010             if ($node->competency->get('ruletype') && array_key_exists($oldcompid, $matchids)) {
 4011                 try {
 4012                     // Get the new competency.
 4013                     $competency = $matchids[$oldcompid];
 4014                     $class = $node->competency->get('ruletype');
 4015                     $newruleconfig = $class::migrate_config($node->competency->get('ruleconfig'), $matchids);
 4016                     $competency->set('ruleconfig', $newruleconfig);
 4017                     $competency->set('ruletype', $class);
 4018                     $competency->set('ruleoutcome', $node->competency->get('ruleoutcome'));
 4019                     $competency->update();
 4020                 } catch (\Exception $e) {
 4021                     debugging('Could not migrate competency rule from: ' . $oldcompid . ' to: ' . $competency->get('id') . '.' .
 4022                         ' Exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
 4023                     $competency->reset_rule();
 4024                 }
 4025             }
 4026 
 4027             if (!empty($node->children)) {
 4028                 self::migrate_competency_tree_rules($node->children, $matchids);
 4029             }
 4030         }
 4031     }
 4032 
 4033     /**
 4034      * Archive user competencies in a plan.
 4035      *
 4036      * @param int $plan The plan object.
 4037      * @return void
 4038      */
 4039     protected static function archive_user_competencies_in_plan($plan) {
 4040 
 4041         // Check if the plan was already completed.
 4042         if ($plan->get('status') == plan::STATUS_COMPLETE) {
 4043             throw new coding_exception('The plan is already completed.');
 4044         }
 4045 
 4046         $competencies = $plan->get_competencies();
 4047         $usercompetencies = user_competency::get_multiple($plan->get('userid'), $competencies);
 4048 
 4049         $i = 0;
 4050         foreach ($competencies as $competency) {
 4051             $found = false;
 4052 
 4053             foreach ($usercompetencies as $uckey => $uc) {
 4054                 if ($uc->get('competencyid') == $competency->get('id')) {
 4055                     $found = true;
 4056 
 4057                     $ucprecord = $uc->to_record();
 4058                     $ucprecord->planid = $plan->get('id');
 4059                     $ucprecord->sortorder = $i;
 4060                     unset($ucprecord->id);
 4061                     unset($ucprecord->status);
 4062                     unset($ucprecord->reviewerid);
 4063 
 4064                     $usercompetencyplan = new user_competency_plan(0, $ucprecord);
 4065                     $usercompetencyplan->create();
 4066 
 4067                     unset($usercompetencies[$uckey]);
 4068                     break;
 4069                 }
 4070             }
 4071 
 4072             // If the user competency doesn't exist, we create a new relation in user_competency_plan.
 4073             if (!$found) {
 4074                 $usercompetencyplan = user_competency_plan::create_relation($plan->get('userid'), $competency->get('id'),
 4075                         $plan->get('id'));
 4076                 $usercompetencyplan->set('sortorder', $i);
 4077                 $usercompetencyplan->create();
 4078             }
 4079             $i++;
 4080         }
 4081     }
 4082 
 4083     /**
 4084      * Delete archived user competencies in a plan.
 4085      *
 4086      * @param int $plan The plan object.
 4087      * @return void
 4088      */
 4089     protected static function remove_archived_user_competencies_in_plan($plan) {
 4090         $competencies = $plan->get_competencies();
 4091         $usercompetenciesplan = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), $competencies);
 4092 
 4093         foreach ($usercompetenciesplan as $ucpkey => $ucp) {
 4094             $ucp->delete();
 4095         }
 4096     }
 4097 
 4098     /**
 4099      * List all the evidence for a user competency.
 4100      *
 4101      * @param int $userid The user id - only used if usercompetencyid is 0.
 4102      * @param int $competencyid The competency id - only used it usercompetencyid is 0.
 4103      * @param int $planid The plan id - not used yet - but can be used to only list archived evidence if a plan is completed.
 4104      * @param string $sort The field to sort the evidence by.
 4105      * @param string $order The ordering of the sorting.
 4106      * @param int $skip Number of records to skip.
 4107      * @param int $limit Number of records to return.
 4108      * @return \core_competency\evidence[]
 4109      * @return array of \core_competency\evidence
 4110      */
 4111     public static function list_evidence($userid = 0, $competencyid = 0, $planid = 0, $sort = 'timecreated',
 4112                                          $order = 'DESC', $skip = 0, $limit = 0) {
 4113         static::require_enabled();
 4114 
 4115         if (!user_competency::can_read_user($userid)) {
 4116             $context = context_user::instance($userid);
 4117             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
 4118         }
 4119 
 4120         $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
 4121         if (!$usercompetency) {
 4122             return array();
 4123         }
 4124 
 4125         $plancompleted = false;
 4126         if ($planid != 0) {
 4127             $plan = new plan($planid);
 4128             if ($plan->get('status') == plan::STATUS_COMPLETE) {
 4129                 $plancompleted = true;
 4130             }
 4131         }
 4132 
 4133         $select = 'usercompetencyid = :usercompetencyid';
 4134         $params = array('usercompetencyid' => $usercompetency->get('id'));
 4135         if ($plancompleted) {
 4136             $select .= ' AND timecreated <= :timecompleted';
 4137             $params['timecompleted'] = $plan->get('timemodified');
 4138         }
 4139 
 4140         $orderby = $sort . ' ' . $order;
 4141         $orderby .= !empty($orderby) ? ', id DESC' : 'id DESC'; // Prevent random ordering.
 4142 
 4143         $evidence = evidence::get_records_select($select, $params, $orderby, '*', $skip, $limit);
 4144         return $evidence;
 4145     }
 4146 
 4147     /**
 4148      * List all the evidence for a user competency in a course.
 4149      *
 4150      * @param int $userid The user ID.
 4151      * @param int $courseid The course ID.
 4152      * @param int $competencyid The competency ID.
 4153      * @param string $sort The field to sort the evidence by.
 4154      * @param string $order The ordering of the sorting.
 4155      * @param int $skip Number of records to skip.
 4156      * @param int $limit Number of records to return.
 4157      * @return \core_competency\evidence[]
 4158      */
 4159     public static function list_evidence_in_course($userid = 0, $courseid = 0, $competencyid = 0, $sort = 'timecreated',
 4160                                                    $order = 'DESC', $skip = 0, $limit = 0) {
 4161         static::require_enabled();
 4162 
 4163         if (!user_competency::can_read_user_in_course($userid, $courseid)) {
 4164             $context = context_user::instance($userid);
 4165             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
 4166         }
 4167 
 4168         $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
 4169         if (!$usercompetency) {
 4170             return array();
 4171         }
 4172 
 4173         $context = context_course::instance($courseid);
 4174         return evidence::get_records_for_usercompetency($usercompetency->get('id'), $context, $sort, $order, $skip, $limit);
 4175     }
 4176 
 4177     /**
 4178      * Create an evidence from a list of parameters.
 4179      *
 4180      * Requires no capability because evidence can be added in many situations under any user.
 4181      *
 4182      * @param int $userid The user id for which evidence is added.
 4183      * @param competency|int $competencyorid The competency, or its id for which evidence is added.
 4184      * @param context|int $contextorid The context in which the evidence took place.
 4185      * @param int $action The type of action to take on the competency. \core_competency\evidence::ACTION_*.
 4186      * @param string $descidentifier The strings identifier.
 4187      * @param string $desccomponent The strings component.
 4188      * @param mixed $desca Any arguments the string requires.
 4189      * @param bool $recommend When true, the user competency will be sent for review.
 4190      * @param string $url The url the evidence may link to.
 4191      * @param int $grade The grade, or scale ID item.
 4192      * @param int $actionuserid The ID of the user who took the action of adding the evidence. Null when system.
 4193      *                          This should be used when the action was taken by a real person, this will allow
 4194      *                          to keep track of all the evidence given by a certain person.
 4195      * @param string $note A note to attach to the evidence.
 4196      * @return evidence
 4197      * @throws coding_exception
 4198      * @throws invalid_persistent_exception
 4199      * @throws moodle_exception
 4200      */
 4201     public static function add_evidence($userid, $competencyorid, $contextorid, $action, $descidentifier, $desccomponent,
 4202                                         $desca = null, $recommend = false, $url = null, $grade = null, $actionuserid = null,
 4203                                         $note = null) {
 4204         global $DB;
 4205         static::require_enabled();
 4206 
 4207         // Some clearly important variable assignments right there.
 4208         $competencyid = $competencyorid;
 4209         $competency = null;
 4210         if (is_object($competencyid)) {
 4211             $competency = $competencyid;
 4212             $competencyid = $competency->get('id');
 4213         }
 4214         $contextid = $contextorid;
 4215         $context = $contextorid;
 4216         if (is_object($contextorid)) {
 4217             $contextid = $contextorid->id;
 4218         } else {
 4219             $context = context::instance_by_id($contextorid);
 4220         }
 4221         $setucgrade = false;
 4222         $ucgrade = null;
 4223         $ucproficiency = null;
 4224         $usercompetencycourse = null;
 4225 
 4226         // Fetch or create the user competency.
 4227         $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
 4228         if (!$usercompetency) {
 4229             $usercompetency = user_competency::create_relation($userid, $competencyid);
 4230             $usercompetency->create();
 4231         }
 4232 
 4233         // What should we be doing?
 4234         switch ($action) {
 4235 
 4236             // Completing a competency.
 4237             case evidence::ACTION_COMPLETE:
 4238                 // The logic here goes like this:
 4239                 //
 4240                 // if rating outside a course
 4241                 // - set the default grade and proficiency ONLY if there is no current grade
 4242                 // else we are in a course
 4243                 // - set the defautl grade and proficiency in the course ONLY if there is no current grade in the course
 4244                 // - then check the course settings to see if we should push the rating outside the course
 4245                 // - if we should push it
 4246                 // --- push it only if the user_competency (outside the course) has no grade
 4247                 // Done.
 4248 
 4249                 if ($grade !== null) {
 4250                     throw new coding_exception("The grade MUST NOT be set with a 'completing' evidence.");
 4251                 }
 4252 
 4253                 // Fetch the default grade to attach to the evidence.
 4254                 if (empty($competency)) {
 4255                     $competency = new competency($competencyid);
 4256                 }
 4257                 list($grade, $proficiency) = $competency->get_default_grade();
 4258 
 4259                 // Add user_competency_course record when in a course or module.
 4260                 if (in_array($context->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE))) {
 4261                     $coursecontext = $context->get_course_context();
 4262                     $courseid = $coursecontext->instanceid;
 4263                     $filterparams = array(
 4264                         'userid' => $userid,
 4265                         'competencyid' => $competencyid,
 4266                         'courseid' => $courseid
 4267                     );
 4268                     // Fetch or create user competency course.
 4269                     $usercompetencycourse = user_competency_course::get_record($filterparams);
 4270                     if (!$usercompetencycourse) {
 4271                         $usercompetencycourse = user_competency_course::create_relation($userid, $competencyid, $courseid);
 4272                         $usercompetencycourse->create();
 4273                     }
 4274                     // Only update the grade and proficiency if there is not already a grade.
 4275                     if ($usercompetencycourse->get('grade') === null) {
 4276                         // Set grade.
 4277                         $usercompetencycourse->set('grade', $grade);
 4278                         // Set proficiency.
 4279                         $usercompetencycourse->set('proficiency', $proficiency);
 4280                     }
 4281 
 4282                     // Check the course settings to see if we should push to user plans.
 4283                     $coursesettings = course_competency_settings::get_by_courseid($courseid);
 4284                     $setucgrade = $coursesettings->get('pushratingstouserplans');
 4285 
 4286                     if ($setucgrade) {
 4287                         // Only push to user plans if there is not already a grade.
 4288                         if ($usercompetency->get('grade') !== null) {
 4289                             $setucgrade = false;
 4290                         } else {
 4291                             $ucgrade = $grade;
 4292                             $ucproficiency = $proficiency;
 4293                         }
 4294                     }
 4295                 } else {
 4296 
 4297                     // When completing the competency we fetch the default grade from the competency. But we only mark
 4298                     // the user competency when a grade has not been set yet. Complete is an action to use with automated systems.
 4299                     if ($usercompetency->get('grade') === null) {
 4300                         $setucgrade = true;
 4301                         $ucgrade = $grade;
 4302                         $ucproficiency = $proficiency;
 4303                     }
 4304                 }
 4305 
 4306                 break;
 4307 
 4308             // We override the grade, even overriding back to not set.
 4309             case evidence::ACTION_OVERRIDE:
 4310                 $setucgrade = true;
 4311                 $ucgrade = $grade;
 4312                 if (empty($competency)) {
 4313                     $competency = new competency($competencyid);
 4314                 }
 4315                 if ($ucgrade !== null) {
 4316                     $ucproficiency = $competency->get_proficiency_of_grade($ucgrade);
 4317                 }
 4318 
 4319                 // Add user_competency_course record when in a course or module.
 4320                 if (in_array($context->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE))) {
 4321                     $coursecontext = $context->get_course_context();
 4322                     $courseid = $coursecontext->instanceid;
 4323                     $filterparams = array(
 4324                         'userid' => $userid,
 4325                         'competencyid' => $competencyid,
 4326                         'courseid' => $courseid
 4327                     );
 4328                     // Fetch or create user competency course.
 4329                     $usercompetencycourse = user_competency_course::get_record($filterparams);
 4330                     if (!$usercompetencycourse) {
 4331                         $usercompetencycourse = user_competency_course::create_relation($userid, $competencyid, $courseid);
 4332                         $usercompetencycourse->create();
 4333                     }
 4334                     // Get proficiency.
 4335                     $proficiency = $ucproficiency;
 4336                     if ($proficiency === null) {
 4337                         if (empty($competency)) {
 4338                             $competency = new competency($competencyid);
 4339                         }
 4340                         $proficiency = $competency->get_proficiency_of_grade($grade);
 4341                     }
 4342                     // Set grade.
 4343                     $usercompetencycourse->set('grade', $grade);
 4344                     // Set proficiency.
 4345                     $usercompetencycourse->set('proficiency', $proficiency);
 4346 
 4347                     $coursesettings = course_competency_settings::get_by_courseid($courseid);
 4348                     if (!$coursesettings->get('pushratingstouserplans')) {
 4349                         $setucgrade = false;
 4350                     }
 4351                 }
 4352 
 4353                 break;
 4354 
 4355             // Simply logging an evidence.
 4356             case evidence::ACTION_LOG:
 4357                 if ($grade !== null) {
 4358                     throw new coding_exception("The grade MUST NOT be set when 'logging' an evidence.");
 4359                 }
 4360                 break;
 4361 
 4362             // Whoops, this is not expected.
 4363             default:
 4364                 throw new coding_exception('Unexpected action parameter when registering an evidence.');
 4365                 break;
 4366         }
 4367 
 4368         // Should we recommend?
 4369         if ($recommend && $usercompetency->get('status') == user_competency::STATUS_IDLE) {
 4370             $usercompetency->set('status', user_competency::STATUS_WAITING_FOR_REVIEW);
 4371         }
 4372 
 4373         // Setting the grade and proficiency for the user competency.
 4374         $wascompleted = false;
 4375         if ($setucgrade == true) {
 4376             if (!$usercompetency->get('proficiency') && $ucproficiency) {
 4377                 $wascompleted = true;
 4378             }
 4379             $usercompetency->set('grade', $ucgrade);
 4380             $usercompetency->set('proficiency', $ucproficiency);
 4381         }
 4382 
 4383         // Prepare the evidence.
 4384         $record = new stdClass();
 4385         $record->usercompetencyid = $usercompetency->get('id');
 4386         $record->contextid = $contextid;
 4387         $record->action = $action;
 4388         $record->descidentifier = $descidentifier;
 4389         $record->desccomponent = $desccomponent;
 4390         $record->grade = $grade;
 4391         $record->actionuserid = $actionuserid;
 4392         $record->note = $note;
 4393         $evidence = new evidence(0, $record);
 4394         $evidence->set('desca', $desca);
 4395         $evidence->set('url', $url);
 4396 
 4397         // Validate both models, we should not operate on one if the other will not save.
 4398         if (!$usercompetency->is_valid()) {
 4399             throw new invalid_persistent_exception($usercompetency->get_errors());
 4400         } else if (!$evidence->is_valid()) {
 4401             throw new invalid_persistent_exception($evidence->get_errors());
 4402         }
 4403 
 4404         // Save the user_competency_course record.
 4405         if ($usercompetencycourse !== null) {
 4406             // Validate and update.
 4407             if (!$usercompetencycourse->is_valid()) {
 4408                 throw new invalid_persistent_exception($usercompetencycourse->get_errors());
 4409             }
 4410             $usercompetencycourse->update();
 4411         }
 4412 
 4413         // Finally save. Pheww!
 4414         $usercompetency->update();
 4415         $evidence->create();
 4416 
 4417         // Trigger the evidence_created event.
 4418         \core\event\competency_evidence_created::create_from_evidence($evidence, $usercompetency, $recommend)->trigger();
 4419 
 4420         // The competency was marked as completed, apply the rules.
 4421         if ($wascompleted) {
 4422             self::apply_competency_rules_from_usercompetency($usercompetency, $competency);
 4423         }
 4424 
 4425         return $evidence;
 4426     }
 4427 
 4428     /**
 4429      * Read an evidence.
 4430      * @param int $evidenceid The evidence ID.
 4431      * @return evidence
 4432      */
 4433     public static function read_evidence($evidenceid) {
 4434         static::require_enabled();
 4435 
 4436         $evidence = new evidence($evidenceid);
 4437         $uc = new user_competency($evidence->get('usercompetencyid'));
 4438         if (!$uc->can_read()) {
 4439             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
 4440                 'nopermissions', '');
 4441         }
 4442 
 4443         return $evidence;
 4444     }
 4445 
 4446     /**
 4447      * Delete an evidence.
 4448      *
 4449      * @param evidence|int $evidenceorid The evidence, or its ID.
 4450      * @return bool
 4451      */
 4452     public static function delete_evidence($evidenceorid) {
 4453         $evidence = $evidenceorid;
 4454         if (!is_object($evidence)) {
 4455             $evidence = new evidence($evidenceorid);
 4456         }
 4457 
 4458         $uc = new user_competency($evidence->get('usercompetencyid'));
 4459         if (!evidence::can_delete_user($uc->get('userid'))) {
 4460             throw new required_capability_exception($uc->get_context(), 'moodle/competency:evidencedelete', 'nopermissions', '');
 4461         }
 4462 
 4463         return $evidence->delete();
 4464     }
 4465 
 4466     /**
 4467      * Apply the competency rules from a user competency.
 4468      *
 4469      * The user competency passed should be one that was recently marked as complete.
 4470      * A user competency is considered 'complete' when it's proficiency value is true.
 4471      *
 4472      * This method will check if the parent of this usercompetency's competency has any
 4473      * rules and if so will see if they match. When matched it will take the required
 4474      * step to add evidence and trigger completion, etc...
 4475      *
 4476      * @param  user_competency $usercompetency The user competency recently completed.
 4477      * @param  competency|null $competency     The competency of the user competency, useful to avoid unnecessary read.
 4478      * @return void
 4479      */
 4480     protected static function apply_competency_rules_from_usercompetency(user_competency $usercompetency,
 4481                                                                          competency $competency = null) {
 4482 
 4483         // Perform some basic checks.
 4484         if (!$usercompetency->get('proficiency')) {
 4485             throw new coding_exception('The user competency passed is not completed.');
 4486         }
 4487         if ($competency === null) {
 4488             $competency = $usercompetency->get_competency();
 4489         }
 4490         if ($competency->get('id') != $usercompetency->get('competencyid')) {
 4491             throw new coding_exception('Mismatch between user competency and competency.');
 4492         }
 4493 
 4494         // Fetch the parent.
 4495         $parent = $competency->get_parent();
 4496         if ($parent === null) {
 4497             return;
 4498         }
 4499 
 4500         // The parent should have a rule, and a meaningful outcome.
 4501         $ruleoutcome = $parent->get('ruleoutcome');
 4502         if ($ruleoutcome == competency::OUTCOME_NONE) {
 4503             return;
 4504         }
 4505         $rule = $parent->get_rule_object();
 4506         if ($rule === null) {
 4507             return;
 4508         }
 4509 
 4510         // Fetch or create the user competency for the parent.
 4511         $userid = $usercompetency->get('userid');
 4512         $parentuc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $parent->get('id')));
 4513         if (!$parentuc) {
 4514             $parentuc = user_competency::create_relation($userid, $parent->get('id'));
 4515             $parentuc->create();
 4516         }
 4517 
 4518         // Does the rule match?
 4519         if (!$rule->matches($parentuc)) {
 4520             return;
 4521         }
 4522 
 4523         // Figuring out what to do.
 4524         $recommend = false;
 4525         if ($ruleoutcome == competency::OUTCOME_EVIDENCE) {
 4526             $action = evidence::ACTION_LOG;
 4527 
 4528         } else if ($ruleoutcome == competency::OUTCOME_RECOMMEND) {
 4529             $action = evidence::ACTION_LOG;
 4530             $recommend = true;
 4531 
 4532         } else if ($ruleoutcome == competency::OUTCOME_COMPLETE) {
 4533             $action = evidence::ACTION_COMPLETE;
 4534 
 4535         } else {
 4536             throw new moodle_exception('Unexpected rule outcome: ' + $ruleoutcome);
 4537         }
 4538 
 4539         // Finally add an evidence.
 4540         static::add_evidence(
 4541             $userid,
 4542             $parent,
 4543             $parent->get_context()->id,
 4544             $action,
 4545             'evidence_competencyrule',
 4546             'core_competency',
 4547             null,
 4548             $recommend
 4549         );
 4550     }
 4551 
 4552     /**
 4553      * Observe when a course module is marked as completed.
 4554      *
 4555      * Note that the user being logged in while this happens may be anyone.
 4556      * Do not rely on capability checks here!
 4557      *
 4558      * @param  \core\event\course_module_completion_updated $event
 4559      * @return void
 4560      */
 4561     public static function observe_course_module_completion_updated(\core\event\course_module_completion_updated $event) {
 4562         if (!static::is_enabled()) {
 4563             return;
 4564         }
 4565 
 4566         $eventdata = $event->get_record_snapshot('course_modules_completion', $event->objectid);
 4567 
 4568         if ($eventdata->completionstate == COMPLETION_COMPLETE
 4569                 || $eventdata->completionstate == COMPLETION_COMPLETE_PASS) {
 4570             $coursemodulecompetencies = course_module_competency::list_course_module_competencies($eventdata->coursemoduleid);
 4571 
 4572             $cm = get_coursemodule_from_id(null, $eventdata->coursemoduleid);
 4573             $fastmodinfo = get_fast_modinfo($cm->course)->cms[$cm->id];
 4574 
 4575             $cmname = $fastmodinfo->name;
 4576             $url = $fastmodinfo->url;
 4577 
 4578             foreach ($coursemodulecompetencies as $coursemodulecompetency) {
 4579                 $outcome = $coursemodulecompetency->get('ruleoutcome');
 4580                 $action = null;
 4581                 $recommend = false;
 4582                 $strdesc = 'evidence_coursemodulecompleted';
 4583 
 4584                 if ($outcome == course_module_competency::OUTCOME_EVIDENCE) {
 4585                     $action = evidence::ACTION_LOG;
 4586 
 4587                 } else if ($outcome == course_module_competency::OUTCOME_RECOMMEND) {
 4588                     $action = evidence::ACTION_LOG;
 4589                     $recommend = true;
 4590 
 4591                 } else if ($outcome == course_module_competency::OUTCOME_COMPLETE) {
 4592                     $action = evidence::ACTION_COMPLETE;
 4593 
 4594                 } else {
 4595                     throw new moodle_exception('Unexpected rule outcome: ' + $outcome);
 4596                 }
 4597 
 4598                 static::add_evidence(
 4599                     $event->relateduserid,
 4600                     $coursemodulecompetency->get('competencyid'),
 4601                     $event->contextid,
 4602                     $action,
 4603                     $strdesc,
 4604                     'core_competency',
 4605                     $cmname,
 4606                     $recommend,
 4607                     $url
 4608                 );
 4609             }
 4610         }
 4611     }
 4612 
 4613     /**
 4614      * Observe when a course is marked as completed.
 4615      *
 4616      * Note that the user being logged in while this happens may be anyone.
 4617      * Do not rely on capability checks here!
 4618      *
 4619      * @param  \core\event\course_completed $event
 4620      * @return void
 4621      */
 4622     public static function observe_course_completed(\core\event\course_completed $event) {
 4623         if (!static::is_enabled()) {
 4624             return;
 4625         }
 4626 
 4627         $sql = 'courseid = :courseid AND ruleoutcome != :nooutcome';
 4628         $params = array(
 4629             'courseid' => $event->courseid,
 4630             'nooutcome' => course_competency::OUTCOME_NONE
 4631         );
 4632         $coursecompetencies = course_competency::get_records_select($sql, $params);
 4633 
 4634         $course = get_course($event->courseid);
 4635         $courseshortname = format_string($course->shortname, null, array('context' => $event->contextid));
 4636 
 4637         foreach ($coursecompetencies as $coursecompetency) {
 4638 
 4639             $outcome = $coursecompetency->get('ruleoutcome');
 4640             $action = null;
 4641             $recommend = false;
 4642             $strdesc = 'evidence_coursecompleted';
 4643 
 4644             if ($outcome == course_competency::OUTCOME_EVIDENCE) {
 4645                 $action = evidence::ACTION_LOG;
 4646 
 4647             } else if ($outcome == course_competency::OUTCOME_RECOMMEND) {
 4648                 $action = evidence::ACTION_LOG;
 4649                 $recommend = true;
 4650 
 4651             } else if ($outcome == course_competency::OUTCOME_COMPLETE) {
 4652                 $action = evidence::ACTION_COMPLETE;
 4653 
 4654             } else {
 4655                 throw new moodle_exception('Unexpected rule outcome: ' + $outcome);
 4656             }
 4657 
 4658             static::add_evidence(
 4659                 $event->relateduserid,
 4660                 $coursecompetency->get('competencyid'),
 4661                 $event->contextid,
 4662                 $action,
 4663                 $strdesc,
 4664                 'core_competency',
 4665                 $courseshortname,
 4666                 $recommend,
 4667                 $event->get_url()
 4668             );
 4669         }
 4670     }
 4671 
 4672     /**
 4673      * Action to perform when a course module is deleted.
 4674      *
 4675      * Do not call this directly, this is reserved for core use.
 4676      *
 4677      * @param stdClass $cm The CM object.
 4678      * @return void
 4679      */
 4680     public static function hook_course_module_deleted(stdClass $cm) {
 4681         global $DB;
 4682         $DB->delete_records(course_module_competency::TABLE, array('cmid' => $cm->id));
 4683     }
 4684 
 4685     /**
 4686      * Action to perform when a course is deleted.
 4687      *
 4688      * Do not call this directly, this is reserved for core use.
 4689      *
 4690      * @param stdClass $course The course object.
 4691      * @return void
 4692      */
 4693     public static function hook_course_deleted(stdClass $course) {
 4694         global $DB;
 4695         $DB->delete_records(course_competency::TABLE, array('courseid' => $course->id));
 4696         $DB->delete_records(course_competency_settings::TABLE, array('courseid' => $course->id));
 4697         $DB->delete_records(user_competency_course::TABLE, array('courseid' => $course->id));
 4698     }
 4699 
 4700     /**
 4701      * Action to perform when a course is being reset.
 4702      *
 4703      * Do not call this directly, this is reserved for core use.
 4704      *
 4705      * @param int $courseid The course ID.
 4706      * @return void
 4707      */
 4708     public static function hook_course_reset_competency_ratings($courseid) {
 4709         global $DB;
 4710         $DB->delete_records(user_competency_course::TABLE, array('courseid' => $courseid));
 4711     }
 4712 
 4713     /**
 4714      * Action to perform when a cohort is deleted.
 4715      *
 4716      * Do not call this directly, this is reserved for core use.
 4717      *
 4718      * @param \stdClass $cohort The cohort object.
 4719      * @return void
 4720      */
 4721     public static function hook_cohort_deleted(\stdClass $cohort) {
 4722         global $DB;
 4723         $DB->delete_records(template_cohort::TABLE, array('cohortid' => $cohort->id));
 4724     }
 4725 
 4726     /**
 4727      * Manually grade a user competency.
 4728      *
 4729      * @param int $userid
 4730      * @param int $competencyid
 4731      * @param int $grade
 4732      * @param string $note A note to attach to the evidence
 4733      * @return array of \core_competency\user_competency
 4734      */
 4735     public static function grade_competency($userid, $competencyid, $grade, $note = null) {
 4736         global $USER;
 4737         static::require_enabled();
 4738 
 4739         $uc = static::get_user_competency($userid, $competencyid);
 4740         $context = $uc->get_context();
 4741         if (!user_competency::can_grade_user($uc->get('userid'))) {
 4742             throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', '');
 4743         }
 4744 
 4745         // Throws exception if competency not in plan.
 4746         $competency = $uc->get_competency();
 4747         $competencycontext = $competency->get_context();
 4748         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
 4749                 $competencycontext)) {
 4750             throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', '');
 4751         }
 4752 
 4753         $action = evidence::ACTION_OVERRIDE;
 4754         $desckey = 'evidence_manualoverride';
 4755 
 4756         $result = self::add_evidence($uc->get('userid'),
 4757                                   $competency,
 4758                                   $context->id,
 4759                                   $action,
 4760                                   $desckey,
 4761                                   'core_competency',
 4762                                   null,
 4763                                   false,
 4764                                   null,
 4765                                   $grade,
 4766                                   $USER->id,
 4767                                   $note);
 4768         if ($result) {
 4769             $uc->read();
 4770             $event = \core\event\competency_user_competency_rated::create_from_user_competency($uc);
 4771             $event->trigger();
 4772         }
 4773         return $result;
 4774     }
 4775 
 4776     /**
 4777      * Manually grade a user competency from the plans page.
 4778      *
 4779      * @param mixed $planorid
 4780      * @param int $competencyid
 4781      * @param int $grade
 4782      * @param string $note A note to attach to the evidence
 4783      * @return array of \core_competency\user_competency
 4784      */
 4785     public static function grade_competency_in_plan($planorid, $competencyid, $grade, $note = null) {
 4786         global $USER;
 4787         static::require_enabled();
 4788 
 4789         $plan = $planorid;
 4790         if (!is_object($planorid)) {
 4791             $plan = new plan($planorid);
 4792         }
 4793 
 4794         $context = $plan->get_context();
 4795         if (!user_competency::can_grade_user($plan->get('userid'))) {
 4796             throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', '');
 4797         }
 4798 
 4799         // Throws exception if competency not in plan.
 4800         $competency = $plan->get_competency($competencyid);
 4801         $competencycontext = $competency->get_context();
 4802         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
 4803                 $competencycontext)) {
 4804             throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', '');
 4805         }
 4806 
 4807         $action = evidence::ACTION_OVERRIDE;
 4808         $desckey = 'evidence_manualoverrideinplan';
 4809 
 4810         $result = self::add_evidence($plan->get('userid'),
 4811                                   $competency,
 4812                                   $context->id,
 4813                                   $action,
 4814                                   $desckey,
 4815                                   'core_competency',
 4816                                   $plan->get('name'),
 4817                                   false,
 4818                                   null,
 4819                                   $grade,
 4820                                   $USER->id,
 4821                                   $note);
 4822         if ($result) {
 4823             $uc = static::get_user_competency($plan->get('userid'), $competency->get('id'));
 4824             $event = \core\event\competency_user_competency_rated_in_plan::create_from_user_competency($uc, $plan->get('id'));
 4825             $event->trigger();
 4826         }
 4827         return $result;
 4828     }
 4829 
 4830     /**
 4831      * Manually grade a user course competency from the course page.
 4832      *
 4833      * This may push the rating to the user competency
 4834      * if the course is configured this way.
 4835      *
 4836      * @param mixed $courseorid
 4837      * @param int $userid
 4838      * @param int $competencyid
 4839      * @param int $grade
 4840      * @param string $note A note to attach to the evidence
 4841      * @return array of \core_competency\user_competency
 4842      */
 4843     public static function grade_competency_in_course($courseorid, $userid, $competencyid, $grade, $note = null) {
 4844         global $USER, $DB;
 4845         static::require_enabled();
 4846 
 4847         $course = $courseorid;
 4848         if (!is_object($courseorid)) {
 4849             $course = $DB->get_record('course', array('id' => $courseorid));
 4850         }
 4851         $context = context_course::instance($course->id);
 4852 
 4853         // Check that we can view the user competency details in the course.
 4854         if (!user_competency::can_read_user_in_course($userid, $course->id)) {
 4855             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
 4856         }
 4857 
 4858         // Validate the permission to grade.
 4859         if (!user_competency::can_grade_user_in_course($userid, $course->id)) {
 4860             throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', '');
 4861         }
 4862 
 4863         // Check that competency is in course and visible to the current user.
 4864         $competency = course_competency::get_competency($course->id, $competencyid);
 4865         $competencycontext = $competency->get_context();
 4866         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
 4867                 $competencycontext)) {
 4868             throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', '');
 4869         }
 4870 
 4871         // Check that the user is enrolled in the course, and is "gradable".
 4872         if (!is_enrolled($context, $userid, 'moodle/competency:coursecompetencygradable')) {
 4873             throw new coding_exception('The competency may not be rated at this time.');
 4874         }
 4875 
 4876         $action = evidence::ACTION_OVERRIDE;
 4877         $desckey = 'evidence_manualoverrideincourse';
 4878 
 4879         $result = self::add_evidence($userid,
 4880                                   $competency,
 4881                                   $context->id,
 4882                                   $action,
 4883                                   $desckey,
 4884                                   'core_competency',
 4885                                   $context->get_context_name(),
 4886                                   false,
 4887                                   null,
 4888                                   $grade,
 4889                                   $USER->id,
 4890                                   $note);
 4891         if ($result) {
 4892             $all = user_competency_course::get_multiple($userid, $course->id, array($competency->get('id')));
 4893             $uc = reset($all);
 4894             $event = \core\event\competency_user_competency_rated_in_course::create_from_user_competency_course($uc);
 4895             $event->trigger();
 4896         }
 4897         return $result;
 4898     }
 4899 
 4900     /**
 4901      * Count the plans in the template, filtered by status.
 4902      *
 4903      * Requires moodle/competency:templateview capability at the system context.
 4904      *
 4905      * @param mixed $templateorid The id or the template.
 4906      * @param int $status One of the plan status constants (or 0 for all plans).
 4907      * @return int
 4908      */
 4909     public static function count_plans_for_template($templateorid, $status = 0) {
 4910         static::require_enabled();
 4911         $template = $templateorid;
 4912         if (!is_object($template)) {
 4913             $template = new template($template);
 4914         }
 4915 
 4916         // First we do a permissions check.
 4917         if (!$template->can_read()) {
 4918             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
 4919                 'nopermissions', '');
 4920         }
 4921 
 4922         return plan::count_records_for_template($template->get('id'), $status);
 4923     }
 4924 
 4925     /**
 4926      * Count the user-completency-plans in the template, optionally filtered by proficiency.
 4927      *
 4928      * Requires moodle/competency:templateview capability at the system context.
 4929      *
 4930      * @param mixed $templateorid The id or the template.
 4931      * @param mixed $proficiency If true, filter by proficiency, if false filter by not proficient, if null - no filter.
 4932      * @return int
 4933      */
 4934     public static function count_user_competency_plans_for_template($templateorid, $proficiency = null) {
 4935         static::require_enabled();
 4936         $template = $templateorid;
 4937         if (!is_object($template)) {
 4938             $template = new template($template);
 4939         }
 4940 
 4941         // First we do a permissions check.
 4942         if (!$template->can_read()) {
 4943              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
 4944                 'nopermissions', '');
 4945         }
 4946 
 4947         return user_competency_plan::count_records_for_template($template->get('id'), $proficiency);
 4948     }
 4949 
 4950     /**
 4951      * List the plans in the template, filtered by status.
 4952      *
 4953      * Requires moodle/competency:templateview capability at the system context.
 4954      *
 4955      * @param mixed $templateorid The id or the template.
 4956      * @param int $status One of the plan status constants (or 0 for all plans).
 4957      * @param int $skip The number of records to skip
 4958      * @param int $limit The max number of records to return
 4959      * @return plan[]
 4960      */
 4961     public static function list_plans_for_template($templateorid, $status = 0, $skip = 0, $limit = 100) {
 4962         $template = $templateorid;
 4963         if (!is_object($template)) {
 4964             $template = new template($template);
 4965         }
 4966 
 4967         // First we do a permissions check.
 4968         if (!$template->can_read()) {
 4969              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
 4970                 'nopermissions', '');
 4971         }
 4972 
 4973         return plan::get_records_for_template($template->get('id'), $status, $skip, $limit);
 4974     }
 4975 
 4976     /**
 4977      * Get the most often not completed competency for this course.
 4978      *
 4979      * Requires moodle/competency:coursecompetencyview capability at the course context.
 4980      *
 4981      * @param int $courseid The course id
 4982      * @param int $skip The number of records to skip
 4983      * @param int $limit The max number of records to return
 4984      * @return competency[]
 4985      */
 4986     public static function get_least_proficient_competencies_for_course($courseid, $skip = 0, $limit = 100) {
 4987         static::require_enabled();
 4988         $coursecontext = context_course::instance($courseid);
 4989 
 4990         if (!has_any_capability(array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'),
 4991                 $coursecontext)) {
 4992             throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
 4993         }
 4994 
 4995         return user_competency_course::get_least_proficient_competencies_for_course($courseid, $skip, $limit);
 4996     }
 4997 
 4998     /**
 4999      * Get the most often not completed competency for this template.
 5000      *
 5001      * Requires moodle/competency:templateview capability at the system context.
 5002      *
 5003      * @param mixed $templateorid The id or the template.
 5004      * @param int $skip The number of records to skip
 5005      * @param int $limit The max number of records to return
 5006      * @return competency[]
 5007      */
 5008     public static function get_least_proficient_competencies_for_template($templateorid, $skip = 0, $limit = 100) {
 5009         static::require_enabled();
 5010         $template = $templateorid;
 5011         if (!is_object($template)) {
 5012             $template = new template($template);
 5013         }
 5014 
 5015         // First we do a permissions check.
 5016         if (!$template->can_read()) {
 5017             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
 5018                 'nopermissions', '');
 5019         }
 5020 
 5021         return user_competency_plan::get_least_proficient_competencies_for_template($template->get('id'), $skip, $limit);
 5022     }
 5023 
 5024     /**
 5025      * Template event viewed.
 5026      *
 5027      * Requires moodle/competency:templateview capability at the system context.
 5028      *
 5029      * @param mixed $templateorid The id or the template.
 5030      * @return boolean
 5031      */
 5032     public static function template_viewed($templateorid) {
 5033         static::require_enabled();
 5034         $template = $templateorid;
 5035         if (!is_object($template)) {
 5036             $template = new template($template);
 5037         }
 5038 
 5039         // First we do a permissions check.
 5040         if (!$template->can_read()) {
 5041             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
 5042                 'nopermissions', '');
 5043         }
 5044 
 5045         // Trigger a template viewed event.
 5046         \core\event\competency_template_viewed::create_from_template($template)->trigger();
 5047 
 5048         return true;
 5049     }
 5050 
 5051     /**
 5052      * Get the competency settings for a course.
 5053      *
 5054      * Requires moodle/competency:coursecompetencyview capability at the course context.
 5055      *
 5056      * @param int $courseid The course id
 5057      * @return course_competency_settings
 5058      */
 5059     public static function read_course_competency_settings($courseid) {
 5060         static::require_enabled();
 5061 
 5062         // First we do a permissions check.
 5063         if (!course_competency_settings::can_read($courseid)) {
 5064             $context = context_course::instance($courseid);
 5065             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
 5066         }
 5067 
 5068         return course_competency_settings::get_by_courseid($courseid);
 5069     }
 5070 
 5071     /**
 5072      * Update the competency settings for a course.
 5073      *
 5074      * Requires moodle/competency:coursecompetencyconfigure capability at the course context.
 5075      *
 5076      * @param int $courseid The course id
 5077      * @param stdClass $settings List of settings. The only valid setting ATM is pushratginstouserplans (boolean).
 5078      * @return bool
 5079      */
 5080     public static function update_course_competency_settings($courseid, $settings) {
 5081         static::require_enabled();
 5082 
 5083         $settings = (object) $settings;
 5084 
 5085         // Get all the valid settings.
 5086         $pushratingstouserplans = isset($settings->pushratingstouserplans) ? $settings->pushratingstouserplans : false;
 5087 
 5088         // First we do a permissions check.
 5089         if (!course_competency_settings::can_manage_course($courseid)) {
 5090             $context = context_course::instance($courseid);
 5091             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyconfigure', 'nopermissions', '');
 5092         }
 5093 
 5094         $exists = course_competency_settings::get_record(array('courseid' => $courseid));
 5095 
 5096         // Now update or insert.
 5097         if ($exists) {
 5098             $settings = $exists;
 5099             $settings->set('pushratingstouserplans', $pushratingstouserplans);
 5100             return $settings->update();
 5101         } else {
 5102             $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $pushratingstouserplans);
 5103             $settings = new course_competency_settings(0, $data);
 5104             $result = $settings->create();
 5105             return !empty($result);
 5106         }
 5107     }
 5108 
 5109 
 5110     /**
 5111      * Function used to return a list of users where the given user has a particular capability.
 5112      *
 5113      * This is used e.g. to find all the users where someone is able to manage their learning plans,
 5114      * it also would be useful for mentees etc.
 5115      *
 5116      * @param string $capability - The capability string we are filtering for. If '' is passed,
 5117      *                             an always matching filter is returned.
 5118      * @param int $userid - The user id we are using for the access checks. Defaults to current user.
 5119      * @param int $type - The type of named params to return (passed to $DB->get_in_or_equal).
 5120      * @param string $prefix - The type prefix for the db table (passed to $DB->get_in_or_equal).
 5121      * @return list($sql, $params) Same as $DB->get_in_or_equal().
 5122      * @todo MDL-52243 Move this function to lib/accesslib.php
 5123      */
 5124     public static function filter_users_with_capability_on_user_context_sql($capability, $userid = 0, $type = SQL_PARAMS_QM,
 5125                                                                             $prefix='param') {
 5126 
 5127         global $USER, $DB;
 5128         $allresultsfilter = array('> 0', array());
 5129         $noresultsfilter = array('= -1', array());
 5130 
 5131         if (empty($capability)) {
 5132             return $allresultsfilter;
 5133         }
 5134 
 5135         if (!$capinfo = get_capability_info($capability)) {
 5136             throw new coding_exception('Capability does not exist: ' . $capability);
 5137         }
 5138 
 5139         if (empty($userid)) {
 5140             $userid = $USER->id;
 5141         }
 5142 
 5143         // Make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
 5144         if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
 5145             if (isguestuser($userid) or $userid == 0) {
 5146                 return $noresultsfilter;
 5147             }
 5148         }
 5149 
 5150         if (is_siteadmin($userid)) {
 5151             // No filtering for site admins.
 5152             return $allresultsfilter;
 5153         }
 5154 
 5155         // Check capability on system level.
 5156         $syscontext = context_system::instance();
 5157         $hassystem = has_capability($capability, $syscontext, $userid);
 5158 
 5159         $access = get_user_roles_sitewide_accessdata($userid);
 5160         // Build up a list of level 2 contexts (candidates to be user context).
 5161         $filtercontexts = array();
 5162         // Build list of roles to check overrides.
 5163         $roles = array();
 5164 
 5165         foreach ($access['ra'] as $path => $role) {
 5166             $parts = explode('/', $path);
 5167             if (count($parts) == 3) {
 5168                 $filtercontexts[$parts[2]] = $parts[2];
 5169             } else if (count($parts) > 3) {
 5170                 // We know this is not a user context because there is another path with more than 2 levels.
 5171                 unset($filtercontexts[$parts[2]]);
 5172             }
 5173             $roles = array_merge($roles, $role);
 5174         }
 5175 
 5176         // Add all contexts in which a role may be overidden.
 5177         $rdefs = get_role_definitions($roles);
 5178         foreach ($rdefs as $roledef) {
 5179             foreach ($roledef as $path => $caps) {
 5180                 if (!isset($caps[$capability])) {
 5181                     // The capability is not mentioned, we can ignore.
 5182                     continue;
 5183                 }
 5184                 $parts = explode('/', $path);
 5185                 if (count($parts) === 3) {
 5186                     // Only get potential user contexts, they only ever have 2 slashes /parentId/Id.
 5187                     $filtercontexts[$parts[2]] = $parts[2];
 5188                 }
 5189             }
 5190         }
 5191 
 5192         // No interesting contexts - return all or no results.
 5193         if (empty($filtercontexts)) {
 5194             if ($hassystem) {
 5195                 return $allresultsfilter;
 5196             } else {
 5197                 return $noresultsfilter;
 5198             }
 5199         }
 5200         // Fetch all interesting contexts for further examination.
 5201         list($insql, $params) = $DB->get_in_or_equal($filtercontexts, SQL_PARAMS_NAMED);
 5202         $params['level'] = CONTEXT_USER;
 5203         $fields = context_helper::get_preload_record_columns_sql('ctx');
 5204         $interestingcontexts = $DB->get_recordset_sql('SELECT ' . $fields . '
 5205                                                        FROM {context} ctx
 5206                                                        WHERE ctx.contextlevel = :level
 5207                                                          AND ctx.id ' . $insql . '
 5208                                                        ORDER BY ctx.id', $params);
 5209         if ($hassystem) {
 5210             // If allowed at system, search for exceptions prohibiting the capability at user context.
 5211             $excludeusers = array();
 5212             foreach ($interestingcontexts as $contextrecord) {
 5213                 $candidateuserid = $contextrecord->ctxinstance;
 5214                 context_helper::preload_from_record($contextrecord);
 5215                 $usercontext = context_user::instance($candidateuserid);
 5216                 // Has capability should use the data already preloaded.
 5217                 if (!has_capability($capability, $usercontext, $userid)) {
 5218                     $excludeusers[$candidateuserid] = $candidateuserid;
 5219                 }
 5220             }
 5221 
 5222             // Construct SQL excluding users with this role assigned for this user.
 5223             if (empty($excludeusers)) {
 5224                 $interestingcontexts->close();
 5225                 return $allresultsfilter;
 5226             }
 5227             list($sql, $params) = $DB->get_in_or_equal($excludeusers, $type, $prefix, false);
 5228         } else {
 5229             // If not allowed at system, search for exceptions allowing the capability at user context.
 5230             $allowusers = array();
 5231             foreach ($interestingcontexts as $contextrecord) {
 5232                 $candidateuserid = $contextrecord->ctxinstance;
 5233                 context_helper::preload_from_record($contextrecord);
 5234                 $usercontext = context_user::instance($candidateuserid);
 5235                 // Has capability should use the data already preloaded.
 5236                 if (has_capability($capability, $usercontext, $userid)) {
 5237                     $allowusers[$candidateuserid] = $candidateuserid;
 5238                 }
 5239             }
 5240 
 5241             // Construct SQL excluding users with this role assigned for this user.
 5242             if (empty($allowusers)) {
 5243                 $interestingcontexts->close();
 5244                 return $noresultsfilter;
 5245             }
 5246             list($sql, $params) = $DB->get_in_or_equal($allowusers, $type, $prefix);
 5247         }
 5248         $interestingcontexts->close();
 5249 
 5250         // Return the goods!.
 5251         return array($sql, $params);
 5252     }
 5253 
 5254 }