Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 9 May 2022 (12 months).
  • Bug fixes for security issues in 3.11.x will end 14 November 2022 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 35 and 311] [Versions 36 and 311] [Versions 37 and 311]

       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      /** @var boolean Allow api functions even if competencies are not enabled for the site. */
      50      private static $skipenabled = false;
      51  
      52      /**
      53       * Returns whether competencies are enabled.
      54       *
      55       * This method should never do more than checking the config setting, the reason
      56       * being that some other code could be checking the config value directly
      57       * to avoid having to load this entire file into memory.
      58       *
      59       * @return boolean True when enabled.
      60       */
      61      public static function is_enabled() {
      62          return self::$skipenabled || get_config('core_competency', 'enabled');
      63      }
      64  
      65      /**
      66       * When competencies used to be enabled, we can show the text but do not include links.
      67       *
      68       * @return boolean True means show links.
      69       */
      70      public static function show_links() {
      71          return isloggedin() && !isguestuser() && get_config('core_competency', 'enabled');
      72      }
      73  
      74      /**
      75       * Allow calls to competency api functions even if competencies are not currently enabled.
      76       */
      77      public static function skip_enabled() {
      78          self::$skipenabled = true;
      79      }
      80  
      81      /**
      82       * Restore the checking that competencies are enabled with any api function.
      83       */
      84      public static function check_enabled() {
      85          self::$skipenabled = false;
      86      }
      87  
      88      /**
      89       * Throws an exception if competencies are not enabled.
      90       *
      91       * @return void
      92       * @throws moodle_exception
      93       */
      94      public static function require_enabled() {
      95          if (!static::is_enabled()) {
      96              throw new moodle_exception('competenciesarenotenabled', 'core_competency');
      97          }
      98      }
      99  
     100      /**
     101       * Checks whether a scale is used anywhere in the plugin.
     102       *
     103       * This public API has two exceptions:
     104       * - It MUST NOT perform any capability checks.
     105       * - It MUST ignore whether competencies are enabled or not ({@link self::is_enabled()}).
     106       *
     107       * @param int $scaleid The scale ID.
     108       * @return bool
     109       */
     110      public static function is_scale_used_anywhere($scaleid) {
     111          global $DB;
     112          $sql = "SELECT s.id
     113                    FROM {scale} s
     114               LEFT JOIN {" . competency_framework::TABLE ."} f
     115                      ON f.scaleid = :scaleid1
     116               LEFT JOIN {" . competency::TABLE ."} c
     117                      ON c.scaleid = :scaleid2
     118                   WHERE f.id IS NOT NULL
     119                      OR c.id IS NOT NULL";
     120          return $DB->record_exists_sql($sql, ['scaleid1' => $scaleid, 'scaleid2' => $scaleid]);
     121      }
     122  
     123      /**
     124       * Validate if current user have acces to the course_module if hidden.
     125       *
     126       * @param mixed $cmmixed The cm_info class, course module record or its ID.
     127       * @param bool $throwexception Throw an exception or not.
     128       * @return bool
     129       */
     130      protected static function validate_course_module($cmmixed, $throwexception = true) {
     131          $cm = $cmmixed;
     132          if (!is_object($cm)) {
     133              $cmrecord = get_coursemodule_from_id(null, $cmmixed);
     134              $modinfo = get_fast_modinfo($cmrecord->course);
     135              $cm = $modinfo->get_cm($cmmixed);
     136          } else if (!$cm instanceof cm_info) {
     137              // Assume we got a course module record.
     138              $modinfo = get_fast_modinfo($cm->course);
     139              $cm = $modinfo->get_cm($cm->id);
     140          }
     141  
     142          if (!$cm->uservisible) {
     143              if ($throwexception) {
     144                  throw new require_login_exception('Course module is hidden');
     145              } else {
     146                  return false;
     147              }
     148          }
     149  
     150          return true;
     151      }
     152  
     153      /**
     154       * Validate if current user have acces to the course if hidden.
     155       *
     156       * @param mixed $courseorid The course or it ID.
     157       * @param bool $throwexception Throw an exception or not.
     158       * @return bool
     159       */
     160      protected static function validate_course($courseorid, $throwexception = true) {
     161          $course = $courseorid;
     162          if (!is_object($course)) {
     163              $course = get_course($course);
     164          }
     165  
     166          $coursecontext = context_course::instance($course->id);
     167          if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
     168              if ($throwexception) {
     169                  throw new require_login_exception('Course is hidden');
     170              } else {
     171                  return false;
     172              }
     173          }
     174  
     175          return true;
     176      }
     177  
     178      /**
     179       * Create a competency from a record containing all the data for the class.
     180       *
     181       * Requires moodle/competency:competencymanage capability at the system context.
     182       *
     183       * @param stdClass $record Record containing all the data for an instance of the class.
     184       * @return competency
     185       */
     186      public static function create_competency(stdClass $record) {
     187          static::require_enabled();
     188          $competency = new competency(0, $record);
     189  
     190          // First we do a permissions check.
     191          require_capability('moodle/competency:competencymanage', $competency->get_context());
     192  
     193          // Reset the sortorder, use reorder instead.
     194          $competency->set('sortorder', 0);
     195          $competency->create();
     196  
     197          \core\event\competency_created::create_from_competency($competency)->trigger();
     198  
     199          // Reset the rule of the parent.
     200          $parent = $competency->get_parent();
     201          if ($parent) {
     202              $parent->reset_rule();
     203              $parent->update();
     204          }
     205  
     206          return $competency;
     207      }
     208  
     209      /**
     210       * Delete a competency by id.
     211       *
     212       * Requires moodle/competency:competencymanage capability at the system context.
     213       *
     214       * @param int $id The record to delete. This will delete alot of related data - you better be sure.
     215       * @return boolean
     216       */
     217      public static function delete_competency($id) {
     218          global $DB;
     219          static::require_enabled();
     220          $competency = new competency($id);
     221  
     222          // First we do a permissions check.
     223          require_capability('moodle/competency:competencymanage', $competency->get_context());
     224  
     225          $events = array();
     226          $competencyids = array(intval($competency->get('id')));
     227          $contextid = $competency->get_context()->id;
     228          $competencyids = array_merge(competency::get_descendants_ids($competency), $competencyids);
     229          if (!competency::can_all_be_deleted($competencyids)) {
     230              return false;
     231          }
     232          $transaction = $DB->start_delegated_transaction();
     233  
     234          try {
     235  
     236              // Reset the rule of the parent.
     237              $parent = $competency->get_parent();
     238              if ($parent) {
     239                  $parent->reset_rule();
     240                  $parent->update();
     241              }
     242  
     243              // Delete the competency separately so the after_delete event can be triggered.
     244              $competency->delete();
     245  
     246              // Delete the competencies.
     247              competency::delete_multiple($competencyids);
     248  
     249              // Delete the competencies relation.
     250              related_competency::delete_multiple_relations($competencyids);
     251  
     252              // Delete competency evidences.
     253              user_evidence_competency::delete_by_competencyids($competencyids);
     254  
     255              // Register the competencies deleted events.
     256              $events = \core\event\competency_deleted::create_multiple_from_competencyids($competencyids, $contextid);
     257  
     258          } catch (\Exception $e) {
     259              $transaction->rollback($e);
     260          }
     261  
     262          $transaction->allow_commit();
     263          // Trigger events.
     264          foreach ($events as $event) {
     265              $event->trigger();
     266          }
     267  
     268          return true;
     269      }
     270  
     271      /**
     272       * Reorder this competency.
     273       *
     274       * Requires moodle/competency:competencymanage capability at the system context.
     275       *
     276       * @param int $id The id of the competency to move.
     277       * @return boolean
     278       */
     279      public static function move_down_competency($id) {
     280          static::require_enabled();
     281          $current = new competency($id);
     282  
     283          // First we do a permissions check.
     284          require_capability('moodle/competency:competencymanage', $current->get_context());
     285  
     286          $max = self::count_competencies(array('parentid' => $current->get('parentid'),
     287                                                'competencyframeworkid' => $current->get('competencyframeworkid')));
     288          if ($max > 0) {
     289              $max--;
     290          }
     291  
     292          $sortorder = $current->get('sortorder');
     293          if ($sortorder >= $max) {
     294              return false;
     295          }
     296          $sortorder = $sortorder + 1;
     297          $current->set('sortorder', $sortorder);
     298  
     299          $filters = array('parentid' => $current->get('parentid'),
     300                           'competencyframeworkid' => $current->get('competencyframeworkid'),
     301                           'sortorder' => $sortorder);
     302          $children = self::list_competencies($filters, 'id');
     303          foreach ($children as $needtoswap) {
     304              $needtoswap->set('sortorder', $sortorder - 1);
     305              $needtoswap->update();
     306          }
     307  
     308          // OK - all set.
     309          $result = $current->update();
     310  
     311          return $result;
     312      }
     313  
     314      /**
     315       * Reorder this competency.
     316       *
     317       * Requires moodle/competency:competencymanage capability at the system context.
     318       *
     319       * @param int $id The id of the competency to move.
     320       * @return boolean
     321       */
     322      public static function move_up_competency($id) {
     323          static::require_enabled();
     324          $current = new competency($id);
     325  
     326          // First we do a permissions check.
     327          require_capability('moodle/competency:competencymanage', $current->get_context());
     328  
     329          $sortorder = $current->get('sortorder');
     330          if ($sortorder == 0) {
     331              return false;
     332          }
     333  
     334          $sortorder = $sortorder - 1;
     335          $current->set('sortorder', $sortorder);
     336  
     337          $filters = array('parentid' => $current->get('parentid'),
     338                           'competencyframeworkid' => $current->get('competencyframeworkid'),
     339                           'sortorder' => $sortorder);
     340          $children = self::list_competencies($filters, 'id');
     341          foreach ($children as $needtoswap) {
     342              $needtoswap->set('sortorder', $sortorder + 1);
     343              $needtoswap->update();
     344          }
     345  
     346          // OK - all set.
     347          $result = $current->update();
     348  
     349          return $result;
     350      }
     351  
     352      /**
     353       * Move this competency so it sits in a new parent.
     354       *
     355       * Requires moodle/competency:competencymanage capability at the system context.
     356       *
     357       * @param int $id The id of the competency to move.
     358       * @param int $newparentid The new parent id for the competency.
     359       * @return boolean
     360       */
     361      public static function set_parent_competency($id, $newparentid) {
     362          global $DB;
     363          static::require_enabled();
     364          $current = new competency($id);
     365  
     366          // First we do a permissions check.
     367          require_capability('moodle/competency:competencymanage', $current->get_context());
     368          if ($id == $newparentid) {
     369              throw new coding_exception('Can not set a competency as a parent of itself.');
     370          } if ($newparentid == $current->get('parentid')) {
     371              throw new coding_exception('Can not move a competency to the same location.');
     372          }
     373  
     374          // Some great variable assignment right here.
     375          $currentparent = $current->get_parent();
     376          $parent = !empty($newparentid) ? new competency($newparentid) : null;
     377          $parentpath = !empty($parent) ? $parent->get('path') : '/0/';
     378  
     379          // We're going to change quite a few things.
     380          $transaction = $DB->start_delegated_transaction();
     381  
     382          // If we are moving a node to a child of itself:
     383          // - promote all the child nodes by one level.
     384          // - remove the rule on self.
     385          // - re-read the parent.
     386          $newparents = explode('/', $parentpath);
     387          if (in_array($current->get('id'), $newparents)) {
     388              $children = competency::get_records(array('parentid' => $current->get('id')), 'id');
     389              foreach ($children as $child) {
     390                  $child->set('parentid', $current->get('parentid'));
     391                  $child->update();
     392              }
     393  
     394              // Reset the rule on self as our children have changed.
     395              $current->reset_rule();
     396  
     397              // The destination parent is one of our descendants, we need to re-fetch its values (path, parentid).
     398              $parent->read();
     399          }
     400  
     401          // Reset the rules of initial parent and destination.
     402          if (!empty($currentparent)) {
     403              $currentparent->reset_rule();
     404              $currentparent->update();
     405          }
     406          if (!empty($parent)) {
     407              $parent->reset_rule();
     408              $parent->update();
     409          }
     410  
     411          // Do the actual move.
     412          $current->set('parentid', $newparentid);
     413          $result = $current->update();
     414  
     415          // All right, let's commit this.
     416          $transaction->allow_commit();
     417  
     418          return $result;
     419      }
     420  
     421      /**
     422       * Update the details for a competency.
     423       *
     424       * Requires moodle/competency:competencymanage capability at the system context.
     425       *
     426       * @param stdClass $record The new details for the competency.
     427       *                         Note - must contain an id that points to the competency to update.
     428       *
     429       * @return boolean
     430       */
     431      public static function update_competency($record) {
     432          static::require_enabled();
     433          $competency = new competency($record->id);
     434  
     435          // First we do a permissions check.
     436          require_capability('moodle/competency:competencymanage', $competency->get_context());
     437  
     438          // Some things should not be changed in an update - they should use a more specific method.
     439          $record->sortorder = $competency->get('sortorder');
     440          $record->parentid = $competency->get('parentid');
     441          $record->competencyframeworkid = $competency->get('competencyframeworkid');
     442  
     443          $competency->from_record($record);
     444          require_capability('moodle/competency:competencymanage', $competency->get_context());
     445  
     446          // OK - all set.
     447          $result = $competency->update();
     448  
     449          // Trigger the update event.
     450          \core\event\competency_updated::create_from_competency($competency)->trigger();
     451  
     452          return $result;
     453      }
     454  
     455      /**
     456       * Read a the details for a single competency and return a record.
     457       *
     458       * Requires moodle/competency:competencyview capability at the system context.
     459       *
     460       * @param int $id The id of the competency to read.
     461       * @param bool $includerelated Include related tags or not.
     462       * @return stdClass
     463       */
     464      public static function read_competency($id, $includerelated = false) {
     465          static::require_enabled();
     466          $competency = new competency($id);
     467  
     468          // First we do a permissions check.
     469          $context = $competency->get_context();
     470          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
     471               throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
     472          }
     473  
     474          // OK - all set.
     475          if ($includerelated) {
     476              $relatedcompetency = new related_competency();
     477              if ($related = $relatedcompetency->list_relations($id)) {
     478                  $competency->relatedcompetencies = $related;
     479              }
     480          }
     481  
     482          return $competency;
     483      }
     484  
     485      /**
     486       * Perform a text search based and return all results and their parents.
     487       *
     488       * Requires moodle/competency:competencyview capability at the framework context.
     489       *
     490       * @param string $textsearch A string to search for.
     491       * @param int $competencyframeworkid The id of the framework to limit the search.
     492       * @return array of competencies
     493       */
     494      public static function search_competencies($textsearch, $competencyframeworkid) {
     495          static::require_enabled();
     496          $framework = new competency_framework($competencyframeworkid);
     497  
     498          // First we do a permissions check.
     499          $context = $framework->get_context();
     500          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
     501               throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
     502          }
     503  
     504          // OK - all set.
     505          $competencies = competency::search($textsearch, $competencyframeworkid);
     506          return $competencies;
     507      }
     508  
     509      /**
     510       * Perform a search based on the provided filters and return a paginated list of records.
     511       *
     512       * Requires moodle/competency:competencyview capability at some context.
     513       *
     514       * @param array $filters A list of filters to apply to the list.
     515       * @param string $sort The column to sort on
     516       * @param string $order ('ASC' or 'DESC')
     517       * @param int $skip Number of records to skip (pagination)
     518       * @param int $limit Max of records to return (pagination)
     519       * @return array of competencies
     520       */
     521      public static function list_competencies($filters, $sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
     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::get_records($filters, $sort, $order, $skip, $limit);
     537      }
     538  
     539      /**
     540       * Perform a search based on the provided filters and return a paginated list of records.
     541       *
     542       * Requires moodle/competency:competencyview capability at some context.
     543       *
     544       * @param array $filters A list of filters to apply to the list.
     545       * @return int
     546       */
     547      public static function count_competencies($filters) {
     548          static::require_enabled();
     549          if (!isset($filters['competencyframeworkid'])) {
     550              $context = context_system::instance();
     551          } else {
     552              $framework = new competency_framework($filters['competencyframeworkid']);
     553              $context = $framework->get_context();
     554          }
     555  
     556          // First we do a permissions check.
     557          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
     558               throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
     559          }
     560  
     561          // OK - all set.
     562          return competency::count_records($filters);
     563      }
     564  
     565      /**
     566       * Create a competency framework from a record containing all the data for the class.
     567       *
     568       * Requires moodle/competency:competencymanage capability at the system context.
     569       *
     570       * @param stdClass $record Record containing all the data for an instance of the class.
     571       * @return competency_framework
     572       */
     573      public static function create_framework(stdClass $record) {
     574          static::require_enabled();
     575          $framework = new competency_framework(0, $record);
     576          require_capability('moodle/competency:competencymanage', $framework->get_context());
     577  
     578          // Account for different formats of taxonomies.
     579          if (isset($record->taxonomies)) {
     580              $framework->set('taxonomies', $record->taxonomies);
     581          }
     582  
     583          $framework = $framework->create();
     584  
     585          // Trigger a competency framework created event.
     586          \core\event\competency_framework_created::create_from_framework($framework)->trigger();
     587  
     588          return $framework;
     589      }
     590  
     591      /**
     592       * Duplicate a competency framework by id.
     593       *
     594       * Requires moodle/competency:competencymanage capability at the system context.
     595       *
     596       * @param int $id The record to duplicate. All competencies associated and related will be duplicated.
     597       * @return competency_framework the framework duplicated
     598       */
     599      public static function duplicate_framework($id) {
     600          global $DB;
     601          static::require_enabled();
     602  
     603          $framework = new competency_framework($id);
     604          require_capability('moodle/competency:competencymanage', $framework->get_context());
     605          // Starting transaction.
     606          $transaction = $DB->start_delegated_transaction();
     607  
     608          try {
     609              // Get a uniq idnumber based on the origin framework.
     610              $idnumber = competency_framework::get_unused_idnumber($framework->get('idnumber'));
     611              $framework->set('idnumber', $idnumber);
     612              // Adding the suffix copy to the shortname.
     613              $framework->set('shortname', get_string('duplicateditemname', 'core_competency', $framework->get('shortname')));
     614              $framework->set('id', 0);
     615              $framework = $framework->create();
     616  
     617              // Array that match the old competencies ids with the new one to use when copying related competencies.
     618              $frameworkcompetency = competency::get_framework_tree($id);
     619              $matchids = self::duplicate_competency_tree($framework->get('id'), $frameworkcompetency, 0, 0);
     620  
     621              // Copy the related competencies.
     622              $relcomps = related_competency::get_multiple_relations(array_keys($matchids));
     623  
     624              foreach ($relcomps as $relcomp) {
     625                  $compid = $relcomp->get('competencyid');
     626                  $relcompid = $relcomp->get('relatedcompetencyid');
     627                  if (isset($matchids[$compid]) && isset($matchids[$relcompid])) {
     628                      $newcompid = $matchids[$compid]->get('id');
     629                      $newrelcompid = $matchids[$relcompid]->get('id');
     630                      if ($newcompid < $newrelcompid) {
     631                          $relcomp->set('competencyid', $newcompid);
     632                          $relcomp->set('relatedcompetencyid', $newrelcompid);
     633                      } else {
     634                          $relcomp->set('competencyid', $newrelcompid);
     635                          $relcomp->set('relatedcompetencyid', $newcompid);
     636                      }
     637                      $relcomp->set('id', 0);
     638                      $relcomp->create();
     639                  } else {
     640                      // Debugging message when there is no match found.
     641                      debugging('related competency id not found');
     642                  }
     643              }
     644  
     645              // Setting rules on duplicated competencies.
     646              self::migrate_competency_tree_rules($frameworkcompetency, $matchids);
     647  
     648              $transaction->allow_commit();
     649  
     650          } catch (\Exception $e) {
     651              $transaction->rollback($e);
     652          }
     653  
     654          // Trigger a competency framework created event.
     655          \core\event\competency_framework_created::create_from_framework($framework)->trigger();
     656  
     657          return $framework;
     658      }
     659  
     660      /**
     661       * Delete a competency framework by id.
     662       *
     663       * Requires moodle/competency:competencymanage capability at the system context.
     664       *
     665       * @param int $id The record to delete. This will delete alot of related data - you better be sure.
     666       * @return boolean
     667       */
     668      public static function delete_framework($id) {
     669          global $DB;
     670          static::require_enabled();
     671          $framework = new competency_framework($id);
     672          require_capability('moodle/competency:competencymanage', $framework->get_context());
     673  
     674          $events = array();
     675          $competenciesid = competency::get_ids_by_frameworkid($id);
     676          $contextid = $framework->get('contextid');
     677          if (!competency::can_all_be_deleted($competenciesid)) {
     678              return false;
     679          }
     680          $transaction = $DB->start_delegated_transaction();
     681          try {
     682              if (!empty($competenciesid)) {
     683                  // Delete competencies.
     684                  competency::delete_by_frameworkid($id);
     685  
     686                  // Delete the related competencies.
     687                  related_competency::delete_multiple_relations($competenciesid);
     688  
     689                  // Delete the evidences for competencies.
     690                  user_evidence_competency::delete_by_competencyids($competenciesid);
     691              }
     692  
     693              // Create a competency framework deleted event.
     694              $event = \core\event\competency_framework_deleted::create_from_framework($framework);
     695              $result = $framework->delete();
     696  
     697              // Register the deleted events competencies.
     698              $events = \core\event\competency_deleted::create_multiple_from_competencyids($competenciesid, $contextid);
     699  
     700          } catch (\Exception $e) {
     701              $transaction->rollback($e);
     702          }
     703  
     704          // Commit the transaction.
     705          $transaction->allow_commit();
     706  
     707          // If all operations are successfull then trigger the delete event.
     708          $event->trigger();
     709  
     710          // Trigger deleted event competencies.
     711          foreach ($events as $event) {
     712              $event->trigger();
     713          }
     714  
     715          return $result;
     716      }
     717  
     718      /**
     719       * Update the details for a competency framework.
     720       *
     721       * Requires moodle/competency:competencymanage capability at the system context.
     722       *
     723       * @param stdClass $record The new details for the framework. Note - must contain an id that points to the framework to update.
     724       * @return boolean
     725       */
     726      public static function update_framework($record) {
     727          static::require_enabled();
     728          $framework = new competency_framework($record->id);
     729  
     730          // Check the permissions before update.
     731          require_capability('moodle/competency:competencymanage', $framework->get_context());
     732  
     733          // Account for different formats of taxonomies.
     734          $framework->from_record($record);
     735          if (isset($record->taxonomies)) {
     736              $framework->set('taxonomies', $record->taxonomies);
     737          }
     738  
     739          // Trigger a competency framework updated event.
     740          \core\event\competency_framework_updated::create_from_framework($framework)->trigger();
     741  
     742          return $framework->update();
     743      }
     744  
     745      /**
     746       * Read a the details for a single competency framework and return a record.
     747       *
     748       * Requires moodle/competency:competencyview capability at the system context.
     749       *
     750       * @param int $id The id of the framework to read.
     751       * @return competency_framework
     752       */
     753      public static function read_framework($id) {
     754          static::require_enabled();
     755          $framework = new competency_framework($id);
     756          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
     757                  $framework->get_context())) {
     758              throw new required_capability_exception($framework->get_context(), 'moodle/competency:competencyview',
     759                  'nopermissions', '');
     760          }
     761          return $framework;
     762      }
     763  
     764      /**
     765       * Logg the competency framework viewed event.
     766       *
     767       * @param competency_framework|int $frameworkorid The competency_framework object or competency framework id
     768       * @return bool
     769       */
     770      public static function competency_framework_viewed($frameworkorid) {
     771          static::require_enabled();
     772          $framework = $frameworkorid;
     773          if (!is_object($framework)) {
     774              $framework = new competency_framework($framework);
     775          }
     776          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
     777                  $framework->get_context())) {
     778              throw new required_capability_exception($framework->get_context(), 'moodle/competency:competencyview',
     779                  'nopermissions', '');
     780          }
     781          \core\event\competency_framework_viewed::create_from_framework($framework)->trigger();
     782          return true;
     783      }
     784  
     785      /**
     786       * Logg the competency viewed event.
     787       *
     788       * @param competency|int $competencyorid The competency object or competency id
     789       * @return bool
     790       */
     791      public static function competency_viewed($competencyorid) {
     792          static::require_enabled();
     793          $competency = $competencyorid;
     794          if (!is_object($competency)) {
     795              $competency = new competency($competency);
     796          }
     797  
     798          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
     799                  $competency->get_context())) {
     800              throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview',
     801                  'nopermissions', '');
     802          }
     803  
     804          \core\event\competency_viewed::create_from_competency($competency)->trigger();
     805          return true;
     806      }
     807  
     808      /**
     809       * Perform a search based on the provided filters and return a paginated list of records.
     810       *
     811       * Requires moodle/competency:competencyview capability at the system context.
     812       *
     813       * @param string $sort The column to sort on
     814       * @param string $order ('ASC' or 'DESC')
     815       * @param int $skip Number of records to skip (pagination)
     816       * @param int $limit Max of records to return (pagination)
     817       * @param context $context The parent context of the frameworks.
     818       * @param string $includes Defines what other contexts to fetch frameworks from.
     819       *                         Accepted values are:
     820       *                          - children: All descendants
     821       *                          - parents: All parents, grand parents, etc...
     822       *                          - self: Context passed only.
     823       * @param bool $onlyvisible If true return only visible frameworks
     824       * @param string $query A string to use to filter down the frameworks.
     825       * @return array of competency_framework
     826       */
     827      public static function list_frameworks($sort, $order, $skip, $limit, $context, $includes = 'children',
     828                                             $onlyvisible = false, $query = '') {
     829          global $DB;
     830          static::require_enabled();
     831  
     832          // Get all the relevant contexts.
     833          $contexts = self::get_related_contexts($context, $includes,
     834              array('moodle/competency:competencyview', 'moodle/competency:competencymanage'));
     835  
     836          if (empty($contexts)) {
     837              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
     838          }
     839  
     840          // OK - all set.
     841          list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
     842          $select = "contextid $insql";
     843          if ($onlyvisible) {
     844              $select .= " AND visible = :visible";
     845              $inparams['visible'] = 1;
     846          }
     847  
     848          if (!empty($query) || is_numeric($query)) {
     849              $sqlnamelike = $DB->sql_like('shortname', ':namelike', false);
     850              $sqlidnlike = $DB->sql_like('idnumber', ':idnlike', false);
     851  
     852              $select .= " AND ($sqlnamelike OR $sqlidnlike) ";
     853              $inparams['namelike'] = '%' . $DB->sql_like_escape($query) . '%';
     854              $inparams['idnlike'] = '%' . $DB->sql_like_escape($query) . '%';
     855          }
     856  
     857          return competency_framework::get_records_select($select, $inparams, $sort . ' ' . $order, '*', $skip, $limit);
     858      }
     859  
     860      /**
     861       * Perform a search based on the provided filters and return a paginated list of records.
     862       *
     863       * Requires moodle/competency:competencyview capability at the system context.
     864       *
     865       * @param context $context The parent context of the frameworks.
     866       * @param string $includes Defines what other contexts to fetch frameworks from.
     867       *                         Accepted values are:
     868       *                          - children: All descendants
     869       *                          - parents: All parents, grand parents, etc...
     870       *                          - self: Context passed only.
     871       * @return int
     872       */
     873      public static function count_frameworks($context, $includes) {
     874          global $DB;
     875          static::require_enabled();
     876  
     877          // Get all the relevant contexts.
     878          $contexts = self::get_related_contexts($context, $includes,
     879              array('moodle/competency:competencyview', 'moodle/competency:competencymanage'));
     880  
     881          if (empty($contexts)) {
     882              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
     883          }
     884  
     885          // OK - all set.
     886          list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
     887          return competency_framework::count_records_select("contextid $insql", $inparams);
     888      }
     889  
     890      /**
     891       * Fetches all the relevant contexts.
     892       *
     893       * Note: This currently only supports system, category and user contexts. However user contexts
     894       * behave a bit differently and will fallback on the system context. This is what makes the most
     895       * sense because a user context does not have descendants, and only has system as a parent.
     896       *
     897       * @param context $context The context to start from.
     898       * @param string $includes Defines what other contexts to find.
     899       *                         Accepted values are:
     900       *                          - children: All descendants
     901       *                          - parents: All parents, grand parents, etc...
     902       *                          - self: Context passed only.
     903       * @param array $hasanycapability Array of capabilities passed to {@link has_any_capability()} in each context.
     904       * @return context[] An array of contexts where keys are context IDs.
     905       */
     906      public static function get_related_contexts($context, $includes, array $hasanycapability = null) {
     907          global $DB;
     908          static::require_enabled();
     909  
     910          if (!in_array($includes, array('children', 'parents', 'self'))) {
     911              throw new coding_exception('Invalid parameter value for \'includes\'.');
     912          }
     913  
     914          // If context user swap it for the context_system.
     915          if ($context->contextlevel == CONTEXT_USER) {
     916              $context = context_system::instance();
     917          }
     918  
     919          $contexts = array($context->id => $context);
     920  
     921          if ($includes == 'children') {
     922              $params = array('coursecatlevel' => CONTEXT_COURSECAT, 'path' => $context->path . '/%');
     923              $pathlike = $DB->sql_like('path', ':path');
     924              $sql = "contextlevel = :coursecatlevel AND $pathlike";
     925              $rs = $DB->get_recordset_select('context', $sql, $params);
     926              foreach ($rs as $record) {
     927                  $ctxid = $record->id;
     928                  context_helper::preload_from_record($record);
     929                  $contexts[$ctxid] = context::instance_by_id($ctxid);
     930              }
     931              $rs->close();
     932  
     933          } else if ($includes == 'parents') {
     934              $children = $context->get_parent_contexts();
     935              foreach ($children as $ctx) {
     936                  $contexts[$ctx->id] = $ctx;
     937              }
     938          }
     939  
     940          // Filter according to the capabilities required.
     941          if (!empty($hasanycapability)) {
     942              foreach ($contexts as $key => $ctx) {
     943                  if (!has_any_capability($hasanycapability, $ctx)) {
     944                      unset($contexts[$key]);
     945                  }
     946              }
     947          }
     948  
     949          return $contexts;
     950      }
     951  
     952      /**
     953       * Count all the courses using a competency.
     954       *
     955       * @param int $competencyid The id of the competency to check.
     956       * @return int
     957       */
     958      public static function count_courses_using_competency($competencyid) {
     959          static::require_enabled();
     960  
     961          // OK - all set.
     962          $courses = course_competency::list_courses_min($competencyid);
     963          $count = 0;
     964  
     965          // Now check permissions on each course.
     966          foreach ($courses as $course) {
     967              if (!self::validate_course($course, false)) {
     968                  continue;
     969              }
     970  
     971              $context = context_course::instance($course->id);
     972              $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
     973              if (!has_any_capability($capabilities, $context)) {
     974                  continue;
     975              }
     976  
     977              $count++;
     978          }
     979  
     980          return $count;
     981      }
     982  
     983      /**
     984       * List all the courses modules using a competency in a course.
     985       *
     986       * @param int $competencyid The id of the competency to check.
     987       * @param int $courseid The id of the course to check.
     988       * @return array[int] Array of course modules ids.
     989       */
     990      public static function list_course_modules_using_competency($competencyid, $courseid) {
     991          static::require_enabled();
     992  
     993          $result = array();
     994          self::validate_course($courseid);
     995  
     996          $coursecontext = context_course::instance($courseid);
     997  
     998          // We will not check each module - course permissions should be enough.
     999          $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
    1000          if (!has_any_capability($capabilities, $coursecontext)) {
    1001              throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
    1002          }
    1003  
    1004          $cmlist = course_module_competency::list_course_modules($competencyid, $courseid);
    1005          foreach ($cmlist as $cmid) {
    1006              if (self::validate_course_module($cmid, false)) {
    1007                  array_push($result, $cmid);
    1008              }
    1009          }
    1010  
    1011          return $result;
    1012      }
    1013  
    1014      /**
    1015       * List all the competencies linked to a course module.
    1016       *
    1017       * @param mixed $cmorid The course module, or its ID.
    1018       * @return array[competency] Array of competency records.
    1019       */
    1020      public static function list_course_module_competencies_in_course_module($cmorid) {
    1021          static::require_enabled();
    1022          $cm = $cmorid;
    1023          if (!is_object($cmorid)) {
    1024              $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
    1025          }
    1026  
    1027          // Check the user have access to the course module.
    1028          self::validate_course_module($cm);
    1029          $context = context_module::instance($cm->id);
    1030  
    1031          $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
    1032          if (!has_any_capability($capabilities, $context)) {
    1033              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
    1034          }
    1035  
    1036          $result = array();
    1037  
    1038          $cmclist = course_module_competency::list_course_module_competencies($cm->id);
    1039          foreach ($cmclist as $id => $cmc) {
    1040              array_push($result, $cmc);
    1041          }
    1042  
    1043          return $result;
    1044      }
    1045  
    1046      /**
    1047       * List all the courses using a competency.
    1048       *
    1049       * @param int $competencyid The id of the competency to check.
    1050       * @return array[stdClass] Array of stdClass containing id and shortname.
    1051       */
    1052      public static function list_courses_using_competency($competencyid) {
    1053          static::require_enabled();
    1054  
    1055          // OK - all set.
    1056          $courses = course_competency::list_courses($competencyid);
    1057          $result = array();
    1058  
    1059          // Now check permissions on each course.
    1060          foreach ($courses as $id => $course) {
    1061              $context = context_course::instance($course->id);
    1062              $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
    1063              if (!has_any_capability($capabilities, $context)) {
    1064                  unset($courses[$id]);
    1065                  continue;
    1066              }
    1067              if (!self::validate_course($course, false)) {
    1068                  unset($courses[$id]);
    1069                  continue;
    1070              }
    1071              array_push($result, $course);
    1072          }
    1073  
    1074          return $result;
    1075      }
    1076  
    1077      /**
    1078       * Count the proficient competencies in a course for one user.
    1079       *
    1080       * @param int $courseid The id of the course to check.
    1081       * @param int $userid The id of the user to check.
    1082       * @return int
    1083       */
    1084      public static function count_proficient_competencies_in_course_for_user($courseid, $userid) {
    1085          static::require_enabled();
    1086          // Check the user have access to the course.
    1087          self::validate_course($courseid);
    1088  
    1089          // First we do a permissions check.
    1090          $context = context_course::instance($courseid);
    1091  
    1092          $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
    1093          if (!has_any_capability($capabilities, $context)) {
    1094               throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
    1095          }
    1096  
    1097          // OK - all set.
    1098          return user_competency_course::count_proficient_competencies($courseid, $userid);
    1099      }
    1100  
    1101      /**
    1102       * Count all the competencies in a course.
    1103       *
    1104       * @param int $courseid The id of the course to check.
    1105       * @return int
    1106       */
    1107      public static function count_competencies_in_course($courseid) {
    1108          static::require_enabled();
    1109          // Check the user have access to the course.
    1110          self::validate_course($courseid);
    1111  
    1112          // First we do a permissions check.
    1113          $context = context_course::instance($courseid);
    1114  
    1115          $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
    1116          if (!has_any_capability($capabilities, $context)) {
    1117               throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
    1118          }
    1119  
    1120          // OK - all set.
    1121          return course_competency::count_competencies($courseid);
    1122      }
    1123  
    1124      /**
    1125       * List the competencies associated to a course.
    1126       *
    1127       * @param mixed $courseorid The course, or its ID.
    1128       * @return array( array(
    1129       *                   'competency' => \core_competency\competency,
    1130       *                   'coursecompetency' => \core_competency\course_competency
    1131       *              ))
    1132       */
    1133      public static function list_course_competencies($courseorid) {
    1134          static::require_enabled();
    1135          $course = $courseorid;
    1136          if (!is_object($courseorid)) {
    1137              $course = get_course($courseorid);
    1138          }
    1139  
    1140          // Check the user have access to the course.
    1141          self::validate_course($course);
    1142          $context = context_course::instance($course->id);
    1143  
    1144          $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
    1145          if (!has_any_capability($capabilities, $context)) {
    1146              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
    1147          }
    1148  
    1149          $result = array();
    1150  
    1151          // TODO We could improve the performance of this into one single query.
    1152          $coursecompetencies = course_competency::list_course_competencies($course->id);
    1153          $competencies = course_competency::list_competencies($course->id);
    1154  
    1155          // Build the return values.
    1156          foreach ($coursecompetencies as $key => $coursecompetency) {
    1157              $result[] = array(
    1158                  'competency' => $competencies[$coursecompetency->get('competencyid')],
    1159                  'coursecompetency' => $coursecompetency
    1160              );
    1161          }
    1162  
    1163          return $result;
    1164      }
    1165  
    1166      /**
    1167       * Get a user competency.
    1168       *
    1169       * @param int $userid The user ID.
    1170       * @param int $competencyid The competency ID.
    1171       * @return user_competency
    1172       */
    1173      public static function get_user_competency($userid, $competencyid) {
    1174          static::require_enabled();
    1175          $existing = user_competency::get_multiple($userid, array($competencyid));
    1176          $uc = array_pop($existing);
    1177  
    1178          if (!$uc) {
    1179              $uc = user_competency::create_relation($userid, $competencyid);
    1180              $uc->create();
    1181          }
    1182  
    1183          if (!$uc->can_read()) {
    1184              throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
    1185                  'nopermissions', '');
    1186          }
    1187          return $uc;
    1188      }
    1189  
    1190      /**
    1191       * Get a user competency by ID.
    1192       *
    1193       * @param int $usercompetencyid The user competency ID.
    1194       * @return user_competency
    1195       */
    1196      public static function get_user_competency_by_id($usercompetencyid) {
    1197          static::require_enabled();
    1198          $uc = new user_competency($usercompetencyid);
    1199          if (!$uc->can_read()) {
    1200              throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
    1201                  'nopermissions', '');
    1202          }
    1203          return $uc;
    1204      }
    1205  
    1206      /**
    1207       * Count the competencies associated to a course module.
    1208       *
    1209       * @param mixed $cmorid The course module, or its ID.
    1210       * @return int
    1211       */
    1212      public static function count_course_module_competencies($cmorid) {
    1213          static::require_enabled();
    1214          $cm = $cmorid;
    1215          if (!is_object($cmorid)) {
    1216              $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
    1217          }
    1218  
    1219          // Check the user have access to the course module.
    1220          self::validate_course_module($cm);
    1221          $context = context_module::instance($cm->id);
    1222  
    1223          $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
    1224          if (!has_any_capability($capabilities, $context)) {
    1225              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
    1226          }
    1227  
    1228          return course_module_competency::count_competencies($cm->id);
    1229      }
    1230  
    1231      /**
    1232       * List the competencies associated to a course module.
    1233       *
    1234       * @param mixed $cmorid The course module, or its ID.
    1235       * @return array( array(
    1236       *                   'competency' => \core_competency\competency,
    1237       *                   'coursemodulecompetency' => \core_competency\course_module_competency
    1238       *              ))
    1239       */
    1240      public static function list_course_module_competencies($cmorid) {
    1241          static::require_enabled();
    1242          $cm = $cmorid;
    1243          if (!is_object($cmorid)) {
    1244              $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
    1245          }
    1246  
    1247          // Check the user have access to the course module.
    1248          self::validate_course_module($cm);
    1249          $context = context_module::instance($cm->id);
    1250  
    1251          $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
    1252          if (!has_any_capability($capabilities, $context)) {
    1253              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
    1254          }
    1255  
    1256          $result = array();
    1257  
    1258          // TODO We could improve the performance of this into one single query.
    1259          $coursemodulecompetencies = course_module_competency::list_course_module_competencies($cm->id);
    1260          $competencies = course_module_competency::list_competencies($cm->id);
    1261  
    1262          // Build the return values.
    1263          foreach ($coursemodulecompetencies as $key => $coursemodulecompetency) {
    1264              $result[] = array(
    1265                  'competency' => $competencies[$coursemodulecompetency->get('competencyid')],
    1266                  'coursemodulecompetency' => $coursemodulecompetency
    1267              );
    1268          }
    1269  
    1270          return $result;
    1271      }
    1272  
    1273      /**
    1274       * Get a user competency in a course.
    1275       *
    1276       * @param int $courseid The id of the course to check.
    1277       * @param int $userid The id of the course to check.
    1278       * @param int $competencyid The id of the competency.
    1279       * @return user_competency_course
    1280       */
    1281      public static function get_user_competency_in_course($courseid, $userid, $competencyid) {
    1282          static::require_enabled();
    1283          // First we do a permissions check.
    1284          $context = context_course::instance($courseid);
    1285  
    1286          $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
    1287          if (!has_any_capability($capabilities, $context)) {
    1288              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
    1289          } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
    1290              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
    1291          }
    1292  
    1293          // This will throw an exception if the competency does not belong to the course.
    1294          $competency = course_competency::get_competency($courseid, $competencyid);
    1295  
    1296          $params = array('courseid' => $courseid, 'userid' => $userid, 'competencyid' => $competencyid);
    1297          $exists = user_competency_course::get_record($params);
    1298          // Create missing.
    1299          if ($exists) {
    1300              $ucc = $exists;
    1301          } else {
    1302              $ucc = user_competency_course::create_relation($userid, $competency->get('id'), $courseid);
    1303              $ucc->create();
    1304          }
    1305  
    1306          return $ucc;
    1307      }
    1308  
    1309      /**
    1310       * List all the user competencies in a course.
    1311       *
    1312       * @param int $courseid The id of the course to check.
    1313       * @param int $userid The id of the course to check.
    1314       * @return array of user_competency_course objects
    1315       */
    1316      public static function list_user_competencies_in_course($courseid, $userid) {
    1317          static::require_enabled();
    1318          // First we do a permissions check.
    1319          $context = context_course::instance($courseid);
    1320          $onlyvisible = 1;
    1321  
    1322          $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
    1323          if (!has_any_capability($capabilities, $context)) {
    1324              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
    1325          } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
    1326              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
    1327          }
    1328  
    1329          // OK - all set.
    1330          $competencylist = course_competency::list_competencies($courseid, false);
    1331  
    1332          $existing = user_competency_course::get_multiple($userid, $courseid, $competencylist);
    1333          // Create missing.
    1334          $orderedusercompetencycourses = array();
    1335  
    1336          $somemissing = false;
    1337          foreach ($competencylist as $coursecompetency) {
    1338              $found = false;
    1339              foreach ($existing as $usercompetencycourse) {
    1340                  if ($usercompetencycourse->get('competencyid') == $coursecompetency->get('id')) {
    1341                      $found = true;
    1342                      $orderedusercompetencycourses[$usercompetencycourse->get('id')] = $usercompetencycourse;
    1343                      break;
    1344                  }
    1345              }
    1346              if (!$found) {
    1347                  $ucc = user_competency_course::create_relation($userid, $coursecompetency->get('id'), $courseid);
    1348                  $ucc->create();
    1349                  $orderedusercompetencycourses[$ucc->get('id')] = $ucc;
    1350              }
    1351          }
    1352  
    1353          return $orderedusercompetencycourses;
    1354      }
    1355  
    1356      /**
    1357       * List the user competencies to review.
    1358       *
    1359       * The method returns values in this format:
    1360       *
    1361       * array(
    1362       *     'competencies' => array(
    1363       *         (stdClass)(
    1364       *             'usercompetency' => (user_competency),
    1365       *             'competency' => (competency),
    1366       *             'user' => (user)
    1367       *         )
    1368       *     ),
    1369       *     'count' => (int)
    1370       * )
    1371       *
    1372       * @param int $skip The number of records to skip.
    1373       * @param int $limit The number of results to return.
    1374       * @param int $userid The user we're getting the competencies to review for.
    1375       * @return array Containing the keys 'count', and 'competencies'. The 'competencies' key contains an object
    1376       *               which contains 'competency', 'usercompetency' and 'user'.
    1377       */
    1378      public static function list_user_competencies_to_review($skip = 0, $limit = 50, $userid = null) {
    1379          global $DB, $USER;
    1380          static::require_enabled();
    1381          if ($userid === null) {
    1382              $userid = $USER->id;
    1383          }
    1384  
    1385          $capability = 'moodle/competency:usercompetencyreview';
    1386          $ucfields = user_competency::get_sql_fields('uc', 'uc_');
    1387          $compfields = competency::get_sql_fields('c', 'c_');
    1388          $usercols = array('id') + get_user_fieldnames();
    1389          $userfields = array();
    1390          foreach ($usercols as $field) {
    1391              $userfields[] = "u." . $field . " AS usr_" . $field;
    1392          }
    1393          $userfields = implode(',', $userfields);
    1394  
    1395          $select = "SELECT $ucfields, $compfields, $userfields";
    1396          $countselect = "SELECT COUNT('x')";
    1397          $sql = "  FROM {" . user_competency::TABLE . "} uc
    1398                    JOIN {" . competency::TABLE . "} c
    1399                      ON c.id = uc.competencyid
    1400                    JOIN {user} u
    1401                      ON u.id = uc.userid
    1402                   WHERE (uc.status = :waitingforreview
    1403                      OR (uc.status = :inreview AND uc.reviewerid = :reviewerid))
    1404                     AND u.deleted = 0";
    1405          $ordersql = " ORDER BY c.shortname ASC";
    1406          $params = array(
    1407              'inreview' => user_competency::STATUS_IN_REVIEW,
    1408              'reviewerid' => $userid,
    1409              'waitingforreview' => user_competency::STATUS_WAITING_FOR_REVIEW,
    1410          );
    1411          $countsql = $countselect . $sql;
    1412  
    1413          // Primary check to avoid the hard work of getting the users in which the user has permission.
    1414          $count = $DB->count_records_sql($countselect . $sql, $params);
    1415          if ($count < 1) {
    1416              return array('count' => 0, 'competencies' => array());
    1417          }
    1418  
    1419          // TODO MDL-52243 Use core function.
    1420          list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql(
    1421              $capability, $userid, SQL_PARAMS_NAMED);
    1422          $params += $inparams;
    1423          $countsql = $countselect . $sql . " AND uc.userid $insql";
    1424          $getsql = $select . $sql . " AND uc.userid $insql " . $ordersql;
    1425  
    1426          // Extracting the results.
    1427          $competencies = array();
    1428          $records = $DB->get_recordset_sql($getsql, $params, $skip, $limit);
    1429          foreach ($records as $record) {
    1430              $objects = (object) array(
    1431                  'usercompetency' => new user_competency(0, user_competency::extract_record($record, 'uc_')),
    1432                  'competency' => new competency(0, competency::extract_record($record, 'c_')),
    1433                  'user' => persistent::extract_record($record, 'usr_'),
    1434              );
    1435              $competencies[] = $objects;
    1436          }
    1437          $records->close();
    1438  
    1439          return array(
    1440              'count' => $DB->count_records_sql($countsql, $params),
    1441              'competencies' => $competencies
    1442          );
    1443      }
    1444  
    1445      /**
    1446       * Add a competency to this course module.
    1447       *
    1448       * @param mixed $cmorid The course module, or id of the course module
    1449       * @param int $competencyid The id of the competency
    1450       * @return bool
    1451       */
    1452      public static function add_competency_to_course_module($cmorid, $competencyid) {
    1453          static::require_enabled();
    1454          $cm = $cmorid;
    1455          if (!is_object($cmorid)) {
    1456              $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
    1457          }
    1458  
    1459          // Check the user have access to the course module.
    1460          self::validate_course_module($cm);
    1461  
    1462          // First we do a permissions check.
    1463          $context = context_module::instance($cm->id);
    1464  
    1465          require_capability('moodle/competency:coursecompetencymanage', $context);
    1466  
    1467          // Check that the competency belongs to the course.
    1468          $exists = course_competency::get_records(array('courseid' => $cm->course, 'competencyid' => $competencyid));
    1469          if (!$exists) {
    1470              throw new coding_exception('Cannot add a competency to a module if it does not belong to the course');
    1471          }
    1472  
    1473          $record = new stdClass();
    1474          $record->cmid = $cm->id;
    1475          $record->competencyid = $competencyid;
    1476  
    1477          $coursemodulecompetency = new course_module_competency();
    1478          $exists = $coursemodulecompetency->get_records(array('cmid' => $cm->id, 'competencyid' => $competencyid));
    1479          if (!$exists) {
    1480              $coursemodulecompetency->from_record($record);
    1481              if ($coursemodulecompetency->create()) {
    1482                  return true;
    1483              }
    1484          }
    1485          return false;
    1486      }
    1487  
    1488      /**
    1489       * Remove a competency from this course module.
    1490       *
    1491       * @param mixed $cmorid The course module, or id of the course module
    1492       * @param int $competencyid The id of the competency
    1493       * @return bool
    1494       */
    1495      public static function remove_competency_from_course_module($cmorid, $competencyid) {
    1496          static::require_enabled();
    1497          $cm = $cmorid;
    1498          if (!is_object($cmorid)) {
    1499              $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
    1500          }
    1501          // Check the user have access to the course module.
    1502          self::validate_course_module($cm);
    1503  
    1504          // First we do a permissions check.
    1505          $context = context_module::instance($cm->id);
    1506  
    1507          require_capability('moodle/competency:coursecompetencymanage', $context);
    1508  
    1509          $record = new stdClass();
    1510          $record->cmid = $cm->id;
    1511          $record->competencyid = $competencyid;
    1512  
    1513          $competency = new competency($competencyid);
    1514          $exists = course_module_competency::get_record(array('cmid' => $cm->id, 'competencyid' => $competencyid));
    1515          if ($exists) {
    1516              return $exists->delete();
    1517          }
    1518          return false;
    1519      }
    1520  
    1521      /**
    1522       * Move the course module competency up or down in the display list.
    1523       *
    1524       * Requires moodle/competency:coursecompetencymanage capability at the course module context.
    1525       *
    1526       * @param mixed $cmorid The course module, or id of the course module
    1527       * @param int $competencyidfrom The id of the competency we are moving.
    1528       * @param int $competencyidto The id of the competency we are moving to.
    1529       * @return boolean
    1530       */
    1531      public static function reorder_course_module_competency($cmorid, $competencyidfrom, $competencyidto) {
    1532          static::require_enabled();
    1533          $cm = $cmorid;
    1534          if (!is_object($cmorid)) {
    1535              $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
    1536          }
    1537          // Check the user have access to the course module.
    1538          self::validate_course_module($cm);
    1539  
    1540          // First we do a permissions check.
    1541          $context = context_module::instance($cm->id);
    1542  
    1543          require_capability('moodle/competency:coursecompetencymanage', $context);
    1544  
    1545          $down = true;
    1546          $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidfrom));
    1547          if (count($matches) == 0) {
    1548               throw new coding_exception('The link does not exist');
    1549          }
    1550  
    1551          $competencyfrom = array_pop($matches);
    1552          $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidto));
    1553          if (count($matches) == 0) {
    1554               throw new coding_exception('The link does not exist');
    1555          }
    1556  
    1557          $competencyto = array_pop($matches);
    1558  
    1559          $all = course_module_competency::get_records(array('cmid' => $cm->id), 'sortorder', 'ASC', 0, 0);
    1560  
    1561          if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
    1562              // We are moving up, so put it before the "to" item.
    1563              $down = false;
    1564          }
    1565  
    1566          foreach ($all as $id => $coursemodulecompetency) {
    1567              $sort = $coursemodulecompetency->get('sortorder');
    1568              if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
    1569                  $coursemodulecompetency->set('sortorder', $coursemodulecompetency->get('sortorder') - 1);
    1570                  $coursemodulecompetency->update();
    1571              } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
    1572                  $coursemodulecompetency->set('sortorder', $coursemodulecompetency->get('sortorder') + 1);
    1573                  $coursemodulecompetency->update();
    1574              }
    1575          }
    1576          $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
    1577          return $competencyfrom->update();
    1578      }
    1579  
    1580      /**
    1581       * Update ruleoutcome value for a course module competency.
    1582       *
    1583       * @param int|course_module_competency $coursemodulecompetencyorid The course_module_competency, or its ID.
    1584       * @param int $ruleoutcome The value of ruleoutcome.
    1585       * @return bool True on success.
    1586       */
    1587      public static function set_course_module_competency_ruleoutcome($coursemodulecompetencyorid, $ruleoutcome) {
    1588          static::require_enabled();
    1589          $coursemodulecompetency = $coursemodulecompetencyorid;
    1590          if (!is_object($coursemodulecompetency)) {
    1591              $coursemodulecompetency = new course_module_competency($coursemodulecompetencyorid);
    1592          }
    1593  
    1594          $cm = get_coursemodule_from_id('', $coursemodulecompetency->get('cmid'), 0, true, MUST_EXIST);
    1595  
    1596          self::validate_course_module($cm);
    1597          $context = context_module::instance($cm->id);
    1598  
    1599          require_capability('moodle/competency:coursecompetencymanage', $context);
    1600  
    1601          $coursemodulecompetency->set('ruleoutcome', $ruleoutcome);
    1602          return $coursemodulecompetency->update();
    1603      }
    1604  
    1605      /**
    1606       * Add a competency to this course.
    1607       *
    1608       * @param int $courseid The id of the course
    1609       * @param int $competencyid The id of the competency
    1610       * @return bool
    1611       */
    1612      public static function add_competency_to_course($courseid, $competencyid) {
    1613          static::require_enabled();
    1614          // Check the user have access to the course.
    1615          self::validate_course($courseid);
    1616  
    1617          // First we do a permissions check.
    1618          $context = context_course::instance($courseid);
    1619  
    1620          require_capability('moodle/competency:coursecompetencymanage', $context);
    1621  
    1622          $record = new stdClass();
    1623          $record->courseid = $courseid;
    1624          $record->competencyid = $competencyid;
    1625  
    1626          $competency = new competency($competencyid);
    1627  
    1628          // Can not add a competency that belong to a hidden framework.
    1629          if ($competency->get_framework()->get('visible') == false) {
    1630              throw new coding_exception('A competency belonging to hidden framework can not be linked to course');
    1631          }
    1632  
    1633          $coursecompetency = new course_competency();
    1634          $exists = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyid));
    1635          if (!$exists) {
    1636              $coursecompetency->from_record($record);
    1637              if ($coursecompetency->create()) {
    1638                  return true;
    1639              }
    1640          }
    1641          return false;
    1642      }
    1643  
    1644      /**
    1645       * Remove a competency from this course.
    1646       *
    1647       * @param int $courseid The id of the course
    1648       * @param int $competencyid The id of the competency
    1649       * @return bool
    1650       */
    1651      public static function remove_competency_from_course($courseid, $competencyid) {
    1652          static::require_enabled();
    1653          // Check the user have access to the course.
    1654          self::validate_course($courseid);
    1655  
    1656          // First we do a permissions check.
    1657          $context = context_course::instance($courseid);
    1658  
    1659          require_capability('moodle/competency:coursecompetencymanage', $context);
    1660  
    1661          $record = new stdClass();
    1662          $record->courseid = $courseid;
    1663          $record->competencyid = $competencyid;
    1664  
    1665          $coursecompetency = new course_competency();
    1666          $exists = course_competency::get_record(array('courseid' => $courseid, 'competencyid' => $competencyid));
    1667          if ($exists) {
    1668              // Delete all course_module_competencies for this competency in this course.
    1669              $cmcs = course_module_competency::get_records_by_competencyid_in_course($competencyid, $courseid);
    1670              foreach ($cmcs as $cmc) {
    1671                  $cmc->delete();
    1672              }
    1673              return $exists->delete();
    1674          }
    1675          return false;
    1676      }
    1677  
    1678      /**
    1679       * Move the course competency up or down in the display list.
    1680       *
    1681       * Requires moodle/competency:coursecompetencymanage capability at the course context.
    1682       *
    1683       * @param int $courseid The course
    1684       * @param int $competencyidfrom The id of the competency we are moving.
    1685       * @param int $competencyidto The id of the competency we are moving to.
    1686       * @return boolean
    1687       */
    1688      public static function reorder_course_competency($courseid, $competencyidfrom, $competencyidto) {
    1689          static::require_enabled();
    1690          // Check the user have access to the course.
    1691          self::validate_course($courseid);
    1692  
    1693          // First we do a permissions check.
    1694          $context = context_course::instance($courseid);
    1695  
    1696          require_capability('moodle/competency:coursecompetencymanage', $context);
    1697  
    1698          $down = true;
    1699          $coursecompetency = new course_competency();
    1700          $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidfrom));
    1701          if (count($matches) == 0) {
    1702               throw new coding_exception('The link does not exist');
    1703          }
    1704  
    1705          $competencyfrom = array_pop($matches);
    1706          $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidto));
    1707          if (count($matches) == 0) {
    1708               throw new coding_exception('The link does not exist');
    1709          }
    1710  
    1711          $competencyto = array_pop($matches);
    1712  
    1713          $all = $coursecompetency->get_records(array('courseid' => $courseid), 'sortorder', 'ASC', 0, 0);
    1714  
    1715          if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
    1716              // We are moving up, so put it before the "to" item.
    1717              $down = false;
    1718          }
    1719  
    1720          foreach ($all as $id => $coursecompetency) {
    1721              $sort = $coursecompetency->get('sortorder');
    1722              if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
    1723                  $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') - 1);
    1724                  $coursecompetency->update();
    1725              } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
    1726                  $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') + 1);
    1727                  $coursecompetency->update();
    1728              }
    1729          }
    1730          $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
    1731          return $competencyfrom->update();
    1732      }
    1733  
    1734      /**
    1735       * Update ruleoutcome value for a course competency.
    1736       *
    1737       * @param int|course_competency $coursecompetencyorid The course_competency, or its ID.
    1738       * @param int $ruleoutcome The value of ruleoutcome.
    1739       * @return bool True on success.
    1740       */
    1741      public static function set_course_competency_ruleoutcome($coursecompetencyorid, $ruleoutcome) {
    1742          static::require_enabled();
    1743          $coursecompetency = $coursecompetencyorid;
    1744          if (!is_object($coursecompetency)) {
    1745              $coursecompetency = new course_competency($coursecompetencyorid);
    1746          }
    1747  
    1748          $courseid = $coursecompetency->get('courseid');
    1749          self::validate_course($courseid);
    1750          $coursecontext = context_course::instance($courseid);
    1751  
    1752          require_capability('moodle/competency:coursecompetencymanage', $coursecontext);
    1753  
    1754          $coursecompetency->set('ruleoutcome', $ruleoutcome);
    1755          return $coursecompetency->update();
    1756      }
    1757  
    1758      /**
    1759       * Create a learning plan template from a record containing all the data for the class.
    1760       *
    1761       * Requires moodle/competency:templatemanage capability.
    1762       *
    1763       * @param stdClass $record Record containing all the data for an instance of the class.
    1764       * @return template
    1765       */
    1766      public static function create_template(stdClass $record) {
    1767          static::require_enabled();
    1768          $template = new template(0, $record);
    1769  
    1770          // First we do a permissions check.
    1771          if (!$template->can_manage()) {
    1772              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
    1773                  'nopermissions', '');
    1774          }
    1775  
    1776          // OK - all set.
    1777          $template = $template->create();
    1778  
    1779          // Trigger a template created event.
    1780          \core\event\competency_template_created::create_from_template($template)->trigger();
    1781  
    1782          return $template;
    1783      }
    1784  
    1785      /**
    1786       * Duplicate a learning plan template.
    1787       *
    1788       * Requires moodle/competency:templatemanage capability at the template context.
    1789       *
    1790       * @param int $id the template id.
    1791       * @return template
    1792       */
    1793      public static function duplicate_template($id) {
    1794          static::require_enabled();
    1795          $template = new template($id);
    1796  
    1797          // First we do a permissions check.
    1798          if (!$template->can_manage()) {
    1799              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
    1800                  'nopermissions', '');
    1801          }
    1802  
    1803          // OK - all set.
    1804          $competencies = template_competency::list_competencies($id, false);
    1805  
    1806          // Adding the suffix copy.
    1807          $template->set('shortname', get_string('duplicateditemname', 'core_competency', $template->get('shortname')));
    1808          $template->set('id', 0);
    1809  
    1810          $duplicatedtemplate = $template->create();
    1811  
    1812          // Associate each competency for the duplicated template.
    1813          foreach ($competencies as $competency) {
    1814              self::add_competency_to_template($duplicatedtemplate->get('id'), $competency->get('id'));
    1815          }
    1816  
    1817          // Trigger a template created event.
    1818          \core\event\competency_template_created::create_from_template($duplicatedtemplate)->trigger();
    1819  
    1820          return $duplicatedtemplate;
    1821      }
    1822  
    1823      /**
    1824       * Delete a learning plan template by id.
    1825       * If the learning plan template has associated cohorts they will be deleted.
    1826       *
    1827       * Requires moodle/competency:templatemanage capability.
    1828       *
    1829       * @param int $id The record to delete.
    1830       * @param boolean $deleteplans True to delete plans associaated to template, false to unlink them.
    1831       * @return boolean
    1832       */
    1833      public static function delete_template($id, $deleteplans = true) {
    1834          global $DB;
    1835          static::require_enabled();
    1836          $template = new template($id);
    1837  
    1838          // First we do a permissions check.
    1839          if (!$template->can_manage()) {
    1840              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
    1841                  'nopermissions', '');
    1842          }
    1843  
    1844          $transaction = $DB->start_delegated_transaction();
    1845          $success = true;
    1846  
    1847          // Check if there are cohorts associated.
    1848          $templatecohorts = template_cohort::get_relations_by_templateid($template->get('id'));
    1849          foreach ($templatecohorts as $templatecohort) {
    1850              $success = $templatecohort->delete();
    1851              if (!$success) {
    1852                  break;
    1853              }
    1854          }
    1855  
    1856          // Still OK, delete or unlink the plans from the template.
    1857          if ($success) {
    1858              $plans = plan::get_records(array('templateid' => $template->get('id')));
    1859              foreach ($plans as $plan) {
    1860                  $success = $deleteplans ? self::delete_plan($plan->get('id')) : self::unlink_plan_from_template($plan);
    1861                  if (!$success) {
    1862                      break;
    1863                  }
    1864              }
    1865          }
    1866  
    1867          // Still OK, delete the template comptencies.
    1868          if ($success) {
    1869              $success = template_competency::delete_by_templateid($template->get('id'));
    1870          }
    1871  
    1872          // OK - all set.
    1873          if ($success) {
    1874              // Create a template deleted event.
    1875              $event = \core\event\competency_template_deleted::create_from_template($template);
    1876  
    1877              $success = $template->delete();
    1878          }
    1879  
    1880          if ($success) {
    1881              // Trigger a template deleted event.
    1882              $event->trigger();
    1883  
    1884              // Commit the transaction.
    1885              $transaction->allow_commit();
    1886          } else {
    1887              $transaction->rollback(new moodle_exception('Error while deleting the template.'));
    1888          }
    1889  
    1890          return $success;
    1891      }
    1892  
    1893      /**
    1894       * Update the details for a learning plan template.
    1895       *
    1896       * Requires moodle/competency:templatemanage capability.
    1897       *
    1898       * @param stdClass $record The new details for the template. Note - must contain an id that points to the template to update.
    1899       * @return boolean
    1900       */
    1901      public static function update_template($record) {
    1902          global $DB;
    1903          static::require_enabled();
    1904          $template = new template($record->id);
    1905  
    1906          // First we do a permissions check.
    1907          if (!$template->can_manage()) {
    1908              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
    1909                  'nopermissions', '');
    1910  
    1911          } else if (isset($record->contextid) && $record->contextid != $template->get('contextid')) {
    1912              // We can never change the context of a template.
    1913              throw new coding_exception('Changing the context of an existing tempalte is forbidden.');
    1914  
    1915          }
    1916  
    1917          $updateplans = false;
    1918          $before = $template->to_record();
    1919  
    1920          $template->from_record($record);
    1921          $after = $template->to_record();
    1922  
    1923          // Should we update the related plans?
    1924          if ($before->duedate != $after->duedate ||
    1925                  $before->shortname != $after->shortname ||
    1926                  $before->description != $after->description ||
    1927                  $before->descriptionformat != $after->descriptionformat) {
    1928              $updateplans = true;
    1929          }
    1930  
    1931          $transaction = $DB->start_delegated_transaction();
    1932          $success = $template->update();
    1933  
    1934          if (!$success) {
    1935              $transaction->rollback(new moodle_exception('Error while updating the template.'));
    1936              return $success;
    1937          }
    1938  
    1939          // Trigger a template updated event.
    1940          \core\event\competency_template_updated::create_from_template($template)->trigger();
    1941  
    1942          if ($updateplans) {
    1943              plan::update_multiple_from_template($template);
    1944          }
    1945  
    1946          $transaction->allow_commit();
    1947  
    1948          return $success;
    1949      }
    1950  
    1951      /**
    1952       * Read a the details for a single learning plan template and return a record.
    1953       *
    1954       * Requires moodle/competency:templateview capability at the system context.
    1955       *
    1956       * @param int $id The id of the template to read.
    1957       * @return template
    1958       */
    1959      public static function read_template($id) {
    1960          static::require_enabled();
    1961          $template = new template($id);
    1962          $context = $template->get_context();
    1963  
    1964          // First we do a permissions check.
    1965          if (!$template->can_read()) {
    1966               throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
    1967                  'nopermissions', '');
    1968          }
    1969  
    1970          // OK - all set.
    1971          return $template;
    1972      }
    1973  
    1974      /**
    1975       * Perform a search based on the provided filters and return a paginated list of records.
    1976       *
    1977       * Requires moodle/competency:templateview capability at the system context.
    1978       *
    1979       * @param string $sort The column to sort on
    1980       * @param string $order ('ASC' or 'DESC')
    1981       * @param int $skip Number of records to skip (pagination)
    1982       * @param int $limit Max of records to return (pagination)
    1983       * @param context $context The parent context of the frameworks.
    1984       * @param string $includes Defines what other contexts to fetch frameworks from.
    1985       *                         Accepted values are:
    1986       *                          - children: All descendants
    1987       *                          - parents: All parents, grand parents, etc...
    1988       *                          - self: Context passed only.
    1989       * @param bool $onlyvisible If should list only visible templates
    1990       * @return array of competency_framework
    1991       */
    1992      public static function list_templates($sort, $order, $skip, $limit, $context, $includes = 'children', $onlyvisible = false) {
    1993          global $DB;
    1994          static::require_enabled();
    1995  
    1996          // Get all the relevant contexts.
    1997          $contexts = self::get_related_contexts($context, $includes,
    1998              array('moodle/competency:templateview', 'moodle/competency:templatemanage'));
    1999  
    2000          // First we do a permissions check.
    2001          if (empty($contexts)) {
    2002               throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
    2003          }
    2004  
    2005          // Make the order by.
    2006          $orderby = '';
    2007          if (!empty($sort)) {
    2008              $orderby = $sort . ' ' . $order;
    2009          }
    2010  
    2011          // OK - all set.
    2012          $template = new template();
    2013          list($insql, $params) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
    2014          $select = "contextid $insql";
    2015  
    2016          if ($onlyvisible) {
    2017              $select .= " AND visible = :visible";
    2018              $params['visible'] = 1;
    2019          }
    2020          return $template->get_records_select($select, $params, $orderby, '*', $skip, $limit);
    2021      }
    2022  
    2023      /**
    2024       * Perform a search based on the provided filters and return how many results there are.
    2025       *
    2026       * Requires moodle/competency:templateview capability at the system context.
    2027       *
    2028       * @param context $context The parent context of the frameworks.
    2029       * @param string $includes Defines what other contexts to fetch frameworks from.
    2030       *                         Accepted values are:
    2031       *                          - children: All descendants
    2032       *                          - parents: All parents, grand parents, etc...
    2033       *                          - self: Context passed only.
    2034       * @return int
    2035       */
    2036      public static function count_templates($context, $includes) {
    2037          global $DB;
    2038          static::require_enabled();
    2039  
    2040          // First we do a permissions check.
    2041          $contexts = self::get_related_contexts($context, $includes,
    2042              array('moodle/competency:templateview', 'moodle/competency:templatemanage'));
    2043  
    2044          if (empty($contexts)) {
    2045               throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
    2046          }
    2047  
    2048          // OK - all set.
    2049          $template = new template();
    2050          list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
    2051          return $template->count_records_select("contextid $insql", $inparams);
    2052      }
    2053  
    2054      /**
    2055       * Count all the templates using a competency.
    2056       *
    2057       * @param int $competencyid The id of the competency to check.
    2058       * @return int
    2059       */
    2060      public static function count_templates_using_competency($competencyid) {
    2061          static::require_enabled();
    2062          // First we do a permissions check.
    2063          $context = context_system::instance();
    2064          $onlyvisible = 1;
    2065  
    2066          $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
    2067          if (!has_any_capability($capabilities, $context)) {
    2068               throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
    2069          }
    2070  
    2071          if (has_capability('moodle/competency:templatemanage', $context)) {
    2072              $onlyvisible = 0;
    2073          }
    2074  
    2075          // OK - all set.
    2076          return template_competency::count_templates($competencyid, $onlyvisible);
    2077      }
    2078  
    2079      /**
    2080       * List all the learning plan templatesd using a competency.
    2081       *
    2082       * @param int $competencyid The id of the competency to check.
    2083       * @return array[stdClass] Array of stdClass containing id and shortname.
    2084       */
    2085      public static function list_templates_using_competency($competencyid) {
    2086          static::require_enabled();
    2087          // First we do a permissions check.
    2088          $context = context_system::instance();
    2089          $onlyvisible = 1;
    2090  
    2091          $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
    2092          if (!has_any_capability($capabilities, $context)) {
    2093               throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
    2094          }
    2095  
    2096          if (has_capability('moodle/competency:templatemanage', $context)) {
    2097              $onlyvisible = 0;
    2098          }
    2099  
    2100          // OK - all set.
    2101          return template_competency::list_templates($competencyid, $onlyvisible);
    2102  
    2103      }
    2104  
    2105      /**
    2106       * Count all the competencies in a learning plan template.
    2107       *
    2108       * @param  template|int $templateorid The template or its ID.
    2109       * @return int
    2110       */
    2111      public static function count_competencies_in_template($templateorid) {
    2112          static::require_enabled();
    2113          // First we do a permissions check.
    2114          $template = $templateorid;
    2115          if (!is_object($template)) {
    2116              $template = new template($template);
    2117          }
    2118  
    2119          if (!$template->can_read()) {
    2120              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
    2121                  'nopermissions', '');
    2122          }
    2123  
    2124          // OK - all set.
    2125          return template_competency::count_competencies($template->get('id'));
    2126      }
    2127  
    2128      /**
    2129       * Count all the competencies in a learning plan template with no linked courses.
    2130       *
    2131       * @param  template|int $templateorid The template or its ID.
    2132       * @return int
    2133       */
    2134      public static function count_competencies_in_template_with_no_courses($templateorid) {
    2135          // First we do a permissions check.
    2136          $template = $templateorid;
    2137          if (!is_object($template)) {
    2138              $template = new template($template);
    2139          }
    2140  
    2141          if (!$template->can_read()) {
    2142              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
    2143                  'nopermissions', '');
    2144          }
    2145  
    2146          // OK - all set.
    2147          return template_competency::count_competencies_with_no_courses($template->get('id'));
    2148      }
    2149  
    2150      /**
    2151       * List all the competencies in a template.
    2152       *
    2153       * @param  template|int $templateorid The template or its ID.
    2154       * @return array of competencies
    2155       */
    2156      public static function list_competencies_in_template($templateorid) {
    2157          static::require_enabled();
    2158          // First we do a permissions check.
    2159          $template = $templateorid;
    2160          if (!is_object($template)) {
    2161              $template = new template($template);
    2162          }
    2163  
    2164          if (!$template->can_read()) {
    2165              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
    2166                  'nopermissions', '');
    2167          }
    2168  
    2169          // OK - all set.
    2170          return template_competency::list_competencies($template->get('id'));
    2171      }
    2172  
    2173      /**
    2174       * Add a competency to this template.
    2175       *
    2176       * @param int $templateid The id of the template
    2177       * @param int $competencyid The id of the competency
    2178       * @return bool
    2179       */
    2180      public static function add_competency_to_template($templateid, $competencyid) {
    2181          static::require_enabled();
    2182          // First we do a permissions check.
    2183          $template = new template($templateid);
    2184          if (!$template->can_manage()) {
    2185              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
    2186                  'nopermissions', '');
    2187          }
    2188  
    2189          $record = new stdClass();
    2190          $record->templateid = $templateid;
    2191          $record->competencyid = $competencyid;
    2192  
    2193          $competency = new competency($competencyid);
    2194  
    2195          // Can not add a competency that belong to a hidden framework.
    2196          if ($competency->get_framework()->get('visible') == false) {
    2197              throw new coding_exception('A competency belonging to hidden framework can not be added');
    2198          }
    2199  
    2200          $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
    2201          if (!$exists) {
    2202              $templatecompetency = new template_competency(0, $record);
    2203              $templatecompetency->create();
    2204              return true;
    2205          }
    2206          return false;
    2207      }
    2208  
    2209      /**
    2210       * Remove a competency from this template.
    2211       *
    2212       * @param int $templateid The id of the template
    2213       * @param int $competencyid The id of the competency
    2214       * @return bool
    2215       */
    2216      public static function remove_competency_from_template($templateid, $competencyid) {
    2217          static::require_enabled();
    2218          // First we do a permissions check.
    2219          $template = new template($templateid);
    2220          if (!$template->can_manage()) {
    2221              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
    2222                  'nopermissions', '');
    2223          }
    2224  
    2225          $record = new stdClass();
    2226          $record->templateid = $templateid;
    2227          $record->competencyid = $competencyid;
    2228  
    2229          $competency = new competency($competencyid);
    2230  
    2231          $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
    2232          if ($exists) {
    2233              $link = array_pop($exists);
    2234              return $link->delete();
    2235          }
    2236          return false;
    2237      }
    2238  
    2239      /**
    2240       * Move the template competency up or down in the display list.
    2241       *
    2242       * Requires moodle/competency:templatemanage capability at the system context.
    2243       *
    2244       * @param int $templateid The template id
    2245       * @param int $competencyidfrom The id of the competency we are moving.
    2246       * @param int $competencyidto The id of the competency we are moving to.
    2247       * @return boolean
    2248       */
    2249      public static function reorder_template_competency($templateid, $competencyidfrom, $competencyidto) {
    2250          static::require_enabled();
    2251          $template = new template($templateid);
    2252  
    2253          // First we do a permissions check.
    2254          if (!$template->can_manage()) {
    2255              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
    2256                  'nopermissions', '');
    2257          }
    2258  
    2259          $down = true;
    2260          $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidfrom));
    2261          if (count($matches) == 0) {
    2262              throw new coding_exception('The link does not exist');
    2263          }
    2264  
    2265          $competencyfrom = array_pop($matches);
    2266          $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidto));
    2267          if (count($matches) == 0) {
    2268              throw new coding_exception('The link does not exist');
    2269          }
    2270  
    2271          $competencyto = array_pop($matches);
    2272  
    2273          $all = template_competency::get_records(array('templateid' => $templateid), 'sortorder', 'ASC', 0, 0);
    2274  
    2275          if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
    2276              // We are moving up, so put it before the "to" item.
    2277              $down = false;
    2278          }
    2279  
    2280          foreach ($all as $id => $templatecompetency) {
    2281              $sort = $templatecompetency->get('sortorder');
    2282              if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
    2283                  $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') - 1);
    2284                  $templatecompetency->update();
    2285              } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
    2286                  $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') + 1);
    2287                  $templatecompetency->update();
    2288              }
    2289          }
    2290          $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
    2291          return $competencyfrom->update();
    2292      }
    2293  
    2294      /**
    2295       * Create a relation between a template and a cohort.
    2296       *
    2297       * This silently ignores when the relation already existed.
    2298       *
    2299       * @param  template|int $templateorid The template or its ID.
    2300       * @param  stdClass|int $cohortorid   The cohort ot its ID.
    2301       * @return template_cohort
    2302       */
    2303      public static function create_template_cohort($templateorid, $cohortorid) {
    2304          global $DB;
    2305          static::require_enabled();
    2306  
    2307          $template = $templateorid;
    2308          if (!is_object($template)) {
    2309              $template = new template($template);
    2310          }
    2311          require_capability('moodle/competency:templatemanage', $template->get_context());
    2312  
    2313          $cohort = $cohortorid;
    2314          if (!is_object($cohort)) {
    2315              $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
    2316          }
    2317  
    2318          // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
    2319          $cohortcontext = context::instance_by_id($cohort->contextid);
    2320          if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
    2321              throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
    2322          }
    2323  
    2324          $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id);
    2325          if (!$tplcohort->get('id')) {
    2326              $tplcohort->create();
    2327          }
    2328  
    2329          return $tplcohort;
    2330      }
    2331  
    2332      /**
    2333       * Remove a relation between a template and a cohort.
    2334       *
    2335       * @param  template|int $templateorid The template or its ID.
    2336       * @param  stdClass|int $cohortorid   The cohort ot its ID.
    2337       * @return boolean True on success or when the relation did not exist.
    2338       */
    2339      public static function delete_template_cohort($templateorid, $cohortorid) {
    2340          global $DB;
    2341          static::require_enabled();
    2342  
    2343          $template = $templateorid;
    2344          if (!is_object($template)) {
    2345              $template = new template($template);
    2346          }
    2347          require_capability('moodle/competency:templatemanage', $template->get_context());
    2348  
    2349          $cohort = $cohortorid;
    2350          if (!is_object($cohort)) {
    2351              $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
    2352          }
    2353  
    2354          $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id);
    2355          if (!$tplcohort->get('id')) {
    2356              return true;
    2357          }
    2358  
    2359          return $tplcohort->delete();
    2360      }
    2361  
    2362      /**
    2363       * Lists user plans.
    2364       *
    2365       * @param int $userid
    2366       * @return \core_competency\plan[]
    2367       */
    2368      public static function list_user_plans($userid) {
    2369          global $DB, $USER;
    2370          static::require_enabled();
    2371          $select = 'userid = :userid';
    2372          $params = array('userid' => $userid);
    2373          $context = context_user::instance($userid);
    2374  
    2375          // Check that we can read something here.
    2376          if (!plan::can_read_user($userid) && !plan::can_read_user_draft($userid)) {
    2377              throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
    2378          }
    2379  
    2380          // The user cannot view the drafts.
    2381          if (!plan::can_read_user_draft($userid)) {
    2382              list($insql, $inparams) = $DB->get_in_or_equal(plan::get_draft_statuses(), SQL_PARAMS_NAMED, 'param', false);
    2383              $select .= " AND status $insql";
    2384              $params += $inparams;
    2385          }
    2386          // The user cannot view the non-drafts.
    2387          if (!plan::can_read_user($userid)) {
    2388              list($insql, $inparams) = $DB->get_in_or_equal(array(plan::STATUS_ACTIVE, plan::STATUS_COMPLETE),
    2389                  SQL_PARAMS_NAMED, 'param', false);
    2390              $select .= " AND status $insql";
    2391              $params += $inparams;
    2392          }
    2393  
    2394          return plan::get_records_select($select, $params, 'name ASC');
    2395      }
    2396  
    2397      /**
    2398       * List the plans to review.
    2399       *
    2400       * The method returns values in this format:
    2401       *
    2402       * array(
    2403       *     'plans' => array(
    2404       *         (stdClass)(
    2405       *             'plan' => (plan),
    2406       *             'template' => (template),
    2407       *             'owner' => (stdClass)
    2408       *         )
    2409       *     ),
    2410       *     'count' => (int)
    2411       * )
    2412       *
    2413       * @param int $skip The number of records to skip.
    2414       * @param int $limit The number of results to return.
    2415       * @param int $userid The user we're getting the plans to review for.
    2416       * @return array Containing the keys 'count', and 'plans'. The 'plans' key contains an object
    2417       *               which contains 'plan', 'template' and 'owner'.
    2418       */
    2419      public static function list_plans_to_review($skip = 0, $limit = 100, $userid = null) {
    2420          global $DB, $USER;
    2421          static::require_enabled();
    2422  
    2423          if ($userid === null) {
    2424              $userid = $USER->id;
    2425          }
    2426  
    2427          $planfields = plan::get_sql_fields('p', 'plan_');
    2428          $tplfields = template::get_sql_fields('t', 'tpl_');
    2429          $usercols = array('id') + get_user_fieldnames();
    2430          $userfields = array();
    2431          foreach ($usercols as $field) {
    2432              $userfields[] = "u." . $field . " AS usr_" . $field;
    2433          }
    2434          $userfields = implode(',', $userfields);
    2435  
    2436          $select = "SELECT $planfields, $tplfields, $userfields";
    2437          $countselect = "SELECT COUNT('x')";
    2438  
    2439          $sql = "  FROM {" . plan::TABLE . "} p
    2440                    JOIN {user} u
    2441                      ON u.id = p.userid
    2442               LEFT JOIN {" . template::TABLE . "} t
    2443                      ON t.id = p.templateid
    2444                   WHERE (p.status = :waitingforreview
    2445                      OR (p.status = :inreview AND p.reviewerid = :reviewerid))
    2446                     AND p.userid != :userid";
    2447  
    2448          $params = array(
    2449              'waitingforreview' => plan::STATUS_WAITING_FOR_REVIEW,
    2450              'inreview' => plan::STATUS_IN_REVIEW,
    2451              'reviewerid' => $userid,
    2452              'userid' => $userid
    2453          );
    2454  
    2455          // Primary check to avoid the hard work of getting the users in which the user has permission.
    2456          $count = $DB->count_records_sql($countselect . $sql, $params);
    2457          if ($count < 1) {
    2458              return array('count' => 0, 'plans' => array());
    2459          }
    2460  
    2461          // TODO MDL-52243 Use core function.
    2462          list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql('moodle/competency:planreview',
    2463              $userid, SQL_PARAMS_NAMED);
    2464          $sql .= " AND p.userid $insql";
    2465          $params += $inparams;
    2466  
    2467          // Order by ID just to have some ordering in place.
    2468          $ordersql = " ORDER BY p.id ASC";
    2469  
    2470          $plans = array();
    2471          $records = $DB->get_recordset_sql($select . $sql . $ordersql, $params, $skip, $limit);
    2472          foreach ($records as $record) {
    2473              $plan = new plan(0, plan::extract_record($record, 'plan_'));
    2474              $template = null;
    2475  
    2476              if ($plan->is_based_on_template()) {
    2477                  $template = new template(0, template::extract_record($record, 'tpl_'));
    2478              }
    2479  
    2480              $plans[] = (object) array(
    2481                  'plan' => $plan,
    2482                  'template' => $template,
    2483                  'owner' => persistent::extract_record($record, 'usr_'),
    2484              );
    2485          }
    2486          $records->close();
    2487  
    2488          return array(
    2489              'count' => $DB->count_records_sql($countselect . $sql, $params),
    2490              'plans' => $plans
    2491          );
    2492      }
    2493  
    2494      /**
    2495       * Creates a learning plan based on the provided data.
    2496       *
    2497       * @param stdClass $record
    2498       * @return \core_competency\plan
    2499       */
    2500      public static function create_plan(stdClass $record) {
    2501          global $USER;
    2502          static::require_enabled();
    2503          $plan = new plan(0, $record);
    2504  
    2505          if ($plan->is_based_on_template()) {
    2506              throw new coding_exception('To create a plan from a template use api::create_plan_from_template().');
    2507          } else if ($plan->get('status') == plan::STATUS_COMPLETE) {
    2508              throw new coding_exception('A plan cannot be created as complete.');
    2509          }
    2510  
    2511          if (!$plan->can_manage()) {
    2512              $context = context_user::instance($plan->get('userid'));
    2513              throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
    2514          }
    2515  
    2516          $plan->create();
    2517  
    2518          // Trigger created event.
    2519          \core\event\competency_plan_created::create_from_plan($plan)->trigger();
    2520          return $plan;
    2521      }
    2522  
    2523      /**
    2524       * Create a learning plan from a template.
    2525       *
    2526       * @param  mixed $templateorid The template object or ID.
    2527       * @param  int $userid
    2528       * @return false|\core_competency\plan Returns false when the plan already exists.
    2529       */
    2530      public static function create_plan_from_template($templateorid, $userid) {
    2531          static::require_enabled();
    2532          $template = $templateorid;
    2533          if (!is_object($template)) {
    2534              $template = new template($template);
    2535          }
    2536  
    2537          // The user must be able to view the template to use it as a base for a plan.
    2538          if (!$template->can_read()) {
    2539              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
    2540                  'nopermissions', '');
    2541          }
    2542          // Can not create plan from a hidden template.
    2543          if ($template->get('visible') == false) {
    2544              throw new coding_exception('A plan can not be created from a hidden template');
    2545          }
    2546  
    2547          // Convert the template to a plan.
    2548          $record = $template->to_record();
    2549          $record->templateid = $record->id;
    2550          $record->userid = $userid;
    2551          $record->name = $record->shortname;
    2552          $record->status = plan::STATUS_ACTIVE;
    2553  
    2554          unset($record->id);
    2555          unset($record->timecreated);
    2556          unset($record->timemodified);
    2557          unset($record->usermodified);
    2558  
    2559          // Remove extra keys.
    2560          $properties = plan::properties_definition();
    2561          foreach ($record as $key => $value) {
    2562              if (!array_key_exists($key, $properties)) {
    2563                  unset($record->$key);
    2564              }
    2565          }
    2566  
    2567          $plan = new plan(0, $record);
    2568          if (!$plan->can_manage()) {
    2569              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage',
    2570                  'nopermissions', '');
    2571          }
    2572  
    2573          // We first apply the permission checks as we wouldn't want to leak information by returning early that
    2574          // the plan already exists.
    2575          if (plan::record_exists_select('templateid = :templateid AND userid = :userid', array(
    2576                  'templateid' => $template->get('id'), 'userid' => $userid))) {
    2577              return false;
    2578          }
    2579  
    2580          $plan->create();
    2581  
    2582          // Trigger created event.
    2583          \core\event\competency_plan_created::create_from_plan($plan)->trigger();
    2584          return $plan;
    2585      }
    2586  
    2587      /**
    2588       * Create learning plans from a template and cohort.
    2589       *
    2590       * @param  mixed $templateorid The template object or ID.
    2591       * @param  int $cohortid The cohort ID.
    2592       * @param  bool $recreateunlinked When true the plans that were unlinked from this template will be re-created.
    2593       * @return int The number of plans created.
    2594       */
    2595      public static function create_plans_from_template_cohort($templateorid, $cohortid, $recreateunlinked = false) {
    2596          global $DB, $CFG;
    2597          static::require_enabled();
    2598          require_once($CFG->dirroot . '/cohort/lib.php');
    2599  
    2600          $template = $templateorid;
    2601          if (!is_object($template)) {
    2602              $template = new template($template);
    2603          }
    2604  
    2605          // The user must be able to view the template to use it as a base for a plan.
    2606          if (!$template->can_read()) {
    2607              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
    2608                  'nopermissions', '');
    2609          }
    2610  
    2611          // Can not create plan from a hidden template.
    2612          if ($template->get('visible') == false) {
    2613              throw new coding_exception('A plan can not be created from a hidden template');
    2614          }
    2615  
    2616          // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
    2617          $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
    2618          $cohortcontext = context::instance_by_id($cohort->contextid);
    2619          if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
    2620              throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
    2621          }
    2622  
    2623          // Convert the template to a plan.
    2624          $recordbase = $template->to_record();
    2625          $recordbase->templateid = $recordbase->id;
    2626          $recordbase->name = $recordbase->shortname;
    2627          $recordbase->status = plan::STATUS_ACTIVE;
    2628  
    2629          unset($recordbase->id);
    2630          unset($recordbase->timecreated);
    2631          unset($recordbase->timemodified);
    2632          unset($recordbase->usermodified);
    2633  
    2634          // Remove extra keys.
    2635          $properties = plan::properties_definition();
    2636          foreach ($recordbase as $key => $value) {
    2637              if (!array_key_exists($key, $properties)) {
    2638                  unset($recordbase->$key);
    2639              }
    2640          }
    2641  
    2642          // Create the plans.
    2643          $created = 0;
    2644          $userids = template_cohort::get_missing_plans($template->get('id'), $cohortid, $recreateunlinked);
    2645          foreach ($userids as $userid) {
    2646              $record = (object) (array) $recordbase;
    2647              $record->userid = $userid;
    2648  
    2649              $plan = new plan(0, $record);
    2650              if (!$plan->can_manage()) {
    2651                  // Silently skip members where permissions are lacking.
    2652                  continue;
    2653              }
    2654  
    2655              $plan->create();
    2656              // Trigger created event.
    2657              \core\event\competency_plan_created::create_from_plan($plan)->trigger();
    2658              $created++;
    2659          }
    2660  
    2661          return $created;
    2662      }
    2663  
    2664      /**
    2665       * Unlink a plan from its template.
    2666       *
    2667       * @param  \core_competency\plan|int $planorid The plan or its ID.
    2668       * @return bool
    2669       */
    2670      public static function unlink_plan_from_template($planorid) {
    2671          global $DB;
    2672          static::require_enabled();
    2673  
    2674          $plan = $planorid;
    2675          if (!is_object($planorid)) {
    2676              $plan = new plan($planorid);
    2677          }
    2678  
    2679          // The user must be allowed to manage the plans of the user, nothing about the template.
    2680          if (!$plan->can_manage()) {
    2681              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
    2682          }
    2683  
    2684          // Only plan with status DRAFT or ACTIVE can be unliked..
    2685          if ($plan->get('status') == plan::STATUS_COMPLETE) {
    2686              throw new coding_exception('Only draft or active plan can be unliked from a template');
    2687          }
    2688  
    2689          // Early exit, it's already done...
    2690          if (!$plan->is_based_on_template()) {
    2691              return true;
    2692          }
    2693  
    2694          // Fetch the template.
    2695          $template = new template($plan->get('templateid'));
    2696  
    2697          // Now, proceed by copying all competencies to the plan, then update the plan.
    2698          $transaction = $DB->start_delegated_transaction();
    2699          $competencies = template_competency::list_competencies($template->get('id'), false);
    2700          $i = 0;
    2701          foreach ($competencies as $competency) {
    2702              $record = (object) array(
    2703                  'planid' => $plan->get('id'),
    2704                  'competencyid' => $competency->get('id'),
    2705                  'sortorder' => $i++
    2706              );
    2707              $pc = new plan_competency(null, $record);
    2708              $pc->create();
    2709          }
    2710          $plan->set('origtemplateid', $template->get('id'));
    2711          $plan->set('templateid', null);
    2712          $success = $plan->update();
    2713          $transaction->allow_commit();
    2714  
    2715          // Trigger unlinked event.
    2716          \core\event\competency_plan_unlinked::create_from_plan($plan)->trigger();
    2717  
    2718          return $success;
    2719      }
    2720  
    2721      /**
    2722       * Updates a plan.
    2723       *
    2724       * @param stdClass $record
    2725       * @return \core_competency\plan
    2726       */
    2727      public static function update_plan(stdClass $record) {
    2728          static::require_enabled();
    2729  
    2730          $plan = new plan($record->id);
    2731  
    2732          // Validate that the plan as it is can be managed.
    2733          if (!$plan->can_manage()) {
    2734              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
    2735  
    2736          } else if ($plan->get('status') == plan::STATUS_COMPLETE) {
    2737              // A completed plan cannot be edited.
    2738              throw new coding_exception('Completed plan cannot be edited.');
    2739  
    2740          } else if ($plan->is_based_on_template()) {
    2741              // Prevent a plan based on a template to be edited.
    2742              throw new coding_exception('Cannot update a plan that is based on a template.');
    2743  
    2744          } else if (isset($record->templateid) && $plan->get('templateid') != $record->templateid) {
    2745              // Prevent a plan to be based on a template.
    2746              throw new coding_exception('Cannot base a plan on a template.');
    2747  
    2748          } else if (isset($record->userid) && $plan->get('userid') != $record->userid) {
    2749              // Prevent change of ownership as the capabilities are checked against that.
    2750              throw new coding_exception('A plan cannot be transfered to another user');
    2751  
    2752          } else if (isset($record->status) && $plan->get('status') != $record->status) {
    2753              // Prevent change of status.
    2754              throw new coding_exception('To change the status of a plan use the appropriate methods.');
    2755  
    2756          }
    2757  
    2758          $plan->from_record($record);
    2759          $plan->update();
    2760  
    2761          // Trigger updated event.
    2762          \core\event\competency_plan_updated::create_from_plan($plan)->trigger();
    2763  
    2764          return $plan;
    2765      }
    2766  
    2767      /**
    2768       * Returns a plan data.
    2769       *
    2770       * @param int $id
    2771       * @return \core_competency\plan
    2772       */
    2773      public static function read_plan($id) {
    2774          static::require_enabled();
    2775          $plan = new plan($id);
    2776  
    2777          if (!$plan->can_read()) {
    2778              $context = context_user::instance($plan->get('userid'));
    2779              throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
    2780          }
    2781  
    2782          return $plan;
    2783      }
    2784  
    2785      /**
    2786       * Plan event viewed.
    2787       *
    2788       * @param mixed $planorid The id or the plan.
    2789       * @return boolean
    2790       */
    2791      public static function plan_viewed($planorid) {
    2792          static::require_enabled();
    2793          $plan = $planorid;
    2794          if (!is_object($plan)) {
    2795              $plan = new plan($plan);
    2796          }
    2797  
    2798          // First we do a permissions check.
    2799          if (!$plan->can_read()) {
    2800              $context = context_user::instance($plan->get('userid'));
    2801              throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
    2802          }
    2803  
    2804          // Trigger a template viewed event.
    2805          \core\event\competency_plan_viewed::create_from_plan($plan)->trigger();
    2806  
    2807          return true;
    2808      }
    2809  
    2810      /**
    2811       * Deletes a plan.
    2812       *
    2813       * Plans based on a template can be removed just like any other one.
    2814       *
    2815       * @param int $id
    2816       * @return bool Success?
    2817       */
    2818      public static function delete_plan($id) {
    2819          global $DB;
    2820          static::require_enabled();
    2821  
    2822          $plan = new plan($id);
    2823  
    2824          if (!$plan->can_manage()) {
    2825              $context = context_user::instance($plan->get('userid'));
    2826              throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
    2827          }
    2828  
    2829          // Wrap the suppression in a DB transaction.
    2830          $transaction = $DB->start_delegated_transaction();
    2831  
    2832          // Delete plan competencies.
    2833          $plancomps = plan_competency::get_records(array('planid' => $plan->get('id')));
    2834          foreach ($plancomps as $plancomp) {
    2835              $plancomp->delete();
    2836          }
    2837  
    2838          // Delete archive user competencies if the status of the plan is complete.
    2839          if ($plan->get('status') == plan::STATUS_COMPLETE) {
    2840              self::remove_archived_user_competencies_in_plan($plan);
    2841          }
    2842          $event = \core\event\competency_plan_deleted::create_from_plan($plan);
    2843          $success = $plan->delete();
    2844  
    2845          $transaction->allow_commit();
    2846  
    2847          // Trigger deleted event.
    2848          $event->trigger();
    2849  
    2850          return $success;
    2851      }
    2852  
    2853      /**
    2854       * Cancel the review of a plan.
    2855       *
    2856       * @param int|plan $planorid The plan, or its ID.
    2857       * @return bool
    2858       */
    2859      public static function plan_cancel_review_request($planorid) {
    2860          static::require_enabled();
    2861          $plan = $planorid;
    2862          if (!is_object($plan)) {
    2863              $plan = new plan($plan);
    2864          }
    2865  
    2866          // We need to be able to view the plan at least.
    2867          if (!$plan->can_read()) {
    2868              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
    2869          }
    2870  
    2871          if ($plan->is_based_on_template()) {
    2872              throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
    2873          } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) {
    2874              throw new coding_exception('The plan review cannot be cancelled at this stage.');
    2875          } else if (!$plan->can_request_review()) {
    2876              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
    2877          }
    2878  
    2879          $plan->set('status', plan::STATUS_DRAFT);
    2880          $result = $plan->update();
    2881  
    2882          // Trigger review request cancelled event.
    2883          \core\event\competency_plan_review_request_cancelled::create_from_plan($plan)->trigger();
    2884  
    2885          return $result;
    2886      }
    2887  
    2888      /**
    2889       * Request the review of a plan.
    2890       *
    2891       * @param int|plan $planorid The plan, or its ID.
    2892       * @return bool
    2893       */
    2894      public static function plan_request_review($planorid) {
    2895          static::require_enabled();
    2896          $plan = $planorid;
    2897          if (!is_object($plan)) {
    2898              $plan = new plan($plan);
    2899          }
    2900  
    2901          // We need to be able to view the plan at least.
    2902          if (!$plan->can_read()) {
    2903              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
    2904          }
    2905  
    2906          if ($plan->is_based_on_template()) {
    2907              throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
    2908          } else if ($plan->get('status') != plan::STATUS_DRAFT) {
    2909              throw new coding_exception('The plan cannot be sent for review at this stage.');
    2910          } else if (!$plan->can_request_review()) {
    2911              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
    2912          }
    2913  
    2914          $plan->set('status', plan::STATUS_WAITING_FOR_REVIEW);
    2915          $result = $plan->update();
    2916  
    2917          // Trigger review requested event.
    2918          \core\event\competency_plan_review_requested::create_from_plan($plan)->trigger();
    2919  
    2920          return $result;
    2921      }
    2922  
    2923      /**
    2924       * Start the review of a plan.
    2925       *
    2926       * @param int|plan $planorid The plan, or its ID.
    2927       * @return bool
    2928       */
    2929      public static function plan_start_review($planorid) {
    2930          global $USER;
    2931          static::require_enabled();
    2932          $plan = $planorid;
    2933          if (!is_object($plan)) {
    2934              $plan = new plan($plan);
    2935          }
    2936  
    2937          // We need to be able to view the plan at least.
    2938          if (!$plan->can_read()) {
    2939              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
    2940          }
    2941  
    2942          if ($plan->is_based_on_template()) {
    2943              throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
    2944          } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) {
    2945              throw new coding_exception('The plan review cannot be started at this stage.');
    2946          } else if (!$plan->can_review()) {
    2947              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
    2948          }
    2949  
    2950          $plan->set('status', plan::STATUS_IN_REVIEW);
    2951          $plan->set('reviewerid', $USER->id);
    2952          $result = $plan->update();
    2953  
    2954          // Trigger review started event.
    2955          \core\event\competency_plan_review_started::create_from_plan($plan)->trigger();
    2956  
    2957          return $result;
    2958      }
    2959  
    2960      /**
    2961       * Stop reviewing a plan.
    2962       *
    2963       * @param  int|plan $planorid The plan, or its ID.
    2964       * @return bool
    2965       */
    2966      public static function plan_stop_review($planorid) {
    2967          static::require_enabled();
    2968          $plan = $planorid;
    2969          if (!is_object($plan)) {
    2970              $plan = new plan($plan);
    2971          }
    2972  
    2973          // We need to be able to view the plan at least.
    2974          if (!$plan->can_read()) {
    2975              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
    2976          }
    2977  
    2978          if ($plan->is_based_on_template()) {
    2979              throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
    2980          } else if ($plan->get('status') != plan::STATUS_IN_REVIEW) {
    2981              throw new coding_exception('The plan review cannot be stopped at this stage.');
    2982          } else if (!$plan->can_review()) {
    2983              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
    2984          }
    2985  
    2986          $plan->set('status', plan::STATUS_DRAFT);
    2987          $plan->set('reviewerid', null);
    2988          $result = $plan->update();
    2989  
    2990          // Trigger review stopped event.
    2991          \core\event\competency_plan_review_stopped::create_from_plan($plan)->trigger();
    2992  
    2993          return $result;
    2994      }
    2995  
    2996      /**
    2997       * Approve a plan.
    2998       *
    2999       * This means making the plan active.
    3000       *
    3001       * @param  int|plan $planorid The plan, or its ID.
    3002       * @return bool
    3003       */
    3004      public static function approve_plan($planorid) {
    3005          static::require_enabled();
    3006          $plan = $planorid;
    3007          if (!is_object($plan)) {
    3008              $plan = new plan($plan);
    3009          }
    3010  
    3011          // We need to be able to view the plan at least.
    3012          if (!$plan->can_read()) {
    3013              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
    3014          }
    3015  
    3016          // We can approve a plan that is either a draft, in review, or waiting for review.
    3017          if ($plan->is_based_on_template()) {
    3018              throw new coding_exception('Template plans are already approved.');   // This should never happen.
    3019          } else if (!$plan->is_draft()) {
    3020              throw new coding_exception('The plan cannot be approved at this stage.');
    3021          } else if (!$plan->can_review()) {
    3022              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
    3023          }
    3024  
    3025          $plan->set('status', plan::STATUS_ACTIVE);
    3026          $plan->set('reviewerid', null);
    3027          $result = $plan->update();
    3028  
    3029          // Trigger approved event.
    3030          \core\event\competency_plan_approved::create_from_plan($plan)->trigger();
    3031  
    3032          return $result;
    3033      }
    3034  
    3035      /**
    3036       * Unapprove a plan.
    3037       *
    3038       * This means making the plan draft.
    3039       *
    3040       * @param  int|plan $planorid The plan, or its ID.
    3041       * @return bool
    3042       */
    3043      public static function unapprove_plan($planorid) {
    3044          static::require_enabled();
    3045          $plan = $planorid;
    3046          if (!is_object($plan)) {
    3047              $plan = new plan($plan);
    3048          }
    3049  
    3050          // We need to be able to view the plan at least.
    3051          if (!$plan->can_read()) {
    3052              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
    3053          }
    3054  
    3055          if ($plan->is_based_on_template()) {
    3056              throw new coding_exception('Template plans are always approved.');   // This should never happen.
    3057          } else if ($plan->get('status') != plan::STATUS_ACTIVE) {
    3058              throw new coding_exception('The plan cannot be sent back to draft at this stage.');
    3059          } else if (!$plan->can_review()) {
    3060              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
    3061          }
    3062  
    3063          $plan->set('status', plan::STATUS_DRAFT);
    3064          $result = $plan->update();
    3065  
    3066          // Trigger unapproved event.
    3067          \core\event\competency_plan_unapproved::create_from_plan($plan)->trigger();
    3068  
    3069          return $result;
    3070      }
    3071  
    3072      /**
    3073       * Complete a plan.
    3074       *
    3075       * @param int|plan $planorid The plan, or its ID.
    3076       * @return bool
    3077       */
    3078      public static function complete_plan($planorid) {
    3079          global $DB;
    3080          static::require_enabled();
    3081  
    3082          $plan = $planorid;
    3083          if (!is_object($planorid)) {
    3084              $plan = new plan($planorid);
    3085          }
    3086  
    3087          // Validate that the plan can be managed.
    3088          if (!$plan->can_manage()) {
    3089              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
    3090          }
    3091  
    3092          // Check if the plan was already completed.
    3093          if ($plan->get('status') == plan::STATUS_COMPLETE) {
    3094              throw new coding_exception('The plan is already completed.');
    3095          }
    3096  
    3097          $originalstatus = $plan->get('status');
    3098          $plan->set('status', plan::STATUS_COMPLETE);
    3099  
    3100          // The user should also be able to manage the plan when it's completed.
    3101          if (!$plan->can_manage()) {
    3102              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
    3103          }
    3104  
    3105          // Put back original status because archive needs it to extract competencies from the right table.
    3106          $plan->set('status', $originalstatus);
    3107  
    3108          // Do the things.
    3109          $transaction = $DB->start_delegated_transaction();
    3110          self::archive_user_competencies_in_plan($plan);
    3111          $plan->set('status', plan::STATUS_COMPLETE);
    3112          $success = $plan->update();
    3113  
    3114          if (!$success) {
    3115              $transaction->rollback(new moodle_exception('The plan could not be updated.'));
    3116              return $success;
    3117          }
    3118  
    3119          $transaction->allow_commit();
    3120  
    3121          // Trigger updated event.
    3122          \core\event\competency_plan_completed::create_from_plan($plan)->trigger();
    3123  
    3124          return $success;
    3125      }
    3126  
    3127      /**
    3128       * Reopen a plan.
    3129       *
    3130       * @param int|plan $planorid The plan, or its ID.
    3131       * @return bool
    3132       */
    3133      public static function reopen_plan($planorid) {
    3134          global $DB;
    3135          static::require_enabled();
    3136  
    3137          $plan = $planorid;
    3138          if (!is_object($planorid)) {
    3139              $plan = new plan($planorid);
    3140          }
    3141  
    3142          // Validate that the plan as it is can be managed.
    3143          if (!$plan->can_manage()) {
    3144              $context = context_user::instance($plan->get('userid'));
    3145              throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
    3146          }
    3147  
    3148          $beforestatus = $plan->get('status');
    3149          $plan->set('status', plan::STATUS_ACTIVE);
    3150  
    3151          // Validate if status can be changed.
    3152          if (!$plan->can_manage()) {
    3153              $context = context_user::instance($plan->get('userid'));
    3154              throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
    3155          }
    3156  
    3157          // Wrap the updates in a DB transaction.
    3158          $transaction = $DB->start_delegated_transaction();
    3159  
    3160          // Delete archived user competencies if the status of the plan is changed from complete to another status.
    3161          $mustremovearchivedcompetencies = ($beforestatus == plan::STATUS_COMPLETE && $plan->get('status') != plan::STATUS_COMPLETE);
    3162          if ($mustremovearchivedcompetencies) {
    3163              self::remove_archived_user_competencies_in_plan($plan);
    3164          }
    3165  
    3166          // If duedate less than or equal to duedate_threshold unset it.
    3167          if ($plan->get('duedate') <= time() + plan::DUEDATE_THRESHOLD) {
    3168              $plan->set('duedate', 0);
    3169          }
    3170  
    3171          $success = $plan->update();
    3172  
    3173          if (!$success) {
    3174              $transaction->rollback(new moodle_exception('The plan could not be updated.'));
    3175              return $success;
    3176          }
    3177  
    3178          $transaction->allow_commit();
    3179  
    3180          // Trigger reopened event.
    3181          \core\event\competency_plan_reopened::create_from_plan($plan)->trigger();
    3182  
    3183          return $success;
    3184      }
    3185  
    3186      /**
    3187       * Get a single competency from the user plan.
    3188       *
    3189       * @param  int $planorid The plan, or its ID.
    3190       * @param  int $competencyid The competency id.
    3191       * @return (object) array(
    3192       *                      'competency' => \core_competency\competency,
    3193       *                      'usercompetency' => \core_competency\user_competency
    3194       *                      'usercompetencyplan' => \core_competency\user_competency_plan
    3195       *                  )
    3196       *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
    3197       */
    3198      public static function get_plan_competency($planorid, $competencyid) {
    3199          static::require_enabled();
    3200          $plan = $planorid;
    3201          if (!is_object($planorid)) {
    3202              $plan = new plan($planorid);
    3203          }
    3204  
    3205          if (!user_competency::can_read_user($plan->get('userid'))) {
    3206              throw new required_capability_exception($plan->get_context(), 'moodle/competency:usercompetencyview',
    3207                  'nopermissions', '');
    3208          }
    3209  
    3210          $competency = $plan->get_competency($competencyid);
    3211  
    3212          // Get user competencies from user_competency_plan if the plan status is set to complete.
    3213          $iscompletedplan = $plan->get('status') == plan::STATUS_COMPLETE;
    3214          if ($iscompletedplan) {
    3215              $usercompetencies = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), array($competencyid));
    3216              $ucresultkey = 'usercompetencyplan';
    3217          } else {
    3218              $usercompetencies = user_competency::get_multiple($plan->get('userid'), array($competencyid));
    3219              $ucresultkey = 'usercompetency';
    3220          }
    3221  
    3222          $found = count($usercompetencies);
    3223  
    3224          if ($found) {
    3225              $uc = array_pop($usercompetencies);
    3226          } else {
    3227              if ($iscompletedplan) {
    3228                  throw new coding_exception('A user competency plan is missing');
    3229              } else {
    3230                  $uc = user_competency::create_relation($plan->get('userid'), $competency->get('id'));
    3231                  $uc->create();
    3232              }
    3233          }
    3234  
    3235          $plancompetency = (object) array(
    3236              'competency' => $competency,
    3237              'usercompetency' => null,
    3238              'usercompetencyplan' => null
    3239          );
    3240          $plancompetency->$ucresultkey = $uc;
    3241  
    3242          return $plancompetency;
    3243      }
    3244  
    3245      /**
    3246       * List the plans with a competency.
    3247       *
    3248       * @param  int $userid The user id we want the plans for.
    3249       * @param  int $competencyorid The competency, or its ID.
    3250       * @return array[plan] Array of learning plans.
    3251       */
    3252      public static function list_plans_with_competency($userid, $competencyorid) {
    3253          global $USER;
    3254  
    3255          static::require_enabled();
    3256          $competencyid = $competencyorid;
    3257          $competency = null;
    3258          if (is_object($competencyid)) {
    3259              $competency = $competencyid;
    3260              $competencyid = $competency->get('id');
    3261          }
    3262  
    3263          $plans = plan::get_by_user_and_competency($userid, $competencyid);
    3264          foreach ($plans as $index => $plan) {
    3265              // Filter plans we cannot read.
    3266              if (!$plan->can_read()) {
    3267                  unset($plans[$index]);
    3268              }
    3269          }
    3270          return $plans;
    3271      }
    3272  
    3273      /**
    3274       * List the competencies in a user plan.
    3275       *
    3276       * @param  int $planorid The plan, or its ID.
    3277       * @return array((object) array(
    3278       *                            'competency' => \core_competency\competency,
    3279       *                            'usercompetency' => \core_competency\user_competency
    3280       *                            'usercompetencyplan' => \core_competency\user_competency_plan
    3281       *                        ))
    3282       *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
    3283       */
    3284      public static function list_plan_competencies($planorid) {
    3285          static::require_enabled();
    3286          $plan = $planorid;
    3287          if (!is_object($planorid)) {
    3288              $plan = new plan($planorid);
    3289          }
    3290  
    3291          if (!$plan->can_read()) {
    3292              $context = context_user::instance($plan->get('userid'));
    3293              throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
    3294          }
    3295  
    3296          $result = array();
    3297          $competencies = $plan->get_competencies();
    3298  
    3299          // Get user competencies from user_competency_plan if the plan status is set to complete.
    3300          $iscompletedplan = $plan->get('status') == plan::STATUS_COMPLETE;
    3301          if ($iscompletedplan) {
    3302              $usercompetencies = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), $competencies);
    3303              $ucresultkey = 'usercompetencyplan';
    3304          } else {
    3305              $usercompetencies = user_competency::get_multiple($plan->get('userid'), $competencies);
    3306              $ucresultkey = 'usercompetency';
    3307          }
    3308  
    3309          // Build the return values.
    3310          foreach ($competencies as $key => $competency) {
    3311              $found = false;
    3312  
    3313              foreach ($usercompetencies as $uckey => $uc) {
    3314                  if ($uc->get('competencyid') == $competency->get('id')) {
    3315                      $found = true;
    3316                      unset($usercompetencies[$uckey]);
    3317                      break;
    3318                  }
    3319              }
    3320  
    3321              if (!$found) {
    3322                  if ($iscompletedplan) {
    3323                      throw new coding_exception('A user competency plan is missing');
    3324                  } else {
    3325                      $uc = user_competency::create_relation($plan->get('userid'), $competency->get('id'));
    3326                  }
    3327              }
    3328  
    3329              $plancompetency = (object) array(
    3330                  'competency' => $competency,
    3331                  'usercompetency' => null,
    3332                  'usercompetencyplan' => null
    3333              );
    3334              $plancompetency->$ucresultkey = $uc;
    3335              $result[] = $plancompetency;
    3336          }
    3337  
    3338          return $result;
    3339      }
    3340  
    3341      /**
    3342       * Add a competency to a plan.
    3343       *
    3344       * @param int $planid The id of the plan
    3345       * @param int $competencyid The id of the competency
    3346       * @return bool
    3347       */
    3348      public static function add_competency_to_plan($planid, $competencyid) {
    3349          static::require_enabled();
    3350          $plan = new plan($planid);
    3351  
    3352          // First we do a permissions check.
    3353          if (!$plan->can_manage()) {
    3354              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
    3355  
    3356          } else if ($plan->is_based_on_template()) {
    3357              throw new coding_exception('A competency can not be added to a learning plan based on a template');
    3358          }
    3359  
    3360          if (!$plan->can_be_edited()) {
    3361              throw new coding_exception('A competency can not be added to a learning plan completed');
    3362          }
    3363  
    3364          $competency = new competency($competencyid);
    3365  
    3366          // Can not add a competency that belong to a hidden framework.
    3367          if ($competency->get_framework()->get('visible') == false) {
    3368              throw new coding_exception('A competency belonging to hidden framework can not be added');
    3369          }
    3370  
    3371          $exists = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
    3372          if (!$exists) {
    3373              $record = new stdClass();
    3374              $record->planid = $planid;
    3375              $record->competencyid = $competencyid;
    3376              $plancompetency = new plan_competency(0, $record);
    3377              $plancompetency->create();
    3378          }
    3379  
    3380          return true;
    3381      }
    3382  
    3383      /**
    3384       * Remove a competency from a plan.
    3385       *
    3386       * @param int $planid The plan id
    3387       * @param int $competencyid The id of the competency
    3388       * @return bool
    3389       */
    3390      public static function remove_competency_from_plan($planid, $competencyid) {
    3391          static::require_enabled();
    3392          $plan = new plan($planid);
    3393  
    3394          // First we do a permissions check.
    3395          if (!$plan->can_manage()) {
    3396              $context = context_user::instance($plan->get('userid'));
    3397              throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
    3398  
    3399          } else if ($plan->is_based_on_template()) {
    3400              throw new coding_exception('A competency can not be removed from a learning plan based on a template');
    3401          }
    3402  
    3403          if (!$plan->can_be_edited()) {
    3404              throw new coding_exception('A competency can not be removed from a learning plan completed');
    3405          }
    3406  
    3407          $link = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
    3408          if ($link) {
    3409              return $link->delete();
    3410          }
    3411          return false;
    3412      }
    3413  
    3414      /**
    3415       * Move the plan competency up or down in the display list.
    3416       *
    3417       * Requires moodle/competency:planmanage capability at the system context.
    3418       *
    3419       * @param int $planid The plan  id
    3420       * @param int $competencyidfrom The id of the competency we are moving.
    3421       * @param int $competencyidto The id of the competency we are moving to.
    3422       * @return boolean
    3423       */
    3424      public static function reorder_plan_competency($planid, $competencyidfrom, $competencyidto) {
    3425          static::require_enabled();
    3426          $plan = new plan($planid);
    3427  
    3428          // First we do a permissions check.
    3429          if (!$plan->can_manage()) {
    3430              $context = context_user::instance($plan->get('userid'));
    3431              throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
    3432  
    3433          } else if ($plan->is_based_on_template()) {
    3434              throw new coding_exception('A competency can not be reordered in a learning plan based on a template');
    3435          }
    3436  
    3437          if (!$plan->can_be_edited()) {
    3438              throw new coding_exception('A competency can not be reordered in a learning plan completed');
    3439          }
    3440  
    3441          $down = true;
    3442          $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidfrom));
    3443          if (count($matches) == 0) {
    3444              throw new coding_exception('The link does not exist');
    3445          }
    3446  
    3447          $competencyfrom = array_pop($matches);
    3448          $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidto));
    3449          if (count($matches) == 0) {
    3450              throw new coding_exception('The link does not exist');
    3451          }
    3452  
    3453          $competencyto = array_pop($matches);
    3454  
    3455          $all = plan_competency::get_records(array('planid' => $planid), 'sortorder', 'ASC', 0, 0);
    3456  
    3457          if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
    3458              // We are moving up, so put it before the "to" item.
    3459              $down = false;
    3460          }
    3461  
    3462          foreach ($all as $id => $plancompetency) {
    3463              $sort = $plancompetency->get('sortorder');
    3464              if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
    3465                  $plancompetency->set('sortorder', $plancompetency->get('sortorder') - 1);
    3466                  $plancompetency->update();
    3467              } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
    3468                  $plancompetency->set('sortorder', $plancompetency->get('sortorder') + 1);
    3469                  $plancompetency->update();
    3470              }
    3471          }
    3472          $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
    3473          return $competencyfrom->update();
    3474      }
    3475  
    3476      /**
    3477       * Cancel a user competency review request.
    3478       *
    3479       * @param  int $userid       The user ID.
    3480       * @param  int $competencyid The competency ID.
    3481       * @return bool
    3482       */
    3483      public static function user_competency_cancel_review_request($userid, $competencyid) {
    3484          static::require_enabled();
    3485          $context = context_user::instance($userid);
    3486          $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
    3487          if (!$uc || !$uc->can_read()) {
    3488              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
    3489          } else if ($uc->get('status') != user_competency::STATUS_WAITING_FOR_REVIEW) {
    3490              throw new coding_exception('The competency can not be cancel review request at this stage.');
    3491          } else if (!$uc->can_request_review()) {
    3492              throw new required_capability_exception($context, 'moodle/competency:usercompetencyrequestreview', 'nopermissions', '');
    3493          }
    3494  
    3495          $uc->set('status', user_competency::STATUS_IDLE);
    3496          $result = $uc->update();
    3497          if ($result) {
    3498              \core\event\competency_user_competency_review_request_cancelled::create_from_user_competency($uc)->trigger();
    3499          }
    3500          return $result;
    3501      }
    3502  
    3503      /**
    3504       * Request a user competency review.
    3505       *
    3506       * @param  int $userid       The user ID.
    3507       * @param  int $competencyid The competency ID.
    3508       * @return bool
    3509       */
    3510      public static function user_competency_request_review($userid, $competencyid) {
    3511          static::require_enabled();
    3512          $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
    3513          if (!$uc) {
    3514              $uc = user_competency::create_relation($userid, $competencyid);
    3515              $uc->create();
    3516          }
    3517  
    3518          if (!$uc->can_read()) {
    3519              throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
    3520                  'nopermissions', '');
    3521          } else if ($uc->get('status') != user_competency::STATUS_IDLE) {
    3522              throw new coding_exception('The competency can not be sent for review at this stage.');
    3523          } else if (!$uc->can_request_review()) {
    3524              throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyrequestreview',
    3525                  'nopermissions', '');
    3526          }
    3527  
    3528          $uc->set('status', user_competency::STATUS_WAITING_FOR_REVIEW);
    3529          $result = $uc->update();
    3530          if ($result) {
    3531              \core\event\competency_user_competency_review_requested::create_from_user_competency($uc)->trigger();
    3532          }
    3533          return $result;
    3534      }
    3535  
    3536      /**
    3537       * Start a user competency review.
    3538       *
    3539       * @param  int $userid       The user ID.
    3540       * @param  int $competencyid The competency ID.
    3541       * @return bool
    3542       */
    3543      public static function user_competency_start_review($userid, $competencyid) {
    3544          global $USER;
    3545          static::require_enabled();
    3546  
    3547          $context = context_user::instance($userid);
    3548          $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
    3549          if (!$uc || !$uc->can_read()) {
    3550              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
    3551          } else if ($uc->get('status') != user_competency::STATUS_WAITING_FOR_REVIEW) {
    3552              throw new coding_exception('The competency review can not be started at this stage.');
    3553          } else if (!$uc->can_review()) {
    3554              throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', '');
    3555          }
    3556  
    3557          $uc->set('status', user_competency::STATUS_IN_REVIEW);
    3558          $uc->set('reviewerid', $USER->id);
    3559          $result = $uc->update();
    3560          if ($result) {
    3561              \core\event\competency_user_competency_review_started::create_from_user_competency($uc)->trigger();
    3562          }
    3563          return $result;
    3564      }
    3565  
    3566      /**
    3567       * Stop a user competency review.
    3568       *
    3569       * @param  int $userid       The user ID.
    3570       * @param  int $competencyid The competency ID.
    3571       * @return bool
    3572       */
    3573      public static function user_competency_stop_review($userid, $competencyid) {
    3574          static::require_enabled();
    3575          $context = context_user::instance($userid);
    3576          $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
    3577          if (!$uc || !$uc->can_read()) {
    3578              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
    3579          } else if ($uc->get('status') != user_competency::STATUS_IN_REVIEW) {
    3580              throw new coding_exception('The competency review can not be stopped at this stage.');
    3581          } else if (!$uc->can_review()) {
    3582              throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', '');
    3583          }
    3584  
    3585          $uc->set('status', user_competency::STATUS_IDLE);
    3586          $result = $uc->update();
    3587          if ($result) {
    3588              \core\event\competency_user_competency_review_stopped::create_from_user_competency($uc)->trigger();
    3589          }
    3590          return $result;
    3591      }
    3592  
    3593      /**
    3594       * Log user competency viewed event.
    3595       *
    3596       * @param user_competency|int $usercompetencyorid The user competency object or user competency id
    3597       * @return bool
    3598       */
    3599      public static function user_competency_viewed($usercompetencyorid) {
    3600          static::require_enabled();
    3601          $uc = $usercompetencyorid;
    3602          if (!is_object($uc)) {
    3603              $uc = new user_competency($uc);
    3604          }
    3605  
    3606          if (!$uc || !$uc->can_read()) {
    3607              throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
    3608                  'nopermissions', '');
    3609          }
    3610  
    3611          \core\event\competency_user_competency_viewed::create_from_user_competency_viewed($uc)->trigger();
    3612          return true;
    3613      }
    3614  
    3615      /**
    3616       * Log user competency viewed in plan event.
    3617       *
    3618       * @param user_competency|int $usercompetencyorid The user competency object or user competency id
    3619       * @param int $planid The plan ID
    3620       * @return bool
    3621       */
    3622      public static function user_competency_viewed_in_plan($usercompetencyorid, $planid) {
    3623          static::require_enabled();
    3624          $uc = $usercompetencyorid;
    3625          if (!is_object($uc)) {
    3626              $uc = new user_competency($uc);
    3627          }
    3628  
    3629          if (!$uc || !$uc->can_read()) {
    3630              throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
    3631                  'nopermissions', '');
    3632          }
    3633          $plan = new plan($planid);
    3634          if ($plan->get('status') == plan::STATUS_COMPLETE) {
    3635              throw new coding_exception('To log the user competency in completed plan use user_competency_plan_viewed method.');
    3636          }
    3637  
    3638          \core\event\competency_user_competency_viewed_in_plan::create_from_user_competency_viewed_in_plan($uc, $planid)->trigger();
    3639          return true;
    3640      }
    3641  
    3642      /**
    3643       * Log user competency viewed in course event.
    3644       *
    3645       * @param user_competency_course|int $usercoursecompetencyorid The user competency course object or its ID.
    3646       * @param int $courseid The course ID
    3647       * @return bool
    3648       */
    3649      public static function user_competency_viewed_in_course($usercoursecompetencyorid) {
    3650          static::require_enabled();
    3651          $ucc = $usercoursecompetencyorid;
    3652          if (!is_object($ucc)) {
    3653              $ucc = new user_competency_course($ucc);
    3654          }
    3655  
    3656          if (!$ucc || !user_competency::can_read_user_in_course($ucc->get('userid'), $ucc->get('courseid'))) {
    3657              throw new required_capability_exception($ucc->get_context(), 'moodle/competency:usercompetencyview',
    3658                  'nopermissions', '');
    3659          }
    3660  
    3661          // Validate the course, this will throw an exception if not valid.
    3662          self::validate_course($ucc->get('courseid'));
    3663  
    3664          \core\event\competency_user_competency_viewed_in_course::create_from_user_competency_viewed_in_course($ucc)->trigger();
    3665          return true;
    3666      }
    3667  
    3668      /**
    3669       * Log user competency plan viewed event.
    3670       *
    3671       * @param user_competency_plan|int $usercompetencyplanorid The user competency plan object or user competency plan id
    3672       * @return bool
    3673       */
    3674      public static function user_competency_plan_viewed($usercompetencyplanorid) {
    3675          static::require_enabled();
    3676          $ucp = $usercompetencyplanorid;
    3677          if (!is_object($ucp)) {
    3678              $ucp = new user_competency_plan($ucp);
    3679          }
    3680  
    3681          if (!$ucp || !user_competency::can_read_user($ucp->get('userid'))) {
    3682              throw new required_capability_exception($ucp->get_context(), 'moodle/competency:usercompetencyview',
    3683                  'nopermissions', '');
    3684          }
    3685          $plan = new plan($ucp->get('planid'));
    3686          if ($plan->get('status') != plan::STATUS_COMPLETE) {
    3687              throw new coding_exception('To log the user competency in non-completed plan use '
    3688                  . 'user_competency_viewed_in_plan method.');
    3689          }
    3690  
    3691          \core\event\competency_user_competency_plan_viewed::create_from_user_competency_plan($ucp)->trigger();
    3692          return true;
    3693      }
    3694  
    3695      /**
    3696       * Check if template has related data.
    3697       *
    3698       * @param int $templateid The id of the template to check.
    3699       * @return boolean
    3700       */
    3701      public static function template_has_related_data($templateid) {
    3702          static::require_enabled();
    3703          // First we do a permissions check.
    3704          $template = new template($templateid);
    3705  
    3706          if (!$template->can_read()) {
    3707              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
    3708                  'nopermissions', '');
    3709          }
    3710  
    3711          // OK - all set.
    3712          return $template->has_plans();
    3713      }
    3714  
    3715      /**
    3716       * List all the related competencies.
    3717       *
    3718       * @param int $competencyid The id of the competency to check.
    3719       * @return competency[]
    3720       */
    3721      public static function list_related_competencies($competencyid) {
    3722          static::require_enabled();
    3723          $competency = new competency($competencyid);
    3724  
    3725          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
    3726                  $competency->get_context())) {
    3727              throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview',
    3728                  'nopermissions', '');
    3729          }
    3730  
    3731          return $competency->get_related_competencies();
    3732      }
    3733  
    3734      /**
    3735       * Add a related competency.
    3736       *
    3737       * @param int $competencyid The id of the competency
    3738       * @param int $relatedcompetencyid The id of the related competency.
    3739       * @return bool False when create failed, true on success, or if the relation already existed.
    3740       */
    3741      public static function add_related_competency($competencyid, $relatedcompetencyid) {
    3742          static::require_enabled();
    3743          $competency1 = new competency($competencyid);
    3744          $competency2 = new competency($relatedcompetencyid);
    3745  
    3746          require_capability('moodle/competency:competencymanage', $competency1->get_context());
    3747  
    3748          $relatedcompetency = related_competency::get_relation($competency1->get('id'), $competency2->get('id'));
    3749          if (!$relatedcompetency->get('id')) {
    3750              $relatedcompetency->create();
    3751              return true;
    3752          }
    3753  
    3754          return true;
    3755      }
    3756  
    3757      /**
    3758       * Remove a related competency.
    3759       *
    3760       * @param int $competencyid The id of the competency.
    3761       * @param int $relatedcompetencyid The id of the related competency.
    3762       * @return bool True when it was deleted, false when it wasn't or the relation doesn't exist.
    3763       */
    3764      public static function remove_related_competency($competencyid, $relatedcompetencyid) {
    3765          static::require_enabled();
    3766          $competency = new competency($competencyid);
    3767  
    3768          // This only check if we have the permission in either competency because both competencies
    3769          // should belong to the same framework.
    3770          require_capability('moodle/competency:competencymanage', $competency->get_context());
    3771  
    3772          $relatedcompetency = related_competency::get_relation($competencyid, $relatedcompetencyid);
    3773          if ($relatedcompetency->get('id')) {
    3774              return $relatedcompetency->delete();
    3775          }
    3776  
    3777          return false;
    3778      }
    3779  
    3780      /**
    3781       * Read a user evidence.
    3782       *
    3783       * @param int $id
    3784       * @return user_evidence
    3785       */
    3786      public static function read_user_evidence($id) {
    3787          static::require_enabled();
    3788          $userevidence = new user_evidence($id);
    3789  
    3790          if (!$userevidence->can_read()) {
    3791              $context = $userevidence->get_context();
    3792              throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', '');
    3793          }
    3794  
    3795          return $userevidence;
    3796      }
    3797  
    3798      /**
    3799       * Create a new user evidence.
    3800       *
    3801       * @param  object $data        The data.
    3802       * @param  int    $draftitemid The draft ID in which files have been saved.
    3803       * @return user_evidence
    3804       */
    3805      public static function create_user_evidence($data, $draftitemid = null) {
    3806          static::require_enabled();
    3807          $userevidence = new user_evidence(null, $data);
    3808          $context = $userevidence->get_context();
    3809  
    3810          if (!$userevidence->can_manage()) {
    3811              throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
    3812          }
    3813  
    3814          $userevidence->create();
    3815          if (!empty($draftitemid)) {
    3816              $fileareaoptions = array('subdirs' => true);
    3817              $itemid = $userevidence->get('id');
    3818              file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions);
    3819          }
    3820  
    3821          // Trigger an evidence of prior learning created event.
    3822          \core\event\competency_user_evidence_created::create_from_user_evidence($userevidence)->trigger();
    3823  
    3824          return $userevidence;
    3825      }
    3826  
    3827      /**
    3828       * Create a new user evidence.
    3829       *
    3830       * @param  object $data        The data.
    3831       * @param  int    $draftitemid The draft ID in which files have been saved.
    3832       * @return user_evidence
    3833       */
    3834      public static function update_user_evidence($data, $draftitemid = null) {
    3835          static::require_enabled();
    3836          $userevidence = new user_evidence($data->id);
    3837          $context = $userevidence->get_context();
    3838  
    3839          if (!$userevidence->can_manage()) {
    3840              throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
    3841  
    3842          } else if (property_exists($data, 'userid') && $data->userid != $userevidence->get('userid')) {
    3843              throw new coding_exception('Can not change the userid of a user evidence.');
    3844          }
    3845  
    3846          $userevidence->from_record($data);
    3847          $userevidence->update();
    3848  
    3849          if (!empty($draftitemid)) {
    3850              $fileareaoptions = array('subdirs' => true);
    3851              $itemid = $userevidence->get('id');
    3852              file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions);
    3853          }
    3854  
    3855          // Trigger an evidence of prior learning updated event.
    3856          \core\event\competency_user_evidence_updated::create_from_user_evidence($userevidence)->trigger();
    3857  
    3858          return $userevidence;
    3859      }
    3860  
    3861      /**
    3862       * Delete a user evidence.
    3863       *
    3864       * @param  int $id The user evidence ID.
    3865       * @return bool
    3866       */
    3867      public static function delete_user_evidence($id) {
    3868          static::require_enabled();
    3869          $userevidence = new user_evidence($id);
    3870          $context = $userevidence->get_context();
    3871  
    3872          if (!$userevidence->can_manage()) {
    3873              throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
    3874          }
    3875  
    3876          // Delete the user evidence.
    3877          $userevidence->delete();
    3878  
    3879          // Delete associated files.
    3880          $fs = get_file_storage();
    3881          $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $id);
    3882  
    3883          // Delete relation between evidence and competencies.
    3884          $userevidence->set('id', $id);     // Restore the ID to fully mock the object.
    3885          $competencies = user_evidence_competency::get_competencies_by_userevidenceid($id);
    3886          foreach ($competencies as $competency) {
    3887              static::delete_user_evidence_competency($userevidence, $competency->get('id'));
    3888          }
    3889  
    3890          // Trigger an evidence of prior learning deleted event.
    3891          \core\event\competency_user_evidence_deleted::create_from_user_evidence($userevidence)->trigger();
    3892  
    3893          $userevidence->set('id', 0);       // Restore the object.
    3894  
    3895          return true;
    3896      }
    3897  
    3898      /**
    3899       * List the user evidence of a user.
    3900       *
    3901       * @param  int $userid The user ID.
    3902       * @return user_evidence[]
    3903       */
    3904      public static function list_user_evidence($userid) {
    3905          static::require_enabled();
    3906          if (!user_evidence::can_read_user($userid)) {
    3907              $context = context_user::instance($userid);
    3908              throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', '');
    3909          }
    3910  
    3911          $evidence = user_evidence::get_records(array('userid' => $userid), 'name');
    3912          return $evidence;
    3913      }
    3914  
    3915      /**
    3916       * Link a user evidence with a competency.
    3917       *
    3918       * @param  user_evidence|int $userevidenceorid User evidence or its ID.
    3919       * @param  int $competencyid Competency ID.
    3920       * @return user_evidence_competency
    3921       */
    3922      public static function create_user_evidence_competency($userevidenceorid, $competencyid) {
    3923          global $USER;
    3924          static::require_enabled();
    3925  
    3926          $userevidence = $userevidenceorid;
    3927          if (!is_object($userevidence)) {
    3928              $userevidence = self::read_user_evidence($userevidence);
    3929          }
    3930  
    3931          // Perform user evidence capability checks.
    3932          if (!$userevidence->can_manage()) {
    3933              $context = $userevidence->get_context();
    3934              throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
    3935          }
    3936  
    3937          // Perform competency capability checks.
    3938          $competency = self::read_competency($competencyid);
    3939  
    3940          // Get (and create) the relation.
    3941          $relation = user_evidence_competency::get_relation($userevidence->get('id'), $competency->get('id'));
    3942          if (!$relation->get('id')) {
    3943              $relation->create();
    3944  
    3945              $link = url::user_evidence($userevidence->get('id'));
    3946              self::add_evidence(
    3947                  $userevidence->get('userid'),
    3948                  $competency,
    3949                  $userevidence->get_context(),
    3950                  evidence::ACTION_LOG,
    3951                  'evidence_evidenceofpriorlearninglinked',
    3952                  'core_competency',
    3953                  $userevidence->get('name'),
    3954                  false,
    3955                  $link->out(false),
    3956                  null,
    3957                  $USER->id
    3958              );
    3959          }
    3960  
    3961          return $relation;
    3962      }
    3963  
    3964      /**
    3965       * Delete a relationship between a user evidence and a competency.
    3966       *
    3967       * @param  user_evidence|int $userevidenceorid User evidence or its ID.
    3968       * @param  int $competencyid Competency ID.
    3969       * @return bool
    3970       */
    3971      public static function delete_user_evidence_competency($userevidenceorid, $competencyid) {
    3972          global $USER;
    3973          static::require_enabled();
    3974  
    3975          $userevidence = $userevidenceorid;
    3976          if (!is_object($userevidence)) {
    3977              $userevidence = self::read_user_evidence($userevidence);
    3978          }
    3979  
    3980          // Perform user evidence capability checks.
    3981          if (!$userevidence->can_manage()) {
    3982              $context = $userevidence->get_context();
    3983              throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
    3984          }
    3985  
    3986