Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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       * @param bool $overridegrade If true, will override existing grades in related competencies.
1586       * @return bool True on success.
1587       */
1588      public static function set_course_module_competency_ruleoutcome($coursemodulecompetencyorid, $ruleoutcome,
1589          $overridegrade = false) {
1590          static::require_enabled();
1591          $coursemodulecompetency = $coursemodulecompetencyorid;
1592          if (!is_object($coursemodulecompetency)) {
1593              $coursemodulecompetency = new course_module_competency($coursemodulecompetencyorid);
1594          }
1595  
1596          $cm = get_coursemodule_from_id('', $coursemodulecompetency->get('cmid'), 0, true, MUST_EXIST);
1597  
1598          self::validate_course_module($cm);
1599          $context = context_module::instance($cm->id);
1600  
1601          require_capability('moodle/competency:coursecompetencymanage', $context);
1602  
1603          $coursemodulecompetency->set('ruleoutcome', $ruleoutcome);
1604          $coursemodulecompetency->set('overridegrade', $overridegrade);
1605  
1606          return $coursemodulecompetency->update();
1607      }
1608  
1609      /**
1610       * Add a competency to this course.
1611       *
1612       * @param int $courseid The id of the course
1613       * @param int $competencyid The id of the competency
1614       * @return bool
1615       */
1616      public static function add_competency_to_course($courseid, $competencyid) {
1617          static::require_enabled();
1618          // Check the user have access to the course.
1619          self::validate_course($courseid);
1620  
1621          // First we do a permissions check.
1622          $context = context_course::instance($courseid);
1623  
1624          require_capability('moodle/competency:coursecompetencymanage', $context);
1625  
1626          $record = new stdClass();
1627          $record->courseid = $courseid;
1628          $record->competencyid = $competencyid;
1629  
1630          $competency = new competency($competencyid);
1631  
1632          // Can not add a competency that belong to a hidden framework.
1633          if ($competency->get_framework()->get('visible') == false) {
1634              throw new coding_exception('A competency belonging to hidden framework can not be linked to course');
1635          }
1636  
1637          $coursecompetency = new course_competency();
1638          $exists = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyid));
1639          if (!$exists) {
1640              $coursecompetency->from_record($record);
1641              if ($coursecompetency->create()) {
1642                  return true;
1643              }
1644          }
1645          return false;
1646      }
1647  
1648      /**
1649       * Remove a competency from this course.
1650       *
1651       * @param int $courseid The id of the course
1652       * @param int $competencyid The id of the competency
1653       * @return bool
1654       */
1655      public static function remove_competency_from_course($courseid, $competencyid) {
1656          static::require_enabled();
1657          // Check the user have access to the course.
1658          self::validate_course($courseid);
1659  
1660          // First we do a permissions check.
1661          $context = context_course::instance($courseid);
1662  
1663          require_capability('moodle/competency:coursecompetencymanage', $context);
1664  
1665          $record = new stdClass();
1666          $record->courseid = $courseid;
1667          $record->competencyid = $competencyid;
1668  
1669          $coursecompetency = new course_competency();
1670          $exists = course_competency::get_record(array('courseid' => $courseid, 'competencyid' => $competencyid));
1671          if ($exists) {
1672              // Delete all course_module_competencies for this competency in this course.
1673              $cmcs = course_module_competency::get_records_by_competencyid_in_course($competencyid, $courseid);
1674              foreach ($cmcs as $cmc) {
1675                  $cmc->delete();
1676              }
1677              return $exists->delete();
1678          }
1679          return false;
1680      }
1681  
1682      /**
1683       * Move the course competency up or down in the display list.
1684       *
1685       * Requires moodle/competency:coursecompetencymanage capability at the course context.
1686       *
1687       * @param int $courseid The course
1688       * @param int $competencyidfrom The id of the competency we are moving.
1689       * @param int $competencyidto The id of the competency we are moving to.
1690       * @return boolean
1691       */
1692      public static function reorder_course_competency($courseid, $competencyidfrom, $competencyidto) {
1693          static::require_enabled();
1694          // Check the user have access to the course.
1695          self::validate_course($courseid);
1696  
1697          // First we do a permissions check.
1698          $context = context_course::instance($courseid);
1699  
1700          require_capability('moodle/competency:coursecompetencymanage', $context);
1701  
1702          $down = true;
1703          $coursecompetency = new course_competency();
1704          $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidfrom));
1705          if (count($matches) == 0) {
1706               throw new coding_exception('The link does not exist');
1707          }
1708  
1709          $competencyfrom = array_pop($matches);
1710          $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidto));
1711          if (count($matches) == 0) {
1712               throw new coding_exception('The link does not exist');
1713          }
1714  
1715          $competencyto = array_pop($matches);
1716  
1717          $all = $coursecompetency->get_records(array('courseid' => $courseid), 'sortorder', 'ASC', 0, 0);
1718  
1719          if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
1720              // We are moving up, so put it before the "to" item.
1721              $down = false;
1722          }
1723  
1724          foreach ($all as $id => $coursecompetency) {
1725              $sort = $coursecompetency->get('sortorder');
1726              if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
1727                  $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') - 1);
1728                  $coursecompetency->update();
1729              } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
1730                  $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') + 1);
1731                  $coursecompetency->update();
1732              }
1733          }
1734          $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
1735          return $competencyfrom->update();
1736      }
1737  
1738      /**
1739       * Update ruleoutcome value for a course competency.
1740       *
1741       * @param int|course_competency $coursecompetencyorid The course_competency, or its ID.
1742       * @param int $ruleoutcome The value of ruleoutcome.
1743       * @return bool True on success.
1744       */
1745      public static function set_course_competency_ruleoutcome($coursecompetencyorid, $ruleoutcome) {
1746          static::require_enabled();
1747          $coursecompetency = $coursecompetencyorid;
1748          if (!is_object($coursecompetency)) {
1749              $coursecompetency = new course_competency($coursecompetencyorid);
1750          }
1751  
1752          $courseid = $coursecompetency->get('courseid');
1753          self::validate_course($courseid);
1754          $coursecontext = context_course::instance($courseid);
1755  
1756          require_capability('moodle/competency:coursecompetencymanage', $coursecontext);
1757  
1758          $coursecompetency->set('ruleoutcome', $ruleoutcome);
1759          return $coursecompetency->update();
1760      }
1761  
1762      /**
1763       * Create a learning plan template from a record containing all the data for the class.
1764       *
1765       * Requires moodle/competency:templatemanage capability.
1766       *
1767       * @param stdClass $record Record containing all the data for an instance of the class.
1768       * @return template
1769       */
1770      public static function create_template(stdClass $record) {
1771          static::require_enabled();
1772          $template = new template(0, $record);
1773  
1774          // First we do a permissions check.
1775          if (!$template->can_manage()) {
1776              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1777                  'nopermissions', '');
1778          }
1779  
1780          // OK - all set.
1781          $template = $template->create();
1782  
1783          // Trigger a template created event.
1784          \core\event\competency_template_created::create_from_template($template)->trigger();
1785  
1786          return $template;
1787      }
1788  
1789      /**
1790       * Duplicate a learning plan template.
1791       *
1792       * Requires moodle/competency:templatemanage capability at the template context.
1793       *
1794       * @param int $id the template id.
1795       * @return template
1796       */
1797      public static function duplicate_template($id) {
1798          static::require_enabled();
1799          $template = new template($id);
1800  
1801          // First we do a permissions check.
1802          if (!$template->can_manage()) {
1803              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1804                  'nopermissions', '');
1805          }
1806  
1807          // OK - all set.
1808          $competencies = template_competency::list_competencies($id, false);
1809  
1810          // Adding the suffix copy.
1811          $template->set('shortname', get_string('duplicateditemname', 'core_competency', $template->get('shortname')));
1812          $template->set('id', 0);
1813  
1814          $duplicatedtemplate = $template->create();
1815  
1816          // Associate each competency for the duplicated template.
1817          foreach ($competencies as $competency) {
1818              self::add_competency_to_template($duplicatedtemplate->get('id'), $competency->get('id'));
1819          }
1820  
1821          // Trigger a template created event.
1822          \core\event\competency_template_created::create_from_template($duplicatedtemplate)->trigger();
1823  
1824          return $duplicatedtemplate;
1825      }
1826  
1827      /**
1828       * Delete a learning plan template by id.
1829       * If the learning plan template has associated cohorts they will be deleted.
1830       *
1831       * Requires moodle/competency:templatemanage capability.
1832       *
1833       * @param int $id The record to delete.
1834       * @param boolean $deleteplans True to delete plans associaated to template, false to unlink them.
1835       * @return boolean
1836       */
1837      public static function delete_template($id, $deleteplans = true) {
1838          global $DB;
1839          static::require_enabled();
1840          $template = new template($id);
1841  
1842          // First we do a permissions check.
1843          if (!$template->can_manage()) {
1844              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1845                  'nopermissions', '');
1846          }
1847  
1848          $transaction = $DB->start_delegated_transaction();
1849          $success = true;
1850  
1851          // Check if there are cohorts associated.
1852          $templatecohorts = template_cohort::get_relations_by_templateid($template->get('id'));
1853          foreach ($templatecohorts as $templatecohort) {
1854              $success = $templatecohort->delete();
1855              if (!$success) {
1856                  break;
1857              }
1858          }
1859  
1860          // Still OK, delete or unlink the plans from the template.
1861          if ($success) {
1862              $plans = plan::get_records(array('templateid' => $template->get('id')));
1863              foreach ($plans as $plan) {
1864                  $success = $deleteplans ? self::delete_plan($plan->get('id')) : self::unlink_plan_from_template($plan);
1865                  if (!$success) {
1866                      break;
1867                  }
1868              }
1869          }
1870  
1871          // Still OK, delete the template comptencies.
1872          if ($success) {
1873              $success = template_competency::delete_by_templateid($template->get('id'));
1874          }
1875  
1876          // OK - all set.
1877          if ($success) {
1878              // Create a template deleted event.
1879              $event = \core\event\competency_template_deleted::create_from_template($template);
1880  
1881              $success = $template->delete();
1882          }
1883  
1884          if ($success) {
1885              // Trigger a template deleted event.
1886              $event->trigger();
1887  
1888              // Commit the transaction.
1889              $transaction->allow_commit();
1890          } else {
1891              $transaction->rollback(new moodle_exception('Error while deleting the template.'));
1892          }
1893  
1894          return $success;
1895      }
1896  
1897      /**
1898       * Update the details for a learning plan template.
1899       *
1900       * Requires moodle/competency:templatemanage capability.
1901       *
1902       * @param stdClass $record The new details for the template. Note - must contain an id that points to the template to update.
1903       * @return boolean
1904       */
1905      public static function update_template($record) {
1906          global $DB;
1907          static::require_enabled();
1908          $template = new template($record->id);
1909  
1910          // First we do a permissions check.
1911          if (!$template->can_manage()) {
1912              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1913                  'nopermissions', '');
1914  
1915          } else if (isset($record->contextid) && $record->contextid != $template->get('contextid')) {
1916              // We can never change the context of a template.
1917              throw new coding_exception('Changing the context of an existing tempalte is forbidden.');
1918  
1919          }
1920  
1921          $updateplans = false;
1922          $before = $template->to_record();
1923  
1924          $template->from_record($record);
1925          $after = $template->to_record();
1926  
1927          // Should we update the related plans?
1928          if ($before->duedate != $after->duedate ||
1929                  $before->shortname != $after->shortname ||
1930                  $before->description != $after->description ||
1931                  $before->descriptionformat != $after->descriptionformat) {
1932              $updateplans = true;
1933          }
1934  
1935          $transaction = $DB->start_delegated_transaction();
1936          $success = $template->update();
1937  
1938          if (!$success) {
1939              $transaction->rollback(new moodle_exception('Error while updating the template.'));
1940              return $success;
1941          }
1942  
1943          // Trigger a template updated event.
1944          \core\event\competency_template_updated::create_from_template($template)->trigger();
1945  
1946          if ($updateplans) {
1947              plan::update_multiple_from_template($template);
1948          }
1949  
1950          $transaction->allow_commit();
1951  
1952          return $success;
1953      }
1954  
1955      /**
1956       * Read a the details for a single learning plan template and return a record.
1957       *
1958       * Requires moodle/competency:templateview capability at the system context.
1959       *
1960       * @param int $id The id of the template to read.
1961       * @return template
1962       */
1963      public static function read_template($id) {
1964          static::require_enabled();
1965          $template = new template($id);
1966          $context = $template->get_context();
1967  
1968          // First we do a permissions check.
1969          if (!$template->can_read()) {
1970               throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
1971                  'nopermissions', '');
1972          }
1973  
1974          // OK - all set.
1975          return $template;
1976      }
1977  
1978      /**
1979       * Perform a search based on the provided filters and return a paginated list of records.
1980       *
1981       * Requires moodle/competency:templateview capability at the system context.
1982       *
1983       * @param string $sort The column to sort on
1984       * @param string $order ('ASC' or 'DESC')
1985       * @param int $skip Number of records to skip (pagination)
1986       * @param int $limit Max of records to return (pagination)
1987       * @param context $context The parent context of the frameworks.
1988       * @param string $includes Defines what other contexts to fetch frameworks from.
1989       *                         Accepted values are:
1990       *                          - children: All descendants
1991       *                          - parents: All parents, grand parents, etc...
1992       *                          - self: Context passed only.
1993       * @param bool $onlyvisible If should list only visible templates
1994       * @return array of competency_framework
1995       */
1996      public static function list_templates($sort, $order, $skip, $limit, $context, $includes = 'children', $onlyvisible = false) {
1997          global $DB;
1998          static::require_enabled();
1999  
2000          // Get all the relevant contexts.
2001          $contexts = self::get_related_contexts($context, $includes,
2002              array('moodle/competency:templateview', 'moodle/competency:templatemanage'));
2003  
2004          // First we do a permissions check.
2005          if (empty($contexts)) {
2006               throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
2007          }
2008  
2009          // Make the order by.
2010          $orderby = '';
2011          if (!empty($sort)) {
2012              $orderby = $sort . ' ' . $order;
2013          }
2014  
2015          // OK - all set.
2016          $template = new template();
2017          list($insql, $params) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
2018          $select = "contextid $insql";
2019  
2020          if ($onlyvisible) {
2021              $select .= " AND visible = :visible";
2022              $params['visible'] = 1;
2023          }
2024          return $template->get_records_select($select, $params, $orderby, '*', $skip, $limit);
2025      }
2026  
2027      /**
2028       * Perform a search based on the provided filters and return how many results there are.
2029       *
2030       * Requires moodle/competency:templateview capability at the system context.
2031       *
2032       * @param context $context The parent context of the frameworks.
2033       * @param string $includes Defines what other contexts to fetch frameworks from.
2034       *                         Accepted values are:
2035       *                          - children: All descendants
2036       *                          - parents: All parents, grand parents, etc...
2037       *                          - self: Context passed only.
2038       * @return int
2039       */
2040      public static function count_templates($context, $includes) {
2041          global $DB;
2042          static::require_enabled();
2043  
2044          // First we do a permissions check.
2045          $contexts = self::get_related_contexts($context, $includes,
2046              array('moodle/competency:templateview', 'moodle/competency:templatemanage'));
2047  
2048          if (empty($contexts)) {
2049               throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
2050          }
2051  
2052          // OK - all set.
2053          $template = new template();
2054          list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
2055          return $template->count_records_select("contextid $insql", $inparams);
2056      }
2057  
2058      /**
2059       * Count all the templates using a competency.
2060       *
2061       * @param int $competencyid The id of the competency to check.
2062       * @return int
2063       */
2064      public static function count_templates_using_competency($competencyid) {
2065          static::require_enabled();
2066          // First we do a permissions check.
2067          $context = context_system::instance();
2068          $onlyvisible = 1;
2069  
2070          $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
2071          if (!has_any_capability($capabilities, $context)) {
2072               throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
2073          }
2074  
2075          if (has_capability('moodle/competency:templatemanage', $context)) {
2076              $onlyvisible = 0;
2077          }
2078  
2079          // OK - all set.
2080          return template_competency::count_templates($competencyid, $onlyvisible);
2081      }
2082  
2083      /**
2084       * List all the learning plan templatesd using a competency.
2085       *
2086       * @param int $competencyid The id of the competency to check.
2087       * @return array[stdClass] Array of stdClass containing id and shortname.
2088       */
2089      public static function list_templates_using_competency($competencyid) {
2090          static::require_enabled();
2091          // First we do a permissions check.
2092          $context = context_system::instance();
2093          $onlyvisible = 1;
2094  
2095          $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
2096          if (!has_any_capability($capabilities, $context)) {
2097               throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
2098          }
2099  
2100          if (has_capability('moodle/competency:templatemanage', $context)) {
2101              $onlyvisible = 0;
2102          }
2103  
2104          // OK - all set.
2105          return template_competency::list_templates($competencyid, $onlyvisible);
2106  
2107      }
2108  
2109      /**
2110       * Count all the competencies in a learning plan template.
2111       *
2112       * @param  template|int $templateorid The template or its ID.
2113       * @return int
2114       */
2115      public static function count_competencies_in_template($templateorid) {
2116          static::require_enabled();
2117          // First we do a permissions check.
2118          $template = $templateorid;
2119          if (!is_object($template)) {
2120              $template = new template($template);
2121          }
2122  
2123          if (!$template->can_read()) {
2124              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2125                  'nopermissions', '');
2126          }
2127  
2128          // OK - all set.
2129          return template_competency::count_competencies($template->get('id'));
2130      }
2131  
2132      /**
2133       * Count all the competencies in a learning plan template with no linked courses.
2134       *
2135       * @param  template|int $templateorid The template or its ID.
2136       * @return int
2137       */
2138      public static function count_competencies_in_template_with_no_courses($templateorid) {
2139          // First we do a permissions check.
2140          $template = $templateorid;
2141          if (!is_object($template)) {
2142              $template = new template($template);
2143          }
2144  
2145          if (!$template->can_read()) {
2146              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2147                  'nopermissions', '');
2148          }
2149  
2150          // OK - all set.
2151          return template_competency::count_competencies_with_no_courses($template->get('id'));
2152      }
2153  
2154      /**
2155       * List all the competencies in a template.
2156       *
2157       * @param  template|int $templateorid The template or its ID.
2158       * @return array of competencies
2159       */
2160      public static function list_competencies_in_template($templateorid) {
2161          static::require_enabled();
2162          // First we do a permissions check.
2163          $template = $templateorid;
2164          if (!is_object($template)) {
2165              $template = new template($template);
2166          }
2167  
2168          if (!$template->can_read()) {
2169              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2170                  'nopermissions', '');
2171          }
2172  
2173          // OK - all set.
2174          return template_competency::list_competencies($template->get('id'));
2175      }
2176  
2177      /**
2178       * Add a competency to this template.
2179       *
2180       * @param int $templateid The id of the template
2181       * @param int $competencyid The id of the competency
2182       * @return bool
2183       */
2184      public static function add_competency_to_template($templateid, $competencyid) {
2185          static::require_enabled();
2186          // First we do a permissions check.
2187          $template = new template($templateid);
2188          if (!$template->can_manage()) {
2189              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
2190                  'nopermissions', '');
2191          }
2192  
2193          $record = new stdClass();
2194          $record->templateid = $templateid;
2195          $record->competencyid = $competencyid;
2196  
2197          $competency = new competency($competencyid);
2198  
2199          // Can not add a competency that belong to a hidden framework.
2200          if ($competency->get_framework()->get('visible') == false) {
2201              throw new coding_exception('A competency belonging to hidden framework can not be added');
2202          }
2203  
2204          $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
2205          if (!$exists) {
2206              $templatecompetency = new template_competency(0, $record);
2207              $templatecompetency->create();
2208              return true;
2209          }
2210          return false;
2211      }
2212  
2213      /**
2214       * Remove a competency from this template.
2215       *
2216       * @param int $templateid The id of the template
2217       * @param int $competencyid The id of the competency
2218       * @return bool
2219       */
2220      public static function remove_competency_from_template($templateid, $competencyid) {
2221          static::require_enabled();
2222          // First we do a permissions check.
2223          $template = new template($templateid);
2224          if (!$template->can_manage()) {
2225              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
2226                  'nopermissions', '');
2227          }
2228  
2229          $record = new stdClass();
2230          $record->templateid = $templateid;
2231          $record->competencyid = $competencyid;
2232  
2233          $competency = new competency($competencyid);
2234  
2235          $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
2236          if ($exists) {
2237              $link = array_pop($exists);
2238              return $link->delete();
2239          }
2240          return false;
2241      }
2242  
2243      /**
2244       * Move the template competency up or down in the display list.
2245       *
2246       * Requires moodle/competency:templatemanage capability at the system context.
2247       *
2248       * @param int $templateid The template id
2249       * @param int $competencyidfrom The id of the competency we are moving.
2250       * @param int $competencyidto The id of the competency we are moving to.
2251       * @return boolean
2252       */
2253      public static function reorder_template_competency($templateid, $competencyidfrom, $competencyidto) {
2254          static::require_enabled();
2255          $template = new template($templateid);
2256  
2257          // First we do a permissions check.
2258          if (!$template->can_manage()) {
2259              throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
2260                  'nopermissions', '');
2261          }
2262  
2263          $down = true;
2264          $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidfrom));
2265          if (count($matches) == 0) {
2266              throw new coding_exception('The link does not exist');
2267          }
2268  
2269          $competencyfrom = array_pop($matches);
2270          $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidto));
2271          if (count($matches) == 0) {
2272              throw new coding_exception('The link does not exist');
2273          }
2274  
2275          $competencyto = array_pop($matches);
2276  
2277          $all = template_competency::get_records(array('templateid' => $templateid), 'sortorder', 'ASC', 0, 0);
2278  
2279          if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
2280              // We are moving up, so put it before the "to" item.
2281              $down = false;
2282          }
2283  
2284          foreach ($all as $id => $templatecompetency) {
2285              $sort = $templatecompetency->get('sortorder');
2286              if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
2287                  $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') - 1);
2288                  $templatecompetency->update();
2289              } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
2290                  $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') + 1);
2291                  $templatecompetency->update();
2292              }
2293          }
2294          $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
2295          return $competencyfrom->update();
2296      }
2297  
2298      /**
2299       * Create a relation between a template and a cohort.
2300       *
2301       * This silently ignores when the relation already existed.
2302       *
2303       * @param  template|int $templateorid The template or its ID.
2304       * @param  stdClass|int $cohortorid   The cohort ot its ID.
2305       * @return template_cohort
2306       */
2307      public static function create_template_cohort($templateorid, $cohortorid) {
2308          global $DB;
2309          static::require_enabled();
2310  
2311          $template = $templateorid;
2312          if (!is_object($template)) {
2313              $template = new template($template);
2314          }
2315          require_capability('moodle/competency:templatemanage', $template->get_context());
2316  
2317          $cohort = $cohortorid;
2318          if (!is_object($cohort)) {
2319              $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
2320          }
2321  
2322          // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
2323          $cohortcontext = context::instance_by_id($cohort->contextid);
2324          if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
2325              throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
2326          }
2327  
2328          $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id);
2329          if (!$tplcohort->get('id')) {
2330              $tplcohort->create();
2331          }
2332  
2333          return $tplcohort;
2334      }
2335  
2336      /**
2337       * Remove a relation between a template and a cohort.
2338       *
2339       * @param  template|int $templateorid The template or its ID.
2340       * @param  stdClass|int $cohortorid   The cohort ot its ID.
2341       * @return boolean True on success or when the relation did not exist.
2342       */
2343      public static function delete_template_cohort($templateorid, $cohortorid) {
2344          global $DB;
2345          static::require_enabled();
2346  
2347          $template = $templateorid;
2348          if (!is_object($template)) {
2349              $template = new template($template);
2350          }
2351          require_capability('moodle/competency:templatemanage', $template->get_context());
2352  
2353          $cohort = $cohortorid;
2354          if (!is_object($cohort)) {
2355              $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
2356          }
2357  
2358          $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id);
2359          if (!$tplcohort->get('id')) {
2360              return true;
2361          }
2362  
2363          return $tplcohort->delete();
2364      }
2365  
2366      /**
2367       * Lists user plans.
2368       *
2369       * @param int $userid
2370       * @return \core_competency\plan[]
2371       */
2372      public static function list_user_plans($userid) {
2373          global $DB, $USER;
2374          static::require_enabled();
2375          $select = 'userid = :userid';
2376          $params = array('userid' => $userid);
2377          $context = context_user::instance($userid);
2378  
2379          // Check that we can read something here.
2380          if (!plan::can_read_user($userid) && !plan::can_read_user_draft($userid)) {
2381              throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
2382          }
2383  
2384          // The user cannot view the drafts.
2385          if (!plan::can_read_user_draft($userid)) {
2386              list($insql, $inparams) = $DB->get_in_or_equal(plan::get_draft_statuses(), SQL_PARAMS_NAMED, 'param', false);
2387              $select .= " AND status $insql";
2388              $params += $inparams;
2389          }
2390          // The user cannot view the non-drafts.
2391          if (!plan::can_read_user($userid)) {
2392              list($insql, $inparams) = $DB->get_in_or_equal(array(plan::STATUS_ACTIVE, plan::STATUS_COMPLETE),
2393                  SQL_PARAMS_NAMED, 'param', false);
2394              $select .= " AND status $insql";
2395              $params += $inparams;
2396          }
2397  
2398          return plan::get_records_select($select, $params, 'name ASC');
2399      }
2400  
2401      /**
2402       * List the plans to review.
2403       *
2404       * The method returns values in this format:
2405       *
2406       * array(
2407       *     'plans' => array(
2408       *         (stdClass)(
2409       *             'plan' => (plan),
2410       *             'template' => (template),
2411       *             'owner' => (stdClass)
2412       *         )
2413       *     ),
2414       *     'count' => (int)
2415       * )
2416       *
2417       * @param int $skip The number of records to skip.
2418       * @param int $limit The number of results to return.
2419       * @param int $userid The user we're getting the plans to review for.
2420       * @return array Containing the keys 'count', and 'plans'. The 'plans' key contains an object
2421       *               which contains 'plan', 'template' and 'owner'.
2422       */
2423      public static function list_plans_to_review($skip = 0, $limit = 100, $userid = null) {
2424          global $DB, $USER;
2425          static::require_enabled();
2426  
2427          if ($userid === null) {
2428              $userid = $USER->id;
2429          }
2430  
2431          $planfields = plan::get_sql_fields('p', 'plan_');
2432          $tplfields = template::get_sql_fields('t', 'tpl_');
2433          $usercols = array('id') + get_user_fieldnames();
2434          $userfields = array();
2435          foreach ($usercols as $field) {
2436              $userfields[] = "u." . $field . " AS usr_" . $field;
2437          }
2438          $userfields = implode(',', $userfields);
2439  
2440          $select = "SELECT $planfields, $tplfields, $userfields";
2441          $countselect = "SELECT COUNT('x')";
2442  
2443          $sql = "  FROM {" . plan::TABLE . "} p
2444                    JOIN {user} u
2445                      ON u.id = p.userid
2446               LEFT JOIN {" . template::TABLE . "} t
2447                      ON t.id = p.templateid
2448                   WHERE (p.status = :waitingforreview
2449                      OR (p.status = :inreview AND p.reviewerid = :reviewerid))
2450                     AND p.userid != :userid";
2451  
2452          $params = array(
2453              'waitingforreview' => plan::STATUS_WAITING_FOR_REVIEW,
2454              'inreview' => plan::STATUS_IN_REVIEW,
2455              'reviewerid' => $userid,
2456              'userid' => $userid
2457          );
2458  
2459          // Primary check to avoid the hard work of getting the users in which the user has permission.
2460          $count = $DB->count_records_sql($countselect . $sql, $params);
2461          if ($count < 1) {
2462              return array('count' => 0, 'plans' => array());
2463          }
2464  
2465          // TODO MDL-52243 Use core function.
2466          list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql('moodle/competency:planreview',
2467              $userid, SQL_PARAMS_NAMED);
2468          $sql .= " AND p.userid $insql";
2469          $params += $inparams;
2470  
2471          // Order by ID just to have some ordering in place.
2472          $ordersql = " ORDER BY p.id ASC";
2473  
2474          $plans = array();
2475          $records = $DB->get_recordset_sql($select . $sql . $ordersql, $params, $skip, $limit);
2476          foreach ($records as $record) {
2477              $plan = new plan(0, plan::extract_record($record, 'plan_'));
2478              $template = null;
2479  
2480              if ($plan->is_based_on_template()) {
2481                  $template = new template(0, template::extract_record($record, 'tpl_'));
2482              }
2483  
2484              $plans[] = (object) array(
2485                  'plan' => $plan,
2486                  'template' => $template,
2487                  'owner' => persistent::extract_record($record, 'usr_'),
2488              );
2489          }
2490          $records->close();
2491  
2492          return array(
2493              'count' => $DB->count_records_sql($countselect . $sql, $params),
2494              'plans' => $plans
2495          );
2496      }
2497  
2498      /**
2499       * Creates a learning plan based on the provided data.
2500       *
2501       * @param stdClass $record
2502       * @return \core_competency\plan
2503       */
2504      public static function create_plan(stdClass $record) {
2505          global $USER;
2506          static::require_enabled();
2507          $plan = new plan(0, $record);
2508  
2509          if ($plan->is_based_on_template()) {
2510              throw new coding_exception('To create a plan from a template use api::create_plan_from_template().');
2511          } else if ($plan->get('status') == plan::STATUS_COMPLETE) {
2512              throw new coding_exception('A plan cannot be created as complete.');
2513          }
2514  
2515          if (!$plan->can_manage()) {
2516              $context = context_user::instance($plan->get('userid'));
2517              throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
2518          }
2519  
2520          $plan->create();
2521  
2522          // Trigger created event.
2523          \core\event\competency_plan_created::create_from_plan($plan)->trigger();
2524          return $plan;
2525      }
2526  
2527      /**
2528       * Create a learning plan from a template.
2529       *
2530       * @param  mixed $templateorid The template object or ID.
2531       * @param  int $userid
2532       * @return false|\core_competency\plan Returns false when the plan already exists.
2533       */
2534      public static function create_plan_from_template($templateorid, $userid) {
2535          static::require_enabled();
2536          $template = $templateorid;
2537          if (!is_object($template)) {
2538              $template = new template($template);
2539          }
2540  
2541          // The user must be able to view the template to use it as a base for a plan.
2542          if (!$template->can_read()) {
2543              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2544                  'nopermissions', '');
2545          }
2546          // Can not create plan from a hidden template.
2547          if ($template->get('visible') == false) {
2548              throw new coding_exception('A plan can not be created from a hidden template');
2549          }
2550  
2551          // Convert the template to a plan.
2552          $record = $template->to_record();
2553          $record->templateid = $record->id;
2554          $record->userid = $userid;
2555          $record->name = $record->shortname;
2556          $record->status = plan::STATUS_ACTIVE;
2557  
2558          unset($record->id);
2559          unset($record->timecreated);
2560          unset($record->timemodified);
2561          unset($record->usermodified);
2562  
2563          // Remove extra keys.
2564          $properties = plan::properties_definition();
2565          foreach ($record as $key => $value) {
2566              if (!array_key_exists($key, $properties)) {
2567                  unset($record->$key);
2568              }
2569          }
2570  
2571          $plan = new plan(0, $record);
2572          if (!$plan->can_manage()) {
2573              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage',
2574                  'nopermissions', '');
2575          }
2576  
2577          // We first apply the permission checks as we wouldn't want to leak information by returning early that
2578          // the plan already exists.
2579          if (plan::record_exists_select('templateid = :templateid AND userid = :userid', array(
2580                  'templateid' => $template->get('id'), 'userid' => $userid))) {
2581              return false;
2582          }
2583  
2584          $plan->create();
2585  
2586          // Trigger created event.
2587          \core\event\competency_plan_created::create_from_plan($plan)->trigger();
2588          return $plan;
2589      }
2590  
2591      /**
2592       * Create learning plans from a template and cohort.
2593       *
2594       * @param  mixed $templateorid The template object or ID.
2595       * @param  int $cohortid The cohort ID.
2596       * @param  bool $recreateunlinked When true the plans that were unlinked from this template will be re-created.
2597       * @return int The number of plans created.
2598       */
2599      public static function create_plans_from_template_cohort($templateorid, $cohortid, $recreateunlinked = false) {
2600          global $DB, $CFG;
2601          static::require_enabled();
2602          require_once($CFG->dirroot . '/cohort/lib.php');
2603  
2604          $template = $templateorid;
2605          if (!is_object($template)) {
2606              $template = new template($template);
2607          }
2608  
2609          // The user must be able to view the template to use it as a base for a plan.
2610          if (!$template->can_read()) {
2611              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2612                  'nopermissions', '');
2613          }
2614  
2615          // Can not create plan from a hidden template.
2616          if ($template->get('visible') == false) {
2617              throw new coding_exception('A plan can not be created from a hidden template');
2618          }
2619  
2620          // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
2621          $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
2622          $cohortcontext = context::instance_by_id($cohort->contextid);
2623          if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
2624              throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
2625          }
2626  
2627          // Convert the template to a plan.
2628          $recordbase = $template->to_record();
2629          $recordbase->templateid = $recordbase->id;
2630          $recordbase->name = $recordbase->shortname;
2631          $recordbase->status = plan::STATUS_ACTIVE;
2632  
2633          unset($recordbase->id);
2634          unset($recordbase->timecreated);
2635          unset($recordbase->timemodified);
2636          unset($recordbase->usermodified);
2637  
2638          // Remove extra keys.
2639          $properties = plan::properties_definition();
2640          foreach ($recordbase as $key => $value) {
2641              if (!array_key_exists($key, $properties)) {
2642                  unset($recordbase->$key);
2643              }
2644          }
2645  
2646          // Create the plans.
2647          $created = 0;
2648          $userids = template_cohort::get_missing_plans($template->get('id'), $cohortid, $recreateunlinked);
2649          foreach ($userids as $userid) {
2650              $record = (object) (array) $recordbase;
2651              $record->userid = $userid;
2652  
2653              $plan = new plan(0, $record);
2654              if (!$plan->can_manage()) {
2655                  // Silently skip members where permissions are lacking.
2656                  continue;
2657              }
2658  
2659              $plan->create();
2660              // Trigger created event.
2661              \core\event\competency_plan_created::create_from_plan($plan)->trigger();
2662              $created++;
2663          }
2664  
2665          return $created;
2666      }
2667  
2668      /**
2669       * Unlink a plan from its template.
2670       *
2671       * @param  \core_competency\plan|int $planorid The plan or its ID.
2672       * @return bool
2673       */
2674      public static function unlink_plan_from_template($planorid) {
2675          global $DB;
2676          static::require_enabled();
2677  
2678          $plan = $planorid;
2679          if (!is_object($planorid)) {
2680              $plan = new plan($planorid);
2681          }
2682  
2683          // The user must be allowed to manage the plans of the user, nothing about the template.
2684          if (!$plan->can_manage()) {
2685              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2686          }
2687  
2688          // Only plan with status DRAFT or ACTIVE can be unliked..
2689          if ($plan->get('status') == plan::STATUS_COMPLETE) {
2690              throw new coding_exception('Only draft or active plan can be unliked from a template');
2691          }
2692  
2693          // Early exit, it's already done...
2694          if (!$plan->is_based_on_template()) {
2695              return true;
2696          }
2697  
2698          // Fetch the template.
2699          $template = new template($plan->get('templateid'));
2700  
2701          // Now, proceed by copying all competencies to the plan, then update the plan.
2702          $transaction = $DB->start_delegated_transaction();
2703          $competencies = template_competency::list_competencies($template->get('id'), false);
2704          $i = 0;
2705          foreach ($competencies as $competency) {
2706              $record = (object) array(
2707                  'planid' => $plan->get('id'),
2708                  'competencyid' => $competency->get('id'),
2709                  'sortorder' => $i++
2710              );
2711              $pc = new plan_competency(null, $record);
2712              $pc->create();
2713          }
2714          $plan->set('origtemplateid', $template->get('id'));
2715          $plan->set('templateid', null);
2716          $success = $plan->update();
2717          $transaction->allow_commit();
2718  
2719          // Trigger unlinked event.
2720          \core\event\competency_plan_unlinked::create_from_plan($plan)->trigger();
2721  
2722          return $success;
2723      }
2724  
2725      /**
2726       * Updates a plan.
2727       *
2728       * @param stdClass $record
2729       * @return \core_competency\plan
2730       */
2731      public static function update_plan(stdClass $record) {
2732          static::require_enabled();
2733  
2734          $plan = new plan($record->id);
2735  
2736          // Validate that the plan as it is can be managed.
2737          if (!$plan->can_manage()) {
2738              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2739  
2740          } else if ($plan->get('status') == plan::STATUS_COMPLETE) {
2741              // A completed plan cannot be edited.
2742              throw new coding_exception('Completed plan cannot be edited.');
2743  
2744          } else if ($plan->is_based_on_template()) {
2745              // Prevent a plan based on a template to be edited.
2746              throw new coding_exception('Cannot update a plan that is based on a template.');
2747  
2748          } else if (isset($record->templateid) && $plan->get('templateid') != $record->templateid) {
2749              // Prevent a plan to be based on a template.
2750              throw new coding_exception('Cannot base a plan on a template.');
2751  
2752          } else if (isset($record->userid) && $plan->get('userid') != $record->userid) {
2753              // Prevent change of ownership as the capabilities are checked against that.
2754              throw new coding_exception('A plan cannot be transfered to another user');
2755  
2756          } else if (isset($record->status) && $plan->get('status') != $record->status) {
2757              // Prevent change of status.
2758              throw new coding_exception('To change the status of a plan use the appropriate methods.');
2759  
2760          }
2761  
2762          $plan->from_record($record);
2763          $plan->update();
2764  
2765          // Trigger updated event.
2766          \core\event\competency_plan_updated::create_from_plan($plan)->trigger();
2767  
2768          return $plan;
2769      }
2770  
2771      /**
2772       * Returns a plan data.
2773       *
2774       * @param int $id
2775       * @return \core_competency\plan
2776       */
2777      public static function read_plan($id) {
2778          static::require_enabled();
2779          $plan = new plan($id);
2780  
2781          if (!$plan->can_read()) {
2782              $context = context_user::instance($plan->get('userid'));
2783              throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
2784          }
2785  
2786          return $plan;
2787      }
2788  
2789      /**
2790       * Plan event viewed.
2791       *
2792       * @param mixed $planorid The id or the plan.
2793       * @return boolean
2794       */
2795      public static function plan_viewed($planorid) {
2796          static::require_enabled();
2797          $plan = $planorid;
2798          if (!is_object($plan)) {
2799              $plan = new plan($plan);
2800          }
2801  
2802          // First we do a permissions check.
2803          if (!$plan->can_read()) {
2804              $context = context_user::instance($plan->get('userid'));
2805              throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
2806          }
2807  
2808          // Trigger a template viewed event.
2809          \core\event\competency_plan_viewed::create_from_plan($plan)->trigger();
2810  
2811          return true;
2812      }
2813  
2814      /**
2815       * Deletes a plan.
2816       *
2817       * Plans based on a template can be removed just like any other one.
2818       *
2819       * @param int $id
2820       * @return bool Success?
2821       */
2822      public static function delete_plan($id) {
2823          global $DB;
2824          static::require_enabled();
2825  
2826          $plan = new plan($id);
2827  
2828          if (!$plan->can_manage()) {
2829              $context = context_user::instance($plan->get('userid'));
2830              throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
2831          }
2832  
2833          // Wrap the suppression in a DB transaction.
2834          $transaction = $DB->start_delegated_transaction();
2835  
2836          // Delete plan competencies.
2837          $plancomps = plan_competency::get_records(array('planid' => $plan->get('id')));
2838          foreach ($plancomps as $plancomp) {
2839              $plancomp->delete();
2840          }
2841  
2842          // Delete archive user competencies if the status of the plan is complete.
2843          if ($plan->get('status') == plan::STATUS_COMPLETE) {
2844              self::remove_archived_user_competencies_in_plan($plan);
2845          }
2846          $event = \core\event\competency_plan_deleted::create_from_plan($plan);
2847          $success = $plan->delete();
2848  
2849          $transaction->allow_commit();
2850  
2851          // Trigger deleted event.
2852          $event->trigger();
2853  
2854          return $success;
2855      }
2856  
2857      /**
2858       * Cancel the review of a plan.
2859       *
2860       * @param int|plan $planorid The plan, or its ID.
2861       * @return bool
2862       */
2863      public static function plan_cancel_review_request($planorid) {
2864          static::require_enabled();
2865          $plan = $planorid;
2866          if (!is_object($plan)) {
2867              $plan = new plan($plan);
2868          }
2869  
2870          // We need to be able to view the plan at least.
2871          if (!$plan->can_read()) {
2872              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2873          }
2874  
2875          if ($plan->is_based_on_template()) {
2876              throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2877          } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) {
2878              throw new coding_exception('The plan review cannot be cancelled at this stage.');
2879          } else if (!$plan->can_request_review()) {
2880              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2881          }
2882  
2883          $plan->set('status', plan::STATUS_DRAFT);
2884          $result = $plan->update();
2885  
2886          // Trigger review request cancelled event.
2887          \core\event\competency_plan_review_request_cancelled::create_from_plan($plan)->trigger();
2888  
2889          return $result;
2890      }
2891  
2892      /**
2893       * Request the review of a plan.
2894       *
2895       * @param int|plan $planorid The plan, or its ID.
2896       * @return bool
2897       */
2898      public static function plan_request_review($planorid) {
2899          static::require_enabled();
2900          $plan = $planorid;
2901          if (!is_object($plan)) {
2902              $plan = new plan($plan);
2903          }
2904  
2905          // We need to be able to view the plan at least.
2906          if (!$plan->can_read()) {
2907              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2908          }
2909  
2910          if ($plan->is_based_on_template()) {
2911              throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2912          } else if ($plan->get('status') != plan::STATUS_DRAFT) {
2913              throw new coding_exception('The plan cannot be sent for review at this stage.');
2914          } else if (!$plan->can_request_review()) {
2915              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2916          }
2917  
2918          $plan->set('status', plan::STATUS_WAITING_FOR_REVIEW);
2919          $result = $plan->update();
2920  
2921          // Trigger review requested event.
2922          \core\event\competency_plan_review_requested::create_from_plan($plan)->trigger();
2923  
2924          return $result;
2925      }
2926  
2927      /**
2928       * Start the review of a plan.
2929       *
2930       * @param int|plan $planorid The plan, or its ID.
2931       * @return bool
2932       */
2933      public static function plan_start_review($planorid) {
2934          global $USER;
2935          static::require_enabled();
2936          $plan = $planorid;
2937          if (!is_object($plan)) {
2938              $plan = new plan($plan);
2939          }
2940  
2941          // We need to be able to view the plan at least.
2942          if (!$plan->can_read()) {
2943              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2944          }
2945  
2946          if ($plan->is_based_on_template()) {
2947              throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2948          } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) {
2949              throw new coding_exception('The plan review cannot be started at this stage.');
2950          } else if (!$plan->can_review()) {
2951              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2952          }
2953  
2954          $plan->set('status', plan::STATUS_IN_REVIEW);
2955          $plan->set('reviewerid', $USER->id);
2956          $result = $plan->update();
2957  
2958          // Trigger review started event.
2959          \core\event\competency_plan_review_started::create_from_plan($plan)->trigger();
2960  
2961          return $result;
2962      }
2963  
2964      /**
2965       * Stop reviewing a plan.
2966       *
2967       * @param  int|plan $planorid The plan, or its ID.
2968       * @return bool
2969       */
2970      public static function plan_stop_review($planorid) {
2971          static::require_enabled();
2972          $plan = $planorid;
2973          if (!is_object($plan)) {
2974              $plan = new plan($plan);
2975          }
2976  
2977          // We need to be able to view the plan at least.
2978          if (!$plan->can_read()) {
2979              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2980          }
2981  
2982          if ($plan->is_based_on_template()) {
2983              throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2984          } else if ($plan->get('status') != plan::STATUS_IN_REVIEW) {
2985              throw new coding_exception('The plan review cannot be stopped at this stage.');
2986          } else if (!$plan->can_review()) {
2987              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2988          }
2989  
2990          $plan->set('status', plan::STATUS_DRAFT);
2991          $plan->set('reviewerid', null);
2992          $result = $plan->update();
2993  
2994          // Trigger review stopped event.
2995          \core\event\competency_plan_review_stopped::create_from_plan($plan)->trigger();
2996  
2997          return $result;
2998      }
2999  
3000      /**
3001       * Approve a plan.
3002       *
3003       * This means making the plan active.
3004       *
3005       * @param  int|plan $planorid The plan, or its ID.
3006       * @return bool
3007       */
3008      public static function approve_plan($planorid) {
3009          static::require_enabled();
3010          $plan = $planorid;
3011          if (!is_object($plan)) {
3012              $plan = new plan($plan);
3013          }
3014  
3015          // We need to be able to view the plan at least.
3016          if (!$plan->can_read()) {
3017              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
3018          }
3019  
3020          // We can approve a plan that is either a draft, in review, or waiting for review.
3021          if ($plan->is_based_on_template()) {
3022              throw new coding_exception('Template plans are already approved.');   // This should never happen.
3023          } else if (!$plan->is_draft()) {
3024              throw new coding_exception('The plan cannot be approved at this stage.');
3025          } else if (!$plan->can_review()) {
3026              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3027          }
3028  
3029          $plan->set('status', plan::STATUS_ACTIVE);
3030          $plan->set('reviewerid', null);
3031          $result = $plan->update();
3032  
3033          // Trigger approved event.
3034          \core\event\competency_plan_approved::create_from_plan($plan)->trigger();
3035  
3036          return $result;
3037      }
3038  
3039      /**
3040       * Unapprove a plan.
3041       *
3042       * This means making the plan draft.
3043       *
3044       * @param  int|plan $planorid The plan, or its ID.
3045       * @return bool
3046       */
3047      public static function unapprove_plan($planorid) {
3048          static::require_enabled();
3049          $plan = $planorid;
3050          if (!is_object($plan)) {
3051              $plan = new plan($plan);
3052          }
3053  
3054          // We need to be able to view the plan at least.
3055          if (!$plan->can_read()) {
3056              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
3057          }
3058  
3059          if ($plan->is_based_on_template()) {
3060              throw new coding_exception('Template plans are always approved.');   // This should never happen.
3061          } else if ($plan->get('status') != plan::STATUS_ACTIVE) {
3062              throw new coding_exception('The plan cannot be sent back to draft at this stage.');
3063          } else if (!$plan->can_review()) {
3064              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3065          }
3066  
3067          $plan->set('status', plan::STATUS_DRAFT);
3068          $result = $plan->update();
3069  
3070          // Trigger unapproved event.
3071          \core\event\competency_plan_unapproved::create_from_plan($plan)->trigger();
3072  
3073          return $result;
3074      }
3075  
3076      /**
3077       * Complete a plan.
3078       *
3079       * @param int|plan $planorid The plan, or its ID.
3080       * @return bool
3081       */
3082      public static function complete_plan($planorid) {
3083          global $DB;
3084          static::require_enabled();
3085  
3086          $plan = $planorid;
3087          if (!is_object($planorid)) {
3088              $plan = new plan($planorid);
3089          }
3090  
3091          // Validate that the plan can be managed.
3092          if (!$plan->can_manage()) {
3093              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3094          }
3095  
3096          // Check if the plan was already completed.
3097          if ($plan->get('status') == plan::STATUS_COMPLETE) {
3098              throw new coding_exception('The plan is already completed.');
3099          }
3100  
3101          $originalstatus = $plan->get('status');
3102          $plan->set('status', plan::STATUS_COMPLETE);
3103  
3104          // The user should also be able to manage the plan when it's completed.
3105          if (!$plan->can_manage()) {
3106              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3107          }
3108  
3109          // Put back original status because archive needs it to extract competencies from the right table.
3110          $plan->set('status', $originalstatus);
3111  
3112          // Do the things.
3113          $transaction = $DB->start_delegated_transaction();
3114          self::archive_user_competencies_in_plan($plan);
3115          $plan->set('status', plan::STATUS_COMPLETE);
3116          $success = $plan->update();
3117  
3118          if (!$success) {
3119              $transaction->rollback(new moodle_exception('The plan could not be updated.'));
3120              return $success;
3121          }
3122  
3123          $transaction->allow_commit();
3124  
3125          // Trigger updated event.
3126          \core\event\competency_plan_completed::create_from_plan($plan)->trigger();
3127  
3128          return $success;
3129      }
3130  
3131      /**
3132       * Reopen a plan.
3133       *
3134       * @param int|plan $planorid The plan, or its ID.
3135       * @return bool
3136       */
3137      public static function reopen_plan($planorid) {
3138          global $DB;
3139          static::require_enabled();
3140  
3141          $plan = $planorid;
3142          if (!is_object($planorid)) {
3143              $plan = new plan($planorid);
3144          }
3145  
3146          // Validate that the plan as it is can be managed.
3147          if (!$plan->can_manage()) {
3148              $context = context_user::instance($plan->get('userid'));
3149              throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
3150          }
3151  
3152          $beforestatus = $plan->get('status');
3153          $plan->set('status', plan::STATUS_ACTIVE);
3154  
3155          // Validate if status can be changed.
3156          if (!$plan->can_manage()) {
3157              $context = context_user::instance($plan->get('userid'));
3158              throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
3159          }
3160  
3161          // Wrap the updates in a DB transaction.
3162          $transaction = $DB->start_delegated_transaction();
3163  
3164          // Delete archived user competencies if the status of the plan is changed from complete to another status.
3165          $mustremovearchivedcompetencies = ($beforestatus == plan::STATUS_COMPLETE && $plan->get('status') != plan::STATUS_COMPLETE);
3166          if ($mustremovearchivedcompetencies) {
3167              self::remove_archived_user_competencies_in_plan($plan);
3168          }
3169  
3170          // If duedate less than or equal to duedate_threshold unset it.
3171          if ($plan->get('duedate') <= time() + plan::DUEDATE_THRESHOLD) {
3172              $plan->set('duedate', 0);
3173          }
3174  
3175          $success = $plan->update();
3176  
3177          if (!$success) {
3178              $transaction->rollback(new moodle_exception('The plan could not be updated.'));
3179              return $success;
3180          }
3181  
3182          $transaction->allow_commit();
3183  
3184          // Trigger reopened event.
3185          \core\event\competency_plan_reopened::create_from_plan($plan)->trigger();
3186  
3187          return $success;
3188      }
3189  
3190      /**
3191       * Get a single competency from the user plan.
3192       *
3193       * @param  int $planorid The plan, or its ID.
3194       * @param  int $competencyid The competency id.
3195       * @return (object) array(
3196       *                      'competency' => \core_competency\competency,
3197       *                      'usercompetency' => \core_competency\user_competency
3198       *                      'usercompetencyplan' => \core_competency\user_competency_plan
3199       *                  )
3200       *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
3201       */
3202      public static function get_plan_competency($planorid, $competencyid) {
3203          static::require_enabled();
3204          $plan = $planorid;
3205          if (!is_object($planorid)) {
3206              $plan = new plan($planorid);
3207          }
3208  
3209          if (!user_competency::can_read_user($plan->get('userid'))) {
3210              throw new required_capability_exception($plan->get_context(), 'moodle/competency:usercompetencyview',
3211                  'nopermissions', '');
3212          }
3213  
3214          $competency = $plan->get_competency($competencyid);
3215  
3216          // Get user competencies from user_competency_plan if the plan status is set to complete.
3217          $iscompletedplan = $plan->get('status') == plan::STATUS_COMPLETE;
3218          if ($iscompletedplan) {
3219              $usercompetencies = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), array($competencyid));
3220              $ucresultkey = 'usercompetencyplan';
3221          } else {
3222              $usercompetencies = user_competency::get_multiple($plan->get('userid'), array($competencyid));
3223              $ucresultkey = 'usercompetency';
3224          }
3225  
3226          $found = count($usercompetencies);
3227  
3228          if ($found) {
3229              $uc = array_pop($usercompetencies);
3230          } else {
3231              if ($iscompletedplan) {
3232                  throw new coding_exception('A user competency plan is missing');
3233              } else {
3234                  $uc = user_competency::create_relation($plan->get('userid'), $competency->get('id'));
3235                  $uc->create();
3236              }
3237          }
3238  
3239          $plancompetency = (object) array(
3240              'competency' => $competency,
3241              'usercompetency' => null,
3242              'usercompetencyplan' => null
3243          );
3244          $plancompetency->$ucresultkey = $uc;
3245  
3246          return $plancompetency;
3247      }
3248  
3249      /**
3250       * List the plans with a competency.
3251       *
3252       * @param  int $userid The user id we want the plans for.
3253       * @param  int $competencyorid The competency, or its ID.
3254       * @return array[plan] Array of learning plans.
3255       */
3256      public static function list_plans_with_competency($userid, $competencyorid) {
3257          global $USER;
3258  
3259          static::require_enabled();
3260          $competencyid = $competencyorid;
3261          $competency = null;
3262          if (is_object($competencyid)) {
3263              $competency = $competencyid;
3264              $competencyid = $competency->get('id');
3265          }
3266  
3267          $plans = plan::get_by_user_and_competency($userid, $competencyid);
3268          foreach ($plans as $index => $plan) {
3269              // Filter plans we cannot read.
3270              if (!$plan->can_read()) {
3271                  unset($plans[$index]);
3272              }
3273          }
3274          return $plans;
3275      }
3276  
3277      /**
3278       * List the competencies in a user plan.
3279       *
3280       * @param  int $planorid The plan, or its ID.
3281       * @return array((object) array(
3282       *                            'competency' => \core_competency\competency,
3283       *                            'usercompetency' => \core_competency\user_competency
3284       *                            'usercompetencyplan' => \core_competency\user_competency_plan
3285       *                        ))
3286       *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
3287       */
3288      public static function list_plan_competencies($planorid) {
3289          static::require_enabled();
3290          $plan = $planorid;
3291          if (!is_object($planorid)) {
3292              $plan = new plan($planorid);
3293          }
3294  
3295          if (!$plan->can_read()) {
3296              $context = context_user::instance($plan->get('userid'));
3297              throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
3298          }
3299  
3300          $result = array();
3301          $competencies = $plan->get_competencies();
3302  
3303          // Get user competencies from user_competency_plan if the plan status is set to complete.
3304          $iscompletedplan = $plan->get('status') == plan::STATUS_COMPLETE;
3305          if ($iscompletedplan) {
3306              $usercompetencies = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), $competencies);
3307              $ucresultkey = 'usercompetencyplan';
3308          } else {
3309              $usercompetencies = user_competency::get_multiple($plan->get('userid'), $competencies);
3310              $ucresultkey = 'usercompetency';
3311          }
3312  
3313          // Build the return values.
3314          foreach ($competencies as $key => $competency) {
3315              $found = false;
3316  
3317              foreach ($usercompetencies as $uckey => $uc) {
3318                  if ($uc->get('competencyid') == $competency->get('id')) {
3319                      $found = true;
3320                      unset($usercompetencies[$uckey]);
3321                      break;
3322                  }
3323              }
3324  
3325              if (!$found) {
3326                  if ($iscompletedplan) {
3327                      throw new coding_exception('A user competency plan is missing');
3328                  } else {
3329                      $uc = user_competency::create_relation($plan->get('userid'), $competency->get('id'));
3330                  }
3331              }
3332  
3333              $plancompetency = (object) array(
3334                  'competency' => $competency,
3335                  'usercompetency' => null,
3336                  'usercompetencyplan' => null
3337              );
3338              $plancompetency->$ucresultkey = $uc;
3339              $result[] = $plancompetency;
3340          }
3341  
3342          return $result;
3343      }
3344  
3345      /**
3346       * Add a competency to a plan.
3347       *
3348       * @param int $planid The id of the plan
3349       * @param int $competencyid The id of the competency
3350       * @return bool
3351       */
3352      public static function add_competency_to_plan($planid, $competencyid) {
3353          static::require_enabled();
3354          $plan = new plan($planid);
3355  
3356          // First we do a permissions check.
3357          if (!$plan->can_manage()) {
3358              throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3359  
3360          } else if ($plan->is_based_on_template()) {
3361              throw new coding_exception('A competency can not be added to a learning plan based on a template');
3362          }
3363  
3364          if (!$plan->can_be_edited()) {
3365              throw new coding_exception('A competency can not be added to a learning plan completed');
3366          }
3367  
3368          $competency = new competency($competencyid);
3369  
3370          // Can not add a competency that belong to a hidden framework.
3371          if ($competency->get_framework()->get('visible') == false) {
3372              throw new coding_exception('A competency belonging to hidden framework can not be added');
3373          }
3374  
3375          $exists = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
3376          if (!$exists) {
3377              $record = new stdClass();
3378              $record->planid = $planid;
3379              $record->competencyid = $competencyid;
3380              $plancompetency = new plan_competency(0, $record);
3381              $plancompetency->create();
3382          }
3383  
3384          return true;
3385      }
3386  
3387      /**
3388       * Remove a competency from a plan.
3389       *
3390       * @param int $planid The plan id
3391       * @param int $competencyid The id of the competency
3392       * @return bool
3393       */
3394      public static function remove_competency_from_plan($planid, $competencyid) {
3395          static::require_enabled();
3396          $plan = new plan($planid);
3397  
3398          // First we do a permissions check.
3399          if (!$plan->can_manage()) {
3400              $context = context_user::instance($plan->get('userid'));
3401              throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
3402  
3403          } else if ($plan->is_based_on_template()) {
3404              throw new coding_exception('A competency can not be removed from a learning plan based on a template');
3405          }
3406  
3407          if (!$plan->can_be_edited()) {
3408              throw new coding_exception('A competency can not be removed from a learning plan completed');
3409          }
3410  
3411          $link = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
3412          if ($link) {
3413              return $link->delete();
3414          }
3415          return false;
3416      }
3417  
3418      /**
3419       * Move the plan competency up or down in the display list.
3420       *
3421       * Requires moodle/competency:planmanage capability at the system context.
3422       *
3423       * @param int $planid The plan  id
3424       * @param int $competencyidfrom The id of the competency we are moving.
3425       * @param int $competencyidto The id of the competency we are moving to.
3426       * @return boolean
3427       */
3428      public static function reorder_plan_competency($planid, $competencyidfrom, $competencyidto) {
3429          static::require_enabled();
3430          $plan = new plan($planid);
3431  
3432          // First we do a permissions check.
3433          if (!$plan->can_manage()) {
3434              $context = context_user::instance($plan->get('userid'));
3435              throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
3436  
3437          } else if ($plan->is_based_on_template()) {
3438              throw new coding_exception('A competency can not be reordered in a learning plan based on a template');
3439          }
3440  
3441          if (!$plan->can_be_edited()) {
3442              throw new coding_exception('A competency can not be reordered in a learning plan completed');
3443          }
3444  
3445          $down = true;
3446          $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidfrom));
3447          if (count($matches) == 0) {
3448              throw new coding_exception('The link does not exist');
3449          }
3450  
3451          $competencyfrom = array_pop($matches);
3452          $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidto));
3453          if (count($matches) == 0) {
3454              throw new coding_exception('The link does not exist');
3455          }
3456  
3457          $competencyto = array_pop($matches);
3458  
3459          $all = plan_competency::get_records(array('planid' => $planid), 'sortorder', 'ASC', 0, 0);
3460  
3461          if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
3462              // We are moving up, so put it before the "to" item.
3463              $down = false;
3464          }
3465  
3466          foreach ($all as $id => $plancompetency) {
3467              $sort = $plancompetency->get('sortorder');
3468              if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
3469                  $plancompetency->set('sortorder', $plancompetency->get('sortorder') - 1);
3470                  $plancompetency->update();
3471              } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
3472                  $plancompetency->set('sortorder', $plancompetency->get('sortorder') + 1);
3473                  $plancompetency->update();
3474              }
3475          }
3476          $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
3477          return $competencyfrom->update();
3478      }
3479  
3480      /**
3481       * Cancel a user competency review request.
3482       *
3483       * @param  int $userid       The user ID.
3484       * @param  int $competencyid The competency ID.
3485       * @return bool
3486       */
3487      public static function user_competency_cancel_review_request($userid, $competencyid) {
3488          static::require_enabled();
3489          $context = context_user::instance($userid);
3490          $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3491          if (!$uc || !$uc->can_read()) {
3492              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
3493          } else if ($uc->get('status') != user_competency::STATUS_WAITING_FOR_REVIEW) {
3494              throw new coding_exception('The competency can not be cancel review request at this stage.');
3495          } else if (!$uc->can_request_review()) {
3496              throw new required_capability_exception($context, 'moodle/competency:usercompetencyrequestreview', 'nopermissions', '');
3497          }
3498  
3499          $uc->set('status', user_competency::STATUS_IDLE);
3500          $result = $uc->update();
3501          if ($result) {
3502              \core\event\competency_user_competency_review_request_cancelled::create_from_user_competency($uc)->trigger();
3503          }
3504          return $result;
3505      }
3506  
3507      /**
3508       * Request a user competency review.
3509       *
3510       * @param  int $userid       The user ID.
3511       * @param  int $competencyid The competency ID.
3512       * @return bool
3513       */
3514      public static function user_competency_request_review($userid, $competencyid) {
3515          static::require_enabled();
3516          $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3517          if (!$uc) {
3518              $uc = user_competency::create_relation($userid, $competencyid);
3519              $uc->create();
3520          }
3521  
3522          if (!$uc->can_read()) {
3523              throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
3524                  'nopermissions', '');
3525          } else if ($uc->get('status') != user_competency::STATUS_IDLE) {
3526              throw new coding_exception('The competency can not be sent for review at this stage.');
3527          } else if (!$uc->can_request_review()) {
3528              throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyrequestreview',
3529                  'nopermissions', '');
3530          }
3531  
3532          $uc->set('status', user_competency::STATUS_WAITING_FOR_REVIEW);
3533          $result = $uc->update();
3534          if ($result) {
3535              \core\event\competency_user_competency_review_requested::create_from_user_competency($uc)->trigger();
3536          }
3537          return $result;
3538      }
3539  
3540      /**
3541       * Start a user competency review.
3542       *
3543       * @param  int $userid       The user ID.
3544       * @param  int $competencyid The competency ID.
3545       * @return bool
3546       */
3547      public static function user_competency_start_review($userid, $competencyid) {
3548          global $USER;
3549          static::require_enabled();
3550  
3551          $context = context_user::instance($userid);
3552          $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3553          if (!$uc || !$uc->can_read()) {
3554              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
3555          } else if ($uc->get('status') != user_competency::STATUS_WAITING_FOR_REVIEW) {
3556              throw new coding_exception('The competency review can not be started at this stage.');
3557          } else if (!$uc->can_review()) {
3558              throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', '');
3559          }
3560  
3561          $uc->set('status', user_competency::STATUS_IN_REVIEW);
3562          $uc->set('reviewerid', $USER->id);
3563          $result = $uc->update();
3564          if ($result) {
3565              \core\event\competency_user_competency_review_started::create_from_user_competency($uc)->trigger();
3566          }
3567          return $result;
3568      }
3569  
3570      /**
3571       * Stop a user competency review.
3572       *
3573       * @param  int $userid       The user ID.
3574       * @param  int $competencyid The competency ID.
3575       * @return bool
3576       */
3577      public static function user_competency_stop_review($userid, $competencyid) {
3578          static::require_enabled();
3579          $context = context_user::instance($userid);
3580          $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3581          if (!$uc || !$uc->can_read()) {
3582              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
3583          } else if ($uc->get('status') != user_competency::STATUS_IN_REVIEW) {
3584              throw new coding_exception('The competency review can not be stopped at this stage.');
3585          } else if (!$uc->can_review()) {
3586              throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', '');
3587          }
3588  
3589          $uc->set('status', user_competency::STATUS_IDLE);
3590          $result = $uc->update();
3591          if ($result) {
3592              \core\event\competency_user_competency_review_stopped::create_from_user_competency($uc)->trigger();
3593          }
3594          return $result;
3595      }
3596  
3597      /**
3598       * Log user competency viewed event.
3599       *
3600       * @param user_competency|int $usercompetencyorid The user competency object or user competency id
3601       * @return bool
3602       */
3603      public static function user_competency_viewed($usercompetencyorid) {
3604          static::require_enabled();
3605          $uc = $usercompetencyorid;
3606          if (!is_object($uc)) {
3607              $uc = new user_competency($uc);
3608          }
3609  
3610          if (!$uc || !$uc->can_read()) {
3611              throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
3612                  'nopermissions', '');
3613          }
3614  
3615          \core\event\competency_user_competency_viewed::create_from_user_competency_viewed($uc)->trigger();
3616          return true;
3617      }
3618  
3619      /**
3620       * Log user competency viewed in plan event.
3621       *
3622       * @param user_competency|int $usercompetencyorid The user competency object or user competency id
3623       * @param int $planid The plan ID
3624       * @return bool
3625       */
3626      public static function user_competency_viewed_in_plan($usercompetencyorid, $planid) {
3627          static::require_enabled();
3628          $uc = $usercompetencyorid;
3629          if (!is_object($uc)) {
3630              $uc = new user_competency($uc);
3631          }
3632  
3633          if (!$uc || !$uc->can_read()) {
3634              throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
3635                  'nopermissions', '');
3636          }
3637          $plan = new plan($planid);
3638          if ($plan->get('status') == plan::STATUS_COMPLETE) {
3639              throw new coding_exception('To log the user competency in completed plan use user_competency_plan_viewed method.');
3640          }
3641  
3642          \core\event\competency_user_competency_viewed_in_plan::create_from_user_competency_viewed_in_plan($uc, $planid)->trigger();
3643          return true;
3644      }
3645  
3646      /**
3647       * Log user competency viewed in course event.
3648       *
3649       * @param user_competency_course|int $usercoursecompetencyorid The user competency course object or its ID.
3650       * @param int $courseid The course ID
3651       * @return bool
3652       */
3653      public static function user_competency_viewed_in_course($usercoursecompetencyorid) {
3654          static::require_enabled();
3655          $ucc = $usercoursecompetencyorid;
3656          if (!is_object($ucc)) {
3657              $ucc = new user_competency_course($ucc);
3658          }
3659  
3660          if (!$ucc || !user_competency::can_read_user_in_course($ucc->get('userid'), $ucc->get('courseid'))) {
3661              throw new required_capability_exception($ucc->get_context(), 'moodle/competency:usercompetencyview',
3662                  'nopermissions', '');
3663          }
3664  
3665          // Validate the course, this will throw an exception if not valid.
3666          self::validate_course($ucc->get('courseid'));
3667  
3668          \core\event\competency_user_competency_viewed_in_course::create_from_user_competency_viewed_in_course($ucc)->trigger();
3669          return true;
3670      }
3671  
3672      /**
3673       * Log user competency plan viewed event.
3674       *
3675       * @param user_competency_plan|int $usercompetencyplanorid The user competency plan object or user competency plan id
3676       * @return bool
3677       */
3678      public static function user_competency_plan_viewed($usercompetencyplanorid) {
3679          static::require_enabled();
3680          $ucp = $usercompetencyplanorid;
3681          if (!is_object($ucp)) {
3682              $ucp = new user_competency_plan($ucp);
3683          }
3684  
3685          if (!$ucp || !user_competency::can_read_user($ucp->get('userid'))) {
3686              throw new required_capability_exception($ucp->get_context(), 'moodle/competency:usercompetencyview',
3687                  'nopermissions', '');
3688          }
3689          $plan = new plan($ucp->get('planid'));
3690          if ($plan->get('status') != plan::STATUS_COMPLETE) {
3691              throw new coding_exception('To log the user competency in non-completed plan use '
3692                  . 'user_competency_viewed_in_plan method.');
3693          }
3694  
3695          \core\event\competency_user_competency_plan_viewed::create_from_user_competency_plan($ucp)->trigger();
3696          return true;
3697      }
3698  
3699      /**
3700       * Check if template has related data.
3701       *
3702       * @param int $templateid The id of the template to check.
3703       * @return boolean
3704       */
3705      public static function template_has_related_data($templateid) {
3706          static::require_enabled();
3707          // First we do a permissions check.
3708          $template = new template($templateid);
3709  
3710          if (!$template->can_read()) {
3711              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
3712                  'nopermissions', '');
3713          }
3714  
3715          // OK - all set.
3716          return $template->has_plans();
3717      }
3718  
3719      /**
3720       * List all the related competencies.
3721       *
3722       * @param int $competencyid The id of the competency to check.
3723       * @return competency[]
3724       */
3725      public static function list_related_competencies($competencyid) {
3726          static::require_enabled();
3727          $competency = new competency($competencyid);
3728  
3729          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
3730                  $competency->get_context())) {
3731              throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview',
3732                  'nopermissions', '');
3733          }
3734  
3735          return $competency->get_related_competencies();
3736      }
3737  
3738      /**
3739       * Add a related competency.
3740       *
3741       * @param int $competencyid The id of the competency
3742       * @param int $relatedcompetencyid The id of the related competency.
3743       * @return bool False when create failed, true on success, or if the relation already existed.
3744       */
3745      public static function add_related_competency($competencyid, $relatedcompetencyid) {
3746          static::require_enabled();
3747          $competency1 = new competency($competencyid);
3748          $competency2 = new competency($relatedcompetencyid);
3749  
3750          require_capability('moodle/competency:competencymanage', $competency1->get_context());
3751  
3752          $relatedcompetency = related_competency::get_relation($competency1->get('id'), $competency2->get('id'));
3753          if (!$relatedcompetency->get('id')) {
3754              $relatedcompetency->create();
3755              return true;
3756          }
3757  
3758          return true;
3759      }
3760  
3761      /**
3762       * Remove a related competency.
3763       *
3764       * @param int $competencyid The id of the competency.
3765       * @param int $relatedcompetencyid The id of the related competency.
3766       * @return bool True when it was deleted, false when it wasn't or the relation doesn't exist.
3767       */
3768      public static function remove_related_competency($competencyid, $relatedcompetencyid) {
3769          static::require_enabled();
3770          $competency = new competency($competencyid);
3771  
3772          // This only check if we have the permission in either competency because both competencies
3773          // should belong to the same framework.
3774          require_capability('moodle/competency:competencymanage', $competency->get_context());
3775  
3776          $relatedcompetency = related_competency::get_relation($competencyid, $relatedcompetencyid);
3777          if ($relatedcompetency->get('id')) {
3778              return $relatedcompetency->delete();
3779          }
3780  
3781          return false;
3782      }
3783  
3784      /**
3785       * Read a user evidence.
3786       *
3787       * @param int $id
3788       * @return user_evidence
3789       */
3790      public static function read_user_evidence($id) {
3791          static::require_enabled();
3792          $userevidence = new user_evidence($id);
3793  
3794          if (!$userevidence->can_read()) {
3795              $context = $userevidence->get_context();
3796              throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', '');
3797          }
3798  
3799          return $userevidence;
3800      }
3801  
3802      /**
3803       * Create a new user evidence.
3804       *
3805       * @param  object $data        The data.
3806       * @param  int    $draftitemid The draft ID in which files have been saved.
3807       * @return user_evidence
3808       */
3809      public static function create_user_evidence($data, $draftitemid = null) {
3810          static::require_enabled();
3811          $userevidence = new user_evidence(null, $data);
3812          $context = $userevidence->get_context();
3813  
3814          if (!$userevidence->can_manage()) {
3815              throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
3816          }
3817  
3818          $userevidence->create();
3819          if (!empty($draftitemid)) {
3820              $fileareaoptions = array('subdirs' => true);
3821              $itemid = $userevidence->get('id');
3822              file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions);
3823          }
3824  
3825          // Trigger an evidence of prior learning created event.
3826          \core\event\competency_user_evidence_created::create_from_user_evidence($userevidence)->trigger();
3827  
3828          return $userevidence;
3829      }
3830  
3831      /**
3832       * Create a new user evidence.
3833       *
3834       * @param  object $data        The data.
3835       * @param  int    $draftitemid The draft ID in which files have been saved.
3836       * @return user_evidence
3837       */
3838      public static function update_user_evidence($data, $draftitemid = null) {
3839          static::require_enabled();
3840          $userevidence = new user_evidence($data->id);
3841          $context = $userevidence->get_context();
3842  
3843          if (!$userevidence->can_manage()) {
3844              throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
3845  
3846          } else if (property_exists($data, 'userid') && $data->userid != $userevidence->get('userid')) {
3847              throw new coding_exception('Can not change the userid of a user evidence.');
3848          }
3849  
3850          $userevidence->from_record($data);
3851          $userevidence->update();
3852  
3853          if (!empty($draftitemid)) {
3854              $fileareaoptions = array('subdirs' => true);
3855              $itemid = $userevidence->get('id');
3856              file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions);
3857          }
3858  
3859          // Trigger an evidence of prior learning updated event.
3860          \core\event\competency_user_evidence_updated::create_from_user_evidence($userevidence)->trigger();
3861  
3862          return $userevidence;
3863      }
3864  
3865      /**
3866       * Delete a user evidence.
3867       *
3868       * @param  int $id The user evidence ID.
3869       * @return bool
3870       */
3871      public static function delete_user_evidence($id) {
3872          static::require_enabled();
3873          $userevidence = new user_evidence($id);
3874          $context = $userevidence->get_context();
3875  
3876          if (!$userevidence->can_manage()) {
3877              throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
3878          }
3879  
3880          // Delete the user evidence.
3881          $userevidence->delete();
3882  
3883          // Delete associated files.
3884          $fs = get_file_storage();
3885          $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $id);
3886  
3887          // Delete relation between evidence and competencies.
3888          $userevidence->set('id', $id);     // Restore the ID to fully mock the object.
3889          $competencies = user_evidence_competency::get_competencies_by_userevidenceid($id);
3890          foreach ($competencies as $competency) {
3891              static::delete_user_evidence_competency($userevidence, $competency->get('id'));
3892          }
3893  
3894          // Trigger an evidence of prior learning deleted event.
3895          \core\event\competency_user_evidence_deleted::create_from_user_evidence($userevidence)->trigger();
3896  
3897          $userevidence->set('id', 0);       // Restore the object.
3898  
3899          return true;
3900      }
3901  
3902      /**
3903       * List the user evidence of a user.
3904       *
3905       * @param  int $userid The user ID.
3906       * @return user_evidence[]
3907       */
3908      public static function list_user_evidence($userid) {
3909          static::require_enabled();
3910          if (!user_evidence::can_read_user($userid)) {
3911              $context = context_user::instance($userid);
3912              throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', '');
3913          }
3914  
3915          $evidence = user_evidence::get_records(array('userid' => $userid), 'name');
3916          return $evidence;
3917      }
3918  
3919      /**
3920       * Link a user evidence with a competency.
3921       *
3922       * @param  user_evidence|int $userevidenceorid User evidence or its ID.
3923       * @param  int $competencyid Competency ID.
3924       * @return user_evidence_competency
3925       */
3926      public static function create_user_evidence_competency($userevidenceorid, $competencyid) {
3927          global $USER;
3928          static::require_enabled();
3929  
3930          $userevidence = $userevidenceorid;
3931          if (!is_object($userevidence)) {
3932              $userevidence = self::read_user_evidence($userevidence);
3933          }
3934  
3935          // Perform user evidence capability checks.
3936          if (!$userevidence->can_manage()) {
3937              $context = $userevidence->get_context();
3938              throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
3939          }
3940  
3941          // Perform competency capability checks.
3942          $competency = self::read_competency($competencyid);
3943  
3944          // Get (and create) the relation.
3945          $relation = user_evidence_competency::get_relation($userevidence->get('id'), $competency->get('id'));
3946          if (!$relation->get('id')) {
3947              $relation->create();
3948  
3949              $link = url::user_evidence($userevidence->get('id'));
3950              self::add_evidence(
3951                  $userevidence->get('userid'),
3952                  $competency,
3953                  $userevidence->get_context(),
3954                  evidence::ACTION_LOG,
3955                  'evidence_evidenceofpriorlearninglinked',
3956                  'core_competency',
3957                  $userevidence->get('name'),
3958                  false,
3959                  $link->out(false),
3960                  null,
3961                  $USER->id
3962              );
3963          }
3964  
3965          return $relation;
3966      }
3967  
3968      /**
3969       * Delete a relationship between a user evidence and a competency.
3970       *
3971       * @param  user_evidence|int $userevidenceorid User evidence or its ID.
3972       * @param  int $competencyid Competency ID.
3973       * @return bool
3974       */
3975      public static function delete_user_evidence_competency($userevidenceorid, $competencyid) {
3976          global $USER;
3977          static::require_enabled();
3978  
3979          $userevidence = $userevidenceorid;
3980          if (!is_object($userevidence)) {
3981              $userevidence = self::read_user_evidence($userevidence);
3982          }
3983  
3984          // Perform user evidence capability checks.
3985          if (!$userevidence->can_manage()) {
3986              $context = $userevidence->get_context();
3987              throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
3988          }
3989  
3990          // Get (and delete) the relation.
3991          $relation = user_evidence_competency::get_relation($userevidence->get('id'), $competencyid);
3992          if (!$relation->get('id')) {
3993              return true;
3994          }
3995  
3996          $success = $relation->delete();
3997          if ($success) {
3998              self::add_evidence(
3999                  $userevidence->get('userid'),
4000                  $competencyid,
4001                  $userevidence->get_context(),
4002                  evidence::ACTION_LOG,
4003                  'evidence_evidenceofpriorlearningunlinked',
4004                  'core_competency',
4005                  $userevidence->get('name'),
4006                  false,
4007                  null,
4008                  null,
4009                  $USER->id
4010              );
4011          }
4012  
4013          return $success;
4014      }
4015  
4016      /**
4017       * Send request review for user evidence competencies.
4018       *
4019       * @param  int $id The user evidence ID.
4020       * @return bool
4021       */
4022      public static function request_review_of_user_evidence_linked_competencies($id) {
4023          $userevidence = new user_evidence($id);
4024          $context = $userevidence->get_context();
4025          $userid = $userevidence->get('userid');
4026  
4027          if (!$userevidence->can_manage()) {
4028              throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
4029          }
4030  
4031          $usercompetencies = user_evidence_competency::get_user_competencies_by_userevidenceid($id);
4032          foreach ($usercompetencies as $usercompetency) {
4033              if ($usercompetency->get('status') == user_competency::STATUS_IDLE) {
4034                  static::user_competency_request_review($userid, $usercompetency->get('competencyid'));
4035              }
4036          }
4037  
4038          return true;
4039      }
4040  
4041      /**
4042       * Recursively duplicate competencies from a tree, we start duplicating from parents to children to have a correct path.
4043       * This method does not copy the related competencies.
4044       *
4045       * @param int $frameworkid - framework id
4046       * @param competency[] $tree - array of competencies object
4047       * @param int $oldparent - old parent id
4048       * @param int $newparent - new parent id
4049       * @return competency[] $matchids - List of old competencies ids matched with new competencies object.
4050       */
4051      protected static function duplicate_competency_tree($frameworkid, $tree, $oldparent = 0, $newparent = 0) {
4052          $matchids = array();
4053          foreach ($tree as $node) {
4054              if ($node->competency->get('parentid') == $oldparent) {
4055                  $parentid = $node->competency->get('id');
4056  
4057                  // Create the competency.
4058                  $competency = new competency(0, $node->competency->to_record());
4059                  $competency->set('competencyframeworkid', $frameworkid);
4060                  $competency->set('parentid', $newparent);
4061                  $competency->set('path', '');
4062                  $competency->set('id', 0);
4063                  $competency->reset_rule();
4064                  $competency->create();
4065  
4066                  // Trigger the created event competency.
4067                  \core\event\competency_created::create_from_competency($competency)->trigger();
4068  
4069                  // Match the old id with the new one.
4070                  $matchids[$parentid] = $competency;
4071  
4072                  if (!empty($node->children)) {
4073                      // Duplicate children competency.
4074                      $childrenids = self::duplicate_competency_tree($frameworkid, $node->children, $parentid, $competency->get('id'));
4075                      // Array_merge does not keep keys when merging so we use the + operator.
4076                      $matchids = $matchids + $childrenids;
4077                  }
4078              }
4079          }
4080          return $matchids;
4081      }
4082  
4083      /**
4084       * Recursively migrate competency rules.
4085       *
4086       * @param competency[] $tree - array of competencies object
4087       * @param competency[] $matchids - List of old competencies ids matched with new competencies object
4088       */
4089      protected static function migrate_competency_tree_rules($tree, $matchids) {
4090  
4091          foreach ($tree as $node) {
4092              $oldcompid = $node->competency->get('id');
4093              if ($node->competency->get('ruletype') && array_key_exists($oldcompid, $matchids)) {
4094                  try {
4095                      // Get the new competency.
4096                      $competency = $matchids[$oldcompid];
4097                      $class = $node->competency->get('ruletype');
4098                      $newruleconfig = $class::migrate_config($node->competency->get('ruleconfig'), $matchids);
4099                      $competency->set('ruleconfig', $newruleconfig);
4100                      $competency->set('ruletype', $class);
4101                      $competency->set('ruleoutcome', $node->competency->get('ruleoutcome'));
4102                      $competency->update();
4103                  } catch (\Exception $e) {
4104                      debugging('Could not migrate competency rule from: ' . $oldcompid . ' to: ' . $competency->get('id') . '.' .
4105                          ' Exception: ' . $e->getMessage(), DEBUG_DEVELOPER);
4106                      $competency->reset_rule();
4107                  }
4108              }
4109  
4110              if (!empty($node->children)) {
4111                  self::migrate_competency_tree_rules($node->children, $matchids);
4112              }
4113          }
4114      }
4115  
4116      /**
4117       * Archive user competencies in a plan.
4118       *
4119       * @param int $plan The plan object.
4120       * @return void
4121       */
4122      protected static function archive_user_competencies_in_plan($plan) {
4123  
4124          // Check if the plan was already completed.
4125          if ($plan->get('status') == plan::STATUS_COMPLETE) {
4126              throw new coding_exception('The plan is already completed.');
4127          }
4128  
4129          $competencies = $plan->get_competencies();
4130          $usercompetencies = user_competency::get_multiple($plan->get('userid'), $competencies);
4131  
4132          $i = 0;
4133          foreach ($competencies as $competency) {
4134              $found = false;
4135  
4136              foreach ($usercompetencies as $uckey => $uc) {
4137                  if ($uc->get('competencyid') == $competency->get('id')) {
4138                      $found = true;
4139  
4140                      $ucprecord = $uc->to_record();
4141                      $ucprecord->planid = $plan->get('id');
4142                      $ucprecord->sortorder = $i;
4143                      unset($ucprecord->id);
4144                      unset($ucprecord->status);
4145                      unset($ucprecord->reviewerid);
4146  
4147                      $usercompetencyplan = new user_competency_plan(0, $ucprecord);
4148                      $usercompetencyplan->create();
4149  
4150                      unset($usercompetencies[$uckey]);
4151                      break;
4152                  }
4153              }
4154  
4155              // If the user competency doesn't exist, we create a new relation in user_competency_plan.
4156              if (!$found) {
4157                  $usercompetencyplan = user_competency_plan::create_relation($plan->get('userid'), $competency->get('id'),
4158                          $plan->get('id'));
4159                  $usercompetencyplan->set('sortorder', $i);
4160                  $usercompetencyplan->create();
4161              }
4162              $i++;
4163          }
4164      }
4165  
4166      /**
4167       * Delete archived user competencies in a plan.
4168       *
4169       * @param int $plan The plan object.
4170       * @return void
4171       */
4172      protected static function remove_archived_user_competencies_in_plan($plan) {
4173          $competencies = $plan->get_competencies();
4174          $usercompetenciesplan = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), $competencies);
4175  
4176          foreach ($usercompetenciesplan as $ucpkey => $ucp) {
4177              $ucp->delete();
4178          }
4179      }
4180  
4181      /**
4182       * List all the evidence for a user competency.
4183       *
4184       * @param int $userid The user id - only used if usercompetencyid is 0.
4185       * @param int $competencyid The competency id - only used it usercompetencyid is 0.
4186       * @param int $planid The plan id - not used yet - but can be used to only list archived evidence if a plan is completed.
4187       * @param string $sort The field to sort the evidence by.
4188       * @param string $order The ordering of the sorting.
4189       * @param int $skip Number of records to skip.
4190       * @param int $limit Number of records to return.
4191       * @return \core_competency\evidence[]
4192       * @return array of \core_competency\evidence
4193       */
4194      public static function list_evidence($userid = 0, $competencyid = 0, $planid = 0, $sort = 'timecreated',
4195                                           $order = 'DESC', $skip = 0, $limit = 0) {
4196          static::require_enabled();
4197  
4198          if (!user_competency::can_read_user($userid)) {
4199              $context = context_user::instance($userid);
4200              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
4201          }
4202  
4203          $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
4204          if (!$usercompetency) {
4205              return array();
4206          }
4207  
4208          $plancompleted = false;
4209          if ($planid != 0) {
4210              $plan = new plan($planid);
4211              if ($plan->get('status') == plan::STATUS_COMPLETE) {
4212                  $plancompleted = true;
4213              }
4214          }
4215  
4216          $select = 'usercompetencyid = :usercompetencyid';
4217          $params = array('usercompetencyid' => $usercompetency->get('id'));
4218          if ($plancompleted) {
4219              $select .= ' AND timecreated <= :timecompleted';
4220              $params['timecompleted'] = $plan->get('timemodified');
4221          }
4222  
4223          $orderby = $sort . ' ' . $order;
4224          $orderby .= !empty($orderby) ? ', id DESC' : 'id DESC'; // Prevent random ordering.
4225  
4226          $evidence = evidence::get_records_select($select, $params, $orderby, '*', $skip, $limit);
4227          return $evidence;
4228      }
4229  
4230      /**
4231       * List all the evidence for a user competency in a course.
4232       *
4233       * @param int $userid The user ID.
4234       * @param int $courseid The course ID.
4235       * @param int $competencyid The competency ID.
4236       * @param string $sort The field to sort the evidence by.
4237       * @param string $order The ordering of the sorting.
4238       * @param int $skip Number of records to skip.
4239       * @param int $limit Number of records to return.
4240       * @return \core_competency\evidence[]
4241       */
4242      public static function list_evidence_in_course($userid = 0, $courseid = 0, $competencyid = 0, $sort = 'timecreated',
4243                                                     $order = 'DESC', $skip = 0, $limit = 0) {
4244          static::require_enabled();
4245  
4246          if (!user_competency::can_read_user_in_course($userid, $courseid)) {
4247              $context = context_user::instance($userid);
4248              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
4249          }
4250  
4251          $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
4252          if (!$usercompetency) {
4253              return array();
4254          }
4255  
4256          $context = context_course::instance($courseid);
4257          return evidence::get_records_for_usercompetency($usercompetency->get('id'), $context, $sort, $order, $skip, $limit);
4258      }
4259  
4260      /**
4261       * Create an evidence from a list of parameters.
4262       *
4263       * Requires no capability because evidence can be added in many situations under any user.
4264       *
4265       * @param int $userid The user id for which evidence is added.
4266       * @param competency|int $competencyorid The competency, or its id for which evidence is added.
4267       * @param context|int $contextorid The context in which the evidence took place.
4268       * @param int $action The type of action to take on the competency. \core_competency\evidence::ACTION_*.
4269       * @param string $descidentifier The strings identifier.
4270       * @param string $desccomponent The strings component.
4271       * @param mixed $desca Any arguments the string requires.
4272       * @param bool $recommend When true, the user competency will be sent for review.
4273       * @param string $url The url the evidence may link to.
4274       * @param int $grade The grade, or scale ID item.
4275       * @param int $actionuserid The ID of the user who took the action of adding the evidence. Null when system.
4276       *                          This should be used when the action was taken by a real person, this will allow
4277       *                          to keep track of all the evidence given by a certain person.
4278       * @param string $note A note to attach to the evidence.
4279       * @return evidence
4280       * @throws coding_exception
4281       * @throws invalid_persistent_exception
4282       * @throws moodle_exception
4283       */
4284      public static function add_evidence($userid, $competencyorid, $contextorid, $action, $descidentifier, $desccomponent,
4285                                          $desca = null, $recommend = false, $url = null, $grade = null, $actionuserid = null,
4286                                          $note = null, $overridegrade = false) {
4287          global $DB;
4288          static::require_enabled();
4289  
4290          // Some clearly important variable assignments right there.
4291          $competencyid = $competencyorid;
4292          $competency = null;
4293          if (is_object($competencyid)) {
4294              $competency = $competencyid;
4295              $competencyid = $competency->get('id');
4296          }
4297          $contextid = $contextorid;
4298          $context = $contextorid;
4299          if (is_object($contextorid)) {
4300              $contextid = $contextorid->id;
4301          } else {
4302              $context = context::instance_by_id($contextorid);
4303          }
4304          $setucgrade = false;
4305          $ucgrade = null;
4306          $ucproficiency = null;
4307          $usercompetencycourse = null;
4308  
4309          // Fetch or create the user competency.
4310          $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
4311          if (!$usercompetency) {
4312              $usercompetency = user_competency::create_relation($userid, $competencyid);
4313              $usercompetency->create();
4314          }
4315  
4316          // What should we be doing?
4317          switch ($action) {
4318  
4319              // Completing a competency.
4320              case evidence::ACTION_COMPLETE:
4321                  // The logic here goes like this:
4322                  //
4323                  // if rating outside a course
4324                  // - set the default grade and proficiency ONLY if there is no current grade
4325                  // else we are in a course
4326                  // - set the defautl grade and proficiency in the course ONLY if there is no current grade in the course
4327                  // - then check the course settings to see if we should push the rating outside the course
4328                  // - if we should push it
4329                  // --- push it only if the user_competency (outside the course) has no grade
4330                  // Done.
4331  
4332                  if ($grade !== null) {
4333                      throw new coding_exception("The grade MUST NOT be set with a 'completing' evidence.");
4334                  }
4335  
4336                  // Fetch the default grade to attach to the evidence.
4337                  if (empty($competency)) {
4338                      $competency = new competency($competencyid);
4339                  }
4340                  list($grade, $proficiency) = $competency->get_default_grade();
4341  
4342                  // Add user_competency_course record when in a course or module.
4343                  if (in_array($context->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE))) {
4344                      $coursecontext = $context->get_course_context();
4345                      $courseid = $coursecontext->instanceid;
4346                      $filterparams = array(
4347                          'userid' => $userid,
4348                          'competencyid' => $competencyid,
4349                          'courseid' => $courseid
4350                      );
4351                      // Fetch or create user competency course.
4352                      $usercompetencycourse = user_competency_course::get_record($filterparams);
4353                      if (!$usercompetencycourse) {
4354                          $usercompetencycourse = user_competency_course::create_relation($userid, $competencyid, $courseid);
4355                          $usercompetencycourse->create();
4356                      }
4357                      // Only update the grade and proficiency if there is not already a grade or the override option is enabled.
4358                      if ($usercompetencycourse->get('grade') === null || $overridegrade) {
4359                          // Set grade.
4360                          $usercompetencycourse->set('grade', $grade);
4361                          // Set proficiency.
4362                          $usercompetencycourse->set('proficiency', $proficiency);
4363                      }
4364  
4365                      // Check the course settings to see if we should push to user plans.
4366                      $coursesettings = course_competency_settings::get_by_courseid($courseid);
4367                      $setucgrade = $coursesettings->get('pushratingstouserplans');
4368  
4369                      if ($setucgrade) {
4370                          // Only push to user plans if there is not already a grade or the override option is enabled.
4371                          if ($usercompetency->get('grade') !== null && !$overridegrade) {
4372                              $setucgrade = false;
4373                          } else {
4374                              $ucgrade = $grade;
4375                              $ucproficiency = $proficiency;
4376                          }
4377                      }
4378                  } else {
4379  
4380                      // When completing the competency we fetch the default grade from the competency. But we only mark
4381                      // the user competency when a grade has not been set yet or if override option is enabled.
4382                      // Complete is an action to use with automated systems.
4383                      if ($usercompetency->get('grade') === null || $overridegrade) {
4384                          $setucgrade = true;
4385                          $ucgrade = $grade;
4386                          $ucproficiency = $proficiency;
4387                      }
4388                  }
4389  
4390                  break;
4391  
4392              // We override the grade, even overriding back to not set.
4393              case evidence::ACTION_OVERRIDE:
4394                  $setucgrade = true;
4395                  $ucgrade = $grade;
4396                  if (empty($competency)) {
4397                      $competency = new competency($competencyid);
4398                  }
4399                  if ($ucgrade !== null) {
4400                      $ucproficiency = $competency->get_proficiency_of_grade($ucgrade);
4401                  }
4402  
4403                  // Add user_competency_course record when in a course or module.
4404                  if (in_array($context->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE))) {
4405                      $coursecontext = $context->get_course_context();
4406                      $courseid = $coursecontext->instanceid;
4407                      $filterparams = array(
4408                          'userid' => $userid,
4409                          'competencyid' => $competencyid,
4410                          'courseid' => $courseid
4411                      );
4412                      // Fetch or create user competency course.
4413                      $usercompetencycourse = user_competency_course::get_record($filterparams);
4414                      if (!$usercompetencycourse) {
4415                          $usercompetencycourse = user_competency_course::create_relation($userid, $competencyid, $courseid);
4416                          $usercompetencycourse->create();
4417                      }
4418                      // Get proficiency.
4419                      $proficiency = $ucproficiency;
4420                      if ($proficiency === null) {
4421                          if (empty($competency)) {
4422                              $competency = new competency($competencyid);
4423                          }
4424                          $proficiency = $competency->get_proficiency_of_grade($grade);
4425                      }
4426                      // Set grade.
4427                      $usercompetencycourse->set('grade', $grade);
4428                      // Set proficiency.
4429                      $usercompetencycourse->set('proficiency', $proficiency);
4430  
4431                      $coursesettings = course_competency_settings::get_by_courseid($courseid);
4432                      if (!$coursesettings->get('pushratingstouserplans')) {
4433                          $setucgrade = false;
4434                      }
4435                  }
4436  
4437                  break;
4438  
4439              // Simply logging an evidence.
4440              case evidence::ACTION_LOG:
4441                  if ($grade !== null) {
4442                      throw new coding_exception("The grade MUST NOT be set when 'logging' an evidence.");
4443                  }
4444                  break;
4445  
4446              // Whoops, this is not expected.
4447              default:
4448                  throw new coding_exception('Unexpected action parameter when registering an evidence.');
4449                  break;
4450          }
4451  
4452          // Should we recommend?
4453          if ($recommend && $usercompetency->get('status') == user_competency::STATUS_IDLE) {
4454              $usercompetency->set('status', user_competency::STATUS_WAITING_FOR_REVIEW);
4455          }
4456  
4457          // Setting the grade and proficiency for the user competency.
4458          $wascompleted = false;
4459          if ($setucgrade == true) {
4460              if (!$usercompetency->get('proficiency') && $ucproficiency) {
4461                  $wascompleted = true;
4462              }
4463              $usercompetency->set('grade', $ucgrade);
4464              $usercompetency->set('proficiency', $ucproficiency);
4465          }
4466  
4467          // Prepare the evidence.
4468          $record = new stdClass();
4469          $record->usercompetencyid = $usercompetency->get('id');
4470          $record->contextid = $contextid;
4471          $record->action = $action;
4472          $record->descidentifier = $descidentifier;
4473          $record->desccomponent = $desccomponent;
4474          $record->grade = $grade;
4475          $record->actionuserid = $actionuserid;
4476          $record->note = $note;
4477          $evidence = new evidence(0, $record);
4478          $evidence->set('desca', $desca);
4479          $evidence->set('url', $url);
4480  
4481          // Validate both models, we should not operate on one if the other will not save.
4482          if (!$usercompetency->is_valid()) {
4483              throw new invalid_persistent_exception($usercompetency->get_errors());
4484          } else if (!$evidence->is_valid()) {
4485              throw new invalid_persistent_exception($evidence->get_errors());
4486          }
4487  
4488          // Save the user_competency_course record.
4489          if ($usercompetencycourse !== null) {
4490              // Validate and update.
4491              if (!$usercompetencycourse->is_valid()) {
4492                  throw new invalid_persistent_exception($usercompetencycourse->get_errors());
4493              }
4494              $usercompetencycourse->update();
4495          }
4496  
4497          // Finally save. Pheww!
4498          $usercompetency->update();
4499          $evidence->create();
4500  
4501          // Trigger the evidence_created event.
4502          \core\event\competency_evidence_created::create_from_evidence($evidence, $usercompetency, $recommend)->trigger();
4503  
4504          // The competency was marked as completed, apply the rules.
4505          if ($wascompleted) {
4506              self::apply_competency_rules_from_usercompetency($usercompetency, $competency, $overridegrade);
4507          }
4508  
4509          return $evidence;
4510      }
4511  
4512      /**
4513       * Read an evidence.
4514       * @param int $evidenceid The evidence ID.
4515       * @return evidence
4516       */
4517      public static function read_evidence($evidenceid) {
4518          static::require_enabled();
4519  
4520          $evidence = new evidence($evidenceid);
4521          $uc = new user_competency($evidence->get('usercompetencyid'));
4522          if (!$uc->can_read()) {
4523              throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
4524                  'nopermissions', '');
4525          }
4526  
4527          return $evidence;
4528      }
4529  
4530      /**
4531       * Delete an evidence.
4532       *
4533       * @param evidence|int $evidenceorid The evidence, or its ID.
4534       * @return bool
4535       */
4536      public static function delete_evidence($evidenceorid) {
4537          $evidence = $evidenceorid;
4538          if (!is_object($evidence)) {
4539              $evidence = new evidence($evidenceorid);
4540          }
4541  
4542          $uc = new user_competency($evidence->get('usercompetencyid'));
4543          if (!evidence::can_delete_user($uc->get('userid'))) {
4544              throw new required_capability_exception($uc->get_context(), 'moodle/competency:evidencedelete', 'nopermissions', '');
4545          }
4546  
4547          return $evidence->delete();
4548      }
4549  
4550      /**
4551       * Apply the competency rules from a user competency.
4552       *
4553       * The user competency passed should be one that was recently marked as complete.
4554       * A user competency is considered 'complete' when it's proficiency value is true.
4555       *
4556       * This method will check if the parent of this usercompetency's competency has any
4557       * rules and if so will see if they match. When matched it will take the required
4558       * step to add evidence and trigger completion, etc...
4559       *
4560       * @param  user_competency $usercompetency The user competency recently completed.
4561       * @param  competency|null $competency     The competency of the user competency, useful to avoid unnecessary read.
4562       * @return void
4563       */
4564      protected static function apply_competency_rules_from_usercompetency(user_competency $usercompetency,
4565                                                                           competency $competency = null, $overridegrade = false) {
4566  
4567          // Perform some basic checks.
4568          if (!$usercompetency->get('proficiency')) {
4569              throw new coding_exception('The user competency passed is not completed.');
4570          }
4571          if ($competency === null) {
4572              $competency = $usercompetency->get_competency();
4573          }
4574          if ($competency->get('id') != $usercompetency->get('competencyid')) {
4575              throw new coding_exception('Mismatch between user competency and competency.');
4576          }
4577  
4578          // Fetch the parent.
4579          $parent = $competency->get_parent();
4580          if ($parent === null) {
4581              return;
4582          }
4583  
4584          // The parent should have a rule, and a meaningful outcome.
4585          $ruleoutcome = $parent->get('ruleoutcome');
4586          if ($ruleoutcome == competency::OUTCOME_NONE) {
4587              return;
4588          }
4589          $rule = $parent->get_rule_object();
4590          if ($rule === null) {
4591              return;
4592          }
4593  
4594          // Fetch or create the user competency for the parent.
4595          $userid = $usercompetency->get('userid');
4596          $parentuc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $parent->get('id')));
4597          if (!$parentuc) {
4598              $parentuc = user_competency::create_relation($userid, $parent->get('id'));
4599              $parentuc->create();
4600          }
4601  
4602          // Does the rule match?
4603          if (!$rule->matches($parentuc)) {
4604              return;
4605          }
4606  
4607          // Figuring out what to do.
4608          $recommend = false;
4609          if ($ruleoutcome == competency::OUTCOME_EVIDENCE) {
4610              $action = evidence::ACTION_LOG;
4611  
4612          } else if ($ruleoutcome == competency::OUTCOME_RECOMMEND) {
4613              $action = evidence::ACTION_LOG;
4614              $recommend = true;
4615  
4616          } else if ($ruleoutcome == competency::OUTCOME_COMPLETE) {
4617              $action = evidence::ACTION_COMPLETE;
4618  
4619          } else {
4620              throw new moodle_exception('Unexpected rule outcome: ' + $ruleoutcome);
4621          }
4622  
4623          // Finally add an evidence.
4624          static::add_evidence(
4625              $userid,
4626              $parent,
4627              $parent->get_context()->id,
4628              $action,
4629              'evidence_competencyrule',
4630              'core_competency',
4631              null,
4632              $recommend,
4633              null,
4634              null,
4635              null,
4636              null,
4637              $overridegrade
4638          );
4639      }
4640  
4641      /**
4642       * Observe when a course module is marked as completed.
4643       *
4644       * Note that the user being logged in while this happens may be anyone.
4645       * Do not rely on capability checks here!
4646       *
4647       * @param  \core\event\course_module_completion_updated $event
4648       * @return void
4649       */
4650      public static function observe_course_module_completion_updated(\core\event\course_module_completion_updated $event) {
4651          if (!static::is_enabled()) {
4652              return;
4653          }
4654  
4655          $eventdata = $event->get_record_snapshot('course_modules_completion', $event->objectid);
4656  
4657          if ($eventdata->completionstate == COMPLETION_COMPLETE
4658                  || $eventdata->completionstate == COMPLETION_COMPLETE_PASS) {
4659              $coursemodulecompetencies = course_module_competency::list_course_module_competencies($eventdata->coursemoduleid);
4660  
4661              $cm = get_coursemodule_from_id(null, $eventdata->coursemoduleid);
4662              $fastmodinfo = get_fast_modinfo($cm->course)->cms[$cm->id];
4663  
4664              $cmname = $fastmodinfo->name;
4665              $url = $fastmodinfo->url;
4666  
4667              foreach ($coursemodulecompetencies as $coursemodulecompetency) {
4668                  $outcome = $coursemodulecompetency->get('ruleoutcome');
4669                  $action = null;
4670                  $recommend = false;
4671                  $strdesc = 'evidence_coursemodulecompleted';
4672                  $overridegrade = $coursemodulecompetency->get('overridegrade');
4673  
4674                  if ($outcome == course_module_competency::OUTCOME_NONE) {
4675                      continue;
4676                  }
4677                  if ($outcome == course_module_competency::OUTCOME_EVIDENCE) {
4678                      $action = evidence::ACTION_LOG;
4679  
4680                  } else if ($outcome == course_module_competency::OUTCOME_RECOMMEND) {
4681                      $action = evidence::ACTION_LOG;
4682                      $recommend = true;
4683  
4684                  } else if ($outcome == course_module_competency::OUTCOME_COMPLETE) {
4685                      $action = evidence::ACTION_COMPLETE;
4686  
4687                  } else {
4688                      throw new moodle_exception('Unexpected rule outcome: ' + $outcome);
4689                  }
4690  
4691                  static::add_evidence(
4692                      $event->relateduserid,
4693                      $coursemodulecompetency->get('competencyid'),
4694                      $event->contextid,
4695                      $action,
4696                      $strdesc,
4697                      'core_competency',
4698                      $cmname,
4699                      $recommend,
4700                      $url,
4701                      null,
4702                      null,
4703                      null,
4704                      $overridegrade
4705                  );
4706              }
4707          }
4708      }
4709  
4710      /**
4711       * Observe when a course is marked as completed.
4712       *
4713       * Note that the user being logged in while this happens may be anyone.
4714       * Do not rely on capability checks here!
4715       *
4716       * @param  \core\event\course_completed $event
4717       * @return void
4718       */
4719      public static function observe_course_completed(\core\event\course_completed $event) {
4720          if (!static::is_enabled()) {
4721              return;
4722          }
4723  
4724          $sql = 'courseid = :courseid AND ruleoutcome != :nooutcome';
4725          $params = array(
4726              'courseid' => $event->courseid,
4727              'nooutcome' => course_competency::OUTCOME_NONE
4728          );
4729          $coursecompetencies = course_competency::get_records_select($sql, $params);
4730  
4731          $course = get_course($event->courseid);
4732          $courseshortname = format_string($course->shortname, null, array('context' => $event->contextid));
4733  
4734          foreach ($coursecompetencies as $coursecompetency) {
4735  
4736              $outcome = $coursecompetency->get('ruleoutcome');
4737              $action = null;
4738              $recommend = false;
4739              $strdesc = 'evidence_coursecompleted';
4740  
4741              if ($outcome == course_module_competency::OUTCOME_NONE) {
4742                  continue;
4743              }
4744              if ($outcome == course_competency::OUTCOME_EVIDENCE) {
4745                  $action = evidence::ACTION_LOG;
4746  
4747              } else if ($outcome == course_competency::OUTCOME_RECOMMEND) {
4748                  $action = evidence::ACTION_LOG;
4749                  $recommend = true;
4750  
4751              } else if ($outcome == course_competency::OUTCOME_COMPLETE) {
4752                  $action = evidence::ACTION_COMPLETE;
4753  
4754              } else {
4755                  throw new moodle_exception('Unexpected rule outcome: ' + $outcome);
4756              }
4757  
4758              static::add_evidence(
4759                  $event->relateduserid,
4760                  $coursecompetency->get('competencyid'),
4761                  $event->contextid,
4762                  $action,
4763                  $strdesc,
4764                  'core_competency',
4765                  $courseshortname,
4766                  $recommend,
4767                  $event->get_url()
4768              );
4769          }
4770      }
4771  
4772      /**
4773       * Action to perform when a course module is deleted.
4774       *
4775       * Do not call this directly, this is reserved for core use.
4776       *
4777       * @param stdClass $cm The CM object.
4778       * @return void
4779       */
4780      public static function hook_course_module_deleted(stdClass $cm) {
4781          global $DB;
4782          $DB->delete_records(course_module_competency::TABLE, array('cmid' => $cm->id));
4783      }
4784  
4785      /**
4786       * Action to perform when a course is deleted.
4787       *
4788       * Do not call this directly, this is reserved for core use.
4789       *
4790       * @param stdClass $course The course object.
4791       * @return void
4792       */
4793      public static function hook_course_deleted(stdClass $course) {
4794          global $DB;
4795          $DB->delete_records(course_competency::TABLE, array('courseid' => $course->id));
4796          $DB->delete_records(course_competency_settings::TABLE, array('courseid' => $course->id));
4797          $DB->delete_records(user_competency_course::TABLE, array('courseid' => $course->id));
4798      }
4799  
4800      /**
4801       * Action to perform when a course is being reset.
4802       *
4803       * Do not call this directly, this is reserved for core use.
4804       *
4805       * @param int $courseid The course ID.
4806       * @return void
4807       */
4808      public static function hook_course_reset_competency_ratings($courseid) {
4809          global $DB;
4810          $DB->delete_records(user_competency_course::TABLE, array('courseid' => $courseid));
4811      }
4812  
4813      /**
4814       * Action to perform when a cohort is deleted.
4815       *
4816       * Do not call this directly, this is reserved for core use.
4817       *
4818       * @param \stdClass $cohort The cohort object.
4819       * @return void
4820       */
4821      public static function hook_cohort_deleted(\stdClass $cohort) {
4822          global $DB;
4823          $DB->delete_records(template_cohort::TABLE, array('cohortid' => $cohort->id));
4824      }
4825  
4826      /**
4827       * Action to perform when a user is deleted.
4828       *
4829       * @param int $userid The user id.
4830       */
4831      public static function hook_user_deleted($userid) {
4832          global $DB;
4833  
4834          $usercompetencies = $DB->get_records(user_competency::TABLE, ['userid' => $userid], '', 'id');
4835          foreach ($usercompetencies as $usercomp) {
4836              $DB->delete_records(evidence::TABLE, ['usercompetencyid' => $usercomp->id]);
4837          }
4838  
4839          $DB->delete_records(user_competency::TABLE, ['userid' => $userid]);
4840          $DB->delete_records(user_competency_course::TABLE, ['userid' => $userid]);
4841          $DB->delete_records(user_competency_plan::TABLE, ['userid' => $userid]);
4842  
4843          // Delete any associated files.
4844          $fs = get_file_storage();
4845          $context = context_user::instance($userid);
4846          $userevidences = $DB->get_records(user_evidence::TABLE, ['userid' => $userid], '', 'id');
4847          foreach ($userevidences as $userevidence) {
4848              $DB->delete_records(user_evidence_competency::TABLE, ['userevidenceid' => $userevidence->id]);
4849              $DB->delete_records(user_evidence::TABLE, ['id' => $userevidence->id]);
4850              $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $userevidence->id);
4851          }
4852  
4853          $userplans = $DB->get_records(plan::TABLE, ['userid' => $userid], '', 'id');
4854          foreach ($userplans as $userplan) {
4855              $DB->delete_records(plan_competency::TABLE, ['planid' => $userplan->id]);
4856              $DB->delete_records(plan::TABLE, ['id' => $userplan->id]);
4857          }
4858      }
4859  
4860      /**
4861       * Manually grade a user competency.
4862       *
4863       * @param int $userid
4864       * @param int $competencyid
4865       * @param int $grade
4866       * @param string $note A note to attach to the evidence
4867       * @return array of \core_competency\user_competency
4868       */
4869      public static function grade_competency($userid, $competencyid, $grade, $note = null) {
4870          global $USER;
4871          static::require_enabled();
4872  
4873          $uc = static::get_user_competency($userid, $competencyid);
4874          $context = $uc->get_context();
4875          if (!user_competency::can_grade_user($uc->get('userid'))) {
4876              throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', '');
4877          }
4878  
4879          // Throws exception if competency not in plan.
4880          $competency = $uc->get_competency();
4881          $competencycontext = $competency->get_context();
4882          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
4883                  $competencycontext)) {
4884              throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', '');
4885          }
4886  
4887          $action = evidence::ACTION_OVERRIDE;
4888          $desckey = 'evidence_manualoverride';
4889  
4890          $result = self::add_evidence($uc->get('userid'),
4891                                    $competency,
4892                                    $context->id,
4893                                    $action,
4894                                    $desckey,
4895                                    'core_competency',
4896                                    null,
4897                                    false,
4898                                    null,
4899                                    $grade,
4900                                    $USER->id,
4901                                    $note);
4902          if ($result) {
4903              $uc->read();
4904              $event = \core\event\competency_user_competency_rated::create_from_user_competency($uc);
4905              $event->trigger();
4906          }
4907          return $result;
4908      }
4909  
4910      /**
4911       * Manually grade a user competency from the plans page.
4912       *
4913       * @param mixed $planorid
4914       * @param int $competencyid
4915       * @param int $grade
4916       * @param string $note A note to attach to the evidence
4917       * @return array of \core_competency\user_competency
4918       */
4919      public static function grade_competency_in_plan($planorid, $competencyid, $grade, $note = null) {
4920          global $USER;
4921          static::require_enabled();
4922  
4923          $plan = $planorid;
4924          if (!is_object($planorid)) {
4925              $plan = new plan($planorid);
4926          }
4927  
4928          $context = $plan->get_context();
4929          if (!user_competency::can_grade_user($plan->get('userid'))) {
4930              throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', '');
4931          }
4932  
4933          // Throws exception if competency not in plan.
4934          $competency = $plan->get_competency($competencyid);
4935          $competencycontext = $competency->get_context();
4936          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
4937                  $competencycontext)) {
4938              throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', '');
4939          }
4940  
4941          $action = evidence::ACTION_OVERRIDE;
4942          $desckey = 'evidence_manualoverrideinplan';
4943  
4944          $result = self::add_evidence($plan->get('userid'),
4945                                    $competency,
4946                                    $context->id,
4947                                    $action,
4948                                    $desckey,
4949                                    'core_competency',
4950                                    $plan->get('name'),
4951                                    false,
4952                                    null,
4953                                    $grade,
4954                                    $USER->id,
4955                                    $note);
4956          if ($result) {
4957              $uc = static::get_user_competency($plan->get('userid'), $competency->get('id'));
4958              $event = \core\event\competency_user_competency_rated_in_plan::create_from_user_competency($uc, $plan->get('id'));
4959              $event->trigger();
4960          }
4961          return $result;
4962      }
4963  
4964      /**
4965       * Manually grade a user course competency from the course page.
4966       *
4967       * This may push the rating to the user competency
4968       * if the course is configured this way.
4969       *
4970       * @param mixed $courseorid
4971       * @param int $userid
4972       * @param int $competencyid
4973       * @param int $grade
4974       * @param string $note A note to attach to the evidence
4975       * @return array of \core_competency\user_competency
4976       */
4977      public static function grade_competency_in_course($courseorid, $userid, $competencyid, $grade, $note = null) {
4978          global $USER, $DB;
4979          static::require_enabled();
4980  
4981          $course = $courseorid;
4982          if (!is_object($courseorid)) {
4983              $course = $DB->get_record('course', array('id' => $courseorid));
4984          }
4985          $context = context_course::instance($course->id);
4986  
4987          // Check that we can view the user competency details in the course.
4988          if (!user_competency::can_read_user_in_course($userid, $course->id)) {
4989              throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
4990          }
4991  
4992          // Validate the permission to grade.
4993          if (!user_competency::can_grade_user_in_course($userid, $course->id)) {
4994              throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', '');
4995          }
4996  
4997          // Check that competency is in course and visible to the current user.
4998          $competency = course_competency::get_competency($course->id, $competencyid);
4999          $competencycontext = $competency->get_context();
5000          if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
5001                  $competencycontext)) {
5002              throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', '');
5003          }
5004  
5005          // Check that the user is enrolled in the course, and is "gradable".
5006          if (!is_enrolled($context, $userid, 'moodle/competency:coursecompetencygradable')) {
5007              throw new coding_exception('The competency may not be rated at this time.');
5008          }
5009  
5010          $action = evidence::ACTION_OVERRIDE;
5011          $desckey = 'evidence_manualoverrideincourse';
5012  
5013          $result = self::add_evidence($userid,
5014                                    $competency,
5015                                    $context->id,
5016                                    $action,
5017                                    $desckey,
5018                                    'core_competency',
5019                                    $context->get_context_name(),
5020                                    false,
5021                                    null,
5022                                    $grade,
5023                                    $USER->id,
5024                                    $note);
5025          if ($result) {
5026              $all = user_competency_course::get_multiple($userid, $course->id, array($competency->get('id')));
5027              $uc = reset($all);
5028              $event = \core\event\competency_user_competency_rated_in_course::create_from_user_competency_course($uc);
5029              $event->trigger();
5030          }
5031          return $result;
5032      }
5033  
5034      /**
5035       * Count the plans in the template, filtered by status.
5036       *
5037       * Requires moodle/competency:templateview capability at the system context.
5038       *
5039       * @param mixed $templateorid The id or the template.
5040       * @param int $status One of the plan status constants (or 0 for all plans).
5041       * @return int
5042       */
5043      public static function count_plans_for_template($templateorid, $status = 0) {
5044          static::require_enabled();
5045          $template = $templateorid;
5046          if (!is_object($template)) {
5047              $template = new template($template);
5048          }
5049  
5050          // First we do a permissions check.
5051          if (!$template->can_read()) {
5052              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
5053                  'nopermissions', '');
5054          }
5055  
5056          return plan::count_records_for_template($template->get('id'), $status);
5057      }
5058  
5059      /**
5060       * Count the user-completency-plans in the template, optionally filtered by proficiency.
5061       *
5062       * Requires moodle/competency:templateview capability at the system context.
5063       *
5064       * @param mixed $templateorid The id or the template.
5065       * @param mixed $proficiency If true, filter by proficiency, if false filter by not proficient, if null - no filter.
5066       * @return int
5067       */
5068      public static function count_user_competency_plans_for_template($templateorid, $proficiency = null) {
5069          static::require_enabled();
5070          $template = $templateorid;
5071          if (!is_object($template)) {
5072              $template = new template($template);
5073          }
5074  
5075          // First we do a permissions check.
5076          if (!$template->can_read()) {
5077               throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
5078                  'nopermissions', '');
5079          }
5080  
5081          return user_competency_plan::count_records_for_template($template->get('id'), $proficiency);
5082      }
5083  
5084      /**
5085       * List the plans in the template, filtered by status.
5086       *
5087       * Requires moodle/competency:templateview capability at the system context.
5088       *
5089       * @param mixed $templateorid The id or the template.
5090       * @param int $status One of the plan status constants (or 0 for all plans).
5091       * @param int $skip The number of records to skip
5092       * @param int $limit The max number of records to return
5093       * @return plan[]
5094       */
5095      public static function list_plans_for_template($templateorid, $status = 0, $skip = 0, $limit = 100) {
5096          $template = $templateorid;
5097          if (!is_object($template)) {
5098              $template = new template($template);
5099          }
5100  
5101          // First we do a permissions check.
5102          if (!$template->can_read()) {
5103               throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
5104                  'nopermissions', '');
5105          }
5106  
5107          return plan::get_records_for_template($template->get('id'), $status, $skip, $limit);
5108      }
5109  
5110      /**
5111       * Get the most often not completed competency for this course.
5112       *
5113       * Requires moodle/competency:coursecompetencyview capability at the course context.
5114       *
5115       * @param int $courseid The course id
5116       * @param int $skip The number of records to skip
5117       * @param int $limit The max number of records to return
5118       * @return competency[]
5119       */
5120      public static function get_least_proficient_competencies_for_course($courseid, $skip = 0, $limit = 100) {
5121          static::require_enabled();
5122          $coursecontext = context_course::instance($courseid);
5123  
5124          if (!has_any_capability(array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'),
5125                  $coursecontext)) {
5126              throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
5127          }
5128  
5129          return user_competency_course::get_least_proficient_competencies_for_course($courseid, $skip, $limit);
5130      }
5131  
5132      /**
5133       * Get the most often not completed competency for this template.
5134       *
5135       * Requires moodle/competency:templateview capability at the system context.
5136       *
5137       * @param mixed $templateorid The id or the template.
5138       * @param int $skip The number of records to skip
5139       * @param int $limit The max number of records to return
5140       * @return competency[]
5141       */
5142      public static function get_least_proficient_competencies_for_template($templateorid, $skip = 0, $limit = 100) {
5143          static::require_enabled();
5144          $template = $templateorid;
5145          if (!is_object($template)) {
5146              $template = new template($template);
5147          }
5148  
5149          // First we do a permissions check.
5150          if (!$template->can_read()) {
5151              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
5152                  'nopermissions', '');
5153          }
5154  
5155          return user_competency_plan::get_least_proficient_competencies_for_template($template->get('id'), $skip, $limit);
5156      }
5157  
5158      /**
5159       * Template event viewed.
5160       *
5161       * Requires moodle/competency:templateview capability at the system context.
5162       *
5163       * @param mixed $templateorid The id or the template.
5164       * @return boolean
5165       */
5166      public static function template_viewed($templateorid) {
5167          static::require_enabled();
5168          $template = $templateorid;
5169          if (!is_object($template)) {
5170              $template = new template($template);
5171          }
5172  
5173          // First we do a permissions check.
5174          if (!$template->can_read()) {
5175              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
5176                  'nopermissions', '');
5177          }
5178  
5179          // Trigger a template viewed event.
5180          \core\event\competency_template_viewed::create_from_template($template)->trigger();
5181  
5182          return true;
5183      }
5184  
5185      /**
5186       * Get the competency settings for a course.
5187       *
5188       * Requires moodle/competency:coursecompetencyview capability at the course context.
5189       *
5190       * @param int $courseid The course id
5191       * @return course_competency_settings
5192       */
5193      public static function read_course_competency_settings($courseid) {
5194          static::require_enabled();
5195  
5196          // First we do a permissions check.
5197          if (!course_competency_settings::can_read($courseid)) {
5198              $context = context_course::instance($courseid);
5199              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
5200          }
5201  
5202          return course_competency_settings::get_by_courseid($courseid);
5203      }
5204  
5205      /**
5206       * Update the competency settings for a course.
5207       *
5208       * Requires moodle/competency:coursecompetencyconfigure capability at the course context.
5209       *
5210       * @param int $courseid The course id
5211       * @param stdClass $settings List of settings. The only valid setting ATM is pushratginstouserplans (boolean).
5212       * @return bool
5213       */
5214      public static function update_course_competency_settings($courseid, $settings) {
5215          static::require_enabled();
5216  
5217          $settings = (object) $settings;
5218  
5219          // Get all the valid settings.
5220          $pushratingstouserplans = isset($settings->pushratingstouserplans) ? $settings->pushratingstouserplans : false;
5221  
5222          // First we do a permissions check.
5223          if (!course_competency_settings::can_manage_course($courseid)) {
5224              $context = context_course::instance($courseid);
5225              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyconfigure', 'nopermissions', '');
5226          }
5227  
5228          $exists = course_competency_settings::get_record(array('courseid' => $courseid));
5229  
5230          // Now update or insert.
5231          if ($exists) {
5232              $settings = $exists;
5233              $settings->set('pushratingstouserplans', $pushratingstouserplans);
5234              return $settings->update();
5235          } else {
5236              $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $pushratingstouserplans);
5237              $settings = new course_competency_settings(0, $data);
5238              $result = $settings->create();
5239              return !empty($result);
5240          }
5241      }
5242  
5243  
5244      /**
5245       * Function used to return a list of users where the given user has a particular capability.
5246       *
5247       * This is used e.g. to find all the users where someone is able to manage their learning plans,
5248       * it also would be useful for mentees etc.
5249       *
5250       * @param string $capability - The capability string we are filtering for. If '' is passed,
5251       *                             an always matching filter is returned.
5252       * @param int $userid - The user id we are using for the access checks. Defaults to current user.
5253       * @param int $type - The type of named params to return (passed to $DB->get_in_or_equal).
5254       * @param string $prefix - The type prefix for the db table (passed to $DB->get_in_or_equal).
5255       * @return list($sql, $params) Same as $DB->get_in_or_equal().
5256       * @todo MDL-52243 Move this function to lib/accesslib.php
5257       */
5258      public static function filter_users_with_capability_on_user_context_sql($capability, $userid = 0, $type = SQL_PARAMS_QM,
5259                                                                              $prefix='param') {
5260  
5261          global $USER, $DB;
5262          $allresultsfilter = array('> 0', array());
5263          $noresultsfilter = array('= -1', array());
5264  
5265          if (empty($capability)) {
5266              return $allresultsfilter;
5267          }
5268  
5269          if (!$capinfo = get_capability_info($capability)) {
5270              throw new coding_exception('Capability does not exist: ' . $capability);
5271          }
5272  
5273          if (empty($userid)) {
5274              $userid = $USER->id;
5275          }
5276  
5277          // Make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
5278          if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
5279              if (isguestuser($userid) or $userid == 0) {
5280                  return $noresultsfilter;
5281              }
5282          }
5283  
5284          if (is_siteadmin($userid)) {
5285              // No filtering for site admins.
5286              return $allresultsfilter;
5287          }
5288  
5289          // Check capability on system level.
5290          $syscontext = context_system::instance();
5291          $hassystem = has_capability($capability, $syscontext, $userid);
5292  
5293          $access = get_user_roles_sitewide_accessdata($userid);
5294          // Build up a list of level 2 contexts (candidates to be user context).
5295          $filtercontexts = array();
5296          // Build list of roles to check overrides.
5297          $roles = array();
5298  
5299          foreach ($access['ra'] as $path => $role) {
5300              $parts = explode('/', $path);
5301              if (count($parts) == 3) {
5302                  $filtercontexts[$parts[2]] = $parts[2];
5303              } else if (count($parts) > 3) {
5304                  // We know this is not a user context because there is another path with more than 2 levels.
5305                  unset($filtercontexts[$parts[2]]);
5306              }
5307              $roles = array_merge($roles, $role);
5308          }
5309  
5310          // Add all contexts in which a role may be overidden.
5311          $rdefs = get_role_definitions($roles);
5312          foreach ($rdefs as $roledef) {
5313              foreach ($roledef as $path => $caps) {
5314                  if (!isset($caps[$capability])) {
5315                      // The capability is not mentioned, we can ignore.
5316                      continue;
5317                  }
5318                  $parts = explode('/', $path);
5319                  if (count($parts) === 3) {
5320                      // Only get potential user contexts, they only ever have 2 slashes /parentId/Id.
5321                      $filtercontexts[$parts[2]] = $parts[2];
5322                  }
5323              }
5324          }
5325  
5326          // No interesting contexts - return all or no results.
5327          if (empty($filtercontexts)) {
5328              if ($hassystem) {
5329                  return $allresultsfilter;
5330              } else {
5331                  return $noresultsfilter;
5332              }
5333          }
5334          // Fetch all interesting contexts for further examination.
5335          list($insql, $params) = $DB->get_in_or_equal($filtercontexts, SQL_PARAMS_NAMED);
5336          $params['level'] = CONTEXT_USER;
5337          $fields = context_helper::get_preload_record_columns_sql('ctx');
5338          $interestingcontexts = $DB->get_recordset_sql('SELECT ' . $fields . '
5339                                                         FROM {context} ctx
5340                                                         WHERE ctx.contextlevel = :level
5341                                                           AND ctx.id ' . $insql . '
5342                                                         ORDER BY ctx.id', $params);
5343          if ($hassystem) {
5344              // If allowed at system, search for exceptions prohibiting the capability at user context.
5345              $excludeusers = array();
5346              foreach ($interestingcontexts as $contextrecord) {
5347                  $candidateuserid = $contextrecord->ctxinstance;
5348                  context_helper::preload_from_record($contextrecord);
5349                  $usercontext = context_user::instance($candidateuserid);
5350                  // Has capability should use the data already preloaded.
5351                  if (!has_capability($capability, $usercontext, $userid)) {
5352                      $excludeusers[$candidateuserid] = $candidateuserid;
5353                  }
5354              }
5355  
5356              // Construct SQL excluding users with this role assigned for this user.
5357              if (empty($excludeusers)) {
5358                  $interestingcontexts->close();
5359                  return $allresultsfilter;
5360              }
5361              list($sql, $params) = $DB->get_in_or_equal($excludeusers, $type, $prefix, false);
5362          } else {
5363              // If not allowed at system, search for exceptions allowing the capability at user context.
5364              $allowusers = array();
5365              foreach ($interestingcontexts as $contextrecord) {
5366                  $candidateuserid = $contextrecord->ctxinstance;
5367                  context_helper::preload_from_record($contextrecord);
5368                  $usercontext = context_user::instance($candidateuserid);
5369                  // Has capability should use the data already preloaded.
5370                  if (has_capability($capability, $usercontext, $userid)) {
5371                      $allowusers[$candidateuserid] = $candidateuserid;
5372                  }
5373              }
5374  
5375              // Construct SQL excluding users with this role assigned for this user.
5376              if (empty($allowusers)) {
5377                  $interestingcontexts->close();
5378                  return $noresultsfilter;
5379              }
5380              list($sql, $params) = $DB->get_in_or_equal($allowusers, $type, $prefix);
5381          }
5382          $interestingcontexts->close();
5383  
5384          // Return the goods!.
5385          return array($sql, $params);
5386      }
5387  
5388  }