Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

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

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

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