[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/competency/classes/ -> api.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1