Differences Between: [Versions 311 and 401] [Versions 311 and 402] [Versions 311 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 * @return bool True on success. 1586 */ 1587 public static function set_course_module_competency_ruleoutcome($coursemodulecompetencyorid, $ruleoutcome) { 1588 static::require_enabled(); 1589 $coursemodulecompetency = $coursemodulecompetencyorid; 1590 if (!is_object($coursemodulecompetency)) { 1591 $coursemodulecompetency = new course_module_competency($coursemodulecompetencyorid); 1592 } 1593 1594 $cm = get_coursemodule_from_id('', $coursemodulecompetency->get('cmid'), 0, true, MUST_EXIST); 1595 1596 self::validate_course_module($cm); 1597 $context = context_module::instance($cm->id); 1598 1599 require_capability('moodle/competency:coursecompetencymanage', $context); 1600 1601 $coursemodulecompetency->set('ruleoutcome', $ruleoutcome); 1602 return $coursemodulecompetency->update(); 1603 } 1604 1605 /** 1606 * Add a competency to this course. 1607 * 1608 * @param int $courseid The id of the course 1609 * @param int $competencyid The id of the competency 1610 * @return bool 1611 */ 1612 public static function add_competency_to_course($courseid, $competencyid) { 1613 static::require_enabled(); 1614 // Check the user have access to the course. 1615 self::validate_course($courseid); 1616 1617 // First we do a permissions check. 1618 $context = context_course::instance($courseid); 1619 1620 require_capability('moodle/competency:coursecompetencymanage', $context); 1621 1622 $record = new stdClass(); 1623 $record->courseid = $courseid; 1624 $record->competencyid = $competencyid; 1625 1626 $competency = new competency($competencyid); 1627 1628 // Can not add a competency that belong to a hidden framework. 1629 if ($competency->get_framework()->get('visible') == false) { 1630 throw new coding_exception('A competency belonging to hidden framework can not be linked to course'); 1631 } 1632 1633 $coursecompetency = new course_competency(); 1634 $exists = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyid)); 1635 if (!$exists) { 1636 $coursecompetency->from_record($record); 1637 if ($coursecompetency->create()) { 1638 return true; 1639 } 1640 } 1641 return false; 1642 } 1643 1644 /** 1645 * Remove a competency from this course. 1646 * 1647 * @param int $courseid The id of the course 1648 * @param int $competencyid The id of the competency 1649 * @return bool 1650 */ 1651 public static function remove_competency_from_course($courseid, $competencyid) { 1652 static::require_enabled(); 1653 // Check the user have access to the course. 1654 self::validate_course($courseid); 1655 1656 // First we do a permissions check. 1657 $context = context_course::instance($courseid); 1658 1659 require_capability('moodle/competency:coursecompetencymanage', $context); 1660 1661 $record = new stdClass(); 1662 $record->courseid = $courseid; 1663 $record->competencyid = $competencyid; 1664 1665 $coursecompetency = new course_competency(); 1666 $exists = course_competency::get_record(array('courseid' => $courseid, 'competencyid' => $competencyid)); 1667 if ($exists) { 1668 // Delete all course_module_competencies for this competency in this course. 1669 $cmcs = course_module_competency::get_records_by_competencyid_in_course($competencyid, $courseid); 1670 foreach ($cmcs as $cmc) { 1671 $cmc->delete(); 1672 } 1673 return $exists->delete(); 1674 } 1675 return false; 1676 } 1677 1678 /** 1679 * Move the course competency up or down in the display list. 1680 * 1681 * Requires moodle/competency:coursecompetencymanage capability at the course context. 1682 * 1683 * @param int $courseid The course 1684 * @param int $competencyidfrom The id of the competency we are moving. 1685 * @param int $competencyidto The id of the competency we are moving to. 1686 * @return boolean 1687 */ 1688 public static function reorder_course_competency($courseid, $competencyidfrom, $competencyidto) { 1689 static::require_enabled(); 1690 // Check the user have access to the course. 1691 self::validate_course($courseid); 1692 1693 // First we do a permissions check. 1694 $context = context_course::instance($courseid); 1695 1696 require_capability('moodle/competency:coursecompetencymanage', $context); 1697 1698 $down = true; 1699 $coursecompetency = new course_competency(); 1700 $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidfrom)); 1701 if (count($matches) == 0) { 1702 throw new coding_exception('The link does not exist'); 1703 } 1704 1705 $competencyfrom = array_pop($matches); 1706 $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidto)); 1707 if (count($matches) == 0) { 1708 throw new coding_exception('The link does not exist'); 1709 } 1710 1711 $competencyto = array_pop($matches); 1712 1713 $all = $coursecompetency->get_records(array('courseid' => $courseid), 'sortorder', 'ASC', 0, 0); 1714 1715 if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) { 1716 // We are moving up, so put it before the "to" item. 1717 $down = false; 1718 } 1719 1720 foreach ($all as $id => $coursecompetency) { 1721 $sort = $coursecompetency->get('sortorder'); 1722 if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) { 1723 $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') - 1); 1724 $coursecompetency->update(); 1725 } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) { 1726 $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') + 1); 1727 $coursecompetency->update(); 1728 } 1729 } 1730 $competencyfrom->set('sortorder', $competencyto->get('sortorder')); 1731 return $competencyfrom->update(); 1732 } 1733 1734 /** 1735 * Update ruleoutcome value for a course competency. 1736 * 1737 * @param int|course_competency $coursecompetencyorid The course_competency, or its ID. 1738 * @param int $ruleoutcome The value of ruleoutcome. 1739 * @return bool True on success. 1740 */ 1741 public static function set_course_competency_ruleoutcome($coursecompetencyorid, $ruleoutcome) { 1742 static::require_enabled(); 1743 $coursecompetency = $coursecompetencyorid; 1744 if (!is_object($coursecompetency)) { 1745 $coursecompetency = new course_competency($coursecompetencyorid); 1746 } 1747 1748 $courseid = $coursecompetency->get('courseid'); 1749 self::validate_course($courseid); 1750 $coursecontext = context_course::instance($courseid); 1751 1752 require_capability('moodle/competency:coursecompetencymanage', $coursecontext); 1753 1754 $coursecompetency->set('ruleoutcome', $ruleoutcome); 1755 return $coursecompetency->update(); 1756 } 1757 1758 /** 1759 * Create a learning plan template from a record containing all the data for the class. 1760 * 1761 * Requires moodle/competency:templatemanage capability. 1762 * 1763 * @param stdClass $record Record containing all the data for an instance of the class. 1764 * @return template 1765 */ 1766 public static function create_template(stdClass $record) { 1767 static::require_enabled(); 1768 $template = new template(0, $record); 1769 1770 // First we do a permissions check. 1771 if (!$template->can_manage()) { 1772 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', 1773 'nopermissions', ''); 1774 } 1775 1776 // OK - all set. 1777 $template = $template->create(); 1778 1779 // Trigger a template created event. 1780 \core\event\competency_template_created::create_from_template($template)->trigger(); 1781 1782 return $template; 1783 } 1784 1785 /** 1786 * Duplicate a learning plan template. 1787 * 1788 * Requires moodle/competency:templatemanage capability at the template context. 1789 * 1790 * @param int $id the template id. 1791 * @return template 1792 */ 1793 public static function duplicate_template($id) { 1794 static::require_enabled(); 1795 $template = new template($id); 1796 1797 // First we do a permissions check. 1798 if (!$template->can_manage()) { 1799 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', 1800 'nopermissions', ''); 1801 } 1802 1803 // OK - all set. 1804 $competencies = template_competency::list_competencies($id, false); 1805 1806 // Adding the suffix copy. 1807 $template->set('shortname', get_string('duplicateditemname', 'core_competency', $template->get('shortname'))); 1808 $template->set('id', 0); 1809 1810 $duplicatedtemplate = $template->create(); 1811 1812 // Associate each competency for the duplicated template. 1813 foreach ($competencies as $competency) { 1814 self::add_competency_to_template($duplicatedtemplate->get('id'), $competency->get('id')); 1815 } 1816 1817 // Trigger a template created event. 1818 \core\event\competency_template_created::create_from_template($duplicatedtemplate)->trigger(); 1819 1820 return $duplicatedtemplate; 1821 } 1822 1823 /** 1824 * Delete a learning plan template by id. 1825 * If the learning plan template has associated cohorts they will be deleted. 1826 * 1827 * Requires moodle/competency:templatemanage capability. 1828 * 1829 * @param int $id The record to delete. 1830 * @param boolean $deleteplans True to delete plans associaated to template, false to unlink them. 1831 * @return boolean 1832 */ 1833 public static function delete_template($id, $deleteplans = true) { 1834 global $DB; 1835 static::require_enabled(); 1836 $template = new template($id); 1837 1838 // First we do a permissions check. 1839 if (!$template->can_manage()) { 1840 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', 1841 'nopermissions', ''); 1842 } 1843 1844 $transaction = $DB->start_delegated_transaction(); 1845 $success = true; 1846 1847 // Check if there are cohorts associated. 1848 $templatecohorts = template_cohort::get_relations_by_templateid($template->get('id')); 1849 foreach ($templatecohorts as $templatecohort) { 1850 $success = $templatecohort->delete(); 1851 if (!$success) { 1852 break; 1853 } 1854 } 1855 1856 // Still OK, delete or unlink the plans from the template. 1857 if ($success) { 1858 $plans = plan::get_records(array('templateid' => $template->get('id'))); 1859 foreach ($plans as $plan) { 1860 $success = $deleteplans ? self::delete_plan($plan->get('id')) : self::unlink_plan_from_template($plan); 1861 if (!$success) { 1862 break; 1863 } 1864 } 1865 } 1866 1867 // Still OK, delete the template comptencies. 1868 if ($success) { 1869 $success = template_competency::delete_by_templateid($template->get('id')); 1870 } 1871 1872 // OK - all set. 1873 if ($success) { 1874 // Create a template deleted event. 1875 $event = \core\event\competency_template_deleted::create_from_template($template); 1876 1877 $success = $template->delete(); 1878 } 1879 1880 if ($success) { 1881 // Trigger a template deleted event. 1882 $event->trigger(); 1883 1884 // Commit the transaction. 1885 $transaction->allow_commit(); 1886 } else { 1887 $transaction->rollback(new moodle_exception('Error while deleting the template.')); 1888 } 1889 1890 return $success; 1891 } 1892 1893 /** 1894 * Update the details for a learning plan template. 1895 * 1896 * Requires moodle/competency:templatemanage capability. 1897 * 1898 * @param stdClass $record The new details for the template. Note - must contain an id that points to the template to update. 1899 * @return boolean 1900 */ 1901 public static function update_template($record) { 1902 global $DB; 1903 static::require_enabled(); 1904 $template = new template($record->id); 1905 1906 // First we do a permissions check. 1907 if (!$template->can_manage()) { 1908 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', 1909 'nopermissions', ''); 1910 1911 } else if (isset($record->contextid) && $record->contextid != $template->get('contextid')) { 1912 // We can never change the context of a template. 1913 throw new coding_exception('Changing the context of an existing tempalte is forbidden.'); 1914 1915 } 1916 1917 $updateplans = false; 1918 $before = $template->to_record(); 1919 1920 $template->from_record($record); 1921 $after = $template->to_record(); 1922 1923 // Should we update the related plans? 1924 if ($before->duedate != $after->duedate || 1925 $before->shortname != $after->shortname || 1926 $before->description != $after->description || 1927 $before->descriptionformat != $after->descriptionformat) { 1928 $updateplans = true; 1929 } 1930 1931 $transaction = $DB->start_delegated_transaction(); 1932 $success = $template->update(); 1933 1934 if (!$success) { 1935 $transaction->rollback(new moodle_exception('Error while updating the template.')); 1936 return $success; 1937 } 1938 1939 // Trigger a template updated event. 1940 \core\event\competency_template_updated::create_from_template($template)->trigger(); 1941 1942 if ($updateplans) { 1943 plan::update_multiple_from_template($template); 1944 } 1945 1946 $transaction->allow_commit(); 1947 1948 return $success; 1949 } 1950 1951 /** 1952 * Read a the details for a single learning plan template and return a record. 1953 * 1954 * Requires moodle/competency:templateview capability at the system context. 1955 * 1956 * @param int $id The id of the template to read. 1957 * @return template 1958 */ 1959 public static function read_template($id) { 1960 static::require_enabled(); 1961 $template = new template($id); 1962 $context = $template->get_context(); 1963 1964 // First we do a permissions check. 1965 if (!$template->can_read()) { 1966 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', 1967 'nopermissions', ''); 1968 } 1969 1970 // OK - all set. 1971 return $template; 1972 } 1973 1974 /** 1975 * Perform a search based on the provided filters and return a paginated list of records. 1976 * 1977 * Requires moodle/competency:templateview capability at the system context. 1978 * 1979 * @param string $sort The column to sort on 1980 * @param string $order ('ASC' or 'DESC') 1981 * @param int $skip Number of records to skip (pagination) 1982 * @param int $limit Max of records to return (pagination) 1983 * @param context $context The parent context of the frameworks. 1984 * @param string $includes Defines what other contexts to fetch frameworks from. 1985 * Accepted values are: 1986 * - children: All descendants 1987 * - parents: All parents, grand parents, etc... 1988 * - self: Context passed only. 1989 * @param bool $onlyvisible If should list only visible templates 1990 * @return array of competency_framework 1991 */ 1992 public static function list_templates($sort, $order, $skip, $limit, $context, $includes = 'children', $onlyvisible = false) { 1993 global $DB; 1994 static::require_enabled(); 1995 1996 // Get all the relevant contexts. 1997 $contexts = self::get_related_contexts($context, $includes, 1998 array('moodle/competency:templateview', 'moodle/competency:templatemanage')); 1999 2000 // First we do a permissions check. 2001 if (empty($contexts)) { 2002 throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', ''); 2003 } 2004 2005 // Make the order by. 2006 $orderby = ''; 2007 if (!empty($sort)) { 2008 $orderby = $sort . ' ' . $order; 2009 } 2010 2011 // OK - all set. 2012 $template = new template(); 2013 list($insql, $params) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED); 2014 $select = "contextid $insql"; 2015 2016 if ($onlyvisible) { 2017 $select .= " AND visible = :visible"; 2018 $params['visible'] = 1; 2019 } 2020 return $template->get_records_select($select, $params, $orderby, '*', $skip, $limit); 2021 } 2022 2023 /** 2024 * Perform a search based on the provided filters and return how many results there are. 2025 * 2026 * Requires moodle/competency:templateview capability at the system context. 2027 * 2028 * @param context $context The parent context of the frameworks. 2029 * @param string $includes Defines what other contexts to fetch frameworks from. 2030 * Accepted values are: 2031 * - children: All descendants 2032 * - parents: All parents, grand parents, etc... 2033 * - self: Context passed only. 2034 * @return int 2035 */ 2036 public static function count_templates($context, $includes) { 2037 global $DB; 2038 static::require_enabled(); 2039 2040 // First we do a permissions check. 2041 $contexts = self::get_related_contexts($context, $includes, 2042 array('moodle/competency:templateview', 'moodle/competency:templatemanage')); 2043 2044 if (empty($contexts)) { 2045 throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', ''); 2046 } 2047 2048 // OK - all set. 2049 $template = new template(); 2050 list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED); 2051 return $template->count_records_select("contextid $insql", $inparams); 2052 } 2053 2054 /** 2055 * Count all the templates using a competency. 2056 * 2057 * @param int $competencyid The id of the competency to check. 2058 * @return int 2059 */ 2060 public static function count_templates_using_competency($competencyid) { 2061 static::require_enabled(); 2062 // First we do a permissions check. 2063 $context = context_system::instance(); 2064 $onlyvisible = 1; 2065 2066 $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage'); 2067 if (!has_any_capability($capabilities, $context)) { 2068 throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', ''); 2069 } 2070 2071 if (has_capability('moodle/competency:templatemanage', $context)) { 2072 $onlyvisible = 0; 2073 } 2074 2075 // OK - all set. 2076 return template_competency::count_templates($competencyid, $onlyvisible); 2077 } 2078 2079 /** 2080 * List all the learning plan templatesd using a competency. 2081 * 2082 * @param int $competencyid The id of the competency to check. 2083 * @return array[stdClass] Array of stdClass containing id and shortname. 2084 */ 2085 public static function list_templates_using_competency($competencyid) { 2086 static::require_enabled(); 2087 // First we do a permissions check. 2088 $context = context_system::instance(); 2089 $onlyvisible = 1; 2090 2091 $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage'); 2092 if (!has_any_capability($capabilities, $context)) { 2093 throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', ''); 2094 } 2095 2096 if (has_capability('moodle/competency:templatemanage', $context)) { 2097 $onlyvisible = 0; 2098 } 2099 2100 // OK - all set. 2101 return template_competency::list_templates($competencyid, $onlyvisible); 2102 2103 } 2104 2105 /** 2106 * Count all the competencies in a learning plan template. 2107 * 2108 * @param template|int $templateorid The template or its ID. 2109 * @return int 2110 */ 2111 public static function count_competencies_in_template($templateorid) { 2112 static::require_enabled(); 2113 // First we do a permissions check. 2114 $template = $templateorid; 2115 if (!is_object($template)) { 2116 $template = new template($template); 2117 } 2118 2119 if (!$template->can_read()) { 2120 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', 2121 'nopermissions', ''); 2122 } 2123 2124 // OK - all set. 2125 return template_competency::count_competencies($template->get('id')); 2126 } 2127 2128 /** 2129 * Count all the competencies in a learning plan template with no linked courses. 2130 * 2131 * @param template|int $templateorid The template or its ID. 2132 * @return int 2133 */ 2134 public static function count_competencies_in_template_with_no_courses($templateorid) { 2135 // First we do a permissions check. 2136 $template = $templateorid; 2137 if (!is_object($template)) { 2138 $template = new template($template); 2139 } 2140 2141 if (!$template->can_read()) { 2142 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', 2143 'nopermissions', ''); 2144 } 2145 2146 // OK - all set. 2147 return template_competency::count_competencies_with_no_courses($template->get('id')); 2148 } 2149 2150 /** 2151 * List all the competencies in a template. 2152 * 2153 * @param template|int $templateorid The template or its ID. 2154 * @return array of competencies 2155 */ 2156 public static function list_competencies_in_template($templateorid) { 2157 static::require_enabled(); 2158 // First we do a permissions check. 2159 $template = $templateorid; 2160 if (!is_object($template)) { 2161 $template = new template($template); 2162 } 2163 2164 if (!$template->can_read()) { 2165 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', 2166 'nopermissions', ''); 2167 } 2168 2169 // OK - all set. 2170 return template_competency::list_competencies($template->get('id')); 2171 } 2172 2173 /** 2174 * Add a competency to this template. 2175 * 2176 * @param int $templateid The id of the template 2177 * @param int $competencyid The id of the competency 2178 * @return bool 2179 */ 2180 public static function add_competency_to_template($templateid, $competencyid) { 2181 static::require_enabled(); 2182 // First we do a permissions check. 2183 $template = new template($templateid); 2184 if (!$template->can_manage()) { 2185 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', 2186 'nopermissions', ''); 2187 } 2188 2189 $record = new stdClass(); 2190 $record->templateid = $templateid; 2191 $record->competencyid = $competencyid; 2192 2193 $competency = new competency($competencyid); 2194 2195 // Can not add a competency that belong to a hidden framework. 2196 if ($competency->get_framework()->get('visible') == false) { 2197 throw new coding_exception('A competency belonging to hidden framework can not be added'); 2198 } 2199 2200 $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid)); 2201 if (!$exists) { 2202 $templatecompetency = new template_competency(0, $record); 2203 $templatecompetency->create(); 2204 return true; 2205 } 2206 return false; 2207 } 2208 2209 /** 2210 * Remove a competency from this template. 2211 * 2212 * @param int $templateid The id of the template 2213 * @param int $competencyid The id of the competency 2214 * @return bool 2215 */ 2216 public static function remove_competency_from_template($templateid, $competencyid) { 2217 static::require_enabled(); 2218 // First we do a permissions check. 2219 $template = new template($templateid); 2220 if (!$template->can_manage()) { 2221 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', 2222 'nopermissions', ''); 2223 } 2224 2225 $record = new stdClass(); 2226 $record->templateid = $templateid; 2227 $record->competencyid = $competencyid; 2228 2229 $competency = new competency($competencyid); 2230 2231 $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid)); 2232 if ($exists) { 2233 $link = array_pop($exists); 2234 return $link->delete(); 2235 } 2236 return false; 2237 } 2238 2239 /** 2240 * Move the template competency up or down in the display list. 2241 * 2242 * Requires moodle/competency:templatemanage capability at the system context. 2243 * 2244 * @param int $templateid The template id 2245 * @param int $competencyidfrom The id of the competency we are moving. 2246 * @param int $competencyidto The id of the competency we are moving to. 2247 * @return boolean 2248 */ 2249 public static function reorder_template_competency($templateid, $competencyidfrom, $competencyidto) { 2250 static::require_enabled(); 2251 $template = new template($templateid); 2252 2253 // First we do a permissions check. 2254 if (!$template->can_manage()) { 2255 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage', 2256 'nopermissions', ''); 2257 } 2258 2259 $down = true; 2260 $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidfrom)); 2261 if (count($matches) == 0) { 2262 throw new coding_exception('The link does not exist'); 2263 } 2264 2265 $competencyfrom = array_pop($matches); 2266 $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidto)); 2267 if (count($matches) == 0) { 2268 throw new coding_exception('The link does not exist'); 2269 } 2270 2271 $competencyto = array_pop($matches); 2272 2273 $all = template_competency::get_records(array('templateid' => $templateid), 'sortorder', 'ASC', 0, 0); 2274 2275 if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) { 2276 // We are moving up, so put it before the "to" item. 2277 $down = false; 2278 } 2279 2280 foreach ($all as $id => $templatecompetency) { 2281 $sort = $templatecompetency->get('sortorder'); 2282 if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) { 2283 $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') - 1); 2284 $templatecompetency->update(); 2285 } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) { 2286 $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') + 1); 2287 $templatecompetency->update(); 2288 } 2289 } 2290 $competencyfrom->set('sortorder', $competencyto->get('sortorder')); 2291 return $competencyfrom->update(); 2292 } 2293 2294 /** 2295 * Create a relation between a template and a cohort. 2296 * 2297 * This silently ignores when the relation already existed. 2298 * 2299 * @param template|int $templateorid The template or its ID. 2300 * @param stdClass|int $cohortorid The cohort ot its ID. 2301 * @return template_cohort 2302 */ 2303 public static function create_template_cohort($templateorid, $cohortorid) { 2304 global $DB; 2305 static::require_enabled(); 2306 2307 $template = $templateorid; 2308 if (!is_object($template)) { 2309 $template = new template($template); 2310 } 2311 require_capability('moodle/competency:templatemanage', $template->get_context()); 2312 2313 $cohort = $cohortorid; 2314 if (!is_object($cohort)) { 2315 $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST); 2316 } 2317 2318 // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context. 2319 $cohortcontext = context::instance_by_id($cohort->contextid); 2320 if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) { 2321 throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', ''); 2322 } 2323 2324 $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id); 2325 if (!$tplcohort->get('id')) { 2326 $tplcohort->create(); 2327 } 2328 2329 return $tplcohort; 2330 } 2331 2332 /** 2333 * Remove a relation between a template and a cohort. 2334 * 2335 * @param template|int $templateorid The template or its ID. 2336 * @param stdClass|int $cohortorid The cohort ot its ID. 2337 * @return boolean True on success or when the relation did not exist. 2338 */ 2339 public static function delete_template_cohort($templateorid, $cohortorid) { 2340 global $DB; 2341 static::require_enabled(); 2342 2343 $template = $templateorid; 2344 if (!is_object($template)) { 2345 $template = new template($template); 2346 } 2347 require_capability('moodle/competency:templatemanage', $template->get_context()); 2348 2349 $cohort = $cohortorid; 2350 if (!is_object($cohort)) { 2351 $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST); 2352 } 2353 2354 $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id); 2355 if (!$tplcohort->get('id')) { 2356 return true; 2357 } 2358 2359 return $tplcohort->delete(); 2360 } 2361 2362 /** 2363 * Lists user plans. 2364 * 2365 * @param int $userid 2366 * @return \core_competency\plan[] 2367 */ 2368 public static function list_user_plans($userid) { 2369 global $DB, $USER; 2370 static::require_enabled(); 2371 $select = 'userid = :userid'; 2372 $params = array('userid' => $userid); 2373 $context = context_user::instance($userid); 2374 2375 // Check that we can read something here. 2376 if (!plan::can_read_user($userid) && !plan::can_read_user_draft($userid)) { 2377 throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', ''); 2378 } 2379 2380 // The user cannot view the drafts. 2381 if (!plan::can_read_user_draft($userid)) { 2382 list($insql, $inparams) = $DB->get_in_or_equal(plan::get_draft_statuses(), SQL_PARAMS_NAMED, 'param', false); 2383 $select .= " AND status $insql"; 2384 $params += $inparams; 2385 } 2386 // The user cannot view the non-drafts. 2387 if (!plan::can_read_user($userid)) { 2388 list($insql, $inparams) = $DB->get_in_or_equal(array(plan::STATUS_ACTIVE, plan::STATUS_COMPLETE), 2389 SQL_PARAMS_NAMED, 'param', false); 2390 $select .= " AND status $insql"; 2391 $params += $inparams; 2392 } 2393 2394 return plan::get_records_select($select, $params, 'name ASC'); 2395 } 2396 2397 /** 2398 * List the plans to review. 2399 * 2400 * The method returns values in this format: 2401 * 2402 * array( 2403 * 'plans' => array( 2404 * (stdClass)( 2405 * 'plan' => (plan), 2406 * 'template' => (template), 2407 * 'owner' => (stdClass) 2408 * ) 2409 * ), 2410 * 'count' => (int) 2411 * ) 2412 * 2413 * @param int $skip The number of records to skip. 2414 * @param int $limit The number of results to return. 2415 * @param int $userid The user we're getting the plans to review for. 2416 * @return array Containing the keys 'count', and 'plans'. The 'plans' key contains an object 2417 * which contains 'plan', 'template' and 'owner'. 2418 */ 2419 public static function list_plans_to_review($skip = 0, $limit = 100, $userid = null) { 2420 global $DB, $USER; 2421 static::require_enabled(); 2422 2423 if ($userid === null) { 2424 $userid = $USER->id; 2425 } 2426 2427 $planfields = plan::get_sql_fields('p', 'plan_'); 2428 $tplfields = template::get_sql_fields('t', 'tpl_'); 2429 $usercols = array('id') + get_user_fieldnames(); 2430 $userfields = array(); 2431 foreach ($usercols as $field) { 2432 $userfields[] = "u." . $field . " AS usr_" . $field; 2433 } 2434 $userfields = implode(',', $userfields); 2435 2436 $select = "SELECT $planfields, $tplfields, $userfields"; 2437 $countselect = "SELECT COUNT('x')"; 2438 2439 $sql = " FROM {" . plan::TABLE . "} p 2440 JOIN {user} u 2441 ON u.id = p.userid 2442 LEFT JOIN {" . template::TABLE . "} t 2443 ON t.id = p.templateid 2444 WHERE (p.status = :waitingforreview 2445 OR (p.status = :inreview AND p.reviewerid = :reviewerid)) 2446 AND p.userid != :userid"; 2447 2448 $params = array( 2449 'waitingforreview' => plan::STATUS_WAITING_FOR_REVIEW, 2450 'inreview' => plan::STATUS_IN_REVIEW, 2451 'reviewerid' => $userid, 2452 'userid' => $userid 2453 ); 2454 2455 // Primary check to avoid the hard work of getting the users in which the user has permission. 2456 $count = $DB->count_records_sql($countselect . $sql, $params); 2457 if ($count < 1) { 2458 return array('count' => 0, 'plans' => array()); 2459 } 2460 2461 // TODO MDL-52243 Use core function. 2462 list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql('moodle/competency:planreview', 2463 $userid, SQL_PARAMS_NAMED); 2464 $sql .= " AND p.userid $insql"; 2465 $params += $inparams; 2466 2467 // Order by ID just to have some ordering in place. 2468 $ordersql = " ORDER BY p.id ASC"; 2469 2470 $plans = array(); 2471 $records = $DB->get_recordset_sql($select . $sql . $ordersql, $params, $skip, $limit); 2472 foreach ($records as $record) { 2473 $plan = new plan(0, plan::extract_record($record, 'plan_')); 2474 $template = null; 2475 2476 if ($plan->is_based_on_template()) { 2477 $template = new template(0, template::extract_record($record, 'tpl_')); 2478 } 2479 2480 $plans[] = (object) array( 2481 'plan' => $plan, 2482 'template' => $template, 2483 'owner' => persistent::extract_record($record, 'usr_'), 2484 ); 2485 } 2486 $records->close(); 2487 2488 return array( 2489 'count' => $DB->count_records_sql($countselect . $sql, $params), 2490 'plans' => $plans 2491 ); 2492 } 2493 2494 /** 2495 * Creates a learning plan based on the provided data. 2496 * 2497 * @param stdClass $record 2498 * @return \core_competency\plan 2499 */ 2500 public static function create_plan(stdClass $record) { 2501 global $USER; 2502 static::require_enabled(); 2503 $plan = new plan(0, $record); 2504 2505 if ($plan->is_based_on_template()) { 2506 throw new coding_exception('To create a plan from a template use api::create_plan_from_template().'); 2507 } else if ($plan->get('status') == plan::STATUS_COMPLETE) { 2508 throw new coding_exception('A plan cannot be created as complete.'); 2509 } 2510 2511 if (!$plan->can_manage()) { 2512 $context = context_user::instance($plan->get('userid')); 2513 throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); 2514 } 2515 2516 $plan->create(); 2517 2518 // Trigger created event. 2519 \core\event\competency_plan_created::create_from_plan($plan)->trigger(); 2520 return $plan; 2521 } 2522 2523 /** 2524 * Create a learning plan from a template. 2525 * 2526 * @param mixed $templateorid The template object or ID. 2527 * @param int $userid 2528 * @return false|\core_competency\plan Returns false when the plan already exists. 2529 */ 2530 public static function create_plan_from_template($templateorid, $userid) { 2531 static::require_enabled(); 2532 $template = $templateorid; 2533 if (!is_object($template)) { 2534 $template = new template($template); 2535 } 2536 2537 // The user must be able to view the template to use it as a base for a plan. 2538 if (!$template->can_read()) { 2539 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', 2540 'nopermissions', ''); 2541 } 2542 // Can not create plan from a hidden template. 2543 if ($template->get('visible') == false) { 2544 throw new coding_exception('A plan can not be created from a hidden template'); 2545 } 2546 2547 // Convert the template to a plan. 2548 $record = $template->to_record(); 2549 $record->templateid = $record->id; 2550 $record->userid = $userid; 2551 $record->name = $record->shortname; 2552 $record->status = plan::STATUS_ACTIVE; 2553 2554 unset($record->id); 2555 unset($record->timecreated); 2556 unset($record->timemodified); 2557 unset($record->usermodified); 2558 2559 // Remove extra keys. 2560 $properties = plan::properties_definition(); 2561 foreach ($record as $key => $value) { 2562 if (!array_key_exists($key, $properties)) { 2563 unset($record->$key); 2564 } 2565 } 2566 2567 $plan = new plan(0, $record); 2568 if (!$plan->can_manage()) { 2569 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 2570 'nopermissions', ''); 2571 } 2572 2573 // We first apply the permission checks as we wouldn't want to leak information by returning early that 2574 // the plan already exists. 2575 if (plan::record_exists_select('templateid = :templateid AND userid = :userid', array( 2576 'templateid' => $template->get('id'), 'userid' => $userid))) { 2577 return false; 2578 } 2579 2580 $plan->create(); 2581 2582 // Trigger created event. 2583 \core\event\competency_plan_created::create_from_plan($plan)->trigger(); 2584 return $plan; 2585 } 2586 2587 /** 2588 * Create learning plans from a template and cohort. 2589 * 2590 * @param mixed $templateorid The template object or ID. 2591 * @param int $cohortid The cohort ID. 2592 * @param bool $recreateunlinked When true the plans that were unlinked from this template will be re-created. 2593 * @return int The number of plans created. 2594 */ 2595 public static function create_plans_from_template_cohort($templateorid, $cohortid, $recreateunlinked = false) { 2596 global $DB, $CFG; 2597 static::require_enabled(); 2598 require_once($CFG->dirroot . '/cohort/lib.php'); 2599 2600 $template = $templateorid; 2601 if (!is_object($template)) { 2602 $template = new template($template); 2603 } 2604 2605 // The user must be able to view the template to use it as a base for a plan. 2606 if (!$template->can_read()) { 2607 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', 2608 'nopermissions', ''); 2609 } 2610 2611 // Can not create plan from a hidden template. 2612 if ($template->get('visible') == false) { 2613 throw new coding_exception('A plan can not be created from a hidden template'); 2614 } 2615 2616 // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context. 2617 $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST); 2618 $cohortcontext = context::instance_by_id($cohort->contextid); 2619 if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) { 2620 throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', ''); 2621 } 2622 2623 // Convert the template to a plan. 2624 $recordbase = $template->to_record(); 2625 $recordbase->templateid = $recordbase->id; 2626 $recordbase->name = $recordbase->shortname; 2627 $recordbase->status = plan::STATUS_ACTIVE; 2628 2629 unset($recordbase->id); 2630 unset($recordbase->timecreated); 2631 unset($recordbase->timemodified); 2632 unset($recordbase->usermodified); 2633 2634 // Remove extra keys. 2635 $properties = plan::properties_definition(); 2636 foreach ($recordbase as $key => $value) { 2637 if (!array_key_exists($key, $properties)) { 2638 unset($recordbase->$key); 2639 } 2640 } 2641 2642 // Create the plans. 2643 $created = 0; 2644 $userids = template_cohort::get_missing_plans($template->get('id'), $cohortid, $recreateunlinked); 2645 foreach ($userids as $userid) { 2646 $record = (object) (array) $recordbase; 2647 $record->userid = $userid; 2648 2649 $plan = new plan(0, $record); 2650 if (!$plan->can_manage()) { 2651 // Silently skip members where permissions are lacking. 2652 continue; 2653 } 2654 2655 $plan->create(); 2656 // Trigger created event. 2657 \core\event\competency_plan_created::create_from_plan($plan)->trigger(); 2658 $created++; 2659 } 2660 2661 return $created; 2662 } 2663 2664 /** 2665 * Unlink a plan from its template. 2666 * 2667 * @param \core_competency\plan|int $planorid The plan or its ID. 2668 * @return bool 2669 */ 2670 public static function unlink_plan_from_template($planorid) { 2671 global $DB; 2672 static::require_enabled(); 2673 2674 $plan = $planorid; 2675 if (!is_object($planorid)) { 2676 $plan = new plan($planorid); 2677 } 2678 2679 // The user must be allowed to manage the plans of the user, nothing about the template. 2680 if (!$plan->can_manage()) { 2681 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); 2682 } 2683 2684 // Only plan with status DRAFT or ACTIVE can be unliked.. 2685 if ($plan->get('status') == plan::STATUS_COMPLETE) { 2686 throw new coding_exception('Only draft or active plan can be unliked from a template'); 2687 } 2688 2689 // Early exit, it's already done... 2690 if (!$plan->is_based_on_template()) { 2691 return true; 2692 } 2693 2694 // Fetch the template. 2695 $template = new template($plan->get('templateid')); 2696 2697 // Now, proceed by copying all competencies to the plan, then update the plan. 2698 $transaction = $DB->start_delegated_transaction(); 2699 $competencies = template_competency::list_competencies($template->get('id'), false); 2700 $i = 0; 2701 foreach ($competencies as $competency) { 2702 $record = (object) array( 2703 'planid' => $plan->get('id'), 2704 'competencyid' => $competency->get('id'), 2705 'sortorder' => $i++ 2706 ); 2707 $pc = new plan_competency(null, $record); 2708 $pc->create(); 2709 } 2710 $plan->set('origtemplateid', $template->get('id')); 2711 $plan->set('templateid', null); 2712 $success = $plan->update(); 2713 $transaction->allow_commit(); 2714 2715 // Trigger unlinked event. 2716 \core\event\competency_plan_unlinked::create_from_plan($plan)->trigger(); 2717 2718 return $success; 2719 } 2720 2721 /** 2722 * Updates a plan. 2723 * 2724 * @param stdClass $record 2725 * @return \core_competency\plan 2726 */ 2727 public static function update_plan(stdClass $record) { 2728 static::require_enabled(); 2729 2730 $plan = new plan($record->id); 2731 2732 // Validate that the plan as it is can be managed. 2733 if (!$plan->can_manage()) { 2734 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); 2735 2736 } else if ($plan->get('status') == plan::STATUS_COMPLETE) { 2737 // A completed plan cannot be edited. 2738 throw new coding_exception('Completed plan cannot be edited.'); 2739 2740 } else if ($plan->is_based_on_template()) { 2741 // Prevent a plan based on a template to be edited. 2742 throw new coding_exception('Cannot update a plan that is based on a template.'); 2743 2744 } else if (isset($record->templateid) && $plan->get('templateid') != $record->templateid) { 2745 // Prevent a plan to be based on a template. 2746 throw new coding_exception('Cannot base a plan on a template.'); 2747 2748 } else if (isset($record->userid) && $plan->get('userid') != $record->userid) { 2749 // Prevent change of ownership as the capabilities are checked against that. 2750 throw new coding_exception('A plan cannot be transfered to another user'); 2751 2752 } else if (isset($record->status) && $plan->get('status') != $record->status) { 2753 // Prevent change of status. 2754 throw new coding_exception('To change the status of a plan use the appropriate methods.'); 2755 2756 } 2757 2758 $plan->from_record($record); 2759 $plan->update(); 2760 2761 // Trigger updated event. 2762 \core\event\competency_plan_updated::create_from_plan($plan)->trigger(); 2763 2764 return $plan; 2765 } 2766 2767 /** 2768 * Returns a plan data. 2769 * 2770 * @param int $id 2771 * @return \core_competency\plan 2772 */ 2773 public static function read_plan($id) { 2774 static::require_enabled(); 2775 $plan = new plan($id); 2776 2777 if (!$plan->can_read()) { 2778 $context = context_user::instance($plan->get('userid')); 2779 throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', ''); 2780 } 2781 2782 return $plan; 2783 } 2784 2785 /** 2786 * Plan event viewed. 2787 * 2788 * @param mixed $planorid The id or the plan. 2789 * @return boolean 2790 */ 2791 public static function plan_viewed($planorid) { 2792 static::require_enabled(); 2793 $plan = $planorid; 2794 if (!is_object($plan)) { 2795 $plan = new plan($plan); 2796 } 2797 2798 // First we do a permissions check. 2799 if (!$plan->can_read()) { 2800 $context = context_user::instance($plan->get('userid')); 2801 throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', ''); 2802 } 2803 2804 // Trigger a template viewed event. 2805 \core\event\competency_plan_viewed::create_from_plan($plan)->trigger(); 2806 2807 return true; 2808 } 2809 2810 /** 2811 * Deletes a plan. 2812 * 2813 * Plans based on a template can be removed just like any other one. 2814 * 2815 * @param int $id 2816 * @return bool Success? 2817 */ 2818 public static function delete_plan($id) { 2819 global $DB; 2820 static::require_enabled(); 2821 2822 $plan = new plan($id); 2823 2824 if (!$plan->can_manage()) { 2825 $context = context_user::instance($plan->get('userid')); 2826 throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); 2827 } 2828 2829 // Wrap the suppression in a DB transaction. 2830 $transaction = $DB->start_delegated_transaction(); 2831 2832 // Delete plan competencies. 2833 $plancomps = plan_competency::get_records(array('planid' => $plan->get('id'))); 2834 foreach ($plancomps as $plancomp) { 2835 $plancomp->delete(); 2836 } 2837 2838 // Delete archive user competencies if the status of the plan is complete. 2839 if ($plan->get('status') == plan::STATUS_COMPLETE) { 2840 self::remove_archived_user_competencies_in_plan($plan); 2841 } 2842 $event = \core\event\competency_plan_deleted::create_from_plan($plan); 2843 $success = $plan->delete(); 2844 2845 $transaction->allow_commit(); 2846 2847 // Trigger deleted event. 2848 $event->trigger(); 2849 2850 return $success; 2851 } 2852 2853 /** 2854 * Cancel the review of a plan. 2855 * 2856 * @param int|plan $planorid The plan, or its ID. 2857 * @return bool 2858 */ 2859 public static function plan_cancel_review_request($planorid) { 2860 static::require_enabled(); 2861 $plan = $planorid; 2862 if (!is_object($plan)) { 2863 $plan = new plan($plan); 2864 } 2865 2866 // We need to be able to view the plan at least. 2867 if (!$plan->can_read()) { 2868 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', ''); 2869 } 2870 2871 if ($plan->is_based_on_template()) { 2872 throw new coding_exception('Template plans cannot be reviewed.'); // This should never happen. 2873 } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) { 2874 throw new coding_exception('The plan review cannot be cancelled at this stage.'); 2875 } else if (!$plan->can_request_review()) { 2876 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); 2877 } 2878 2879 $plan->set('status', plan::STATUS_DRAFT); 2880 $result = $plan->update(); 2881 2882 // Trigger review request cancelled event. 2883 \core\event\competency_plan_review_request_cancelled::create_from_plan($plan)->trigger(); 2884 2885 return $result; 2886 } 2887 2888 /** 2889 * Request the review of a plan. 2890 * 2891 * @param int|plan $planorid The plan, or its ID. 2892 * @return bool 2893 */ 2894 public static function plan_request_review($planorid) { 2895 static::require_enabled(); 2896 $plan = $planorid; 2897 if (!is_object($plan)) { 2898 $plan = new plan($plan); 2899 } 2900 2901 // We need to be able to view the plan at least. 2902 if (!$plan->can_read()) { 2903 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', ''); 2904 } 2905 2906 if ($plan->is_based_on_template()) { 2907 throw new coding_exception('Template plans cannot be reviewed.'); // This should never happen. 2908 } else if ($plan->get('status') != plan::STATUS_DRAFT) { 2909 throw new coding_exception('The plan cannot be sent for review at this stage.'); 2910 } else if (!$plan->can_request_review()) { 2911 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); 2912 } 2913 2914 $plan->set('status', plan::STATUS_WAITING_FOR_REVIEW); 2915 $result = $plan->update(); 2916 2917 // Trigger review requested event. 2918 \core\event\competency_plan_review_requested::create_from_plan($plan)->trigger(); 2919 2920 return $result; 2921 } 2922 2923 /** 2924 * Start the review of a plan. 2925 * 2926 * @param int|plan $planorid The plan, or its ID. 2927 * @return bool 2928 */ 2929 public static function plan_start_review($planorid) { 2930 global $USER; 2931 static::require_enabled(); 2932 $plan = $planorid; 2933 if (!is_object($plan)) { 2934 $plan = new plan($plan); 2935 } 2936 2937 // We need to be able to view the plan at least. 2938 if (!$plan->can_read()) { 2939 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', ''); 2940 } 2941 2942 if ($plan->is_based_on_template()) { 2943 throw new coding_exception('Template plans cannot be reviewed.'); // This should never happen. 2944 } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) { 2945 throw new coding_exception('The plan review cannot be started at this stage.'); 2946 } else if (!$plan->can_review()) { 2947 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); 2948 } 2949 2950 $plan->set('status', plan::STATUS_IN_REVIEW); 2951 $plan->set('reviewerid', $USER->id); 2952 $result = $plan->update(); 2953 2954 // Trigger review started event. 2955 \core\event\competency_plan_review_started::create_from_plan($plan)->trigger(); 2956 2957 return $result; 2958 } 2959 2960 /** 2961 * Stop reviewing a plan. 2962 * 2963 * @param int|plan $planorid The plan, or its ID. 2964 * @return bool 2965 */ 2966 public static function plan_stop_review($planorid) { 2967 static::require_enabled(); 2968 $plan = $planorid; 2969 if (!is_object($plan)) { 2970 $plan = new plan($plan); 2971 } 2972 2973 // We need to be able to view the plan at least. 2974 if (!$plan->can_read()) { 2975 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', ''); 2976 } 2977 2978 if ($plan->is_based_on_template()) { 2979 throw new coding_exception('Template plans cannot be reviewed.'); // This should never happen. 2980 } else if ($plan->get('status') != plan::STATUS_IN_REVIEW) { 2981 throw new coding_exception('The plan review cannot be stopped at this stage.'); 2982 } else if (!$plan->can_review()) { 2983 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); 2984 } 2985 2986 $plan->set('status', plan::STATUS_DRAFT); 2987 $plan->set('reviewerid', null); 2988 $result = $plan->update(); 2989 2990 // Trigger review stopped event. 2991 \core\event\competency_plan_review_stopped::create_from_plan($plan)->trigger(); 2992 2993 return $result; 2994 } 2995 2996 /** 2997 * Approve a plan. 2998 * 2999 * This means making the plan active. 3000 * 3001 * @param int|plan $planorid The plan, or its ID. 3002 * @return bool 3003 */ 3004 public static function approve_plan($planorid) { 3005 static::require_enabled(); 3006 $plan = $planorid; 3007 if (!is_object($plan)) { 3008 $plan = new plan($plan); 3009 } 3010 3011 // We need to be able to view the plan at least. 3012 if (!$plan->can_read()) { 3013 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', ''); 3014 } 3015 3016 // We can approve a plan that is either a draft, in review, or waiting for review. 3017 if ($plan->is_based_on_template()) { 3018 throw new coding_exception('Template plans are already approved.'); // This should never happen. 3019 } else if (!$plan->is_draft()) { 3020 throw new coding_exception('The plan cannot be approved at this stage.'); 3021 } else if (!$plan->can_review()) { 3022 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); 3023 } 3024 3025 $plan->set('status', plan::STATUS_ACTIVE); 3026 $plan->set('reviewerid', null); 3027 $result = $plan->update(); 3028 3029 // Trigger approved event. 3030 \core\event\competency_plan_approved::create_from_plan($plan)->trigger(); 3031 3032 return $result; 3033 } 3034 3035 /** 3036 * Unapprove a plan. 3037 * 3038 * This means making the plan draft. 3039 * 3040 * @param int|plan $planorid The plan, or its ID. 3041 * @return bool 3042 */ 3043 public static function unapprove_plan($planorid) { 3044 static::require_enabled(); 3045 $plan = $planorid; 3046 if (!is_object($plan)) { 3047 $plan = new plan($plan); 3048 } 3049 3050 // We need to be able to view the plan at least. 3051 if (!$plan->can_read()) { 3052 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', ''); 3053 } 3054 3055 if ($plan->is_based_on_template()) { 3056 throw new coding_exception('Template plans are always approved.'); // This should never happen. 3057 } else if ($plan->get('status') != plan::STATUS_ACTIVE) { 3058 throw new coding_exception('The plan cannot be sent back to draft at this stage.'); 3059 } else if (!$plan->can_review()) { 3060 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); 3061 } 3062 3063 $plan->set('status', plan::STATUS_DRAFT); 3064 $result = $plan->update(); 3065 3066 // Trigger unapproved event. 3067 \core\event\competency_plan_unapproved::create_from_plan($plan)->trigger(); 3068 3069 return $result; 3070 } 3071 3072 /** 3073 * Complete a plan. 3074 * 3075 * @param int|plan $planorid The plan, or its ID. 3076 * @return bool 3077 */ 3078 public static function complete_plan($planorid) { 3079 global $DB; 3080 static::require_enabled(); 3081 3082 $plan = $planorid; 3083 if (!is_object($planorid)) { 3084 $plan = new plan($planorid); 3085 } 3086 3087 // Validate that the plan can be managed. 3088 if (!$plan->can_manage()) { 3089 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); 3090 } 3091 3092 // Check if the plan was already completed. 3093 if ($plan->get('status') == plan::STATUS_COMPLETE) { 3094 throw new coding_exception('The plan is already completed.'); 3095 } 3096 3097 $originalstatus = $plan->get('status'); 3098 $plan->set('status', plan::STATUS_COMPLETE); 3099 3100 // The user should also be able to manage the plan when it's completed. 3101 if (!$plan->can_manage()) { 3102 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); 3103 } 3104 3105 // Put back original status because archive needs it to extract competencies from the right table. 3106 $plan->set('status', $originalstatus); 3107 3108 // Do the things. 3109 $transaction = $DB->start_delegated_transaction(); 3110 self::archive_user_competencies_in_plan($plan); 3111 $plan->set('status', plan::STATUS_COMPLETE); 3112 $success = $plan->update(); 3113 3114 if (!$success) { 3115 $transaction->rollback(new moodle_exception('The plan could not be updated.')); 3116 return $success; 3117 } 3118 3119 $transaction->allow_commit(); 3120 3121 // Trigger updated event. 3122 \core\event\competency_plan_completed::create_from_plan($plan)->trigger(); 3123 3124 return $success; 3125 } 3126 3127 /** 3128 * Reopen a plan. 3129 * 3130 * @param int|plan $planorid The plan, or its ID. 3131 * @return bool 3132 */ 3133 public static function reopen_plan($planorid) { 3134 global $DB; 3135 static::require_enabled(); 3136 3137 $plan = $planorid; 3138 if (!is_object($planorid)) { 3139 $plan = new plan($planorid); 3140 } 3141 3142 // Validate that the plan as it is can be managed. 3143 if (!$plan->can_manage()) { 3144 $context = context_user::instance($plan->get('userid')); 3145 throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); 3146 } 3147 3148 $beforestatus = $plan->get('status'); 3149 $plan->set('status', plan::STATUS_ACTIVE); 3150 3151 // Validate if status can be changed. 3152 if (!$plan->can_manage()) { 3153 $context = context_user::instance($plan->get('userid')); 3154 throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); 3155 } 3156 3157 // Wrap the updates in a DB transaction. 3158 $transaction = $DB->start_delegated_transaction(); 3159 3160 // Delete archived user competencies if the status of the plan is changed from complete to another status. 3161 $mustremovearchivedcompetencies = ($beforestatus == plan::STATUS_COMPLETE && $plan->get('status') != plan::STATUS_COMPLETE); 3162 if ($mustremovearchivedcompetencies) { 3163 self::remove_archived_user_competencies_in_plan($plan); 3164 } 3165 3166 // If duedate less than or equal to duedate_threshold unset it. 3167 if ($plan->get('duedate') <= time() + plan::DUEDATE_THRESHOLD) { 3168 $plan->set('duedate', 0); 3169 } 3170 3171 $success = $plan->update(); 3172 3173 if (!$success) { 3174 $transaction->rollback(new moodle_exception('The plan could not be updated.')); 3175 return $success; 3176 } 3177 3178 $transaction->allow_commit(); 3179 3180 // Trigger reopened event. 3181 \core\event\competency_plan_reopened::create_from_plan($plan)->trigger(); 3182 3183 return $success; 3184 } 3185 3186 /** 3187 * Get a single competency from the user plan. 3188 * 3189 * @param int $planorid The plan, or its ID. 3190 * @param int $competencyid The competency id. 3191 * @return (object) array( 3192 * 'competency' => \core_competency\competency, 3193 * 'usercompetency' => \core_competency\user_competency 3194 * 'usercompetencyplan' => \core_competency\user_competency_plan 3195 * ) 3196 * The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time. 3197 */ 3198 public static function get_plan_competency($planorid, $competencyid) { 3199 static::require_enabled(); 3200 $plan = $planorid; 3201 if (!is_object($planorid)) { 3202 $plan = new plan($planorid); 3203 } 3204 3205 if (!user_competency::can_read_user($plan->get('userid'))) { 3206 throw new required_capability_exception($plan->get_context(), 'moodle/competency:usercompetencyview', 3207 'nopermissions', ''); 3208 } 3209 3210 $competency = $plan->get_competency($competencyid); 3211 3212 // Get user competencies from user_competency_plan if the plan status is set to complete. 3213 $iscompletedplan = $plan->get('status') == plan::STATUS_COMPLETE; 3214 if ($iscompletedplan) { 3215 $usercompetencies = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), array($competencyid)); 3216 $ucresultkey = 'usercompetencyplan'; 3217 } else { 3218 $usercompetencies = user_competency::get_multiple($plan->get('userid'), array($competencyid)); 3219 $ucresultkey = 'usercompetency'; 3220 } 3221 3222 $found = count($usercompetencies); 3223 3224 if ($found) { 3225 $uc = array_pop($usercompetencies); 3226 } else { 3227 if ($iscompletedplan) { 3228 throw new coding_exception('A user competency plan is missing'); 3229 } else { 3230 $uc = user_competency::create_relation($plan->get('userid'), $competency->get('id')); 3231 $uc->create(); 3232 } 3233 } 3234 3235 $plancompetency = (object) array( 3236 'competency' => $competency, 3237 'usercompetency' => null, 3238 'usercompetencyplan' => null 3239 ); 3240 $plancompetency->$ucresultkey = $uc; 3241 3242 return $plancompetency; 3243 } 3244 3245 /** 3246 * List the plans with a competency. 3247 * 3248 * @param int $userid The user id we want the plans for. 3249 * @param int $competencyorid The competency, or its ID. 3250 * @return array[plan] Array of learning plans. 3251 */ 3252 public static function list_plans_with_competency($userid, $competencyorid) { 3253 global $USER; 3254 3255 static::require_enabled(); 3256 $competencyid = $competencyorid; 3257 $competency = null; 3258 if (is_object($competencyid)) { 3259 $competency = $competencyid; 3260 $competencyid = $competency->get('id'); 3261 } 3262 3263 $plans = plan::get_by_user_and_competency($userid, $competencyid); 3264 foreach ($plans as $index => $plan) { 3265 // Filter plans we cannot read. 3266 if (!$plan->can_read()) { 3267 unset($plans[$index]); 3268 } 3269 } 3270 return $plans; 3271 } 3272 3273 /** 3274 * List the competencies in a user plan. 3275 * 3276 * @param int $planorid The plan, or its ID. 3277 * @return array((object) array( 3278 * 'competency' => \core_competency\competency, 3279 * 'usercompetency' => \core_competency\user_competency 3280 * 'usercompetencyplan' => \core_competency\user_competency_plan 3281 * )) 3282 * The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time. 3283 */ 3284 public static function list_plan_competencies($planorid) { 3285 static::require_enabled(); 3286 $plan = $planorid; 3287 if (!is_object($planorid)) { 3288 $plan = new plan($planorid); 3289 } 3290 3291 if (!$plan->can_read()) { 3292 $context = context_user::instance($plan->get('userid')); 3293 throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', ''); 3294 } 3295 3296 $result = array(); 3297 $competencies = $plan->get_competencies(); 3298 3299 // Get user competencies from user_competency_plan if the plan status is set to complete. 3300 $iscompletedplan = $plan->get('status') == plan::STATUS_COMPLETE; 3301 if ($iscompletedplan) { 3302 $usercompetencies = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), $competencies); 3303 $ucresultkey = 'usercompetencyplan'; 3304 } else { 3305 $usercompetencies = user_competency::get_multiple($plan->get('userid'), $competencies); 3306 $ucresultkey = 'usercompetency'; 3307 } 3308 3309 // Build the return values. 3310 foreach ($competencies as $key => $competency) { 3311 $found = false; 3312 3313 foreach ($usercompetencies as $uckey => $uc) { 3314 if ($uc->get('competencyid') == $competency->get('id')) { 3315 $found = true; 3316 unset($usercompetencies[$uckey]); 3317 break; 3318 } 3319 } 3320 3321 if (!$found) { 3322 if ($iscompletedplan) { 3323 throw new coding_exception('A user competency plan is missing'); 3324 } else { 3325 $uc = user_competency::create_relation($plan->get('userid'), $competency->get('id')); 3326 } 3327 } 3328 3329 $plancompetency = (object) array( 3330 'competency' => $competency, 3331 'usercompetency' => null, 3332 'usercompetencyplan' => null 3333 ); 3334 $plancompetency->$ucresultkey = $uc; 3335 $result[] = $plancompetency; 3336 } 3337 3338 return $result; 3339 } 3340 3341 /** 3342 * Add a competency to a plan. 3343 * 3344 * @param int $planid The id of the plan 3345 * @param int $competencyid The id of the competency 3346 * @return bool 3347 */ 3348 public static function add_competency_to_plan($planid, $competencyid) { 3349 static::require_enabled(); 3350 $plan = new plan($planid); 3351 3352 // First we do a permissions check. 3353 if (!$plan->can_manage()) { 3354 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', ''); 3355 3356 } else if ($plan->is_based_on_template()) { 3357 throw new coding_exception('A competency can not be added to a learning plan based on a template'); 3358 } 3359 3360 if (!$plan->can_be_edited()) { 3361 throw new coding_exception('A competency can not be added to a learning plan completed'); 3362 } 3363 3364 $competency = new competency($competencyid); 3365 3366 // Can not add a competency that belong to a hidden framework. 3367 if ($competency->get_framework()->get('visible') == false) { 3368 throw new coding_exception('A competency belonging to hidden framework can not be added'); 3369 } 3370 3371 $exists = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid)); 3372 if (!$exists) { 3373 $record = new stdClass(); 3374 $record->planid = $planid; 3375 $record->competencyid = $competencyid; 3376 $plancompetency = new plan_competency(0, $record); 3377 $plancompetency->create(); 3378 } 3379 3380 return true; 3381 } 3382 3383 /** 3384 * Remove a competency from a plan. 3385 * 3386 * @param int $planid The plan id 3387 * @param int $competencyid The id of the competency 3388 * @return bool 3389 */ 3390 public static function remove_competency_from_plan($planid, $competencyid) { 3391 static::require_enabled(); 3392 $plan = new plan($planid); 3393 3394 // First we do a permissions check. 3395 if (!$plan->can_manage()) { 3396 $context = context_user::instance($plan->get('userid')); 3397 throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); 3398 3399 } else if ($plan->is_based_on_template()) { 3400 throw new coding_exception('A competency can not be removed from a learning plan based on a template'); 3401 } 3402 3403 if (!$plan->can_be_edited()) { 3404 throw new coding_exception('A competency can not be removed from a learning plan completed'); 3405 } 3406 3407 $link = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid)); 3408 if ($link) { 3409 return $link->delete(); 3410 } 3411 return false; 3412 } 3413 3414 /** 3415 * Move the plan competency up or down in the display list. 3416 * 3417 * Requires moodle/competency:planmanage capability at the system context. 3418 * 3419 * @param int $planid The plan id 3420 * @param int $competencyidfrom The id of the competency we are moving. 3421 * @param int $competencyidto The id of the competency we are moving to. 3422 * @return boolean 3423 */ 3424 public static function reorder_plan_competency($planid, $competencyidfrom, $competencyidto) { 3425 static::require_enabled(); 3426 $plan = new plan($planid); 3427 3428 // First we do a permissions check. 3429 if (!$plan->can_manage()) { 3430 $context = context_user::instance($plan->get('userid')); 3431 throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', ''); 3432 3433 } else if ($plan->is_based_on_template()) { 3434 throw new coding_exception('A competency can not be reordered in a learning plan based on a template'); 3435 } 3436 3437 if (!$plan->can_be_edited()) { 3438 throw new coding_exception('A competency can not be reordered in a learning plan completed'); 3439 } 3440 3441 $down = true; 3442 $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidfrom)); 3443 if (count($matches) == 0) { 3444 throw new coding_exception('The link does not exist'); 3445 } 3446 3447 $competencyfrom = array_pop($matches); 3448 $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidto)); 3449 if (count($matches) == 0) { 3450 throw new coding_exception('The link does not exist'); 3451 } 3452 3453 $competencyto = array_pop($matches); 3454 3455 $all = plan_competency::get_records(array('planid' => $planid), 'sortorder', 'ASC', 0, 0); 3456 3457 if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) { 3458 // We are moving up, so put it before the "to" item. 3459 $down = false; 3460 } 3461 3462 foreach ($all as $id => $plancompetency) { 3463 $sort = $plancompetency->get('sortorder'); 3464 if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) { 3465 $plancompetency->set('sortorder', $plancompetency->get('sortorder') - 1); 3466 $plancompetency->update(); 3467 } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) { 3468 $plancompetency->set('sortorder', $plancompetency->get('sortorder') + 1); 3469 $plancompetency->update(); 3470 } 3471 } 3472 $competencyfrom->set('sortorder', $competencyto->get('sortorder')); 3473 return $competencyfrom->update(); 3474 } 3475 3476 /** 3477 * Cancel a user competency review request. 3478 * 3479 * @param int $userid The user ID. 3480 * @param int $competencyid The competency ID. 3481 * @return bool 3482 */ 3483 public static function user_competency_cancel_review_request($userid, $competencyid) { 3484 static::require_enabled(); 3485 $context = context_user::instance($userid); 3486 $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); 3487 if (!$uc || !$uc->can_read()) { 3488 throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); 3489 } else if ($uc->get('status') != user_competency::STATUS_WAITING_FOR_REVIEW) { 3490 throw new coding_exception('The competency can not be cancel review request at this stage.'); 3491 } else if (!$uc->can_request_review()) { 3492 throw new required_capability_exception($context, 'moodle/competency:usercompetencyrequestreview', 'nopermissions', ''); 3493 } 3494 3495 $uc->set('status', user_competency::STATUS_IDLE); 3496 $result = $uc->update(); 3497 if ($result) { 3498 \core\event\competency_user_competency_review_request_cancelled::create_from_user_competency($uc)->trigger(); 3499 } 3500 return $result; 3501 } 3502 3503 /** 3504 * Request a user competency review. 3505 * 3506 * @param int $userid The user ID. 3507 * @param int $competencyid The competency ID. 3508 * @return bool 3509 */ 3510 public static function user_competency_request_review($userid, $competencyid) { 3511 static::require_enabled(); 3512 $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); 3513 if (!$uc) { 3514 $uc = user_competency::create_relation($userid, $competencyid); 3515 $uc->create(); 3516 } 3517 3518 if (!$uc->can_read()) { 3519 throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview', 3520 'nopermissions', ''); 3521 } else if ($uc->get('status') != user_competency::STATUS_IDLE) { 3522 throw new coding_exception('The competency can not be sent for review at this stage.'); 3523 } else if (!$uc->can_request_review()) { 3524 throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyrequestreview', 3525 'nopermissions', ''); 3526 } 3527 3528 $uc->set('status', user_competency::STATUS_WAITING_FOR_REVIEW); 3529 $result = $uc->update(); 3530 if ($result) { 3531 \core\event\competency_user_competency_review_requested::create_from_user_competency($uc)->trigger(); 3532 } 3533 return $result; 3534 } 3535 3536 /** 3537 * Start a user competency review. 3538 * 3539 * @param int $userid The user ID. 3540 * @param int $competencyid The competency ID. 3541 * @return bool 3542 */ 3543 public static function user_competency_start_review($userid, $competencyid) { 3544 global $USER; 3545 static::require_enabled(); 3546 3547 $context = context_user::instance($userid); 3548 $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); 3549 if (!$uc || !$uc->can_read()) { 3550 throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); 3551 } else if ($uc->get('status') != user_competency::STATUS_WAITING_FOR_REVIEW) { 3552 throw new coding_exception('The competency review can not be started at this stage.'); 3553 } else if (!$uc->can_review()) { 3554 throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', ''); 3555 } 3556 3557 $uc->set('status', user_competency::STATUS_IN_REVIEW); 3558 $uc->set('reviewerid', $USER->id); 3559 $result = $uc->update(); 3560 if ($result) { 3561 \core\event\competency_user_competency_review_started::create_from_user_competency($uc)->trigger(); 3562 } 3563 return $result; 3564 } 3565 3566 /** 3567 * Stop a user competency review. 3568 * 3569 * @param int $userid The user ID. 3570 * @param int $competencyid The competency ID. 3571 * @return bool 3572 */ 3573 public static function user_competency_stop_review($userid, $competencyid) { 3574 static::require_enabled(); 3575 $context = context_user::instance($userid); 3576 $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); 3577 if (!$uc || !$uc->can_read()) { 3578 throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); 3579 } else if ($uc->get('status') != user_competency::STATUS_IN_REVIEW) { 3580 throw new coding_exception('The competency review can not be stopped at this stage.'); 3581 } else if (!$uc->can_review()) { 3582 throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', ''); 3583 } 3584 3585 $uc->set('status', user_competency::STATUS_IDLE); 3586 $result = $uc->update(); 3587 if ($result) { 3588 \core\event\competency_user_competency_review_stopped::create_from_user_competency($uc)->trigger(); 3589 } 3590 return $result; 3591 } 3592 3593 /** 3594 * Log user competency viewed event. 3595 * 3596 * @param user_competency|int $usercompetencyorid The user competency object or user competency id 3597 * @return bool 3598 */ 3599 public static function user_competency_viewed($usercompetencyorid) { 3600 static::require_enabled(); 3601 $uc = $usercompetencyorid; 3602 if (!is_object($uc)) { 3603 $uc = new user_competency($uc); 3604 } 3605 3606 if (!$uc || !$uc->can_read()) { 3607 throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview', 3608 'nopermissions', ''); 3609 } 3610 3611 \core\event\competency_user_competency_viewed::create_from_user_competency_viewed($uc)->trigger(); 3612 return true; 3613 } 3614 3615 /** 3616 * Log user competency viewed in plan event. 3617 * 3618 * @param user_competency|int $usercompetencyorid The user competency object or user competency id 3619 * @param int $planid The plan ID 3620 * @return bool 3621 */ 3622 public static function user_competency_viewed_in_plan($usercompetencyorid, $planid) { 3623 static::require_enabled(); 3624 $uc = $usercompetencyorid; 3625 if (!is_object($uc)) { 3626 $uc = new user_competency($uc); 3627 } 3628 3629 if (!$uc || !$uc->can_read()) { 3630 throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview', 3631 'nopermissions', ''); 3632 } 3633 $plan = new plan($planid); 3634 if ($plan->get('status') == plan::STATUS_COMPLETE) { 3635 throw new coding_exception('To log the user competency in completed plan use user_competency_plan_viewed method.'); 3636 } 3637 3638 \core\event\competency_user_competency_viewed_in_plan::create_from_user_competency_viewed_in_plan($uc, $planid)->trigger(); 3639 return true; 3640 } 3641 3642 /** 3643 * Log user competency viewed in course event. 3644 * 3645 * @param user_competency_course|int $usercoursecompetencyorid The user competency course object or its ID. 3646 * @param int $courseid The course ID 3647 * @return bool 3648 */ 3649 public static function user_competency_viewed_in_course($usercoursecompetencyorid) { 3650 static::require_enabled(); 3651 $ucc = $usercoursecompetencyorid; 3652 if (!is_object($ucc)) { 3653 $ucc = new user_competency_course($ucc); 3654 } 3655 3656 if (!$ucc || !user_competency::can_read_user_in_course($ucc->get('userid'), $ucc->get('courseid'))) { 3657 throw new required_capability_exception($ucc->get_context(), 'moodle/competency:usercompetencyview', 3658 'nopermissions', ''); 3659 } 3660 3661 // Validate the course, this will throw an exception if not valid. 3662 self::validate_course($ucc->get('courseid')); 3663 3664 \core\event\competency_user_competency_viewed_in_course::create_from_user_competency_viewed_in_course($ucc)->trigger(); 3665 return true; 3666 } 3667 3668 /** 3669 * Log user competency plan viewed event. 3670 * 3671 * @param user_competency_plan|int $usercompetencyplanorid The user competency plan object or user competency plan id 3672 * @return bool 3673 */ 3674 public static function user_competency_plan_viewed($usercompetencyplanorid) { 3675 static::require_enabled(); 3676 $ucp = $usercompetencyplanorid; 3677 if (!is_object($ucp)) { 3678 $ucp = new user_competency_plan($ucp); 3679 } 3680 3681 if (!$ucp || !user_competency::can_read_user($ucp->get('userid'))) { 3682 throw new required_capability_exception($ucp->get_context(), 'moodle/competency:usercompetencyview', 3683 'nopermissions', ''); 3684 } 3685 $plan = new plan($ucp->get('planid')); 3686 if ($plan->get('status') != plan::STATUS_COMPLETE) { 3687 throw new coding_exception('To log the user competency in non-completed plan use ' 3688 . 'user_competency_viewed_in_plan method.'); 3689 } 3690 3691 \core\event\competency_user_competency_plan_viewed::create_from_user_competency_plan($ucp)->trigger(); 3692 return true; 3693 } 3694 3695 /** 3696 * Check if template has related data. 3697 * 3698 * @param int $templateid The id of the template to check. 3699 * @return boolean 3700 */ 3701 public static function template_has_related_data($templateid) { 3702 static::require_enabled(); 3703 // First we do a permissions check. 3704 $template = new template($templateid); 3705 3706 if (!$template->can_read()) { 3707 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', 3708 'nopermissions', ''); 3709 } 3710 3711 // OK - all set. 3712 return $template->has_plans(); 3713 } 3714 3715 /** 3716 * List all the related competencies. 3717 * 3718 * @param int $competencyid The id of the competency to check. 3719 * @return competency[] 3720 */ 3721 public static function list_related_competencies($competencyid) { 3722 static::require_enabled(); 3723 $competency = new competency($competencyid); 3724 3725 if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), 3726 $competency->get_context())) { 3727 throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview', 3728 'nopermissions', ''); 3729 } 3730 3731 return $competency->get_related_competencies(); 3732 } 3733 3734 /** 3735 * Add a related competency. 3736 * 3737 * @param int $competencyid The id of the competency 3738 * @param int $relatedcompetencyid The id of the related competency. 3739 * @return bool False when create failed, true on success, or if the relation already existed. 3740 */ 3741 public static function add_related_competency($competencyid, $relatedcompetencyid) { 3742 static::require_enabled(); 3743 $competency1 = new competency($competencyid); 3744 $competency2 = new competency($relatedcompetencyid); 3745 3746 require_capability('moodle/competency:competencymanage', $competency1->get_context()); 3747 3748 $relatedcompetency = related_competency::get_relation($competency1->get('id'), $competency2->get('id')); 3749 if (!$relatedcompetency->get('id')) { 3750 $relatedcompetency->create(); 3751 return true; 3752 } 3753 3754 return true; 3755 } 3756 3757 /** 3758 * Remove a related competency. 3759 * 3760 * @param int $competencyid The id of the competency. 3761 * @param int $relatedcompetencyid The id of the related competency. 3762 * @return bool True when it was deleted, false when it wasn't or the relation doesn't exist. 3763 */ 3764 public static function remove_related_competency($competencyid, $relatedcompetencyid) { 3765 static::require_enabled(); 3766 $competency = new competency($competencyid); 3767 3768 // This only check if we have the permission in either competency because both competencies 3769 // should belong to the same framework. 3770 require_capability('moodle/competency:competencymanage', $competency->get_context()); 3771 3772 $relatedcompetency = related_competency::get_relation($competencyid, $relatedcompetencyid); 3773 if ($relatedcompetency->get('id')) { 3774 return $relatedcompetency->delete(); 3775 } 3776 3777 return false; 3778 } 3779 3780 /** 3781 * Read a user evidence. 3782 * 3783 * @param int $id 3784 * @return user_evidence 3785 */ 3786 public static function read_user_evidence($id) { 3787 static::require_enabled(); 3788 $userevidence = new user_evidence($id); 3789 3790 if (!$userevidence->can_read()) { 3791 $context = $userevidence->get_context(); 3792 throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', ''); 3793 } 3794 3795 return $userevidence; 3796 } 3797 3798 /** 3799 * Create a new user evidence. 3800 * 3801 * @param object $data The data. 3802 * @param int $draftitemid The draft ID in which files have been saved. 3803 * @return user_evidence 3804 */ 3805 public static function create_user_evidence($data, $draftitemid = null) { 3806 static::require_enabled(); 3807 $userevidence = new user_evidence(null, $data); 3808 $context = $userevidence->get_context(); 3809 3810 if (!$userevidence->can_manage()) { 3811 throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); 3812 } 3813 3814 $userevidence->create(); 3815 if (!empty($draftitemid)) { 3816 $fileareaoptions = array('subdirs' => true); 3817 $itemid = $userevidence->get('id'); 3818 file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions); 3819 } 3820 3821 // Trigger an evidence of prior learning created event. 3822 \core\event\competency_user_evidence_created::create_from_user_evidence($userevidence)->trigger(); 3823 3824 return $userevidence; 3825 } 3826 3827 /** 3828 * Create a new user evidence. 3829 * 3830 * @param object $data The data. 3831 * @param int $draftitemid The draft ID in which files have been saved. 3832 * @return user_evidence 3833 */ 3834 public static function update_user_evidence($data, $draftitemid = null) { 3835 static::require_enabled(); 3836 $userevidence = new user_evidence($data->id); 3837 $context = $userevidence->get_context(); 3838 3839 if (!$userevidence->can_manage()) { 3840 throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); 3841 3842 } else if (property_exists($data, 'userid') && $data->userid != $userevidence->get('userid')) { 3843 throw new coding_exception('Can not change the userid of a user evidence.'); 3844 } 3845 3846 $userevidence->from_record($data); 3847 $userevidence->update(); 3848 3849 if (!empty($draftitemid)) { 3850 $fileareaoptions = array('subdirs' => true); 3851 $itemid = $userevidence->get('id'); 3852 file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions); 3853 } 3854 3855 // Trigger an evidence of prior learning updated event. 3856 \core\event\competency_user_evidence_updated::create_from_user_evidence($userevidence)->trigger(); 3857 3858 return $userevidence; 3859 } 3860 3861 /** 3862 * Delete a user evidence. 3863 * 3864 * @param int $id The user evidence ID. 3865 * @return bool 3866 */ 3867 public static function delete_user_evidence($id) { 3868 static::require_enabled(); 3869 $userevidence = new user_evidence($id); 3870 $context = $userevidence->get_context(); 3871 3872 if (!$userevidence->can_manage()) { 3873 throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); 3874 } 3875 3876 // Delete the user evidence. 3877 $userevidence->delete(); 3878 3879 // Delete associated files. 3880 $fs = get_file_storage(); 3881 $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $id); 3882 3883 // Delete relation between evidence and competencies. 3884 $userevidence->set('id', $id); // Restore the ID to fully mock the object. 3885 $competencies = user_evidence_competency::get_competencies_by_userevidenceid($id); 3886 foreach ($competencies as $competency) { 3887 static::delete_user_evidence_competency($userevidence, $competency->get('id')); 3888 } 3889 3890 // Trigger an evidence of prior learning deleted event. 3891 \core\event\competency_user_evidence_deleted::create_from_user_evidence($userevidence)->trigger(); 3892 3893 $userevidence->set('id', 0); // Restore the object. 3894 3895 return true; 3896 } 3897 3898 /** 3899 * List the user evidence of a user. 3900 * 3901 * @param int $userid The user ID. 3902 * @return user_evidence[] 3903 */ 3904 public static function list_user_evidence($userid) { 3905 static::require_enabled(); 3906 if (!user_evidence::can_read_user($userid)) { 3907 $context = context_user::instance($userid); 3908 throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', ''); 3909 } 3910 3911 $evidence = user_evidence::get_records(array('userid' => $userid), 'name'); 3912 return $evidence; 3913 } 3914 3915 /** 3916 * Link a user evidence with a competency. 3917 * 3918 * @param user_evidence|int $userevidenceorid User evidence or its ID. 3919 * @param int $competencyid Competency ID. 3920 * @return user_evidence_competency 3921 */ 3922 public static function create_user_evidence_competency($userevidenceorid, $competencyid) { 3923 global $USER; 3924 static::require_enabled(); 3925 3926 $userevidence = $userevidenceorid; 3927 if (!is_object($userevidence)) { 3928 $userevidence = self::read_user_evidence($userevidence); 3929 } 3930 3931 // Perform user evidence capability checks. 3932 if (!$userevidence->can_manage()) { 3933 $context = $userevidence->get_context(); 3934 throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); 3935 } 3936 3937 // Perform competency capability checks. 3938 $competency = self::read_competency($competencyid); 3939 3940 // Get (and create) the relation. 3941 $relation = user_evidence_competency::get_relation($userevidence->get('id'), $competency->get('id')); 3942 if (!$relation->get('id')) { 3943 $relation->create(); 3944 3945 $link = url::user_evidence($userevidence->get('id')); 3946 self::add_evidence( 3947 $userevidence->get('userid'), 3948 $competency, 3949 $userevidence->get_context(), 3950 evidence::ACTION_LOG, 3951 'evidence_evidenceofpriorlearninglinked', 3952 'core_competency', 3953 $userevidence->get('name'), 3954 false, 3955 $link->out(false), 3956 null, 3957 $USER->id 3958 ); 3959 } 3960 3961 return $relation; 3962 } 3963 3964 /** 3965 * Delete a relationship between a user evidence and a competency. 3966 * 3967 * @param user_evidence|int $userevidenceorid User evidence or its ID. 3968 * @param int $competencyid Competency ID. 3969 * @return bool 3970 */ 3971 public static function delete_user_evidence_competency($userevidenceorid, $competencyid) { 3972 global $USER; 3973 static::require_enabled(); 3974 3975 $userevidence = $userevidenceorid; 3976 if (!is_object($userevidence)) { 3977 $userevidence = self::read_user_evidence($userevidence); 3978 } 3979 3980 // Perform user evidence capability checks. 3981 if (!$userevidence->can_manage()) { 3982 $context = $userevidence->get_context(); 3983 throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); 3984 } 3985 3986 // Get (and delete) the relation. 3987 $relation = user_evidence_competency::get_relation($userevidence->get('id'), $competencyid); 3988 if (!$relation->get('id')) { 3989 return true; 3990 } 3991 3992 $success = $relation->delete(); 3993 if ($success) { 3994 self::add_evidence( 3995 $userevidence->get('userid'), 3996 $competencyid, 3997 $userevidence->get_context(), 3998 evidence::ACTION_LOG, 3999 'evidence_evidenceofpriorlearningunlinked', 4000 'core_competency', 4001 $userevidence->get('name'), 4002 false, 4003 null, 4004 null, 4005 $USER->id 4006 ); 4007 } 4008 4009 return $success; 4010 } 4011 4012 /** 4013 * Send request review for user evidence competencies. 4014 * 4015 * @param int $id The user evidence ID. 4016 * @return bool 4017 */ 4018 public static function request_review_of_user_evidence_linked_competencies($id) { 4019 $userevidence = new user_evidence($id); 4020 $context = $userevidence->get_context(); 4021 $userid = $userevidence->get('userid'); 4022 4023 if (!$userevidence->can_manage()) { 4024 throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', ''); 4025 } 4026 4027 $usercompetencies = user_evidence_competency::get_user_competencies_by_userevidenceid($id); 4028 foreach ($usercompetencies as $usercompetency) { 4029 if ($usercompetency->get('status') == user_competency::STATUS_IDLE) { 4030 static::user_competency_request_review($userid, $usercompetency->get('competencyid')); 4031 } 4032 } 4033 4034 return true; 4035 } 4036 4037 /** 4038 * Recursively duplicate competencies from a tree, we start duplicating from parents to children to have a correct path. 4039 * This method does not copy the related competencies. 4040 * 4041 * @param int $frameworkid - framework id 4042 * @param competency[] $tree - array of competencies object 4043 * @param int $oldparent - old parent id 4044 * @param int $newparent - new parent id 4045 * @return competency[] $matchids - List of old competencies ids matched with new competencies object. 4046 */ 4047 protected static function duplicate_competency_tree($frameworkid, $tree, $oldparent = 0, $newparent = 0) { 4048 $matchids = array(); 4049 foreach ($tree as $node) { 4050 if ($node->competency->get('parentid') == $oldparent) { 4051 $parentid = $node->competency->get('id'); 4052 4053 // Create the competency. 4054 $competency = new competency(0, $node->competency->to_record()); 4055 $competency->set('competencyframeworkid', $frameworkid); 4056 $competency->set('parentid', $newparent); 4057 $competency->set('path', ''); 4058 $competency->set('id', 0); 4059 $competency->reset_rule(); 4060 $competency->create(); 4061 4062 // Trigger the created event competency. 4063 \core\event\competency_created::create_from_competency($competency)->trigger(); 4064 4065 // Match the old id with the new one. 4066 $matchids[$parentid] = $competency; 4067 4068 if (!empty($node->children)) { 4069 // Duplicate children competency. 4070 $childrenids = self::duplicate_competency_tree($frameworkid, $node->children, $parentid, $competency->get('id')); 4071 // Array_merge does not keep keys when merging so we use the + operator. 4072 $matchids = $matchids + $childrenids; 4073 } 4074 } 4075 } 4076 return $matchids; 4077 } 4078 4079 /** 4080 * Recursively migrate competency rules. 4081 * 4082 * @param competency[] $tree - array of competencies object 4083 * @param competency[] $matchids - List of old competencies ids matched with new competencies object 4084 */ 4085 protected static function migrate_competency_tree_rules($tree, $matchids) { 4086 4087 foreach ($tree as $node) { 4088 $oldcompid = $node->competency->get('id'); 4089 if ($node->competency->get('ruletype') && array_key_exists($oldcompid, $matchids)) { 4090 try { 4091 // Get the new competency. 4092 $competency = $matchids[$oldcompid]; 4093 $class = $node->competency->get('ruletype'); 4094 $newruleconfig = $class::migrate_config($node->competency->get('ruleconfig'), $matchids); 4095 $competency->set('ruleconfig', $newruleconfig); 4096 $competency->set('ruletype', $class); 4097 $competency->set('ruleoutcome', $node->competency->get('ruleoutcome')); 4098 $competency->update(); 4099 } catch (\Exception $e) { 4100 debugging('Could not migrate competency rule from: ' . $oldcompid . ' to: ' . $competency->get('id') . '.' . 4101 ' Exception: ' . $e->getMessage(), DEBUG_DEVELOPER); 4102 $competency->reset_rule(); 4103 } 4104 } 4105 4106 if (!empty($node->children)) { 4107 self::migrate_competency_tree_rules($node->children, $matchids); 4108 } 4109 } 4110 } 4111 4112 /** 4113 * Archive user competencies in a plan. 4114 * 4115 * @param int $plan The plan object. 4116 * @return void 4117 */ 4118 protected static function archive_user_competencies_in_plan($plan) { 4119 4120 // Check if the plan was already completed. 4121 if ($plan->get('status') == plan::STATUS_COMPLETE) { 4122 throw new coding_exception('The plan is already completed.'); 4123 } 4124 4125 $competencies = $plan->get_competencies(); 4126 $usercompetencies = user_competency::get_multiple($plan->get('userid'), $competencies); 4127 4128 $i = 0; 4129 foreach ($competencies as $competency) { 4130 $found = false; 4131 4132 foreach ($usercompetencies as $uckey => $uc) { 4133 if ($uc->get('competencyid') == $competency->get('id')) { 4134 $found = true; 4135 4136 $ucprecord = $uc->to_record(); 4137 $ucprecord->planid = $plan->get('id'); 4138 $ucprecord->sortorder = $i; 4139 unset($ucprecord->id); 4140 unset($ucprecord->status); 4141 unset($ucprecord->reviewerid); 4142 4143 $usercompetencyplan = new user_competency_plan(0, $ucprecord); 4144 $usercompetencyplan->create(); 4145 4146 unset($usercompetencies[$uckey]); 4147 break; 4148 } 4149 } 4150 4151 // If the user competency doesn't exist, we create a new relation in user_competency_plan. 4152 if (!$found) { 4153 $usercompetencyplan = user_competency_plan::create_relation($plan->get('userid'), $competency->get('id'), 4154 $plan->get('id')); 4155 $usercompetencyplan->set('sortorder', $i); 4156 $usercompetencyplan->create(); 4157 } 4158 $i++; 4159 } 4160 } 4161 4162 /** 4163 * Delete archived user competencies in a plan. 4164 * 4165 * @param int $plan The plan object. 4166 * @return void 4167 */ 4168 protected static function remove_archived_user_competencies_in_plan($plan) { 4169 $competencies = $plan->get_competencies(); 4170 $usercompetenciesplan = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), $competencies); 4171 4172 foreach ($usercompetenciesplan as $ucpkey => $ucp) { 4173 $ucp->delete(); 4174 } 4175 } 4176 4177 /** 4178 * List all the evidence for a user competency. 4179 * 4180 * @param int $userid The user id - only used if usercompetencyid is 0. 4181 * @param int $competencyid The competency id - only used it usercompetencyid is 0. 4182 * @param int $planid The plan id - not used yet - but can be used to only list archived evidence if a plan is completed. 4183 * @param string $sort The field to sort the evidence by. 4184 * @param string $order The ordering of the sorting. 4185 * @param int $skip Number of records to skip. 4186 * @param int $limit Number of records to return. 4187 * @return \core_competency\evidence[] 4188 * @return array of \core_competency\evidence 4189 */ 4190 public static function list_evidence($userid = 0, $competencyid = 0, $planid = 0, $sort = 'timecreated', 4191 $order = 'DESC', $skip = 0, $limit = 0) { 4192 static::require_enabled(); 4193 4194 if (!user_competency::can_read_user($userid)) { 4195 $context = context_user::instance($userid); 4196 throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); 4197 } 4198 4199 $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); 4200 if (!$usercompetency) { 4201 return array(); 4202 } 4203 4204 $plancompleted = false; 4205 if ($planid != 0) { 4206 $plan = new plan($planid); 4207 if ($plan->get('status') == plan::STATUS_COMPLETE) { 4208 $plancompleted = true; 4209 } 4210 } 4211 4212 $select = 'usercompetencyid = :usercompetencyid'; 4213 $params = array('usercompetencyid' => $usercompetency->get('id')); 4214 if ($plancompleted) { 4215 $select .= ' AND timecreated <= :timecompleted'; 4216 $params['timecompleted'] = $plan->get('timemodified'); 4217 } 4218 4219 $orderby = $sort . ' ' . $order; 4220 $orderby .= !empty($orderby) ? ', id DESC' : 'id DESC'; // Prevent random ordering. 4221 4222 $evidence = evidence::get_records_select($select, $params, $orderby, '*', $skip, $limit); 4223 return $evidence; 4224 } 4225 4226 /** 4227 * List all the evidence for a user competency in a course. 4228 * 4229 * @param int $userid The user ID. 4230 * @param int $courseid The course ID. 4231 * @param int $competencyid The competency ID. 4232 * @param string $sort The field to sort the evidence by. 4233 * @param string $order The ordering of the sorting. 4234 * @param int $skip Number of records to skip. 4235 * @param int $limit Number of records to return. 4236 * @return \core_competency\evidence[] 4237 */ 4238 public static function list_evidence_in_course($userid = 0, $courseid = 0, $competencyid = 0, $sort = 'timecreated', 4239 $order = 'DESC', $skip = 0, $limit = 0) { 4240 static::require_enabled(); 4241 4242 if (!user_competency::can_read_user_in_course($userid, $courseid)) { 4243 $context = context_user::instance($userid); 4244 throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); 4245 } 4246 4247 $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); 4248 if (!$usercompetency) { 4249 return array(); 4250 } 4251 4252 $context = context_course::instance($courseid); 4253 return evidence::get_records_for_usercompetency($usercompetency->get('id'), $context, $sort, $order, $skip, $limit); 4254 } 4255 4256 /** 4257 * Create an evidence from a list of parameters. 4258 * 4259 * Requires no capability because evidence can be added in many situations under any user. 4260 * 4261 * @param int $userid The user id for which evidence is added. 4262 * @param competency|int $competencyorid The competency, or its id for which evidence is added. 4263 * @param context|int $contextorid The context in which the evidence took place. 4264 * @param int $action The type of action to take on the competency. \core_competency\evidence::ACTION_*. 4265 * @param string $descidentifier The strings identifier. 4266 * @param string $desccomponent The strings component. 4267 * @param mixed $desca Any arguments the string requires. 4268 * @param bool $recommend When true, the user competency will be sent for review. 4269 * @param string $url The url the evidence may link to. 4270 * @param int $grade The grade, or scale ID item. 4271 * @param int $actionuserid The ID of the user who took the action of adding the evidence. Null when system. 4272 * This should be used when the action was taken by a real person, this will allow 4273 * to keep track of all the evidence given by a certain person. 4274 * @param string $note A note to attach to the evidence. 4275 * @return evidence 4276 * @throws coding_exception 4277 * @throws invalid_persistent_exception 4278 * @throws moodle_exception 4279 */ 4280 public static function add_evidence($userid, $competencyorid, $contextorid, $action, $descidentifier, $desccomponent, 4281 $desca = null, $recommend = false, $url = null, $grade = null, $actionuserid = null, 4282 $note = null) { 4283 global $DB; 4284 static::require_enabled(); 4285 4286 // Some clearly important variable assignments right there. 4287 $competencyid = $competencyorid; 4288 $competency = null; 4289 if (is_object($competencyid)) { 4290 $competency = $competencyid; 4291 $competencyid = $competency->get('id'); 4292 } 4293 $contextid = $contextorid; 4294 $context = $contextorid; 4295 if (is_object($contextorid)) { 4296 $contextid = $contextorid->id; 4297 } else { 4298 $context = context::instance_by_id($contextorid); 4299 } 4300 $setucgrade = false; 4301 $ucgrade = null; 4302 $ucproficiency = null; 4303 $usercompetencycourse = null; 4304 4305 // Fetch or create the user competency. 4306 $usercompetency = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid)); 4307 if (!$usercompetency) { 4308 $usercompetency = user_competency::create_relation($userid, $competencyid); 4309 $usercompetency->create(); 4310 } 4311 4312 // What should we be doing? 4313 switch ($action) { 4314 4315 // Completing a competency. 4316 case evidence::ACTION_COMPLETE: 4317 // The logic here goes like this: 4318 // 4319 // if rating outside a course 4320 // - set the default grade and proficiency ONLY if there is no current grade 4321 // else we are in a course 4322 // - set the defautl grade and proficiency in the course ONLY if there is no current grade in the course 4323 // - then check the course settings to see if we should push the rating outside the course 4324 // - if we should push it 4325 // --- push it only if the user_competency (outside the course) has no grade 4326 // Done. 4327 4328 if ($grade !== null) { 4329 throw new coding_exception("The grade MUST NOT be set with a 'completing' evidence."); 4330 } 4331 4332 // Fetch the default grade to attach to the evidence. 4333 if (empty($competency)) { 4334 $competency = new competency($competencyid); 4335 } 4336 list($grade, $proficiency) = $competency->get_default_grade(); 4337 4338 // Add user_competency_course record when in a course or module. 4339 if (in_array($context->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE))) { 4340 $coursecontext = $context->get_course_context(); 4341 $courseid = $coursecontext->instanceid; 4342 $filterparams = array( 4343 'userid' => $userid, 4344 'competencyid' => $competencyid, 4345 'courseid' => $courseid 4346 ); 4347 // Fetch or create user competency course. 4348 $usercompetencycourse = user_competency_course::get_record($filterparams); 4349 if (!$usercompetencycourse) { 4350 $usercompetencycourse = user_competency_course::create_relation($userid, $competencyid, $courseid); 4351 $usercompetencycourse->create(); 4352 } 4353 // Only update the grade and proficiency if there is not already a grade. 4354 if ($usercompetencycourse->get('grade') === null) { 4355 // Set grade. 4356 $usercompetencycourse->set('grade', $grade); 4357 // Set proficiency. 4358 $usercompetencycourse->set('proficiency', $proficiency); 4359 } 4360 4361 // Check the course settings to see if we should push to user plans. 4362 $coursesettings = course_competency_settings::get_by_courseid($courseid); 4363 $setucgrade = $coursesettings->get('pushratingstouserplans'); 4364 4365 if ($setucgrade) { 4366 // Only push to user plans if there is not already a grade. 4367 if ($usercompetency->get('grade') !== null) { 4368 $setucgrade = false; 4369 } else { 4370 $ucgrade = $grade; 4371 $ucproficiency = $proficiency; 4372 } 4373 } 4374 } else { 4375 4376 // When completing the competency we fetch the default grade from the competency. But we only mark 4377 // the user competency when a grade has not been set yet. Complete is an action to use with automated systems. 4378 if ($usercompetency->get('grade') === null) { 4379 $setucgrade = true; 4380 $ucgrade = $grade; 4381 $ucproficiency = $proficiency; 4382 } 4383 } 4384 4385 break; 4386 4387 // We override the grade, even overriding back to not set. 4388 case evidence::ACTION_OVERRIDE: 4389 $setucgrade = true; 4390 $ucgrade = $grade; 4391 if (empty($competency)) { 4392 $competency = new competency($competencyid); 4393 } 4394 if ($ucgrade !== null) { 4395 $ucproficiency = $competency->get_proficiency_of_grade($ucgrade); 4396 } 4397 4398 // Add user_competency_course record when in a course or module. 4399 if (in_array($context->contextlevel, array(CONTEXT_COURSE, CONTEXT_MODULE))) { 4400 $coursecontext = $context->get_course_context(); 4401 $courseid = $coursecontext->instanceid; 4402 $filterparams = array( 4403 'userid' => $userid, 4404 'competencyid' => $competencyid, 4405 'courseid' => $courseid 4406 ); 4407 // Fetch or create user competency course. 4408 $usercompetencycourse = user_competency_course::get_record($filterparams); 4409 if (!$usercompetencycourse) { 4410 $usercompetencycourse = user_competency_course::create_relation($userid, $competencyid, $courseid); 4411 $usercompetencycourse->create(); 4412 } 4413 // Get proficiency. 4414 $proficiency = $ucproficiency; 4415 if ($proficiency === null) { 4416 if (empty($competency)) { 4417 $competency = new competency($competencyid); 4418 } 4419 $proficiency = $competency->get_proficiency_of_grade($grade); 4420 } 4421 // Set grade. 4422 $usercompetencycourse->set('grade', $grade); 4423 // Set proficiency. 4424 $usercompetencycourse->set('proficiency', $proficiency); 4425 4426 $coursesettings = course_competency_settings::get_by_courseid($courseid); 4427 if (!$coursesettings->get('pushratingstouserplans')) { 4428 $setucgrade = false; 4429 } 4430 } 4431 4432 break; 4433 4434 // Simply logging an evidence. 4435 case evidence::ACTION_LOG: 4436 if ($grade !== null) { 4437 throw new coding_exception("The grade MUST NOT be set when 'logging' an evidence."); 4438 } 4439 break; 4440 4441 // Whoops, this is not expected. 4442 default: 4443 throw new coding_exception('Unexpected action parameter when registering an evidence.'); 4444 break; 4445 } 4446 4447 // Should we recommend? 4448 if ($recommend && $usercompetency->get('status') == user_competency::STATUS_IDLE) { 4449 $usercompetency->set('status', user_competency::STATUS_WAITING_FOR_REVIEW); 4450 } 4451 4452 // Setting the grade and proficiency for the user competency. 4453 $wascompleted = false; 4454 if ($setucgrade == true) { 4455 if (!$usercompetency->get('proficiency') && $ucproficiency) { 4456 $wascompleted = true; 4457 } 4458 $usercompetency->set('grade', $ucgrade); 4459 $usercompetency->set('proficiency', $ucproficiency); 4460 } 4461 4462 // Prepare the evidence. 4463 $record = new stdClass(); 4464 $record->usercompetencyid = $usercompetency->get('id'); 4465 $record->contextid = $contextid; 4466 $record->action = $action; 4467 $record->descidentifier = $descidentifier; 4468 $record->desccomponent = $desccomponent; 4469 $record->grade = $grade; 4470 $record->actionuserid = $actionuserid; 4471 $record->note = $note; 4472 $evidence = new evidence(0, $record); 4473 $evidence->set('desca', $desca); 4474 $evidence->set('url', $url); 4475 4476 // Validate both models, we should not operate on one if the other will not save. 4477 if (!$usercompetency->is_valid()) { 4478 throw new invalid_persistent_exception($usercompetency->get_errors()); 4479 } else if (!$evidence->is_valid()) { 4480 throw new invalid_persistent_exception($evidence->get_errors()); 4481 } 4482 4483 // Save the user_competency_course record. 4484 if ($usercompetencycourse !== null) { 4485 // Validate and update. 4486 if (!$usercompetencycourse->is_valid()) { 4487 throw new invalid_persistent_exception($usercompetencycourse->get_errors()); 4488 } 4489 $usercompetencycourse->update(); 4490 } 4491 4492 // Finally save. Pheww! 4493 $usercompetency->update(); 4494 $evidence->create(); 4495 4496 // Trigger the evidence_created event. 4497 \core\event\competency_evidence_created::create_from_evidence($evidence, $usercompetency, $recommend)->trigger(); 4498 4499 // The competency was marked as completed, apply the rules. 4500 if ($wascompleted) { 4501 self::apply_competency_rules_from_usercompetency($usercompetency, $competency); 4502 } 4503 4504 return $evidence; 4505 } 4506 4507 /** 4508 * Read an evidence. 4509 * @param int $evidenceid The evidence ID. 4510 * @return evidence 4511 */ 4512 public static function read_evidence($evidenceid) { 4513 static::require_enabled(); 4514 4515 $evidence = new evidence($evidenceid); 4516 $uc = new user_competency($evidence->get('usercompetencyid')); 4517 if (!$uc->can_read()) { 4518 throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview', 4519 'nopermissions', ''); 4520 } 4521 4522 return $evidence; 4523 } 4524 4525 /** 4526 * Delete an evidence. 4527 * 4528 * @param evidence|int $evidenceorid The evidence, or its ID. 4529 * @return bool 4530 */ 4531 public static function delete_evidence($evidenceorid) { 4532 $evidence = $evidenceorid; 4533 if (!is_object($evidence)) { 4534 $evidence = new evidence($evidenceorid); 4535 } 4536 4537 $uc = new user_competency($evidence->get('usercompetencyid')); 4538 if (!evidence::can_delete_user($uc->get('userid'))) { 4539 throw new required_capability_exception($uc->get_context(), 'moodle/competency:evidencedelete', 'nopermissions', ''); 4540 } 4541 4542 return $evidence->delete(); 4543 } 4544 4545 /** 4546 * Apply the competency rules from a user competency. 4547 * 4548 * The user competency passed should be one that was recently marked as complete. 4549 * A user competency is considered 'complete' when it's proficiency value is true. 4550 * 4551 * This method will check if the parent of this usercompetency's competency has any 4552 * rules and if so will see if they match. When matched it will take the required 4553 * step to add evidence and trigger completion, etc... 4554 * 4555 * @param user_competency $usercompetency The user competency recently completed. 4556 * @param competency|null $competency The competency of the user competency, useful to avoid unnecessary read. 4557 * @return void 4558 */ 4559 protected static function apply_competency_rules_from_usercompetency(user_competency $usercompetency, 4560 competency $competency = null) { 4561 4562 // Perform some basic checks. 4563 if (!$usercompetency->get('proficiency')) { 4564 throw new coding_exception('The user competency passed is not completed.'); 4565 } 4566 if ($competency === null) { 4567 $competency = $usercompetency->get_competency(); 4568 } 4569 if ($competency->get('id') != $usercompetency->get('competencyid')) { 4570 throw new coding_exception('Mismatch between user competency and competency.'); 4571 } 4572 4573 // Fetch the parent. 4574 $parent = $competency->get_parent(); 4575 if ($parent === null) { 4576 return; 4577 } 4578 4579 // The parent should have a rule, and a meaningful outcome. 4580 $ruleoutcome = $parent->get('ruleoutcome'); 4581 if ($ruleoutcome == competency::OUTCOME_NONE) { 4582 return; 4583 } 4584 $rule = $parent->get_rule_object(); 4585 if ($rule === null) { 4586 return; 4587 } 4588 4589 // Fetch or create the user competency for the parent. 4590 $userid = $usercompetency->get('userid'); 4591 $parentuc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $parent->get('id'))); 4592 if (!$parentuc) { 4593 $parentuc = user_competency::create_relation($userid, $parent->get('id')); 4594 $parentuc->create(); 4595 } 4596 4597 // Does the rule match? 4598 if (!$rule->matches($parentuc)) { 4599 return; 4600 } 4601 4602 // Figuring out what to do. 4603 $recommend = false; 4604 if ($ruleoutcome == competency::OUTCOME_EVIDENCE) { 4605 $action = evidence::ACTION_LOG; 4606 4607 } else if ($ruleoutcome == competency::OUTCOME_RECOMMEND) { 4608 $action = evidence::ACTION_LOG; 4609 $recommend = true; 4610 4611 } else if ($ruleoutcome == competency::OUTCOME_COMPLETE) { 4612 $action = evidence::ACTION_COMPLETE; 4613 4614 } else { 4615 throw new moodle_exception('Unexpected rule outcome: ' + $ruleoutcome); 4616 } 4617 4618 // Finally add an evidence. 4619 static::add_evidence( 4620 $userid, 4621 $parent, 4622 $parent->get_context()->id, 4623 $action, 4624 'evidence_competencyrule', 4625 'core_competency', 4626 null, 4627 $recommend 4628 ); 4629 } 4630 4631 /** 4632 * Observe when a course module is marked as completed. 4633 * 4634 * Note that the user being logged in while this happens may be anyone. 4635 * Do not rely on capability checks here! 4636 * 4637 * @param \core\event\course_module_completion_updated $event 4638 * @return void 4639 */ 4640 public static function observe_course_module_completion_updated(\core\event\course_module_completion_updated $event) { 4641 if (!static::is_enabled()) { 4642 return; 4643 } 4644 4645 $eventdata = $event->get_record_snapshot('course_modules_completion', $event->objectid); 4646 4647 if ($eventdata->completionstate == COMPLETION_COMPLETE 4648 || $eventdata->completionstate == COMPLETION_COMPLETE_PASS) { 4649 $coursemodulecompetencies = course_module_competency::list_course_module_competencies($eventdata->coursemoduleid); 4650 4651 $cm = get_coursemodule_from_id(null, $eventdata->coursemoduleid); 4652 $fastmodinfo = get_fast_modinfo($cm->course)->cms[$cm->id]; 4653 4654 $cmname = $fastmodinfo->name; 4655 $url = $fastmodinfo->url; 4656 4657 foreach ($coursemodulecompetencies as $coursemodulecompetency) { 4658 $outcome = $coursemodulecompetency->get('ruleoutcome'); 4659 $action = null; 4660 $recommend = false; 4661 $strdesc = 'evidence_coursemodulecompleted'; 4662 4663 if ($outcome == course_module_competency::OUTCOME_NONE) { 4664 continue; 4665 } 4666 if ($outcome == course_module_competency::OUTCOME_EVIDENCE) { 4667 $action = evidence::ACTION_LOG; 4668 4669 } else if ($outcome == course_module_competency::OUTCOME_RECOMMEND) { 4670 $action = evidence::ACTION_LOG; 4671 $recommend = true; 4672 4673 } else if ($outcome == course_module_competency::OUTCOME_COMPLETE) { 4674 $action = evidence::ACTION_COMPLETE; 4675 4676 } else { 4677 throw new moodle_exception('Unexpected rule outcome: ' + $outcome); 4678 } 4679 4680 static::add_evidence( 4681 $event->relateduserid, 4682 $coursemodulecompetency->get('competencyid'), 4683 $event->contextid, 4684 $action, 4685 $strdesc, 4686 'core_competency', 4687 $cmname, 4688 $recommend, 4689 $url 4690 ); 4691 } 4692 } 4693 } 4694 4695 /** 4696 * Observe when a course is marked as completed. 4697 * 4698 * Note that the user being logged in while this happens may be anyone. 4699 * Do not rely on capability checks here! 4700 * 4701 * @param \core\event\course_completed $event 4702 * @return void 4703 */ 4704 public static function observe_course_completed(\core\event\course_completed $event) { 4705 if (!static::is_enabled()) { 4706 return; 4707 } 4708 4709 $sql = 'courseid = :courseid AND ruleoutcome != :nooutcome'; 4710 $params = array( 4711 'courseid' => $event->courseid, 4712 'nooutcome' => course_competency::OUTCOME_NONE 4713 ); 4714 $coursecompetencies = course_competency::get_records_select($sql, $params); 4715 4716 $course = get_course($event->courseid); 4717 $courseshortname = format_string($course->shortname, null, array('context' => $event->contextid)); 4718 4719 foreach ($coursecompetencies as $coursecompetency) { 4720 4721 $outcome = $coursecompetency->get('ruleoutcome'); 4722 $action = null; 4723 $recommend = false; 4724 $strdesc = 'evidence_coursecompleted'; 4725 4726 if ($outcome == course_module_competency::OUTCOME_NONE) { 4727 continue; 4728 } 4729 if ($outcome == course_competency::OUTCOME_EVIDENCE) { 4730 $action = evidence::ACTION_LOG; 4731 4732 } else if ($outcome == course_competency::OUTCOME_RECOMMEND) { 4733 $action = evidence::ACTION_LOG; 4734 $recommend = true; 4735 4736 } else if ($outcome == course_competency::OUTCOME_COMPLETE) { 4737 $action = evidence::ACTION_COMPLETE; 4738 4739 } else { 4740 throw new moodle_exception('Unexpected rule outcome: ' + $outcome); 4741 } 4742 4743 static::add_evidence( 4744 $event->relateduserid, 4745 $coursecompetency->get('competencyid'), 4746 $event->contextid, 4747 $action, 4748 $strdesc, 4749 'core_competency', 4750 $courseshortname, 4751 $recommend, 4752 $event->get_url() 4753 ); 4754 } 4755 } 4756 4757 /** 4758 * Action to perform when a course module is deleted. 4759 * 4760 * Do not call this directly, this is reserved for core use. 4761 * 4762 * @param stdClass $cm The CM object. 4763 * @return void 4764 */ 4765 public static function hook_course_module_deleted(stdClass $cm) { 4766 global $DB; 4767 $DB->delete_records(course_module_competency::TABLE, array('cmid' => $cm->id)); 4768 } 4769 4770 /** 4771 * Action to perform when a course is deleted. 4772 * 4773 * Do not call this directly, this is reserved for core use. 4774 * 4775 * @param stdClass $course The course object. 4776 * @return void 4777 */ 4778 public static function hook_course_deleted(stdClass $course) { 4779 global $DB; 4780 $DB->delete_records(course_competency::TABLE, array('courseid' => $course->id)); 4781 $DB->delete_records(course_competency_settings::TABLE, array('courseid' => $course->id)); 4782 $DB->delete_records(user_competency_course::TABLE, array('courseid' => $course->id)); 4783 } 4784 4785 /** 4786 * Action to perform when a course is being reset. 4787 * 4788 * Do not call this directly, this is reserved for core use. 4789 * 4790 * @param int $courseid The course ID. 4791 * @return void 4792 */ 4793 public static function hook_course_reset_competency_ratings($courseid) { 4794 global $DB; 4795 $DB->delete_records(user_competency_course::TABLE, array('courseid' => $courseid)); 4796 } 4797 4798 /** 4799 * Action to perform when a cohort is deleted. 4800 * 4801 * Do not call this directly, this is reserved for core use. 4802 * 4803 * @param \stdClass $cohort The cohort object. 4804 * @return void 4805 */ 4806 public static function hook_cohort_deleted(\stdClass $cohort) { 4807 global $DB; 4808 $DB->delete_records(template_cohort::TABLE, array('cohortid' => $cohort->id)); 4809 } 4810 4811 /** 4812 * Action to perform when a user is deleted. 4813 * 4814 * @param int $userid The user id. 4815 */ 4816 public static function hook_user_deleted($userid) { 4817 global $DB; 4818 4819 $usercompetencies = $DB->get_records(user_competency::TABLE, ['userid' => $userid], '', 'id'); 4820 foreach ($usercompetencies as $usercomp) { 4821 $DB->delete_records(evidence::TABLE, ['usercompetencyid' => $usercomp->id]); 4822 } 4823 4824 $DB->delete_records(user_competency::TABLE, ['userid' => $userid]); 4825 $DB->delete_records(user_competency_course::TABLE, ['userid' => $userid]); 4826 $DB->delete_records(user_competency_plan::TABLE, ['userid' => $userid]); 4827 4828 // Delete any associated files. 4829 $fs = get_file_storage(); 4830 $context = context_user::instance($userid); 4831 $userevidences = $DB->get_records(user_evidence::TABLE, ['userid' => $userid], '', 'id'); 4832 foreach ($userevidences as $userevidence) { 4833 $DB->delete_records(user_evidence_competency::TABLE, ['userevidenceid' => $userevidence->id]); 4834 $DB->delete_records(user_evidence::TABLE, ['id' => $userevidence->id]); 4835 $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $userevidence->id); 4836 } 4837 4838 $userplans = $DB->get_records(plan::TABLE, ['userid' => $userid], '', 'id'); 4839 foreach ($userplans as $userplan) { 4840 $DB->delete_records(plan_competency::TABLE, ['planid' => $userplan->id]); 4841 $DB->delete_records(plan::TABLE, ['id' => $userplan->id]); 4842 } 4843 } 4844 4845 /** 4846 * Manually grade a user competency. 4847 * 4848 * @param int $userid 4849 * @param int $competencyid 4850 * @param int $grade 4851 * @param string $note A note to attach to the evidence 4852 * @return array of \core_competency\user_competency 4853 */ 4854 public static function grade_competency($userid, $competencyid, $grade, $note = null) { 4855 global $USER; 4856 static::require_enabled(); 4857 4858 $uc = static::get_user_competency($userid, $competencyid); 4859 $context = $uc->get_context(); 4860 if (!user_competency::can_grade_user($uc->get('userid'))) { 4861 throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', ''); 4862 } 4863 4864 // Throws exception if competency not in plan. 4865 $competency = $uc->get_competency(); 4866 $competencycontext = $competency->get_context(); 4867 if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), 4868 $competencycontext)) { 4869 throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', ''); 4870 } 4871 4872 $action = evidence::ACTION_OVERRIDE; 4873 $desckey = 'evidence_manualoverride'; 4874 4875 $result = self::add_evidence($uc->get('userid'), 4876 $competency, 4877 $context->id, 4878 $action, 4879 $desckey, 4880 'core_competency', 4881 null, 4882 false, 4883 null, 4884 $grade, 4885 $USER->id, 4886 $note); 4887 if ($result) { 4888 $uc->read(); 4889 $event = \core\event\competency_user_competency_rated::create_from_user_competency($uc); 4890 $event->trigger(); 4891 } 4892 return $result; 4893 } 4894 4895 /** 4896 * Manually grade a user competency from the plans page. 4897 * 4898 * @param mixed $planorid 4899 * @param int $competencyid 4900 * @param int $grade 4901 * @param string $note A note to attach to the evidence 4902 * @return array of \core_competency\user_competency 4903 */ 4904 public static function grade_competency_in_plan($planorid, $competencyid, $grade, $note = null) { 4905 global $USER; 4906 static::require_enabled(); 4907 4908 $plan = $planorid; 4909 if (!is_object($planorid)) { 4910 $plan = new plan($planorid); 4911 } 4912 4913 $context = $plan->get_context(); 4914 if (!user_competency::can_grade_user($plan->get('userid'))) { 4915 throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', ''); 4916 } 4917 4918 // Throws exception if competency not in plan. 4919 $competency = $plan->get_competency($competencyid); 4920 $competencycontext = $competency->get_context(); 4921 if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), 4922 $competencycontext)) { 4923 throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', ''); 4924 } 4925 4926 $action = evidence::ACTION_OVERRIDE; 4927 $desckey = 'evidence_manualoverrideinplan'; 4928 4929 $result = self::add_evidence($plan->get('userid'), 4930 $competency, 4931 $context->id, 4932 $action, 4933 $desckey, 4934 'core_competency', 4935 $plan->get('name'), 4936 false, 4937 null, 4938 $grade, 4939 $USER->id, 4940 $note); 4941 if ($result) { 4942 $uc = static::get_user_competency($plan->get('userid'), $competency->get('id')); 4943 $event = \core\event\competency_user_competency_rated_in_plan::create_from_user_competency($uc, $plan->get('id')); 4944 $event->trigger(); 4945 } 4946 return $result; 4947 } 4948 4949 /** 4950 * Manually grade a user course competency from the course page. 4951 * 4952 * This may push the rating to the user competency 4953 * if the course is configured this way. 4954 * 4955 * @param mixed $courseorid 4956 * @param int $userid 4957 * @param int $competencyid 4958 * @param int $grade 4959 * @param string $note A note to attach to the evidence 4960 * @return array of \core_competency\user_competency 4961 */ 4962 public static function grade_competency_in_course($courseorid, $userid, $competencyid, $grade, $note = null) { 4963 global $USER, $DB; 4964 static::require_enabled(); 4965 4966 $course = $courseorid; 4967 if (!is_object($courseorid)) { 4968 $course = $DB->get_record('course', array('id' => $courseorid)); 4969 } 4970 $context = context_course::instance($course->id); 4971 4972 // Check that we can view the user competency details in the course. 4973 if (!user_competency::can_read_user_in_course($userid, $course->id)) { 4974 throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', ''); 4975 } 4976 4977 // Validate the permission to grade. 4978 if (!user_competency::can_grade_user_in_course($userid, $course->id)) { 4979 throw new required_capability_exception($context, 'moodle/competency:competencygrade', 'nopermissions', ''); 4980 } 4981 4982 // Check that competency is in course and visible to the current user. 4983 $competency = course_competency::get_competency($course->id, $competencyid); 4984 $competencycontext = $competency->get_context(); 4985 if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), 4986 $competencycontext)) { 4987 throw new required_capability_exception($competencycontext, 'moodle/competency:competencyview', 'nopermissions', ''); 4988 } 4989 4990 // Check that the user is enrolled in the course, and is "gradable". 4991 if (!is_enrolled($context, $userid, 'moodle/competency:coursecompetencygradable')) { 4992 throw new coding_exception('The competency may not be rated at this time.'); 4993 } 4994 4995 $action = evidence::ACTION_OVERRIDE; 4996 $desckey = 'evidence_manualoverrideincourse'; 4997 4998 $result = self::add_evidence($userid, 4999 $competency, 5000 $context->id, 5001 $action, 5002 $desckey, 5003 'core_competency', 5004 $context->get_context_name(), 5005 false, 5006 null, 5007 $grade, 5008 $USER->id, 5009 $note); 5010 if ($result) { 5011 $all = user_competency_course::get_multiple($userid, $course->id, array($competency->get('id'))); 5012 $uc = reset($all); 5013 $event = \core\event\competency_user_competency_rated_in_course::create_from_user_competency_course($uc); 5014 $event->trigger(); 5015 } 5016 return $result; 5017 } 5018 5019 /** 5020 * Count the plans in the template, filtered by status. 5021 * 5022 * Requires moodle/competency:templateview capability at the system context. 5023 * 5024 * @param mixed $templateorid The id or the template. 5025 * @param int $status One of the plan status constants (or 0 for all plans). 5026 * @return int 5027 */ 5028 public static function count_plans_for_template($templateorid, $status = 0) { 5029 static::require_enabled(); 5030 $template = $templateorid; 5031 if (!is_object($template)) { 5032 $template = new template($template); 5033 } 5034 5035 // First we do a permissions check. 5036 if (!$template->can_read()) { 5037 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', 5038 'nopermissions', ''); 5039 } 5040 5041 return plan::count_records_for_template($template->get('id'), $status); 5042 } 5043 5044 /** 5045 * Count the user-completency-plans in the template, optionally filtered by proficiency. 5046 * 5047 * Requires moodle/competency:templateview capability at the system context. 5048 * 5049 * @param mixed $templateorid The id or the template. 5050 * @param mixed $proficiency If true, filter by proficiency, if false filter by not proficient, if null - no filter. 5051 * @return int 5052 */ 5053 public static function count_user_competency_plans_for_template($templateorid, $proficiency = null) { 5054 static::require_enabled(); 5055 $template = $templateorid; 5056 if (!is_object($template)) { 5057 $template = new template($template); 5058 } 5059 5060 // First we do a permissions check. 5061 if (!$template->can_read()) { 5062 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', 5063 'nopermissions', ''); 5064 } 5065 5066 return user_competency_plan::count_records_for_template($template->get('id'), $proficiency); 5067 } 5068 5069 /** 5070 * List the plans in the template, filtered by status. 5071 * 5072 * Requires moodle/competency:templateview capability at the system context. 5073 * 5074 * @param mixed $templateorid The id or the template. 5075 * @param int $status One of the plan status constants (or 0 for all plans). 5076 * @param int $skip The number of records to skip 5077 * @param int $limit The max number of records to return 5078 * @return plan[] 5079 */ 5080 public static function list_plans_for_template($templateorid, $status = 0, $skip = 0, $limit = 100) { 5081 $template = $templateorid; 5082 if (!is_object($template)) { 5083 $template = new template($template); 5084 } 5085 5086 // First we do a permissions check. 5087 if (!$template->can_read()) { 5088 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', 5089 'nopermissions', ''); 5090 } 5091 5092 return plan::get_records_for_template($template->get('id'), $status, $skip, $limit); 5093 } 5094 5095 /** 5096 * Get the most often not completed competency for this course. 5097 * 5098 * Requires moodle/competency:coursecompetencyview capability at the course context. 5099 * 5100 * @param int $courseid The course id 5101 * @param int $skip The number of records to skip 5102 * @param int $limit The max number of records to return 5103 * @return competency[] 5104 */ 5105 public static function get_least_proficient_competencies_for_course($courseid, $skip = 0, $limit = 100) { 5106 static::require_enabled(); 5107 $coursecontext = context_course::instance($courseid); 5108 5109 if (!has_any_capability(array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'), 5110 $coursecontext)) { 5111 throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', 'nopermissions', ''); 5112 } 5113 5114 return user_competency_course::get_least_proficient_competencies_for_course($courseid, $skip, $limit); 5115 } 5116 5117 /** 5118 * Get the most often not completed competency for this template. 5119 * 5120 * Requires moodle/competency:templateview capability at the system context. 5121 * 5122 * @param mixed $templateorid The id or the template. 5123 * @param int $skip The number of records to skip 5124 * @param int $limit The max number of records to return 5125 * @return competency[] 5126 */ 5127 public static function get_least_proficient_competencies_for_template($templateorid, $skip = 0, $limit = 100) { 5128 static::require_enabled(); 5129 $template = $templateorid; 5130 if (!is_object($template)) { 5131 $template = new template($template); 5132 } 5133 5134 // First we do a permissions check. 5135 if (!$template->can_read()) { 5136 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', 5137 'nopermissions', ''); 5138 } 5139 5140 return user_competency_plan::get_least_proficient_competencies_for_template($template->get('id'), $skip, $limit); 5141 } 5142 5143 /** 5144 * Template event viewed. 5145 * 5146 * Requires moodle/competency:templateview capability at the system context. 5147 * 5148 * @param mixed $templateorid The id or the template. 5149 * @return boolean 5150 */ 5151 public static function template_viewed($templateorid) { 5152 static::require_enabled(); 5153 $template = $templateorid; 5154 if (!is_object($template)) { 5155 $template = new template($template); 5156 } 5157 5158 // First we do a permissions check. 5159 if (!$template->can_read()) { 5160 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview', 5161 'nopermissions', ''); 5162 } 5163 5164 // Trigger a template viewed event. 5165 \core\event\competency_template_viewed::create_from_template($template)->trigger(); 5166 5167 return true; 5168 } 5169 5170 /** 5171 * Get the competency settings for a course. 5172 * 5173 * Requires moodle/competency:coursecompetencyview capability at the course context. 5174 * 5175 * @param int $courseid The course id 5176 * @return course_competency_settings 5177 */ 5178 public static function read_course_competency_settings($courseid) { 5179 static::require_enabled(); 5180 5181 // First we do a permissions check. 5182 if (!course_competency_settings::can_read($courseid)) { 5183 $context = context_course::instance($courseid); 5184 throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', ''); 5185 } 5186 5187 return course_competency_settings::get_by_courseid($courseid); 5188 } 5189 5190 /** 5191 * Update the competency settings for a course. 5192 * 5193 * Requires moodle/competency:coursecompetencyconfigure capability at the course context. 5194 * 5195 * @param int $courseid The course id 5196 * @param stdClass $settings List of settings. The only valid setting ATM is pushratginstouserplans (boolean). 5197 * @return bool 5198 */ 5199 public static function update_course_competency_settings($courseid, $settings) { 5200 static::require_enabled(); 5201 5202 $settings = (object) $settings; 5203 5204 // Get all the valid settings. 5205 $pushratingstouserplans = isset($settings->pushratingstouserplans) ? $settings->pushratingstouserplans : false; 5206 5207 // First we do a permissions check. 5208 if (!course_competency_settings::can_manage_course($courseid)) { 5209 $context = context_course::instance($courseid); 5210 throw new required_capability_exception($context, 'moodle/competency:coursecompetencyconfigure', 'nopermissions', ''); 5211 } 5212 5213 $exists = course_competency_settings::get_record(array('courseid' => $courseid)); 5214 5215 // Now update or insert. 5216 if ($exists) { 5217 $settings = $exists; 5218 $settings->set('pushratingstouserplans', $pushratingstouserplans); 5219 return $settings->update(); 5220 } else { 5221 $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $pushratingstouserplans); 5222 $settings = new course_competency_settings(0, $data); 5223 $result = $settings->create(); 5224 return !empty($result); 5225 } 5226 } 5227 5228 5229 /** 5230 * Function used to return a list of users where the given user has a particular capability. 5231 * 5232 * This is used e.g. to find all the users where someone is able to manage their learning plans, 5233 * it also would be useful for mentees etc. 5234 * 5235 * @param string $capability - The capability string we are filtering for. If '' is passed, 5236 * an always matching filter is returned. 5237 * @param int $userid - The user id we are using for the access checks. Defaults to current user. 5238 * @param int $type - The type of named params to return (passed to $DB->get_in_or_equal). 5239 * @param string $prefix - The type prefix for the db table (passed to $DB->get_in_or_equal). 5240 * @return list($sql, $params) Same as $DB->get_in_or_equal(). 5241 * @todo MDL-52243 Move this function to lib/accesslib.php 5242 */ 5243 public static function filter_users_with_capability_on_user_context_sql($capability, $userid = 0, $type = SQL_PARAMS_QM, 5244 $prefix='param') { 5245 5246 global $USER, $DB; 5247 $allresultsfilter = array('> 0', array()); 5248 $noresultsfilter = array('= -1', array()); 5249 5250 if (empty($capability)) { 5251 return $allresultsfilter; 5252 } 5253 5254 if (!$capinfo = get_capability_info($capability)) { 5255 throw new coding_exception('Capability does not exist: ' . $capability); 5256 } 5257 5258 if (empty($userid)) { 5259 $userid = $USER->id; 5260 } 5261 5262 // Make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are. 5263 if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) { 5264 if (isguestuser($userid) or $userid == 0) { 5265 return $noresultsfilter; 5266 } 5267 } 5268 5269 if (is_siteadmin($userid)) { 5270 // No filtering for site admins. 5271 return $allresultsfilter; 5272 } 5273 5274 // Check capability on system level. 5275 $syscontext = context_system::instance(); 5276 $hassystem = has_capability($capability, $syscontext, $userid); 5277 5278 $access = get_user_roles_sitewide_accessdata($userid); 5279 // Build up a list of level 2 contexts (candidates to be user context). 5280 $filtercontexts = array(); 5281 // Build list of roles to check overrides. 5282 $roles = array(); 5283 5284 foreach ($access['ra'] as $path => $role) { 5285 $parts = explode('/', $path); 5286 if (count($parts) == 3) { 5287 $filtercontexts[$parts[2]] = $parts[2]; 5288 } else if (count($parts) > 3) { 5289 // We know this is not a user context because there is another path with more than 2 levels. 5290 unset($filtercontexts[$parts[2]]); 5291 } 5292 $roles = array_merge($roles, $role); 5293 } 5294 5295 // Add all contexts in which a role may be overidden. 5296 $rdefs = get_role_definitions($roles); 5297 foreach ($rdefs as $roledef) { 5298 foreach ($roledef as $path => $caps) { 5299 if (!isset($caps[$capability])) { 5300 // The capability is not mentioned, we can ignore. 5301 continue; 5302 } 5303 $parts = explode('/', $path); 5304 if (count($parts) === 3) { 5305 // Only get potential user contexts, they only ever have 2 slashes /parentId/Id. 5306 $filtercontexts[$parts[2]] = $parts[2]; 5307 } 5308 } 5309 } 5310 5311 // No interesting contexts - return all or no results. 5312 if (empty($filtercontexts)) { 5313 if ($hassystem) { 5314 return $allresultsfilter; 5315 } else { 5316 return $noresultsfilter; 5317 } 5318 } 5319 // Fetch all interesting contexts for further examination. 5320 list($insql, $params) = $DB->get_in_or_equal($filtercontexts, SQL_PARAMS_NAMED); 5321 $params['level'] = CONTEXT_USER; 5322 $fields = context_helper::get_preload_record_columns_sql('ctx'); 5323 $interestingcontexts = $DB->get_recordset_sql('SELECT ' . $fields . ' 5324 FROM {context} ctx 5325 WHERE ctx.contextlevel = :level 5326 AND ctx.id ' . $insql . ' 5327 ORDER BY ctx.id', $params); 5328 if ($hassystem) { 5329 // If allowed at system, search for exceptions prohibiting the capability at user context. 5330 $excludeusers = array(); 5331 foreach ($interestingcontexts as $contextrecord) { 5332 $candidateuserid = $contextrecord->ctxinstance; 5333 context_helper::preload_from_record($contextrecord); 5334 $usercontext = context_user::instance($candidateuserid); 5335 // Has capability should use the data already preloaded. 5336 if (!has_capability($capability, $usercontext, $userid)) { 5337 $excludeusers[$candidateuserid] = $candidateuserid; 5338 } 5339 } 5340 5341 // Construct SQL excluding users with this role assigned for this user. 5342 if (empty($excludeusers)) { 5343 $interestingcontexts->close(); 5344 return $allresultsfilter; 5345 } 5346 list($sql, $params) = $DB->get_in_or_equal($excludeusers, $type, $prefix, false); 5347 } else { 5348 // If not allowed at system, search for exceptions allowing the capability at user context. 5349 $allowusers = array(); 5350 foreach ($interestingcontexts as $contextrecord) { 5351 $candidateuserid = $contextrecord->ctxinstance; 5352 context_helper::preload_from_record($contextrecord); 5353 $usercontext = context_user::instance($candidateuserid); 5354 // Has capability should use the data already preloaded. 5355 if (has_capability($capability, $usercontext, $userid)) { 5356 $allowusers[$candidateuserid] = $candidateuserid; 5357 } 5358 } 5359 5360 // Construct SQL excluding users with this role assigned for this user. 5361 if (empty($allowusers)) { 5362 $interestingcontexts->close(); 5363 return $noresultsfilter; 5364 } 5365 list($sql, $params) = $DB->get_in_or_equal($allowusers, $type, $prefix); 5366 } 5367 $interestingcontexts->close(); 5368 5369 // Return the goods!. 5370 return array($sql, $params); 5371 } 5372 5373 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body