Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

   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 competencies 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 coding_exception;
  28  use context_system;
  29  use lang_string;
  30  use stdClass;
  31  
  32  require_once($CFG->libdir . '/grade/grade_scale.php');
  33  
  34  /**
  35   * Class for loading/storing competencies from the DB.
  36   *
  37   * @copyright  2015 Damyon Wiese
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class competency extends persistent {
  41  
  42      const TABLE = 'competency';
  43  
  44      /** Outcome none. */
  45      const OUTCOME_NONE = 0;
  46      /** Outcome evidence. */
  47      const OUTCOME_EVIDENCE = 1;
  48      /** Outcome complete. */
  49      const OUTCOME_COMPLETE = 2;
  50      /** Outcome recommend. */
  51      const OUTCOME_RECOMMEND = 3;
  52  
  53      /** @var competency Object before update. */
  54      protected $beforeupdate = null;
  55  
  56      /** @var competency|null To store new parent. */
  57      protected $newparent;
  58  
  59      /**
  60       * Return the definition of the properties of this model.
  61       *
  62       * @return array
  63       */
  64      protected static function define_properties() {
  65          return array(
  66              'shortname' => array(
  67                  'type' => PARAM_TEXT
  68              ),
  69              'idnumber' => array(
  70                  'type' => PARAM_RAW
  71              ),
  72              'description' => array(
  73                  'default' => '',
  74                  'type' => PARAM_CLEANHTML
  75              ),
  76              'descriptionformat' => array(
  77                  'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN),
  78                  'type' => PARAM_INT,
  79                  'default' => FORMAT_HTML
  80              ),
  81              'sortorder' => array(
  82                  'default' => 0,
  83                  'type' => PARAM_INT
  84              ),
  85              'parentid' => array(
  86                  'default' => 0,
  87                  'type' => PARAM_INT
  88              ),
  89              'path' => array(
  90                  'default' => '/0/',
  91                  'type' => PARAM_RAW
  92              ),
  93              'ruleoutcome' => array(
  94                  'choices' => array(self::OUTCOME_NONE, self::OUTCOME_EVIDENCE, self::OUTCOME_COMPLETE, self::OUTCOME_RECOMMEND),
  95                  'default' => self::OUTCOME_NONE,
  96                  'type' => PARAM_INT
  97              ),
  98              'ruletype' => array(
  99                  'type' => PARAM_RAW,
 100                  'default' => null,
 101                  'null' => NULL_ALLOWED
 102              ),
 103              'ruleconfig' => array(
 104                  'default' => null,
 105                  'type' => PARAM_RAW,
 106                  'null' => NULL_ALLOWED
 107              ),
 108              'scaleid' => array(
 109                  'default' => null,
 110                  'type' => PARAM_INT,
 111                  'null' => NULL_ALLOWED
 112              ),
 113              'scaleconfiguration' => array(
 114                  'default' => null,
 115                  'type' => PARAM_RAW,
 116                  'null' => NULL_ALLOWED
 117              ),
 118              'competencyframeworkid' => array(
 119                  'default' => 0,
 120                  'type' => PARAM_INT
 121              ),
 122          );
 123      }
 124  
 125      /**
 126       * Hook to execute before validate.
 127       *
 128       * @return void
 129       */
 130      protected function before_validate() {
 131          $this->beforeupdate = null;
 132          $this->newparent = null;
 133  
 134          // During update.
 135          if ($this->get('id')) {
 136              $this->beforeupdate = new competency($this->get('id'));
 137  
 138              // The parent ID has changed.
 139              if ($this->beforeupdate->get('parentid') != $this->get('parentid')) {
 140                  $this->newparent = $this->get_parent();
 141  
 142                  // Update path and sortorder.
 143                  $this->set_new_path($this->newparent);
 144                  $this->set_new_sortorder();
 145              }
 146  
 147          } else {
 148              // During create.
 149  
 150              $this->set_new_path();
 151              // Always generate new sortorder when we create new competency.
 152              $this->set_new_sortorder();
 153  
 154          }
 155      }
 156  
 157      /**
 158       * Hook to execute after an update.
 159       *
 160       * @param bool $result Whether or not the update was successful.
 161       * @return void
 162       */
 163      protected function after_update($result) {
 164          global $DB;
 165  
 166          if (!$result) {
 167              $this->beforeupdate = null;
 168              return;
 169          }
 170  
 171          // The parent ID has changed, we need to fix all the paths of the children.
 172          if ($this->beforeupdate->get('parentid') != $this->get('parentid')) {
 173              $beforepath = $this->beforeupdate->get('path') . $this->get('id') . '/';
 174  
 175              $like = $DB->sql_like('path', '?');
 176              $likesearch = $DB->sql_like_escape($beforepath) . '%';
 177  
 178              $table = '{' . self::TABLE . '}';
 179              $sql = "UPDATE $table SET path = REPLACE(path, ?, ?) WHERE " . $like;
 180              $DB->execute($sql, array(
 181                  $beforepath,
 182                  $this->get('path') . $this->get('id') . '/',
 183                  $likesearch
 184              ));
 185  
 186              // Resolving sortorder holes left after changing parent.
 187              $table = '{' . self::TABLE . '}';
 188              $sql = "UPDATE $table SET sortorder = sortorder -1 "
 189                      . " WHERE  competencyframeworkid = ? AND parentid = ? AND sortorder > ?";
 190              $DB->execute($sql, array($this->get('competencyframeworkid'),
 191                                          $this->beforeupdate->get('parentid'),
 192                                          $this->beforeupdate->get('sortorder')
 193                                      ));
 194          }
 195  
 196          $this->beforeupdate = null;
 197      }
 198  
 199  
 200      /**
 201       * Hook to execute after a delete.
 202       *
 203       * @param bool $result Whether or not the delete was successful.
 204       * @return void
 205       */
 206      protected function after_delete($result) {
 207          global $DB;
 208          if (!$result) {
 209              return;
 210          }
 211  
 212          // Resolving sortorder holes left after delete.
 213          $table = '{' . self::TABLE . '}';
 214          $sql = "UPDATE $table SET sortorder = sortorder -1  WHERE  competencyframeworkid = ? AND parentid = ? AND sortorder > ?";
 215          $DB->execute($sql, array($this->get('competencyframeworkid'), $this->get('parentid'), $this->get('sortorder')));
 216      }
 217  
 218      /**
 219       * Extracts the default grade from the scale configuration.
 220       *
 221       * Returns an array where the first element is the grade, and the second
 222       * is a boolean representing whether or not this grade is considered 'proficient'.
 223       *
 224       * @return array(int grade, bool proficient)
 225       */
 226      public function get_default_grade() {
 227          $scaleid = $this->get('scaleid');
 228          $scaleconfig = $this->get('scaleconfiguration');
 229          if ($scaleid === null) {
 230              $scaleconfig = $this->get_framework()->get('scaleconfiguration');
 231          }
 232          return competency_framework::get_default_grade_from_scale_configuration($scaleconfig);
 233      }
 234  
 235      /**
 236       * Get the competency framework.
 237       *
 238       * @return competency_framework
 239       */
 240      public function get_framework() {
 241          return new competency_framework($this->get('competencyframeworkid'));
 242      }
 243  
 244      /**
 245       * Get the competency level.
 246       *
 247       * @return int
 248       */
 249      public function get_level() {
 250          $path = $this->get('path');
 251          $path = trim($path, '/');
 252          return substr_count($path, '/') + 1;
 253      }
 254  
 255      /**
 256       * Return the parent competency.
 257       *
 258       * @return null|competency
 259       */
 260      public function get_parent() {
 261          $parentid = $this->get('parentid');
 262          if (!$parentid) {
 263              return null;
 264          }
 265          return new competency($parentid);
 266      }
 267  
 268      /**
 269       * Extracts the proficiency of a grade from the scale configuration.
 270       *
 271       * @param int $grade The grade (scale item ID).
 272       * @return array(int grade, bool proficient)
 273       */
 274      public function get_proficiency_of_grade($grade) {
 275          $scaleid = $this->get('scaleid');
 276          $scaleconfig = $this->get('scaleconfiguration');
 277          if ($scaleid === null) {
 278              $scaleconfig = $this->get_framework()->get('scaleconfiguration');
 279          }
 280          return competency_framework::get_proficiency_of_grade_from_scale_configuration($scaleconfig, $grade);
 281      }
 282  
 283      /**
 284       * Return the related competencies.
 285       *
 286       * @return competency[]
 287       */
 288      public function get_related_competencies() {
 289          return related_competency::get_related_competencies($this->get('id'));
 290      }
 291  
 292      /**
 293       * Get the rule object.
 294       *
 295       * @return null|competency_rule
 296       */
 297      public function get_rule_object() {
 298          $rule = $this->get('ruletype');
 299  
 300          if (!$rule || !is_subclass_of($rule, 'core_competency\\competency_rule')) {
 301              // Double check that the rule is extending the right class to avoid bad surprises.
 302              return null;
 303          }
 304  
 305          return new $rule($this);
 306      }
 307  
 308      /**
 309       * Return the scale.
 310       *
 311       * @return \grade_scale
 312       */
 313      public function get_scale() {
 314          $scaleid = $this->get('scaleid');
 315          if ($scaleid === null) {
 316              return $this->get_framework()->get_scale();
 317          }
 318          $scale = \grade_scale::fetch(array('id' => $scaleid));
 319          $scale->load_items();
 320          return $scale;
 321      }
 322  
 323      /**
 324       * Returns true when the competency has user competencies.
 325       *
 326       * This is useful to determine if the competency, or part of it, should be locked down.
 327       *
 328       * @return boolean
 329       */
 330      public function has_user_competencies() {
 331          return user_competency::has_records_for_competency($this->get('id')) ||
 332              user_competency_plan::has_records_for_competency($this->get('id'));
 333      }
 334  
 335      /**
 336       * Check if the competency is the parent of passed competencies.
 337       *
 338       * @param  array $ids IDs of supposedly direct children.
 339       * @return boolean
 340       */
 341      public function is_parent_of(array $ids) {
 342          global $DB;
 343  
 344          list($insql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
 345          $params['parentid'] = $this->get('id');
 346  
 347          return $DB->count_records_select(self::TABLE, "id $insql AND parentid = :parentid", $params) == count($ids);
 348      }
 349  
 350      /**
 351       * Reset the rule.
 352       *
 353       * @return void
 354       */
 355      public function reset_rule() {
 356          $this->raw_set('ruleoutcome', static::OUTCOME_NONE);
 357          $this->raw_set('ruletype', null);
 358          $this->raw_set('ruleconfig', null);
 359      }
 360  
 361      /**
 362       * Helper method to set the path.
 363       *
 364       * @param competency $parent The parent competency object.
 365       * @return void
 366       */
 367      protected function set_new_path(competency $parent = null) {
 368          $path = '/0/';
 369          if ($this->get('parentid')) {
 370              $parent = $parent !== null ? $parent : $this->get_parent();
 371              $path = $parent->get('path') . $this->get('parentid') . '/';
 372          }
 373          $this->raw_set('path', $path);
 374      }
 375  
 376      /**
 377       * Helper method to set the sortorder.
 378       *
 379       * @return void
 380       */
 381      protected function set_new_sortorder() {
 382          $search = array('parentid' => $this->get('parentid'), 'competencyframeworkid' => $this->get('competencyframeworkid'));
 383          $this->raw_set('sortorder', $this->count_records($search));
 384      }
 385  
 386      /**
 387       * This does a specialised search that finds all nodes in the tree with matching text on any text like field,
 388       * and returns this node and all its parents in a displayable sort order.
 389       *
 390       * @param string $searchtext The text to search for.
 391       * @param int $competencyframeworkid The competency framework to limit the search.
 392       * @return persistent[]
 393       */
 394      public static function search($searchtext, $competencyframeworkid) {
 395          global $DB;
 396  
 397          $like1 = $DB->sql_like('shortname', ':like1', false);
 398          $like2 = $DB->sql_like('idnumber', ':like2', false);
 399          $like3 = $DB->sql_like('description', ':like3', false);
 400  
 401          $params = array(
 402              'like1' => '%' . $DB->sql_like_escape($searchtext) . '%',
 403              'like2' => '%' . $DB->sql_like_escape($searchtext) . '%',
 404              'like3' => '%' . $DB->sql_like_escape($searchtext) . '%',
 405              'frameworkid' => $competencyframeworkid
 406          );
 407  
 408          $sql = 'competencyframeworkid = :frameworkid AND ((' . $like1 . ') OR (' . $like2 . ') OR (' . $like3 . '))';
 409          $records = $DB->get_records_select(self::TABLE, $sql, $params, 'path, sortorder ASC', '*');
 410  
 411          // Now get all the parents.
 412          $parents = array();
 413          foreach ($records as $record) {
 414              $split = explode('/', trim($record->path, '/'));
 415              foreach ($split as $parent) {
 416                  $parents[intval($parent)] = true;
 417              }
 418          }
 419          $parents = array_keys($parents);
 420  
 421          // Skip ones we already fetched.
 422          foreach ($parents as $idx => $parent) {
 423              if ($parent == 0 || isset($records[$parent])) {
 424                  unset($parents[$idx]);
 425              }
 426          }
 427  
 428          if (count($parents)) {
 429              list($parentsql, $parentparams) = $DB->get_in_or_equal($parents, SQL_PARAMS_NAMED);
 430  
 431              $parentrecords = $DB->get_records_select(self::TABLE, 'id ' . $parentsql,
 432                      $parentparams, 'path, sortorder ASC', '*');
 433  
 434              foreach ($parentrecords as $id => $record) {
 435                  $records[$id] = $record;
 436              }
 437          }
 438  
 439          $instances = array();
 440          // Convert to instances of this class.
 441          foreach ($records as $record) {
 442              $newrecord = new static(0, $record);
 443              $instances[$newrecord->get('id')] = $newrecord;
 444          }
 445          return $instances;
 446      }
 447  
 448      /**
 449       * Validate the competency framework ID.
 450       *
 451       * @param int $value The framework ID.
 452       * @return true|lang_string
 453       */
 454      protected function validate_competencyframeworkid($value) {
 455  
 456          // During update.
 457          if ($this->get('id')) {
 458  
 459              // Ensure that we are not trying to move the competency across frameworks.
 460              if ($this->beforeupdate->get('competencyframeworkid') != $value) {
 461                  return new lang_string('invaliddata', 'error');
 462              }
 463  
 464          } else {
 465              // During create.
 466  
 467              // Check that the framework exists.
 468              if (!competency_framework::record_exists($value)) {
 469                  return new lang_string('invaliddata', 'error');
 470              }
 471          }
 472  
 473          return true;
 474      }
 475  
 476      /**
 477       * Validate the ID number.
 478       *
 479       * @param string $value The ID number.
 480       * @return true|lang_string
 481       */
 482      protected function validate_idnumber($value) {
 483          global $DB;
 484          $sql = 'idnumber = :idnumber AND competencyframeworkid = :competencyframeworkid AND id <> :id';
 485          $params = array(
 486              'id' => $this->get('id'),
 487              'idnumber' => $value,
 488              'competencyframeworkid' => $this->get('competencyframeworkid')
 489          );
 490          if ($DB->record_exists_select(self::TABLE, $sql, $params)) {
 491              return new lang_string('idnumbertaken', 'error');
 492          }
 493          return true;
 494      }
 495  
 496      /**
 497       * Validate the path.
 498       *
 499       * @param string $value The path.
 500       * @return true|lang_string
 501       */
 502      protected function validate_path($value) {
 503  
 504          // The last item should be the parent ID.
 505          $id = $this->get('parentid');
 506          if (substr($value, -(strlen($id) + 2)) != '/' . $id . '/') {
 507              return new lang_string('invaliddata', 'error');
 508  
 509          } else if (!preg_match('@/([0-9]+/)+@', $value)) {
 510              // The format of the path is not correct.
 511              return new lang_string('invaliddata', 'error');
 512          }
 513  
 514          return true;
 515      }
 516  
 517      /**
 518       * Validate the parent ID.
 519       *
 520       * @param string $value The ID.
 521       * @return true|lang_string
 522       */
 523      protected function validate_parentid($value) {
 524  
 525          // Check that the parent exists. But only if we don't have it already, and we actually have a parent.
 526          if (!empty($value) && !$this->newparent && !self::record_exists($value)) {
 527              return new lang_string('invaliddata', 'error');
 528          }
 529  
 530          // During update.
 531          if ($this->get('id')) {
 532  
 533              // If there is a new parent.
 534              if ($this->beforeupdate->get('parentid') != $value && $this->newparent) {
 535  
 536                  // Check that the new parent belongs to the same framework.
 537                  if ($this->newparent->get('competencyframeworkid') != $this->get('competencyframeworkid')) {
 538                      return new lang_string('invaliddata', 'error');
 539                  }
 540              }
 541          }
 542  
 543          return true;
 544      }
 545  
 546      /**
 547       * Validate the rule.
 548       *
 549       * @param string $value The ID.
 550       * @return true|lang_string
 551       */
 552      protected function validate_ruletype($value) {
 553          if ($value === null) {
 554              return true;
 555          }
 556  
 557          if (!class_exists($value) || !is_subclass_of($value, 'core_competency\\competency_rule')) {
 558              return new lang_string('invaliddata', 'error');
 559          }
 560  
 561          return true;
 562      }
 563  
 564      /**
 565       * Validate the rule config.
 566       *
 567       * @param string $value The ID.
 568       * @return true|lang_string
 569       */
 570      protected function validate_ruleconfig($value) {
 571          $rule = $this->get_rule_object();
 572  
 573          // We don't have a rule.
 574          if (empty($rule)) {
 575              if ($value === null) {
 576                  // No config, perfect.
 577                  return true;
 578              }
 579              // Config but no rules, whoops!
 580              return new lang_string('invaliddata', 'error');
 581          }
 582  
 583          $valid = $rule->validate_config($value);
 584          if ($valid !== true) {
 585              // Whoops!
 586              return new lang_string('invaliddata', 'error');
 587          }
 588  
 589          return true;
 590      }
 591  
 592      /**
 593       * Validate the scale ID.
 594       *
 595       * Note that the value for a scale can never be 0, null has to be used when
 596       * the framework's scale has to be used.
 597       *
 598       * @param  int $value
 599       * @return true|lang_string
 600       */
 601      protected function validate_scaleid($value) {
 602          global $DB;
 603  
 604          if ($value === null) {
 605              return true;
 606          }
 607  
 608          // Always validate that the scale exists.
 609          if (!$DB->record_exists_select('scale', 'id = :id', array('id' => $value))) {
 610              return new lang_string('invalidscaleid', 'error');
 611          }
 612  
 613          // During update.
 614          if ($this->get('id')) {
 615  
 616              // Validate that we can only change the scale when it is not used yet.
 617              if ($this->beforeupdate->get('scaleid') != $value) {
 618                  if ($this->has_user_competencies()) {
 619                      return new lang_string('errorscalealreadyused', 'core_competency');
 620                  }
 621              }
 622  
 623          }
 624  
 625          return true;
 626      }
 627  
 628      /**
 629       * Validate the scale configuration.
 630       *
 631       * This logic is adapted from {@link \core_competency\competency_framework::validate_scaleconfiguration()}.
 632       *
 633       * @param  string $value The scale configuration.
 634       * @return bool|lang_string
 635       */
 636      protected function validate_scaleconfiguration($value) {
 637          $scaleid = $this->get('scaleid');
 638          if ($scaleid === null && $value === null) {
 639              return true;
 640          }
 641  
 642          $scaledefaultselected = false;
 643          $proficientselected = false;
 644          $scaleconfigurations = json_decode($value);
 645  
 646          if (is_array($scaleconfigurations)) {
 647  
 648              // The first element of the array contains the scale ID.
 649              $scaleinfo = array_shift($scaleconfigurations);
 650              if (empty($scaleinfo) || !isset($scaleinfo->scaleid) || $scaleinfo->scaleid != $scaleid) {
 651                  // This should never happen.
 652                  return new lang_string('errorscaleconfiguration', 'core_competency');
 653              }
 654  
 655              // Walk through the array to find proficient and default values.
 656              foreach ($scaleconfigurations as $scaleconfiguration) {
 657                  if (isset($scaleconfiguration->scaledefault) && $scaleconfiguration->scaledefault) {
 658                      $scaledefaultselected = true;
 659                  }
 660                  if (isset($scaleconfiguration->proficient) && $scaleconfiguration->proficient) {
 661                      $proficientselected = true;
 662                  }
 663              }
 664          }
 665  
 666          if (!$scaledefaultselected || !$proficientselected) {
 667              return new lang_string('errorscaleconfiguration', 'core_competency');
 668          }
 669  
 670          return true;
 671      }
 672  
 673      /**
 674       * Return whether or not the competency IDs share the same framework.
 675       *
 676       * @param  array  $ids Competency IDs
 677       * @return bool
 678       */
 679      public static function share_same_framework(array $ids) {
 680          global $DB;
 681          list($insql, $params) = $DB->get_in_or_equal($ids);
 682          $sql = "SELECT COUNT('x') FROM (SELECT DISTINCT(competencyframeworkid) FROM {" . self::TABLE . "} WHERE id {$insql}) f";
 683          return $DB->count_records_sql($sql, $params) == 1;
 684      }
 685  
 686      /**
 687       * Get the available rules.
 688       *
 689       * @return array Keys are the class names, values are the name of the rule.
 690       */
 691      public static function get_available_rules() {
 692          // Fully qualified class names without leading slashes because get_class() does not add them either.
 693          $rules = array(
 694              'core_competency\\competency_rule_all' => competency_rule_all::get_name(),
 695              'core_competency\\competency_rule_points' => competency_rule_points::get_name(),
 696          );
 697          return $rules;
 698      }
 699  
 700      /**
 701       * Return the current depth of a competency framework.
 702       *
 703       * @param int $frameworkid The framework ID.
 704       * @return int
 705       */
 706      public static function get_framework_depth($frameworkid) {
 707          global $DB;
 708          $totallength = $DB->sql_length('path');
 709          $trimmedlength = $DB->sql_length("REPLACE(path, '/', '')");
 710          $sql = "SELECT ($totallength - $trimmedlength - 1) AS depth
 711                    FROM {" . self::TABLE . "}
 712                   WHERE competencyframeworkid = :id
 713                ORDER BY depth DESC";
 714          $record = $DB->get_record_sql($sql, array('id' => $frameworkid), IGNORE_MULTIPLE);
 715          if (!$record) {
 716              $depth = 0;
 717          } else {
 718              $depth = $record->depth;
 719          }
 720          return $depth;
 721      }
 722  
 723      /**
 724       * Build a framework tree with competency nodes.
 725       *
 726       * @param  int  $frameworkid the framework id
 727       * @return stdClass[] tree of framework competency nodes
 728       */
 729      public static function get_framework_tree($frameworkid) {
 730          $competencies = self::search('', $frameworkid);
 731          return self::build_tree($competencies, 0);
 732      }
 733  
 734      /**
 735       * Get the context from the framework.
 736       *
 737       * @return \context
 738       */
 739      public function get_context() {
 740          return $this->get_framework()->get_context();
 741      }
 742  
 743      /**
 744       * Recursively build up the tree of nodes.
 745       *
 746       * @param array $all - List of all competency classes.
 747       * @param int $parentid - The current parent ID. Pass 0 to build the tree from the top.
 748       * @return stdClass[] $tree tree of nodes
 749       */
 750      protected static function build_tree($all, $parentid) {
 751          $tree = array();
 752          foreach ($all as $one) {
 753              if ($one->get('parentid') == $parentid) {
 754                  $node = new stdClass();
 755                  $node->competency = $one;
 756                  $node->children = self::build_tree($all, $one->get('id'));
 757                  $tree[] = $node;
 758              }
 759          }
 760          return $tree;
 761      }
 762  
 763      /**
 764       * Check if we can delete competencies safely.
 765       *
 766       * This moethod does not check any capablities.
 767       * Check if competency is used in a plan and user competency.
 768       * Check if competency is used in a template.
 769       * Check if competency is linked to a course.
 770       *
 771       * @param array $ids Array of competencies ids.
 772       * @return bool True if we can delete the competencies.
 773       */
 774      public static function can_all_be_deleted($ids) {
 775          global $CFG;
 776  
 777          if (empty($ids)) {
 778              return true;
 779          }
 780          // Check if competency is used in template.
 781          if (template_competency::has_records_for_competencies($ids)) {
 782              return false;
 783          }
 784          // Check if competency is used in plan.
 785          if (plan_competency::has_records_for_competencies($ids)) {
 786              return false;
 787          }
 788          // Check if competency is used in course.
 789          if (course_competency::has_records_for_competencies($ids)) {
 790              return false;
 791          }
 792          // Check if competency is used in user_competency.
 793          if (user_competency::has_records_for_competencies($ids)) {
 794              return false;
 795          }
 796          // Check if competency is used in user_competency_plan.
 797          if (user_competency_plan::has_records_for_competencies($ids)) {
 798              return false;
 799          }
 800  
 801          require_once($CFG->libdir . '/badgeslib.php');
 802          // Check if competency is used in a badge.
 803          if (badge_award_criteria_competency_has_records_for_competencies($ids)) {
 804              return false;
 805          }
 806  
 807          return true;
 808      }
 809  
 810      /**
 811       * Delete the competencies.
 812       *
 813       * This method is reserved to core usage.
 814       * This method does not trigger the after_delete event.
 815       * This method does not delete related objects such as related competencies and evidences.
 816       *
 817       * @param array $ids The competencies ids.
 818       * @return bool True if the competencies were deleted successfully.
 819       */
 820      public static function delete_multiple($ids) {
 821          global $DB;
 822          list($insql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
 823          return $DB->delete_records_select(self::TABLE, "id $insql", $params);
 824      }
 825  
 826      /**
 827       * Get descendant ids.
 828       *
 829       * @param competency $competency The competency.
 830       * @return array Array of competencies ids.
 831       */
 832      public static function get_descendants_ids($competency) {
 833          global $DB;
 834  
 835          $path = $DB->sql_like_escape($competency->get('path') . $competency->get('id') . '/') . '%';
 836          $like = $DB->sql_like('path', ':likepath');
 837          return $DB->get_fieldset_select(self::TABLE, 'id', $like, array('likepath' => $path));
 838      }
 839  
 840      /**
 841       * Get competencyids by frameworkid.
 842       *
 843       * @param int $frameworkid The competency framework ID.
 844       * @return array Array of competency ids.
 845       */
 846      public static function get_ids_by_frameworkid($frameworkid) {
 847          global $DB;
 848  
 849          return $DB->get_fieldset_select(self::TABLE, 'id', 'competencyframeworkid = :frmid', array('frmid' => $frameworkid));
 850      }
 851  
 852      /**
 853       * Delete competencies by framework ID.
 854       *
 855       * This method is reserved to core usage.
 856       * This method does not trigger the after_delete event.
 857       * This method does not delete related objects such as related competencies and evidences.
 858       *
 859       * @param int $id the framework ID
 860       * @return bool Return true if delete was successful.
 861       */
 862      public static function delete_by_frameworkid($id) {
 863          global $DB;
 864          return $DB->delete_records(self::TABLE, array('competencyframeworkid' => $id));
 865      }
 866  
 867      /**
 868       * Get competency ancestors.
 869       *
 870       * @return competency[] Return array of ancestors.
 871       */
 872      public function get_ancestors() {
 873          global $DB;
 874          $ancestors = array();
 875          $ancestorsids = explode('/', trim($this->get('path'), '/'));
 876          // Drop the root item from the array /0/.
 877          array_shift($ancestorsids);
 878          if (!empty($ancestorsids)) {
 879              list($insql, $params) = $DB->get_in_or_equal($ancestorsids, SQL_PARAMS_NAMED);
 880              $ancestors = self::get_records_select("id $insql", $params);
 881          }
 882          return $ancestors;
 883      }
 884  
 885  }