Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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          // Get (and delete) the relation.
3987          $relation = user_evidence_competency::get_relation($userevidence->get('id'), $competencyid);
3988          if (!$relation->get('id')) {
3989              return true;
3990          }
3991  
3992          $success = $relation->delete();
3993          if ($success) {
3994              self::add_evidence(
3995                  $userevidence->get('userid'),
3996                  $competencyid,
3997                  $userevidence->get_context(),
3998                  evidence::ACTION_LOG,
3999                  'evidence_evidenceofpriorlearningunlinked',
4000                  'core_competency',
4001                  $userevidence->get('name'),
4002                  false,
4003                  null,
4004                  null,
4005                  $USER->id
4006              );
4007          }
4008  
4009          return $success;
4010      }
4011  
4012      /**
4013       * Send request review for user evidence competencies.
4014       *
4015       * @param  int $id The user evidence ID.
4016       * @return bool
4017       */
4018      public static function request_review_of_user_evidence_linked_competencies($id) {
4019          $userevidence = new user_evidence($id);
4020          $context = $userevidence->get_context();
4021          $userid = $userevidence->get('userid');
4022  
4023          if (!$userevidence->can_manage()) {
4024              throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
4025          }
4026  
4027          $usercompetencies = user_evidence_competency::get_user_competencies_by_userevidenceid($id);
4028          foreach ($usercompetencies as $usercompetency) {
4029              if ($usercompetency->get('status') == user_competency::STATUS_IDLE) {
4030                  static::user_competency_request_review($userid, $usercompetency->get('competencyid'));
4031              }
4032          }
4033  
4034          return true;
4035      }
4036  
4037      /**
4038       * Recursively duplicate competencies from a tree, we start duplicating from parents to children to have a correct path.
4039       * This method does not copy the related competencies.
4040       *
4041       * @param int $frameworkid - framework id
4042       * @param competency[] $tree - array of competencies object
4043       * @param int $oldparent - old parent id
4044       * @param int $newparent - new parent id
4045       * @return competency[] $matchids - List of old competencies ids matched with new competencies object.
4046       */
4047      protected static function duplicate_competency_tree($frameworkid, $tree, $oldparent = 0, $newparent = 0) {
4048          $matchids = array();
4049          foreach ($tree as $node) {
4050              if ($node->competency->get('parentid') == $oldparent) {
4051                  $parentid = $node->competency->get('id');
4052  
4053                  // Create the competency.
4054                  $competency = new competency(0, $node->competency->to_record());
4055                  $competency->set('competencyframeworkid', $frameworkid);
4056                  $competency->set('parentid', $newparent);
4057                  $competency->set('path', '');
4058                  $competency->set('id', 0);
4059                  $competency->reset_rule();
4060                  $competency->create();
4061  
4062                  // Trigger the created event competency.
4063                  \core\event\competency_created::create_from_competency($competency)->trigger();
4064  
4065                  // Match the old id with the new one.
4066                  $matchids[$parentid] = $competency;
4067  
4068                  if (!empty($node->children)) {
4069                      // Duplicate children competency.
4070                      $childrenids = self::duplicate_competency_tree($frameworkid, $node->children, $parentid, $competency->get('id'));
4071                      // Array_merge does not keep keys when merging so we use the + operator.
4072                      $matchids = $matchids + $childrenids;
4073                  }
4074              }
4075          }
4076          return $matchids;
4077      }
4078  
4079      /**
4080       * Recursively migrate competency rules.
4081       *
4082       * @param competency[] $tree - array of competencies object
4083       * @param competency[] $matchids - List of old competencies ids matched with new competencies object
4084       */
4085      protected static function migrate_competency_tree_rules($tree, $matchids) {
4086  
4087          foreach ($tree as $node) {
4088              $oldcompid = $node->competency->get('id');
4089              if ($node->competency->get('ruletype') && array_key_exists($oldcompid, $matchids)) {
4090                  try {
4091                      // Get the new competency.
4092                      $competency = $matchids[$oldcompid];
4093                      $class = $node->competency->get('ruletype');
4094                      $newruleconfig = $class::migrate_config($node->competency->get('ruleconfig'), $matchids);
4095                      $competency->set('ruleconfig', $newruleconfig);
4096                      $competency->set('ruletype', $class);
4097                      $competency->set('ruleoutcome', $node->competency->get('ruleoutcome'));
4098                      $competency->update();
4099                  } catch (\Exception $e) {
4100                      debugging('Could not migrate competency rule from: ' . $oldcompid . ' to: ' . $competency->get('id') . '.' .
4101                          ' Exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
4102                      $competency->reset_rule();
4103                  }
4104              }
4105  
4106              if (!empty($node->children)) {
4107                  self::migrate_competency_tree_rules($node->children, $matchids);
4108              }
4109          }
4110      }
4111  
4112      /**
4113       * Archive user competencies in a plan.
4114       *
4115       * @param int $plan The plan object.
4116       * @return void
4117       */
4118      protected static function archive_user_competencies_in_plan($plan) {
4119  
4120          // Check if the plan was already completed.
4121          if ($plan->get('status') == plan::STATUS_COMPLETE) {
4122              throw new coding_exception('The plan is already completed.');
4123          }
4124  
4125          $competencies = $plan->get_competencies();
4126          $usercompetencies = user_competency::get_multiple($plan->get('userid'), $competencies);
4127  
4128          $i = 0;
4129          foreach ($competencies as $competency) {
4130              $found = false;
4131  
4132              foreach ($usercompetencies as $uckey => $uc) {
4133                  if ($uc->get('competencyid') == $competency->get('id')) {
4134                      $found = true;
4135  
4136                      $ucprecord = $uc->to_record();
4137                      $ucprecord->planid = $plan->get('id');
4138                      $ucprecord->sortorder = $i;
4139                      unset($ucprecord->id);
4140                      unset($ucprecord->status);
4141                      unset($ucprecord->reviewerid);
4142  
4143                      $usercompetencyplan = new user_competency_plan(0, $ucprecord);
4144                      $usercompetencyplan->create();
4145  
4146                      unset($usercompetencies[$uckey]);
4147                      break;
4148                  }
4149              }
4150  
4151              // If the user competency doesn't exist, we create a new relation in user_competency_plan.
4152              if (!$found) {
4153                  $usercompetencyplan = user_competency_plan::create_relation($plan->get('userid'), $competency->get('id'),
4154                          $plan->get('id'));
4155                  $usercompetencyplan->set('sortorder', $i);
4156                  $usercompetencyplan->create();
4157              }
4158              $i++;
4159          }
4160      }
4161  
4162      /**
4163       * Delete archived user competencies in a plan.
4164       *
4165       * @param int $plan The plan object.
4166       * @return void
4167       */
4168      protected static function remove_archived_user_competencies_in_plan($plan) {
4169          $competencies = $plan->get_competencies();
4170          $usercompetenciesplan = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), $competencies);
4171  
4172          foreach ($usercompetenciesplan as $ucpkey => $ucp) {
4173              $ucp->delete();
4174          }
4175      }
4176  
4177      /**
4178       * List all the evidence for a user competency.
4179       *
4180       * @param int $userid The user id - only used if usercompetencyid is 0.
4181       * @param int $competencyid The competency id - only used it usercompetencyid is 0.
4182       * @param int $planid The plan id - not used yet - but can be used to only list archived evidence if a plan is completed.
4183       * @param string $sort The field to sort the evidence by.
4184       * @param string $order The ordering of the sorting.
4185       * @param int $skip Number of records to skip.
4186       * @param int $limit Number of records to return.
4187       * @return \core_competency\evidence[]
4188       * @return array of \core_competency\evidence
4189       */
4190      public static function list_evidence($userid = 0, $competencyid = 0, $planid = 0, $sort = 'timecreated',
4191                                           $order = 'DESC', $skip = 0, $limit = 0) {
4192          static::require_enabled();
4193  
4194          if (!user_competency::can_read_user($userid)) {
4195              $context = context_user::instance($userid);
4196              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
4197          }
4198  
4199          $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
4200          if (!$usercompetency) {
4201              return array();
4202          }
4203  
4204          $plancompleted = false;
4205          if ($planid != 0) {
4206              $plan = new plan($planid);
4207              if ($plan->get('status') == plan::STATUS_COMPLETE) {
4208                  $plancompleted = true;
4209              }
4210          }
4211  
4212          $select = 'usercompetencyid = :usercompetencyid';
4213          $params = array('usercompetencyid' => $usercompetency->get('id'));
4214          if ($plancompleted) {
4215              $select .= ' AND timecreated <= :timecompleted';
4216              $params['timecompleted'] = $plan->get('timemodified');
4217          }
4218  
4219          $orderby = $sort . ' ' . $order;
4220          $orderby .= !empty($orderby) ? ', id DESC' : 'id DESC'; // Prevent random ordering.
4221  
4222          $evidence = evidence::get_records_select($select, $params, $orderby, '*', $skip, $limit);
4223          return $evidence;
4224      }
4225  
4226      /**
4227       * List all the evidence for a user competency in a course.
4228       *
4229       * @param int $userid The user ID.
4230       * @param int $courseid The course ID.
4231       * @param int $competencyid The competency ID.
4232       * @param string $sort The field to sort the evidence by.
4233       * @param string $order The ordering of the sorting.
4234       * @param int $skip Number of records to skip.
4235       * @param int $limit Number of records to return.
4236       * @return \core_competency\evidence[]
4237       */
4238      public static function list_evidence_in_course($userid = 0, $courseid = 0, $competencyid = 0, $sort = 'timecreated',
4239                                                     $order = 'DESC', $skip = 0, $limit = 0) {
4240          static::require_enabled();
4241  
4242          if (!user_competency::can_read_user_in_course($userid, $courseid)) {
4243              $context = context_user::instance($userid);
4244              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
4245          }
4246  
4247          $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
4248          if (!$usercompetency) {
4249              return array();
4250          }
4251  
4252          $context = context_course::instance($courseid);
4253          return evidence::get_records_for_usercompetency($usercompetency->get('id'), $context, $sort, $order, $skip, $limit);
4254      }
4255  
4256      /**
4257       * Create an evidence from a list of parameters.
4258       *
4259       * Requires no capability because evidence can be added in many situations under any user.
4260       *
4261       * @param int $userid The user id for which evidence is added.
4262       * @param competency|int $competencyorid The competency, or its id for which evidence is added.
4263       * @param context|int $contextorid The context in which the evidence took place.
4264       * @param int $action The type of action to take on the competency. \core_competency\evidence::ACTION_*.
4265       * @param string $descidentifier The strings identifier.
4266       * @param string $desccomponent The strings component.
4267       * @param mixed $desca Any arguments the string requires.
4268       * @param bool $recommend When true, the user competency will be sent for review.
4269       * @param string $url The url the evidence may link to.
4270       * @param int $grade The grade, or scale ID item.
4271       * @param int $actionuserid The ID of the user who took the action of adding the evidence. Null when system.
4272       *                          This should be used when the action was taken by a real person, this will allow
4273       *                          to keep track of all the evidence given by a certain person.
4274       * @param string $note A note to attach to the evidence.
4275       * @return evidence
4276       * @throws coding_exception
4277       * @throws invalid_persistent_exception
4278       * @throws moodle_exception
4279       */
4280      public static function add_evidence($userid, $competencyorid, $contextorid, $action, $descidentifier, $desccomponent,
4281                                          $desca = null, $recommend = false, $url = null, $grade = null, $actionuserid = null,
4282                                          $note = null) {
4283          global $DB;
4284          static::require_enabled();
4285  
4286          // Some clearly important variable assignments right there.
4287          $competencyid = $competencyorid;
4288          $competency = null;
4289          if (is_object($competencyid)) {
4290              $competency = $competencyid;
4291              $competencyid = $competency->get('id');
4292          }
4293          $contextid = $contextorid;
4294          $context = $contextorid;
4295          if (is_object($contextorid)) {
4296              $contextid = $contextorid->id;
4297          } else {
4298              $context = context::instance_by_id($contextorid);
4299          }
4300          $setucgrade = false;
4301          $ucgrade = null;
4302          $ucproficiency = null;
4303          $usercompetencycourse = null;
4304  
4305          // Fetch or create the user competency.
4306          $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
4307          if (!$usercompetency) {
4308              $usercompetency = user_competency::create_relation($userid, $competencyid);
4309              $usercompetency->create();
4310          }
4311  
4312          // What should we be doing?
4313          switch ($action) {
4314  
4315              // Completing a competency.
4316              case evidence::ACTION_COMPLETE:
4317                  // The logic here goes like this:
4318                  //
4319                  // if rating outside a course
4320                  // - set the default grade and proficiency ONLY if there is no current grade
4321                  // else we are in a course
4322                  // - set the defautl grade and proficiency in the course ONLY if there is no current grade in the course
4323                  // - then check the course settings to see if we should push the rating outside the course
4324                  // - if we should push it
4325                  // --- push it only if the user_competency (outside the course) has no grade
4326                  // Done.
4327  
4328                  if ($grade !== null) {
4329                      throw new coding_exception("The grade MUST NOT be set with a 'completing' evidence.");
4330                  }
4331  
4332                  // Fetch the default grade to attach to the evidence.
4333                  if (empty($competency)) {
4334                      $competency = new competency($competencyid);
4335                  }
4336                  list($grade, $proficiency) = $competency->get_default_grade();
4337  
4338                  // Add user_competency_course record when in a course or module.
4339                  if (in_array($context->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE))) {
4340                      $coursecontext = $context->get_course_context();
4341                      $courseid = $coursecontext->instanceid;
4342                      $filterparams = array(
4343                          'userid' => $userid,
4344                          'competencyid' => $competencyid,
4345                          'courseid' => $courseid
4346                      );
4347                      // Fetch or create user competency course.
4348                      $usercompetencycourse = user_competency_course::get_record($filterparams);
4349                      if (!$usercompetencycourse) {
4350                          $usercompetencycourse = user_competency_course::create_relation($userid, $competencyid, $courseid);
4351                          $usercompetencycourse->create();
4352                      }
4353                      // Only update the grade and proficiency if there is not already a grade.
4354                      if ($usercompetencycourse->get('grade') === null) {
4355                          // Set grade.
4356                          $usercompetencycourse->set('grade', $grade);
4357                          // Set proficiency.
4358                          $usercompetencycourse->set('proficiency', $proficiency);
4359                      }
4360  
4361                      // Check the course settings to see if we should push to user plans.
4362                      $coursesettings = course_competency_settings::get_by_courseid($courseid);
4363                      $setucgrade = $coursesettings->get('pushratingstouserplans');
4364  
4365                      if ($setucgrade) {
4366                          // Only push to user plans if there is not already a grade.
4367                          if ($usercompetency->get('grade') !== null) {
4368                              $setucgrade = false;
4369                          } else {
4370                              $ucgrade = $grade;
4371                              $ucproficiency = $proficiency;
4372                          }
4373                      }
4374                  } else {
4375  
4376                      // When completing the competency we fetch the default grade from the competency. But we only mark
4377                      // the user competency when a grade has not been set yet. Complete is an action to use with automated systems.
4378                      if ($usercompetency->get('grade') === null) {
4379                          $setucgrade = true;
4380                          $ucgrade = $grade;
4381                          $ucproficiency = $proficiency;
4382                      }
4383                  }
4384  
4385                  break;
4386  
4387              // We override the grade, even overriding back to not set.
4388              case evidence::ACTION_OVERRIDE:
4389                  $setucgrade = true;
4390                  $ucgrade = $grade;
4391                  if (empty($competency)) {
4392                      $competency = new competency($competencyid);
4393                  }
4394                  if ($ucgrade !== null) {
4395                      $ucproficiency = $competency->get_proficiency_of_grade($ucgrade);
4396                  }
4397  
4398                  // Add user_competency_course record when in a course or module.
4399                  if (in_array($context->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE))) {
4400                      $coursecontext = $context->get_course_context();
4401                      $courseid = $coursecontext->instanceid;
4402                      $filterparams = array(
4403                          'userid' => $userid,
4404                          'competencyid' => $competencyid,
4405                          'courseid' => $courseid
4406                      );
4407                      // Fetch or create user competency course.
4408                      $usercompetencycourse = user_competency_course::get_record($filterparams);
4409                      if (!$usercompetencycourse) {
4410                          $usercompetencycourse = user_competency_course::create_relation($userid, $competencyid, $courseid);
4411                          $usercompetencycourse->create();
4412                      }
4413                      // Get proficiency.
4414                      $proficiency = $ucproficiency;
4415                      if ($proficiency === null) {
4416                          if (empty($competency)) {
4417                              $competency = new competency($competencyid);
4418                          }
4419                          $proficiency = $competency->get_proficiency_of_grade($grade);
4420                      }
4421                      // Set grade.
4422                      $usercompetencycourse->set('grade', $grade);
4423                      // Set proficiency.
4424                      $usercompetencycourse->set('proficiency', $proficiency);
4425  
4426                      $coursesettings = course_competency_settings::get_by_courseid($courseid);
4427                      if (!$coursesettings->get('pushratingstouserplans')) {
4428                          $setucgrade = false;
4429                      }
4430                  }
4431  
4432                  break;
4433  
4434              // Simply logging an evidence.
4435              case evidence::ACTION_LOG:
4436                  if ($grade !== null) {
4437                      throw new coding_exception("The grade MUST NOT be set when 'logging' an evidence.");
4438                  }
4439                  break;
4440  
4441              // Whoops, this is not expected.
4442              default:
4443                  throw new coding_exception('Unexpected action parameter when registering an evidence.');
4444                  break;
4445          }
4446  
4447          // Should we recommend?
4448          if ($recommend && $usercompetency->get('status') == user_competency::STATUS_IDLE) {
4449              $usercompetency->set('status', user_competency::STATUS_WAITING_FOR_REVIEW);
4450          }
4451  
4452          // Setting the grade and proficiency for the user competency.
4453          $wascompleted = false;
4454          if ($setucgrade == true) {
4455              if (!$usercompetency->get('proficiency') && $ucproficiency) {
4456                  $wascompleted = true;
4457              }
4458              $usercompetency->set('grade', $ucgrade);
4459              $usercompetency->set('proficiency', $ucproficiency);
4460          }
4461  
4462          // Prepare the evidence.
4463          $record = new stdClass();
4464          $record->usercompetencyid = $usercompetency->get('id');
4465          $record->contextid = $contextid;
4466          $record->action = $action;
4467          $record->descidentifier = $descidentifier;
4468          $record->desccomponent = $desccomponent;
4469          $record->grade = $grade;
4470          $record->actionuserid = $actionuserid;
4471          $record->note = $note;
4472          $evidence = new evidence(0, $record);
4473          $evidence->set('desca', $desca);
4474          $evidence->set('url', $url);
4475  
4476          // Validate both models, we should not operate on one if the other will not save.
4477          if (!$usercompetency->is_valid()) {
4478              throw new invalid_persistent_exception($usercompetency->get_errors());
4479          } else if (!$evidence->is_valid()) {
4480              throw new invalid_persistent_exception($evidence->get_errors());
4481          }
4482  
4483          // Save the user_competency_course record.
4484          if ($usercompetencycourse !== null) {
4485              // Validate and update.
4486              if (!$usercompetencycourse->is_valid()) {
4487                  throw new invalid_persistent_exception($usercompetencycourse->get_errors());
4488              }
4489              $usercompetencycourse->update();
4490          }
4491  
4492          // Finally save. Pheww!
4493          $usercompetency->update();
4494          $evidence->create();
4495  
4496          // Trigger the evidence_created event.
4497          \core\event\competency_evidence_created::create_from_evidence($evidence, $usercompetency, $recommend)->trigger();
4498  
4499          // The competency was marked as completed, apply the rules.
4500          if ($wascompleted) {
4501              self::apply_competency_rules_from_usercompetency($usercompetency, $competency);
4502          }
4503  
4504          return $evidence;
4505      }
4506  
4507      /**
4508       * Read an evidence.
4509       * @param int $evidenceid The evidence ID.
4510       * @return evidence
4511       */
4512      public static function read_evidence($evidenceid) {
4513          static::require_enabled();
4514  
4515          $evidence = new evidence($evidenceid);
4516          $uc = new user_competency($evidence->get('usercompetencyid'));
4517          if (!$uc->can_read()) {
4518              throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
4519                  'nopermissions', '');
4520          }
4521  
4522          return $evidence;
4523      }
4524  
4525      /**
4526       * Delete an evidence.
4527       *
4528       * @param evidence|int $evidenceorid The evidence, or its ID.
4529       * @return bool
4530       */
4531      public static function delete_evidence($evidenceorid) {
4532          $evidence = $evidenceorid;
4533          if (!is_object($evidence)) {
4534              $evidence = new evidence($evidenceorid);
4535          }
4536  
4537          $uc = new user_competency($evidence->get('usercompetencyid'));
4538          if (!evidence::can_delete_user($uc->get('userid'))) {
4539              throw new required_capability_exception($uc->get_context(), 'moodle/competency:evidencedelete', 'nopermissions', '');
4540          }
4541  
4542          return $evidence->delete();
4543      }
4544  
4545      /**
4546       * Apply the competency rules from a user competency.
4547       *
4548       * The user competency passed should be one that was recently marked as complete.
4549       * A user competency is considered 'complete' when it's proficiency value is true.
4550       *
4551       * This method will check if the parent of this usercompetency's competency has any
4552       * rules and if so will see if they match. When matched it will take the required
4553       * step to add evidence and trigger completion, etc...
4554       *
4555       * @param  user_competency $usercompetency The user competency recently completed.
4556       * @param  competency|null $competency     The competency of the user competency, useful to avoid unnecessary read.
4557       * @return void
4558       */
4559      protected static function apply_competency_rules_from_usercompetency(user_competency $usercompetency,
4560                                                                           competency $competency = null) {
4561  
4562          // Perform some basic checks.
4563          if (!$usercompetency->get('proficiency')) {
4564              throw new coding_exception('The user competency passed is not completed.');
4565          }
4566          if ($competency === null) {
4567              $competency = $usercompetency->get_competency();
4568          }
4569          if ($competency->get('id') != $usercompetency->get('competencyid')) {
4570              throw new coding_exception('Mismatch between user competency and competency.');
4571          }
4572  
4573          // Fetch the parent.
4574          $parent = $competency->get_parent();
4575          if ($parent === null) {
4576              return;
4577          }
4578  
4579          // The parent should have a rule, and a meaningful outcome.
4580          $ruleoutcome = $parent->get('ruleoutcome');
4581          if ($ruleoutcome == competency::OUTCOME_NONE) {
4582              return;
4583          }
4584          $rule = $parent->get_rule_object();
4585          if ($rule === null) {
4586              return;
4587          }
4588  
4589          // Fetch or create the user competency for the parent.
4590          $userid = $usercompetency->get('userid');
4591          $parentuc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $parent->get('id')));
4592          if (!$parentuc) {
4593              $parentuc = user_competency::create_relation($userid, $parent->get('id'));
4594              $parentuc->create();
4595          }
4596  
4597          // Does the rule match?
4598          if (!$rule->matches($parentuc)) {
4599              return;
4600          }
4601  
4602          // Figuring out what to do.
4603          $recommend = false;
4604          if ($ruleoutcome == competency::OUTCOME_EVIDENCE) {
4605              $action = evidence::ACTION_LOG;
4606  
4607          } else if ($ruleoutcome == competency::OUTCOME_RECOMMEND) {
4608              $action = evidence::ACTION_LOG;
4609              $recommend = true;
4610  
4611          } else if ($ruleoutcome == competency::OUTCOME_COMPLETE) {
4612              $action = evidence::ACTION_COMPLETE;
4613  
4614          } else {
4615              throw new moodle_exception('Unexpected rule outcome: ' + $ruleoutcome);
4616          }
4617  
4618          // Finally add an evidence.
4619          static::add_evidence(
4620              $userid,
4621              $parent,
4622              $parent->get_context()->id,
4623              $action,
4624              'evidence_competencyrule',
4625              'core_competency',
4626              null,
4627              $recommend
4628          );
4629      }
4630  
4631      /**
4632       * Observe when a course module is marked as completed.
4633       *
4634       * Note that the user being logged in while this happens may be anyone.
4635       * Do not rely on capability checks here!
4636       *
4637       * @param  \core\event\course_module_completion_updated $event
4638       * @return void
4639       */
4640      public static function observe_course_module_completion_updated(\core\event\course_module_completion_updated $event) {
4641          if (!static::is_enabled()) {
4642              return;
4643          }
4644  
4645          $eventdata = $event->get_record_snapshot('course_modules_completion', $event->objectid);
4646  
4647          if ($eventdata->completionstate == COMPLETION_COMPLETE
4648                  || $eventdata->completionstate == COMPLETION_COMPLETE_PASS) {
4649              $coursemodulecompetencies = course_module_competency::list_course_module_competencies($eventdata->coursemoduleid);
4650  
4651              $cm = get_coursemodule_from_id(null, $eventdata->coursemoduleid);
4652              $fastmodinfo = get_fast_modinfo($cm->course)->cms[$cm->id];
4653  
4654              $cmname = $fastmodinfo->name;
4655              $url = $fastmodinfo->url;
4656  
4657              foreach ($coursemodulecompetencies as $coursemodulecompetency) {
4658                  $outcome = $coursemodulecompetency->get('ruleoutcome');
4659                  $action = null;
4660                  $recommend = false;
4661                  $strdesc = 'evidence_coursemodulecompleted';
4662  
4663                  if ($outcome == course_module_competency::OUTCOME_NONE) {
4664                      continue;
4665                  }
4666                  if ($outcome == course_module_competency::OUTCOME_EVIDENCE) {
4667                      $action = evidence::ACTION_LOG;
4668  
4669                  } else if ($outcome == course_module_competency::OUTCOME_RECOMMEND) {
4670                      $action = evidence::ACTION_LOG;
4671                      $recommend = true;
4672  
4673                  } else if ($outcome == course_module_competency::OUTCOME_COMPLETE) {
4674                      $action = evidence::ACTION_COMPLETE;
4675  
4676                  } else {
4677                      throw new moodle_exception('Unexpected rule outcome: ' + $outcome);
4678                  }
4679  
4680                  static::add_evidence(
4681                      $event->relateduserid,
4682                      $coursemodulecompetency->get('competencyid'),
4683                      $event->contextid,
4684                      $action,
4685                      $strdesc,
4686                      'core_competency',
4687                      $cmname,
4688                      $recommend,
4689                      $url
4690                  );
4691              }
4692          }
4693      }
4694  
4695      /**
4696       * Observe when a course is marked as completed.
4697       *
4698       * Note that the user being logged in while this happens may be anyone.
4699       * Do not rely on capability checks here!
4700       *
4701       * @param  \core\event\course_completed $event
4702       * @return void
4703       */
4704      public static function observe_course_completed(\core\event\course_completed $event) {
4705          if (!static::is_enabled()) {
4706              return;
4707          }
4708  
4709          $sql = 'courseid = :courseid AND ruleoutcome != :nooutcome';
4710          $params = array(
4711              'courseid' => $event->courseid,
4712              'nooutcome' => course_competency::OUTCOME_NONE
4713          );
4714          $coursecompetencies = course_competency::get_records_select($sql, $params);
4715  
4716          $course = get_course($event->courseid);
4717          $courseshortname = format_string($course->shortname, null, array('context' => $event->contextid));
4718  
4719          foreach ($coursecompetencies as $coursecompetency) {
4720  
4721              $outcome = $coursecompetency->get('ruleoutcome');
4722              $action = null;
4723              $recommend = false;
4724              $strdesc = 'evidence_coursecompleted';
4725  
4726              if ($outcome == course_module_competency::OUTCOME_NONE) {
4727                  continue;
4728              }
4729              if ($outcome == course_competency::OUTCOME_EVIDENCE) {
4730                  $action = evidence::ACTION_LOG;
4731  
4732              } else if ($outcome == course_competency::OUTCOME_RECOMMEND) {
4733                  $action = evidence::ACTION_LOG;
4734                  $recommend = true;
4735  
4736              } else if ($outcome == course_competency::OUTCOME_COMPLETE) {
4737                  $action = evidence::ACTION_COMPLETE;
4738  
4739              } else {
4740                  throw new moodle_exception('Unexpected rule outcome: ' + $outcome);
4741              }
4742  
4743              static::add_evidence(
4744                  $event->relateduserid,
4745                  $coursecompetency->get('competencyid'),
4746                  $event->contextid,
4747                  $action,
4748                  $strdesc,
4749                  'core_competency',
4750                  $courseshortname,
4751                  $recommend,
4752                  $event->get_url()
4753              );
4754          }
4755      }
4756  
4757      /**
4758       * Action to perform when a course module is deleted.
4759       *
4760       * Do not call this directly, this is reserved for core use.
4761       *
4762       * @param stdClass $cm The CM object.
4763       * @return void
4764       */
4765      public static function hook_course_module_deleted(stdClass $cm) {
4766          global $DB;
4767          $DB->delete_records(course_module_competency::TABLE, array('cmid' => $cm->id));
4768      }
4769  
4770      /**
4771       * Action to perform when a course is deleted.
4772       *
4773       * Do not call this directly, this is reserved for core use.
4774       *
4775       * @param stdClass $course The course object.
4776       * @return void
4777       */
4778      public static function hook_course_deleted(stdClass $course) {
4779          global $DB;
4780          $DB->delete_records(course_competency::TABLE, array('courseid' => $course->id));
4781          $DB->delete_records(course_competency_settings::TABLE, array('courseid' => $course->id));
4782          $DB->delete_records(user_competency_course::TABLE, array('courseid' => $course->id));
4783      }
4784  
4785      /**
4786       * Action to perform when a course is being reset.
4787       *
4788       * Do not call this directly, this is reserved for core use.
4789       *
4790       * @param int $courseid The course ID.
4791       * @return void
4792       */
4793      public static function hook_course_reset_competency_ratings($courseid) {
4794          global $DB;
4795          $DB->delete_records(user_competency_course::TABLE, array('courseid' => $courseid));
4796      }
4797  
4798      /**
4799       * Action to perform when a cohort is deleted.
4800       *
4801       * Do not call this directly, this is reserved for core use.
4802       *
4803       * @param \stdClass $cohort The cohort object.
4804       * @return void
4805       */
4806      public static function hook_cohort_deleted(\stdClass $cohort) {
4807          global $DB;
4808          $DB->delete_records(template_cohort::TABLE, array('cohortid' => $cohort->id));
4809      }
4810  
4811      /**
4812       * Action to perform when a user is deleted.
4813       *
4814       * @param int $userid The user id.
4815       */
4816      public static function hook_user_deleted($userid) {
4817          global $DB;
4818  
4819          $usercompetencies = $DB->get_records(user_competency::TABLE, ['userid' => $userid], '', 'id');
4820          foreach ($usercompetencies as $usercomp) {
4821              $DB->delete_records(evidence::TABLE, ['usercompetencyid' => $usercomp->id]);
4822          }
4823  
4824          $DB->delete_records(user_competency::TABLE, ['userid' => $userid]);
4825          $DB->delete_records(user_competency_course::TABLE, ['userid' => $userid]);
4826          $DB->delete_records(user_competency_plan::TABLE, ['userid' => $userid]);
4827  
4828          // Delete any associated files.
4829          $fs = get_file_storage();
4830          $context = context_user::instance($userid);
4831          $userevidences = $DB->get_records(user_evidence::TABLE, ['userid' => $userid], '', 'id');
4832          foreach ($userevidences as $userevidence) {
4833              $DB->delete_records(user_evidence_competency::TABLE, ['userevidenceid' => $userevidence->id]);
4834              $DB->delete_records(user_evidence::TABLE, ['id' => $userevidence->id]);
4835              $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $userevidence->id);
4836          }
4837  
4838          $userplans = $DB->get_records(plan::TABLE, ['userid' => $userid], '', 'id');
4839          foreach ($userplans as $userplan) {
4840              $DB->delete_records(plan_competency::TABLE, ['planid' => $userplan->id]);
4841              $DB->delete_records(plan::TABLE, ['id' => $userplan->id]);
4842          }
4843      }
4844  
4845      /**
4846       * Manually grade a user competency.
4847       *
4848       * @param int $userid
4849       * @param int $competencyid
4850       * @param int $grade
4851       * @param string $note A note to attach to the evidence
4852       * @return array of \core_competency\user_competency
4853       */
4854      public static function grade_competency($userid, $competencyid, $grade, $note = null) {
4855          global $USER;
4856          static::require_enabled();
4857  
4858          $uc = static::get_user_competency($userid, $competencyid);
4859          $context = $uc->get_context();
4860          if (!user_competency::can_grade_user($uc->get('userid'))) {
4861              throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', '');
4862          }
4863  
4864          // Throws exception if competency not in plan.
4865          $competency = $uc->get_competency();
4866          $competencycontext = $competency->get_context();
4867          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
4868                  $competencycontext)) {
4869              throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', '');
4870          }
4871  
4872          $action = evidence::ACTION_OVERRIDE;
4873          $desckey = 'evidence_manualoverride';
4874  
4875          $result = self::add_evidence($uc->get('userid'),
4876                                    $competency,
4877                                    $context->id,
4878                                    $action,
4879                                    $desckey,
4880                                    'core_competency',
4881                                    null,
4882                                    false,
4883                                    null,
4884                                    $grade,
4885                                    $USER->id,
4886                                    $note);
4887          if ($result) {
4888              $uc->read();
4889              $event = \core\event\competency_user_competency_rated::create_from_user_competency($uc);
4890              $event->trigger();
4891          }
4892          return $result;
4893      }
4894  
4895      /**
4896       * Manually grade a user competency from the plans page.
4897       *
4898       * @param mixed $planorid
4899       * @param int $competencyid
4900       * @param int $grade
4901       * @param string $note A note to attach to the evidence
4902       * @return array of \core_competency\user_competency
4903       */
4904      public static function grade_competency_in_plan($planorid, $competencyid, $grade, $note = null) {
4905          global $USER;
4906          static::require_enabled();
4907  
4908          $plan = $planorid;
4909          if (!is_object($planorid)) {
4910              $plan = new plan($planorid);
4911          }
4912  
4913          $context = $plan->get_context();
4914          if (!user_competency::can_grade_user($plan->get('userid'))) {
4915              throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', '');
4916          }
4917  
4918          // Throws exception if competency not in plan.
4919          $competency = $plan->get_competency($competencyid);
4920          $competencycontext = $competency->get_context();
4921          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
4922                  $competencycontext)) {
4923              throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', '');
4924          }
4925  
4926          $action = evidence::ACTION_OVERRIDE;
4927          $desckey = 'evidence_manualoverrideinplan';
4928  
4929          $result = self::add_evidence($plan->get('userid'),
4930                                    $competency,
4931                                    $context->id,
4932                                    $action,
4933                                    $desckey,
4934                                    'core_competency',
4935                                    $plan->get('name'),
4936                                    false,
4937                                    null,
4938                                    $grade,
4939                                    $USER->id,
4940                                    $note);
4941          if ($result) {
4942              $uc = static::get_user_competency($plan->get('userid'), $competency->get('id'));
4943              $event = \core\event\competency_user_competency_rated_in_plan::create_from_user_competency($uc, $plan->get('id'));
4944              $event->trigger();
4945          }
4946          return $result;
4947      }
4948  
4949      /**
4950       * Manually grade a user course competency from the course page.
4951       *
4952       * This may push the rating to the user competency
4953       * if the course is configured this way.
4954       *
4955       * @param mixed $courseorid
4956       * @param int $userid
4957       * @param int $competencyid
4958       * @param int $grade
4959       * @param string $note A note to attach to the evidence
4960       * @return array of \core_competency\user_competency
4961       */
4962      public static function grade_competency_in_course($courseorid, $userid, $competencyid, $grade, $note = null) {
4963          global $USER, $DB;
4964          static::require_enabled();
4965  
4966          $course = $courseorid;
4967          if (!is_object($courseorid)) {
4968              $course = $DB->get_record('course', array('id' => $courseorid));
4969          }
4970          $context = context_course::instance($course->id);
4971  
4972          // Check that we can view the user competency details in the course.
4973          if (!user_competency::can_read_user_in_course($userid, $course->id)) {
4974              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
4975          }
4976  
4977          // Validate the permission to grade.
4978          if (!user_competency::can_grade_user_in_course($userid, $course->id)) {
4979              throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', '');
4980          }
4981  
4982          // Check that competency is in course and visible to the current user.
4983          $competency = course_competency::get_competency($course->id, $competencyid);
4984          $competencycontext = $competency->get_context();
4985          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
4986                  $competencycontext)) {
4987              throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', '');
4988          }
4989  
4990          // Check that the user is enrolled in the course, and is "gradable".
4991          if (!is_enrolled($context, $userid, 'moodle/competency:coursecompetencygradable')) {
4992              throw new coding_exception('The competency may not be rated at this time.');
4993          }
4994  
4995          $action = evidence::ACTION_OVERRIDE;
4996          $desckey = 'evidence_manualoverrideincourse';
4997  
4998          $result = self::add_evidence($userid,
4999                                    $competency,
5000                                    $context->id,
5001                                    $action,
5002                                    $desckey,
5003                                    'core_competency',
5004                                    $context->get_context_name(),
5005                                    false,
5006                                    null,
5007                                    $grade,
5008                                    $USER->id,
5009                                    $note);
5010          if ($result) {
5011              $all = user_competency_course::get_multiple($userid, $course->id, array($competency->get('id')));
5012              $uc = reset($all);
5013              $event = \core\event\competency_user_competency_rated_in_course::create_from_user_competency_course($uc);
5014              $event->trigger();
5015          }
5016          return $result;
5017      }
5018  
5019      /**
5020       * Count the plans in the template, filtered by status.
5021       *
5022       * Requires moodle/competency:templateview capability at the system context.
5023       *
5024       * @param mixed $templateorid The id or the template.
5025       * @param int $status One of the plan status constants (or 0 for all plans).
5026       * @return int
5027       */
5028      public static function count_plans_for_template($templateorid, $status = 0) {
5029          static::require_enabled();
5030          $template = $templateorid;
5031          if (!is_object($template)) {
5032              $template = new template($template);
5033          }
5034  
5035          // First we do a permissions check.
5036          if (!$template->can_read()) {
5037              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
5038                  'nopermissions', '');
5039          }
5040  
5041          return plan::count_records_for_template($template->get('id'), $status);
5042      }
5043  
5044      /**
5045       * Count the user-completency-plans in the template, optionally filtered by proficiency.
5046       *
5047       * Requires moodle/competency:templateview capability at the system context.
5048       *
5049       * @param mixed $templateorid The id or the template.
5050       * @param mixed $proficiency If true, filter by proficiency, if false filter by not proficient, if null - no filter.
5051       * @return int
5052       */
5053      public static function count_user_competency_plans_for_template($templateorid, $proficiency = null) {
5054          static::require_enabled();
5055          $template = $templateorid;
5056          if (!is_object($template)) {
5057              $template = new template($template);
5058          }
5059  
5060          // First we do a permissions check.
5061          if (!$template->can_read()) {
5062               throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
5063                  'nopermissions', '');
5064          }
5065  
5066          return user_competency_plan::count_records_for_template($template->get('id'), $proficiency);
5067      }
5068  
5069      /**
5070       * List the plans in the template, filtered by status.
5071       *
5072       * Requires moodle/competency:templateview capability at the system context.
5073       *
5074       * @param mixed $templateorid The id or the template.
5075       * @param int $status One of the plan status constants (or 0 for all plans).
5076       * @param int $skip The number of records to skip
5077       * @param int $limit The max number of records to return
5078       * @return plan[]
5079       */
5080      public static function list_plans_for_template($templateorid, $status = 0, $skip = 0, $limit = 100) {
5081          $template = $templateorid;
5082          if (!is_object($template)) {
5083              $template = new template($template);
5084          }
5085  
5086          // First we do a permissions check.
5087          if (!$template->can_read()) {
5088               throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
5089                  'nopermissions', '');
5090          }
5091  
5092          return plan::get_records_for_template($template->get('id'), $status, $skip, $limit);
5093      }
5094  
5095      /**
5096       * Get the most often not completed competency for this course.
5097       *
5098       * Requires moodle/competency:coursecompetencyview capability at the course context.
5099       *
5100       * @param int $courseid The course id
5101       * @param int $skip The number of records to skip
5102       * @param int $limit The max number of records to return
5103       * @return competency[]
5104       */
5105      public static function get_least_proficient_competencies_for_course($courseid, $skip = 0, $limit = 100) {
5106          static::require_enabled();
5107          $coursecontext = context_course::instance($courseid);
5108  
5109          if (!has_any_capability(array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'),
5110                  $coursecontext)) {
5111              throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
5112          }
5113  
5114          return user_competency_course::get_least_proficient_competencies_for_course($courseid, $skip, $limit);
5115      }
5116  
5117      /**
5118       * Get the most often not completed competency for this template.
5119       *
5120       * Requires moodle/competency:templateview capability at the system context.
5121       *
5122       * @param mixed $templateorid The id or the template.
5123       * @param int $skip The number of records to skip
5124       * @param int $limit The max number of records to return
5125       * @return competency[]
5126       */
5127      public static function get_least_proficient_competencies_for_template($templateorid, $skip = 0, $limit = 100) {
5128          static::require_enabled();
5129          $template = $templateorid;
5130          if (!is_object($template)) {
5131              $template = new template($template);
5132          }
5133  
5134          // First we do a permissions check.
5135          if (!$template->can_read()) {
5136              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
5137                  'nopermissions', '');
5138          }
5139  
5140          return user_competency_plan::get_least_proficient_competencies_for_template($template->get('id'), $skip, $limit);
5141      }
5142  
5143      /**
5144       * Template event viewed.
5145       *
5146       * Requires moodle/competency:templateview capability at the system context.
5147       *
5148       * @param mixed $templateorid The id or the template.
5149       * @return boolean
5150       */
5151      public static function template_viewed($templateorid) {
5152          static::require_enabled();
5153          $template = $templateorid;
5154          if (!is_object($template)) {
5155              $template = new template($template);
5156          }
5157  
5158          // First we do a permissions check.
5159          if (!$template->can_read()) {
5160              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
5161                  'nopermissions', '');
5162          }
5163  
5164          // Trigger a template viewed event.
5165          \core\event\competency_template_viewed::create_from_template($template)->trigger();
5166  
5167          return true;
5168      }
5169  
5170      /**
5171       * Get the competency settings for a course.
5172       *
5173       * Requires moodle/competency:coursecompetencyview capability at the course context.
5174       *
5175       * @param int $courseid The course id
5176       * @return course_competency_settings
5177       */
5178      public static function read_course_competency_settings($courseid) {
5179          static::require_enabled();
5180  
5181          // First we do a permissions check.
5182          if (!course_competency_settings::can_read($courseid)) {
5183              $context = context_course::instance($courseid);
5184              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
5185          }
5186  
5187          return course_competency_settings::get_by_courseid($courseid);
5188      }
5189  
5190      /**
5191       * Update the competency settings for a course.
5192       *
5193       * Requires moodle/competency:coursecompetencyconfigure capability at the course context.
5194       *
5195       * @param int $courseid The course id
5196       * @param stdClass $settings List of settings. The only valid setting ATM is pushratginstouserplans (boolean).
5197       * @return bool
5198       */
5199      public static function update_course_competency_settings($courseid, $settings) {
5200          static::require_enabled();
5201  
5202          $settings = (object) $settings;
5203  
5204          // Get all the valid settings.
5205          $pushratingstouserplans = isset($settings->pushratingstouserplans) ? $settings->pushratingstouserplans : false;
5206  
5207          // First we do a permissions check.
5208          if (!course_competency_settings::can_manage_course($courseid)) {
5209              $context = context_course::instance($courseid);
5210              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyconfigure', 'nopermissions', '');
5211          }
5212  
5213          $exists = course_competency_settings::get_record(array('courseid' => $courseid));
5214  
5215          // Now update or insert.
5216          if ($exists) {
5217              $settings = $exists;
5218              $settings->set('pushratingstouserplans', $pushratingstouserplans);
5219              return $settings->update();
5220          } else {
5221              $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $pushratingstouserplans);
5222              $settings = new course_competency_settings(0, $data);
5223              $result = $settings->create();
5224              return !empty($result);
5225          }
5226      }
5227  
5228  
5229      /**
5230       * Function used to return a list of users where the given user has a particular capability.
5231       *
5232       * This is used e.g. to find all the users where someone is able to manage their learning plans,
5233       * it also would be useful for mentees etc.
5234       *
5235       * @param string $capability - The capability string we are filtering for. If '' is passed,
5236       *                             an always matching filter is returned.
5237       * @param int $userid - The user id we are using for the access checks. Defaults to current user.
5238       * @param int $type - The type of named params to return (passed to $DB->get_in_or_equal).
5239       * @param string $prefix - The type prefix for the db table (passed to $DB->get_in_or_equal).
5240       * @return list($sql, $params) Same as $DB->get_in_or_equal().
5241       * @todo MDL-52243 Move this function to lib/accesslib.php
5242       */
5243      public static function filter_users_with_capability_on_user_context_sql($capability, $userid = 0, $type = SQL_PARAMS_QM,
5244                                                                              $prefix='param') {
5245  
5246          global $USER, $DB;
5247          $allresultsfilter = array('> 0', array());
5248          $noresultsfilter = array('= -1', array());
5249  
5250          if (empty($capability)) {
5251              return $allresultsfilter;
5252          }
5253  
5254          if (!$capinfo = get_capability_info($capability)) {
5255              throw new coding_exception('Capability does not exist: ' . $capability);
5256          }
5257  
5258          if (empty($userid)) {
5259              $userid = $USER->id;
5260          }
5261  
5262          // Make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
5263          if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
5264              if (isguestuser($userid) or $userid == 0) {
5265                  return $noresultsfilter;
5266              }
5267          }
5268  
5269          if (is_siteadmin($userid)) {
5270              // No filtering for site admins.
5271              return $allresultsfilter;
5272          }
5273  
5274          // Check capability on system level.
5275          $syscontext = context_system::instance();
5276          $hassystem = has_capability($capability, $syscontext, $userid);
5277  
5278          $access = get_user_roles_sitewide_accessdata($userid);
5279          // Build up a list of level 2 contexts (candidates to be user context).
5280          $filtercontexts = array();
5281          // Build list of roles to check overrides.
5282          $roles = array();
5283  
5284          foreach ($access['ra'] as $path => $role) {
5285              $parts = explode('/', $path);
5286              if (count($parts) == 3) {
5287                  $filtercontexts[$parts[2]] = $parts[2];
5288              } else if (count($parts) > 3) {
5289                  // We know this is not a user context because there is another path with more than 2 levels.
5290                  unset($filtercontexts[$parts[2]]);
5291              }
5292              $roles = array_merge($roles, $role);
5293          }
5294  
5295          // Add all contexts in which a role may be overidden.
5296          $rdefs = get_role_definitions($roles);
5297          foreach ($rdefs as $roledef) {
5298              foreach ($roledef as $path => $caps) {
5299                  if (!isset($caps[$capability])) {
5300                      // The capability is not mentioned, we can ignore.
5301                      continue;
5302                  }
5303                  $parts = explode('/', $path);
5304                  if (count($parts) === 3) {
5305                      // Only get potential user contexts, they only ever have 2 slashes /parentId/Id.
5306                      $filtercontexts[$parts[2]] = $parts[2];
5307                  }
5308              }
5309          }
5310  
5311          // No interesting contexts - return all or no results.
5312          if (empty($filtercontexts)) {
5313              if ($hassystem) {
5314                  return $allresultsfilter;
5315              } else {
5316                  return $noresultsfilter;
5317              }
5318          }
5319          // Fetch all interesting contexts for further examination.
5320          list($insql, $params) = $DB->get_in_or_equal($filtercontexts, SQL_PARAMS_NAMED);
5321          $params['level'] = CONTEXT_USER;
5322          $fields = context_helper::get_preload_record_columns_sql('ctx');
5323          $interestingcontexts = $DB->get_recordset_sql('SELECT ' . $fields . '
5324                                                         FROM {context} ctx
5325                                                         WHERE ctx.contextlevel = :level
5326                                                           AND ctx.id ' . $insql . '
5327                                                         ORDER BY ctx.id', $params);
5328          if ($hassystem) {
5329              // If allowed at system, search for exceptions prohibiting the capability at user context.
5330              $excludeusers = array();
5331              foreach ($interestingcontexts as $contextrecord) {
5332                  $candidateuserid = $contextrecord->ctxinstance;
5333                  context_helper::preload_from_record($contextrecord);
5334                  $usercontext = context_user::instance($candidateuserid);
5335                  // Has capability should use the data already preloaded.
5336                  if (!has_capability($capability, $usercontext, $userid)) {
5337                      $excludeusers[$candidateuserid] = $candidateuserid;
5338                  }
5339              }
5340  
5341              // Construct SQL excluding users with this role assigned for this user.
5342              if (empty($excludeusers)) {
5343                  $interestingcontexts->close();
5344                  return $allresultsfilter;
5345              }
5346              list($sql, $params) = $DB->get_in_or_equal($excludeusers, $type, $prefix, false);
5347          } else {
5348              // If not allowed at system, search for exceptions allowing the capability at user context.
5349              $allowusers = array();
5350              foreach ($interestingcontexts as $contextrecord) {
5351                  $candidateuserid = $contextrecord->ctxinstance;
5352                  context_helper::preload_from_record($contextrecord);
5353                  $usercontext = context_user::instance($candidateuserid);
5354                  // Has capability should use the data already preloaded.
5355                  if (has_capability($capability, $usercontext, $userid)) {
5356                      $allowusers[$candidateuserid] = $candidateuserid;
5357                  }
5358              }
5359  
5360              // Construct SQL excluding users with this role assigned for this user.
5361              if (empty($allowusers)) {
5362                  $interestingcontexts->close();
5363                  return $noresultsfilter;
5364              }
5365              list($sql, $params) = $DB->get_in_or_equal($allowusers, $type, $prefix);
5366          }
5367          $interestingcontexts->close();
5368  
5369          // Return the goods!.
5370          return array($sql, $params);
5371      }
5372  
5373  }