Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   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 plans persistence.
  19   *
  20   * @package    core_competency
  21   * @copyright  2015 David Monllao
  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 comment;
  28  use context_user;
  29  use dml_missing_record_exception;
  30  use lang_string;
  31  
  32  /**
  33   * Class for loading/storing plans from the DB.
  34   *
  35   * @copyright  2015 David Monllao
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class plan extends persistent {
  39  
  40      const TABLE = 'competency_plan';
  41  
  42      /** Draft status. */
  43      const STATUS_DRAFT = 0;
  44  
  45      /** Active status. */
  46      const STATUS_ACTIVE = 1;
  47  
  48      /** Complete status. */
  49      const STATUS_COMPLETE = 2;
  50  
  51      /** Waiting for review. */
  52      const STATUS_WAITING_FOR_REVIEW = 3;
  53  
  54      /** In review. */
  55      const STATUS_IN_REVIEW = 4;
  56  
  57      /** 10 minutes threshold **/
  58      const DUEDATE_THRESHOLD = 600;
  59  
  60      /** @var plan object before update. */
  61      protected $beforeupdate = null;
  62  
  63      /**
  64       * Return the definition of the properties of this model.
  65       *
  66       * @return array
  67       */
  68      protected static function define_properties() {
  69          return array(
  70              'name' => array(
  71                  'type' => PARAM_TEXT,
  72              ),
  73              'description' => array(
  74                  'type' => PARAM_CLEANHTML,
  75                  'default' => ''
  76              ),
  77              'descriptionformat' => array(
  78                  'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN),
  79                  'type' => PARAM_INT,
  80                  'default' => FORMAT_HTML,
  81              ),
  82              'userid' => array(
  83                  'type' => PARAM_INT,
  84              ),
  85              'templateid' => array(
  86                  'type' => PARAM_INT,
  87                  'default' => null,
  88                  'null' => NULL_ALLOWED,
  89              ),
  90              'origtemplateid' => array(
  91                  'type' => PARAM_INT,
  92                  'default' => null,
  93                  'null' => NULL_ALLOWED,
  94              ),
  95              'status' => array(
  96                  'choices' => array(self::STATUS_DRAFT, self::STATUS_COMPLETE, self::STATUS_ACTIVE,
  97                      self::STATUS_WAITING_FOR_REVIEW, self::STATUS_IN_REVIEW),
  98                  'type' => PARAM_INT,
  99                  'default' => self::STATUS_DRAFT,
 100              ),
 101              'duedate' => array(
 102                  'type' => PARAM_INT,
 103                  'default' => 0,
 104              ),
 105              'reviewerid' => array(
 106                  'type' => PARAM_INT,
 107                  'default' => null,
 108                  'null' => NULL_ALLOWED,
 109              )
 110          );
 111      }
 112  
 113      /**
 114       * Hook to execute before validate.
 115       *
 116       * @return void
 117       */
 118      protected function before_validate() {
 119          $this->beforeupdate = null;
 120  
 121          // During update.
 122          if ($this->get('id')) {
 123              $this->beforeupdate = new self($this->get('id'));
 124          }
 125      }
 126  
 127      /**
 128       * Whether the current user can comment on this plan.
 129       *
 130       * @return bool
 131       */
 132      public function can_comment() {
 133          return static::can_comment_user($this->get('userid'));
 134      }
 135  
 136      /**
 137       * Whether the current user can manage the plan.
 138       *
 139       * @return bool
 140       */
 141      public function can_manage() {
 142          if ($this->is_draft()) {
 143              return self::can_manage_user_draft($this->get('userid'));
 144          }
 145          return self::can_manage_user($this->get('userid'));
 146      }
 147  
 148      /**
 149       * Whether the current user can read the plan.
 150       *
 151       * @return bool
 152       */
 153      public function can_read() {
 154          if ($this->is_draft()) {
 155              return self::can_read_user_draft($this->get('userid'));
 156          }
 157          return self::can_read_user($this->get('userid'));
 158      }
 159  
 160      /**
 161       * Whether the current user can read comments on this plan.
 162       *
 163       * @return bool
 164       */
 165      public function can_read_comments() {
 166          return $this->can_read();
 167      }
 168  
 169      /**
 170       * Whether the current user can request a review of the plan.
 171       *
 172       * @return bool
 173       */
 174      public function can_request_review() {
 175          return self::can_request_review_user($this->get('userid'));
 176      }
 177  
 178      /**
 179       * Whether the current user can review the plan.
 180       *
 181       * @return bool
 182       */
 183      public function can_review() {
 184          return self::can_review_user($this->get('userid'));
 185      }
 186  
 187      /**
 188       * Get the comment object.
 189       *
 190       * @return comment
 191       */
 192      public function get_comment_object() {
 193          global $CFG;
 194          require_once($CFG->dirroot . '/comment/lib.php');
 195  
 196          if (!$this->get('id')) {
 197              throw new \coding_exception('The plan must exist.');
 198          }
 199  
 200          $comment = new comment((object) array(
 201              'client_id' => 'plancommentarea' . $this->get('id'),
 202              'context' => $this->get_context(),
 203              'component' => 'competency',    // This cannot be named 'core_competency'.
 204              'itemid' => $this->get('id'),
 205              'area' => 'plan',
 206              'showcount' => true,
 207          ));
 208          $comment->set_fullwidth(true);
 209          return $comment;
 210      }
 211  
 212      /**
 213       * Get the competencies in this plan.
 214       *
 215       * @return competency[]
 216       */
 217      public function get_competencies() {
 218          $competencies = array();
 219  
 220          if ($this->get('status') == self::STATUS_COMPLETE) {
 221              // Get the competencies from the archive of the plan.
 222              $competencies = user_competency_plan::list_competencies($this->get('id'), $this->get('userid'));
 223          } else if ($this->is_based_on_template()) {
 224              // Get the competencies from the template.
 225              $competencies = template_competency::list_competencies($this->get('templateid'));
 226          } else {
 227              // Get the competencies from the plan.
 228              $competencies = plan_competency::list_competencies($this->get('id'));
 229          }
 230  
 231          return $competencies;
 232      }
 233  
 234      /**
 235       * Get a single competency from this plan.
 236       *
 237       * This will throw an exception if the competency does not belong to the plan.
 238       *
 239       * @param int $competencyid The competency ID.
 240       * @return competency
 241       */
 242      public function get_competency($competencyid) {
 243          $competency = null;
 244  
 245          if ($this->get('status') == self::STATUS_COMPLETE) {
 246              // Get the competency from the archive of the plan.
 247              $competency = user_competency_plan::get_competency_by_planid($this->get('id'), $competencyid);
 248          } else if ($this->is_based_on_template()) {
 249              // Get the competency from the template.
 250              $competency = template_competency::get_competency($this->get('templateid'), $competencyid);
 251          } else {
 252              // Get the competency from the plan.
 253              $competency = plan_competency::get_competency($this->get('id'), $competencyid);
 254          }
 255          return $competency;
 256      }
 257  
 258      /**
 259       * Get the context in which the plan is attached.
 260       *
 261       * @return context_user
 262       */
 263      public function get_context() {
 264          return context_user::instance($this->get('userid'));
 265      }
 266  
 267      /**
 268       * Human readable status name.
 269       *
 270       * @return string
 271       */
 272      public function get_statusname() {
 273  
 274          $status = $this->get('status');
 275  
 276          switch ($status) {
 277              case self::STATUS_DRAFT:
 278                  $strname = 'draft';
 279                  break;
 280              case self::STATUS_IN_REVIEW:
 281                  $strname = 'inreview';
 282                  break;
 283              case self::STATUS_WAITING_FOR_REVIEW:
 284                  $strname = 'waitingforreview';
 285                  break;
 286              case self::STATUS_ACTIVE:
 287                  $strname = 'active';
 288                  break;
 289              case self::STATUS_COMPLETE:
 290                  $strname = 'complete';
 291                  break;
 292              default:
 293                  throw new \moodle_exception('errorplanstatus', 'core_competency', '', $status);
 294                  break;
 295          }
 296  
 297          return get_string('planstatus' . $strname, 'core_competency');
 298      }
 299  
 300      /**
 301       * Get the plan template.
 302       *
 303       * @return template|null
 304       */
 305      public function get_template() {
 306          $templateid = $this->get('templateid');
 307          if ($templateid === null) {
 308              return null;
 309          }
 310          return new template($templateid);
 311      }
 312  
 313      /**
 314       * Is the plan in draft mode?
 315       *
 316       * This method is convenient to know if the plan is a draft because whilst a draft
 317       * is being reviewed its status is not "draft" any more, but it still is a draft nonetheless.
 318       *
 319       * @return boolean
 320       */
 321      public function is_draft() {
 322          return in_array($this->get('status'), static::get_draft_statuses());
 323      }
 324  
 325      /**
 326       * Validate the template ID.
 327       *
 328       * @param mixed $value The value.
 329       * @return true|lang_string
 330       */
 331      protected function validate_templateid($value) {
 332  
 333          // Checks that the template exists.
 334          if (!empty($value) && !template::record_exists($value)) {
 335              return new lang_string('invaliddata', 'error');
 336          }
 337  
 338          return true;
 339      }
 340  
 341      /**
 342       * Validate the user ID.
 343       *
 344       * @param  int $value
 345       * @return true|lang_string
 346       */
 347      protected function validate_userid($value) {
 348          global $DB;
 349  
 350          // During create.
 351          if (!$this->get('id')) {
 352  
 353              // Check that the user exists. We do not need to do that on update because
 354              // the userid of a plan should never change.
 355              if (!$DB->record_exists('user', array('id' => $value))) {
 356                  return new lang_string('invaliddata', 'error');
 357              }
 358  
 359          }
 360  
 361          return true;
 362      }
 363  
 364      /**
 365       * Can the current user comment on a user's plan?
 366       *
 367       * @param int $planuserid The user ID the plan belongs to.
 368       * @return bool
 369       */
 370      public static function can_comment_user($planuserid) {
 371          global $USER;
 372  
 373          $capabilities = array('moodle/competency:plancomment');
 374          if ($USER->id == $planuserid) {
 375              $capabilities[] = 'moodle/competency:plancommentown';
 376          }
 377  
 378          return has_any_capability($capabilities, context_user::instance($planuserid));
 379      }
 380  
 381      /**
 382       * Can the current user manage a user's plan?
 383       *
 384       * @param  int $planuserid The user to whom the plan would belong.
 385       * @return bool
 386       */
 387      public static function can_manage_user($planuserid) {
 388          global $USER;
 389          $context = context_user::instance($planuserid);
 390  
 391          $capabilities = array('moodle/competency:planmanage');
 392          if ($context->instanceid == $USER->id) {
 393              $capabilities[] = 'moodle/competency:planmanageown';
 394          }
 395  
 396          return has_any_capability($capabilities, $context);
 397      }
 398  
 399      /**
 400       * Can the current user manage a user's draft plan?
 401       *
 402       * @param  int $planuserid The user to whom the plan would belong.
 403       * @return bool
 404       */
 405      public static function can_manage_user_draft($planuserid) {
 406          global $USER;
 407          $context = context_user::instance($planuserid);
 408  
 409          $capabilities = array('moodle/competency:planmanagedraft');
 410          if ($context->instanceid == $USER->id) {
 411              $capabilities[] = 'moodle/competency:planmanageowndraft';
 412          }
 413  
 414          return has_any_capability($capabilities, $context);
 415      }
 416  
 417      /**
 418       * Can the current user read the comments on a user's plan?
 419       *
 420       * @param int $planuserid The user ID the plan belongs to.
 421       * @return bool
 422       */
 423      public static function can_read_comments_user($planuserid) {
 424          // Everyone who can read the plan can read the comments.
 425          return static::can_read_user($planuserid);
 426      }
 427  
 428      /**
 429       * Can the current user view a user's plan?
 430       *
 431       * @param  int $planuserid The user to whom the plan would belong.
 432       * @return bool
 433       */
 434      public static function can_read_user($planuserid) {
 435          global $USER;
 436          $context = context_user::instance($planuserid);
 437  
 438          $capabilities = array('moodle/competency:planview');
 439          if ($context->instanceid == $USER->id) {
 440              $capabilities[] = 'moodle/competency:planviewown';
 441          }
 442  
 443          return has_any_capability($capabilities, $context)
 444              || self::can_manage_user($planuserid);
 445      }
 446  
 447      /**
 448       * Can the current user view a user's draft plan?
 449       *
 450       * @param  int $planuserid The user to whom the plan would belong.
 451       * @return bool
 452       */
 453      public static function can_read_user_draft($planuserid) {
 454          global $USER;
 455          $context = context_user::instance($planuserid);
 456  
 457          $capabilities = array('moodle/competency:planviewdraft');
 458          if ($context->instanceid == $USER->id) {
 459              $capabilities[] = 'moodle/competency:planviewowndraft';
 460          }
 461  
 462          return has_any_capability($capabilities, $context)
 463              || self::can_manage_user_draft($planuserid);
 464      }
 465  
 466      /**
 467       * Can the current user request the draft to be reviewed.
 468       *
 469       * @param  int $planuserid The user to whom the plan would belong.
 470       * @return bool
 471       */
 472      public static function can_request_review_user($planuserid) {
 473          global $USER;
 474  
 475          $capabilities = array('moodle/competency:planrequestreview');
 476          if ($USER->id == $planuserid) {
 477              $capabilities[] = 'moodle/competency:planrequestreviewown';
 478          }
 479  
 480          return has_any_capability($capabilities, context_user::instance($planuserid));
 481      }
 482  
 483      /**
 484       * Can the current user review the plan.
 485       *
 486       * This means being able to send the plan from draft to active, and vice versa.
 487       *
 488       * @param  int $planuserid The user to whom the plan would belong.
 489       * @return bool
 490       */
 491      public static function can_review_user($planuserid) {
 492          return has_capability('moodle/competency:planreview', context_user::instance($planuserid))
 493              || self::can_manage_user($planuserid);
 494      }
 495  
 496      /**
 497       * Get the plans of a user containing a specific competency.
 498       *
 499       * @param  int $userid       The user ID.
 500       * @param  int $competencyid The competency ID.
 501       * @return plans[]
 502       */
 503      public static function get_by_user_and_competency($userid, $competencyid) {
 504          global $DB;
 505  
 506          $sql = 'SELECT p.*
 507                    FROM {' . self::TABLE . '} p
 508               LEFT JOIN {' . plan_competency::TABLE . '} pc
 509                      ON pc.planid = p.id
 510                     AND pc.competencyid = :competencyid1
 511               LEFT JOIN {' . user_competency_plan::TABLE . '} ucp
 512                      ON ucp.planid = p.id
 513                     AND ucp.competencyid = :competencyid2
 514               LEFT JOIN {' . template_competency::TABLE . '} tc
 515                      ON tc.templateid = p.templateid
 516                     AND tc.competencyid = :competencyid3
 517                   WHERE p.userid = :userid
 518                     AND (pc.id IS NOT NULL
 519                      OR ucp.id IS NOT NULL
 520                      OR tc.id IS NOT NULL)
 521                ORDER BY p.id ASC';
 522  
 523          $params = array(
 524              'competencyid1' => $competencyid,
 525              'competencyid2' => $competencyid,
 526              'competencyid3' => $competencyid,
 527              'userid' => $userid
 528          );
 529  
 530          $plans = array();
 531          $records = $DB->get_records_sql($sql, $params);
 532          foreach ($records as $record) {
 533              $plans[$record->id] = new plan(0, $record);
 534          }
 535  
 536          return $plans;
 537      }
 538  
 539      /**
 540       * Get the list of draft statuses.
 541       *
 542       * @return array Contains the status constants.
 543       */
 544      public static function get_draft_statuses() {
 545          return array(self::STATUS_DRAFT, self::STATUS_WAITING_FOR_REVIEW, self::STATUS_IN_REVIEW);
 546      }
 547  
 548      /**
 549       * Get the recordset of the plans that are due, incomplete and not draft.
 550       *
 551       * @return \moodle_recordset
 552       */
 553      public static function get_recordset_for_due_and_incomplete() {
 554          global $DB;
 555          $sql = "duedate > 0 AND duedate < :now AND status = :status";
 556          $params = array('now' => time(), 'status' => self::STATUS_ACTIVE);
 557          return $DB->get_recordset_select(self::TABLE, $sql, $params);
 558      }
 559  
 560      /**
 561       * Return a list of status depending on capabilities.
 562       *
 563       * @param  int $userid The user to whom the plan would belong.
 564       * @return array
 565       */
 566      public static function get_status_list($userid) {
 567          $status = array();
 568          if (self::can_manage_user_draft($userid)) {
 569              $status[self::STATUS_DRAFT] = get_string('planstatusdraft', 'core_competency');
 570          }
 571          if (self::can_manage_user($userid)) {
 572              $status[self::STATUS_ACTIVE] = get_string('planstatusactive', 'core_competency');
 573          }
 574          return $status;
 575      }
 576  
 577      /**
 578       * Update from template.
 579       *
 580       * Bulk update a lot of plans from a template
 581       *
 582       * @param  template $template
 583       * @return bool
 584       */
 585      public static function update_multiple_from_template(template $template) {
 586          global $DB;
 587          if (!$template->is_valid()) {
 588              // As we will bypass this model's validation we rely on the template being validated.
 589              throw new \coding_exception('The template must be validated before updating plans.');
 590          }
 591  
 592          $params = array(
 593              'templateid' => $template->get('id'),
 594              'status' => self::STATUS_COMPLETE,
 595  
 596              'name' => $template->get('shortname'),
 597              'description' => $template->get('description'),
 598              'descriptionformat' => $template->get('descriptionformat'),
 599              'duedate' => $template->get('duedate'),
 600          );
 601  
 602          $sql = "UPDATE {" . self::TABLE . "}
 603                     SET name = :name,
 604                         description = :description,
 605                         descriptionformat = :descriptionformat,
 606                         duedate = :duedate
 607                   WHERE templateid = :templateid
 608                     AND status != :status";
 609  
 610          return $DB->execute($sql, $params);
 611      }
 612  
 613      /**
 614       * Check if a template is associated to the plan.
 615       *
 616       * @return bool
 617       */
 618      public function is_based_on_template() {
 619          return $this->get('templateid') !== null;
 620      }
 621  
 622      /**
 623       * Check if plan can be edited.
 624       *
 625       * @return bool
 626       */
 627      public function can_be_edited() {
 628          return !$this->is_based_on_template() && $this->get('status') != self::STATUS_COMPLETE && $this->can_manage();
 629      }
 630  
 631      /**
 632       * Validate the due date.
 633       * When setting a due date it must not exceed the DUEDATE_THRESHOLD.
 634       *
 635       * @param  int $value The due date.
 636       * @return bool|lang_string
 637       */
 638      protected function validate_duedate($value) {
 639  
 640          // We do not check duedate when plan is draft, complete, unset, or based on a template.
 641          if ($this->is_based_on_template()
 642                  || $this->is_draft()
 643                  || $this->get('status') == self::STATUS_COMPLETE
 644                  || empty($value)) {
 645              return true;
 646          }
 647  
 648          // During update.
 649          if ($this->get('id')) {
 650              $before = $this->beforeupdate->get('duedate');
 651              $beforestatus = $this->beforeupdate->get('status');
 652  
 653              // The value has not changed, then it's always OK. Though if we're going
 654              // from draft to active it has to has to be validated.
 655              if ($before == $value && !in_array($beforestatus, self::get_draft_statuses())) {
 656                  return true;
 657              }
 658          }
 659  
 660          if ($value <= time()) {
 661              // We cannot set the date in the past.
 662              return new lang_string('errorcannotsetduedateinthepast', 'core_competency');
 663          }
 664  
 665          if ($value <= time() + self::DUEDATE_THRESHOLD) {
 666              // We cannot set the date too soon, but we can leave it empty.
 667              return new lang_string('errorcannotsetduedatetoosoon', 'core_competency');
 668          }
 669  
 670          return true;
 671      }
 672  
 673      /**
 674       * Checks if a template has user plan records.
 675       *
 676       * @param  int $templateid The template ID
 677       * @return boolean
 678       */
 679      public static function has_records_for_template($templateid) {
 680          return self::record_exists_select('templateid = ?', array($templateid));
 681      }
 682  
 683      /**
 684       * Count the number of plans for a template, optionally filtering by status.
 685       *
 686       * @param  int $templateid The template ID
 687       * @param  int $status The plan status. 0 means all statuses.
 688       * @return int
 689       */
 690      public static function count_records_for_template($templateid, $status) {
 691          $filters = array('templateid' => $templateid);
 692          if ($status > 0) {
 693              $filters['status'] = $status;
 694          }
 695          return self::count_records($filters);
 696      }
 697  
 698      /**
 699       * Get the plans for a template, optionally filtering by status.
 700       *
 701       * @param  int $templateid The template ID
 702       * @param  int $status The plan status. 0 means all statuses.
 703       * @param  int $skip The number of plans to skip
 704       * @param  int $limit The max number of plans to return
 705       * @return int
 706       */
 707      public static function get_records_for_template($templateid, $status = 0, $skip = 0, $limit = 100) {
 708          $filters = array('templateid' => $templateid);
 709          if ($status > 0) {
 710              $filters['status'] = $status;
 711          }
 712          return self::get_records($filters, $skip, $limit);
 713      }
 714  }