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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body