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