MDL-67413 competencies: do nothing upon activity completion
[moodle.git] / competency / classes / api.php
CommitLineData
d9a39950
DW
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 *
767f66c0 20 * @package core_competency
d9a39950
DW
21 * @copyright 2015 Damyon Wiese
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
767f66c0 24namespace core_competency;
dd1ce763 25defined('MOODLE_INTERNAL') || die();
d9a39950
DW
26
27use stdClass;
767f66c0 28use cm_info;
2de75345
FM
29use context;
30use context_helper;
d9a39950
DW
31use context_system;
32use context_course;
db650737 33use context_module;
4db373d5 34use context_user;
d9a39950 35use coding_exception;
d4c0a2f6 36use require_login_exception;
914b580e 37use moodle_exception;
cc8348db 38use moodle_url;
d9a39950
DW
39use 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 */
47class api {
48
6bdaf204
DW
49 /** @var boolean Allow api functions even if competencies are not enabled for the site. */
50 private static $skipenabled = false;
51
338386aa
FM
52 /**
53 * Returns whether competencies are enabled.
54 *
5592edb6
FM
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 *
338386aa
FM
59 * @return boolean True when enabled.
60 */
61 public static function is_enabled() {
6bdaf204
DW
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;
338386aa
FM
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()) {
6be1373e 96 throw new moodle_exception('competenciesarenotenabled', 'core_competency');
338386aa
FM
97 }
98 }
99
4e89144c
FM
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
f446b2e1
DW
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
d660824b
FM
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
d9a39950
DW
178 /**
179 * Create a competency from a record containing all the data for the class.
180 *
b90e2205 181 * Requires moodle/competency:competencymanage capability at the system context.
d9a39950
DW
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) {
338386aa 187 static::require_enabled();
2de75345
FM
188 $competency = new competency(0, $record);
189
d9a39950 190 // First we do a permissions check.
b90e2205 191 require_capability('moodle/competency:competencymanage', $competency->get_context());
d9a39950 192
14199b85 193 // Reset the sortorder, use reorder instead.
f9bb74c0 194 $competency->set('sortorder', 0);
20c2fe3e
FM
195 $competency->create();
196
5a1a685c 197 \core\event\competency_created::create_from_competency($competency)->trigger();
d32d64bc 198
20c2fe3e
FM
199 // Reset the rule of the parent.
200 $parent = $competency->get_parent();
201 if ($parent) {
202 $parent->reset_rule();
203 $parent->update();
204 }
14199b85 205
d9a39950
DW
206 return $competency;
207 }
208
209 /**
210 * Delete a competency by id.
211 *
b90e2205 212 * Requires moodle/competency:competencymanage capability at the system context.
d9a39950
DW
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) {
9a325eda 218 global $DB;
338386aa 219 static::require_enabled();
2de75345
FM
220 $competency = new competency($id);
221
d9a39950 222 // First we do a permissions check.
b90e2205 223 require_capability('moodle/competency:competencymanage', $competency->get_context());
d9a39950 224
d32d64bc 225 $events = array();
9c91a959 226 $competencyids = array(intval($competency->get('id')));
d32d64bc 227 $contextid = $competency->get_context()->id;
9a325eda
IT
228 $competencyids = array_merge(competency::get_descendants_ids($competency), $competencyids);
229 if (!competency::can_all_be_deleted($competencyids)) {
230 return false;
20c2fe3e 231 }
9a325eda
IT
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);
20c2fe3e 251
9a325eda
IT
252 // Delete competency evidences.
253 user_evidence_competency::delete_by_competencyids($competencyids);
254
d32d64bc 255 // Register the competencies deleted events.
5a1a685c 256 $events = \core\event\competency_deleted::create_multiple_from_competencyids($competencyids, $contextid);
9a325eda
IT
257
258 } catch (\Exception $e) {
259 $transaction->rollback($e);
260 }
d32d64bc
IT
261
262 $transaction->allow_commit();
263 // Trigger events.
264 foreach ($events as $event) {
265 $event->trigger();
266 }
267
268 return true;
d9a39950
DW
269 }
270
271 /**
272 * Reorder this competency.
273 *
b90e2205 274 * Requires moodle/competency:competencymanage capability at the system context.
d9a39950
DW
275 *
276 * @param int $id The id of the competency to move.
277 * @return boolean
278 */
279 public static function move_down_competency($id) {
338386aa 280 static::require_enabled();
d9a39950
DW
281 $current = new competency($id);
282
2de75345 283 // First we do a permissions check.
b90e2205 284 require_capability('moodle/competency:competencymanage', $current->get_context());
2de75345 285
9c91a959
DW
286 $max = self::count_competencies(array('parentid' => $current->get('parentid'),
287 'competencyframeworkid' => $current->get('competencyframeworkid')));
d9a39950
DW
288 if ($max > 0) {
289 $max--;
290 }
291
9c91a959 292 $sortorder = $current->get('sortorder');
d9a39950
DW
293 if ($sortorder >= $max) {
294 return false;
295 }
296 $sortorder = $sortorder + 1;
9c91a959 297 $current->set('sortorder', $sortorder);
d9a39950 298
9c91a959
DW
299 $filters = array('parentid' => $current->get('parentid'),
300 'competencyframeworkid' => $current->get('competencyframeworkid'),
d9a39950
DW
301 'sortorder' => $sortorder);
302 $children = self::list_competencies($filters, 'id');
303 foreach ($children as $needtoswap) {
9c91a959 304 $needtoswap->set('sortorder', $sortorder - 1);
d9a39950
DW
305 $needtoswap->update();
306 }
307
308 // OK - all set.
d32d64bc 309 $result = $current->update();
aec5363e 310
d32d64bc 311 return $result;
d9a39950
DW
312 }
313
314 /**
315 * Reorder this competency.
316 *
b90e2205 317 * Requires moodle/competency:competencymanage capability at the system context.
d9a39950
DW
318 *
319 * @param int $id The id of the competency to move.
320 * @return boolean
321 */
322 public static function move_up_competency($id) {
338386aa 323 static::require_enabled();
d9a39950
DW
324 $current = new competency($id);
325
2de75345 326 // First we do a permissions check.
b90e2205 327 require_capability('moodle/competency:competencymanage', $current->get_context());
2de75345 328
9c91a959 329 $sortorder = $current->get('sortorder');
d9a39950
DW
330 if ($sortorder == 0) {
331 return false;
332 }
333
334 $sortorder = $sortorder - 1;
9c91a959 335 $current->set('sortorder', $sortorder);
d9a39950 336
9c91a959
DW
337 $filters = array('parentid' => $current->get('parentid'),
338 'competencyframeworkid' => $current->get('competencyframeworkid'),
d9a39950
DW
339 'sortorder' => $sortorder);
340 $children = self::list_competencies($filters, 'id');
341 foreach ($children as $needtoswap) {
9c91a959 342 $needtoswap->set('sortorder', $sortorder + 1);
d9a39950
DW
343 $needtoswap->update();
344 }
345
346 // OK - all set.
d32d64bc
IT
347 $result = $current->update();
348
349 return $result;
d9a39950
DW
350 }
351
352 /**
353 * Move this competency so it sits in a new parent.
354 *
b90e2205 355 * Requires moodle/competency:competencymanage capability at the system context.
d9a39950
DW
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) {
20c2fe3e 362 global $DB;
338386aa 363 static::require_enabled();
2de75345
FM
364 $current = new competency($id);
365
d9a39950 366 // First we do a permissions check.
b90e2205 367 require_capability('moodle/competency:competencymanage', $current->get_context());
20c2fe3e
FM
368 if ($id == $newparentid) {
369 throw new coding_exception('Can not set a competency as a parent of itself.');
9c91a959 370 } if ($newparentid == $current->get('parentid')) {
20c2fe3e 371 throw new coding_exception('Can not move a competency to the same location.');
d9a39950
DW
372 }
373
20c2fe3e
FM
374 // Some great variable assignment right here.
375 $currentparent = $current->get_parent();
376 $parent = !empty($newparentid) ? new competency($newparentid) : null;
9c91a959 377 $parentpath = !empty($parent) ? $parent->get('path') : '/0/';
20c2fe3e
FM
378
379 // We're going to change quite a few things.
380 $transaction = $DB->start_delegated_transaction();
d9a39950 381
20c2fe3e
FM
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.
d9a39950 386 $newparents = explode('/', $parentpath);
9c91a959
DW
387 if (in_array($current->get('id'), $newparents)) {
388 $children = competency::get_records(array('parentid' => $current->get('id')), 'id');
d9a39950 389 foreach ($children as $child) {
9c91a959 390 $child->set('parentid', $current->get('parentid'));
d9a39950
DW
391 $child->update();
392 }
20c2fe3e
FM
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();
d9a39950
DW
399 }
400
20c2fe3e
FM
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.
9c91a959 412 $current->set('parentid', $newparentid);
d32d64bc 413 $result = $current->update();
d9a39950 414
20c2fe3e
FM
415 // All right, let's commit this.
416 $transaction->allow_commit();
417
d32d64bc 418 return $result;
d9a39950
DW
419 }
420
421 /**
422 * Update the details for a competency.
423 *
b90e2205 424 * Requires moodle/competency:competencymanage capability at the system context.
d9a39950 425 *
b17d3d10
AA
426 * @param stdClass $record The new details for the competency.
427 * Note - must contain an id that points to the competency to update.
428 *
d9a39950
DW
429 * @return boolean
430 */
431 public static function update_competency($record) {
338386aa 432 static::require_enabled();
1896274f 433 $competency = new competency($record->id);
2de75345 434
d9a39950 435 // First we do a permissions check.
b90e2205 436 require_capability('moodle/competency:competencymanage', $competency->get_context());
d9a39950
DW
437
438 // Some things should not be changed in an update - they should use a more specific method.
9c91a959
DW
439 $record->sortorder = $competency->get('sortorder');
440 $record->parentid = $competency->get('parentid');
441 $record->competencyframeworkid = $competency->get('competencyframeworkid');
d9a39950 442
1896274f 443 $competency->from_record($record);
b90e2205 444 require_capability('moodle/competency:competencymanage', $competency->get_context());
2de75345
FM
445
446 // OK - all set.
d32d64bc
IT
447 $result = $competency->update();
448
449 // Trigger the update event.
5a1a685c 450 \core\event\competency_updated::create_from_competency($competency)->trigger();
d32d64bc
IT
451
452 return $result;
d9a39950
DW
453 }
454
455 /**
456 * Read a the details for a single competency and return a record.
457 *
b90e2205 458 * Requires moodle/competency:competencyview capability at the system context.
d9a39950
DW
459 *
460 * @param int $id The id of the competency to read.
4f815459 461 * @param bool $includerelated Include related tags or not.
d9a39950
DW
462 * @return stdClass
463 */
4f815459 464 public static function read_competency($id, $includerelated = false) {
338386aa 465 static::require_enabled();
2de75345
FM
466 $competency = new competency($id);
467
d9a39950 468 // First we do a permissions check.
a214d35e 469 $context = $competency->get_context();
b90e2205
FM
470 if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
471 throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
d9a39950
DW
472 }
473
474 // OK - all set.
4f815459
DM
475 if ($includerelated) {
476 $relatedcompetency = new related_competency();
477 if ($related = $relatedcompetency->list_relations($id)) {
478 $competency->relatedcompetencies = $related;
479 }
480 }
481
2de75345 482 return $competency;
d9a39950
DW
483 }
484
485 /**
486 * Perform a text search based and return all results and their parents.
487 *
b90e2205 488 * Requires moodle/competency:competencyview capability at the framework context.
d9a39950
DW
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 */
aec5363e 494 public static function search_competencies($textsearch, $competencyframeworkid) {
338386aa 495 static::require_enabled();
2de75345
FM
496 $framework = new competency_framework($competencyframeworkid);
497
d9a39950 498 // First we do a permissions check.
2de75345 499 $context = $framework->get_context();
b90e2205
FM
500 if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
501 throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
d9a39950
DW
502 }
503
504 // OK - all set.
4f815459 505 $competencies = competency::search($textsearch, $competencyframeworkid);
4f815459 506 return $competencies;
d9a39950
DW
507 }
508
509 /**
510 * Perform a search based on the provided filters and return a paginated list of records.
511 *
b90e2205 512 * Requires moodle/competency:competencyview capability at some context.
d9a39950
DW
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 */
e90c24d0 521 public static function list_competencies($filters, $sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
338386aa 522 static::require_enabled();
2de75345
FM
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
d9a39950 530 // First we do a permissions check.
b90e2205
FM
531 if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
532 throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
d9a39950
DW
533 }
534
535 // OK - all set.
c27113d9 536 return competency::get_records($filters, $sort, $order, $skip, $limit);
d9a39950
DW
537 }
538
539 /**
540 * Perform a search based on the provided filters and return a paginated list of records.
541 *
b90e2205 542 * Requires moodle/competency:competencyview capability at some context.
d9a39950
DW
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) {
338386aa 548 static::require_enabled();
2de75345
FM
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
d9a39950 556 // First we do a permissions check.
b90e2205
FM
557 if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
558 throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
d9a39950
DW
559 }
560
561 // OK - all set.
c27113d9 562 return competency::count_records($filters);
d9a39950
DW
563 }
564
565 /**
566 * Create a competency framework from a record containing all the data for the class.
567 *
b90e2205 568 * Requires moodle/competency:competencymanage capability at the system context.
d9a39950
DW
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) {
338386aa 574 static::require_enabled();
d9a39950 575 $framework = new competency_framework(0, $record);
b90e2205 576 require_capability('moodle/competency:competencymanage', $framework->get_context());
a54d5b04
FM
577
578 // Account for different formats of taxonomies.
579 if (isset($record->taxonomies)) {
9c91a959 580 $framework->set('taxonomies', $record->taxonomies);
a54d5b04
FM
581 }
582
a3acafa0
SG
583 $framework = $framework->create();
584
585 // Trigger a competency framework created event.
5a1a685c 586 \core\event\competency_framework_created::create_from_framework($framework)->trigger();
a3acafa0 587
d9a39950
DW
588 return $framework;
589 }
590
c61db7bb
IT
591 /**
592 * Duplicate a competency framework by id.
593 *
b90e2205 594 * Requires moodle/competency:competencymanage capability at the system context.
c61db7bb
IT
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) {
4c4a8d41 600 global $DB;
338386aa 601 static::require_enabled();
4c4a8d41 602
c61db7bb 603 $framework = new competency_framework($id);
b90e2205 604 require_capability('moodle/competency:competencymanage', $framework->get_context());
4c4a8d41
IT
605 // Starting transaction.
606 $transaction = $DB->start_delegated_transaction();
c61db7bb 607
4c4a8d41
IT
608 try {
609 // Get a uniq idnumber based on the origin framework.
9c91a959
DW
610 $idnumber = competency_framework::get_unused_idnumber($framework->get('idnumber'));
611 $framework->set('idnumber', $idnumber);
4c4a8d41 612 // Adding the suffix copy to the shortname.
9c91a959
DW
613 $framework->set('shortname', get_string('duplicateditemname', 'core_competency', $framework->get('shortname')));
614 $framework->set('id', 0);
a3acafa0 615 $framework = $framework->create();
4c4a8d41
IT
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);
9c91a959 619 $matchids = self::duplicate_competency_tree($framework->get('id'), $frameworkcompetency, 0, 0);
4c4a8d41
IT
620
621 // Copy the related competencies.
622 $relcomps = related_competency::get_multiple_relations(array_keys($matchids));
623
624 foreach ($relcomps as $relcomp) {
9c91a959
DW
625 $compid = $relcomp->get('competencyid');
626 $relcompid = $relcomp->get('relatedcompetencyid');
4c4a8d41 627 if (isset($matchids[$compid]) && isset($matchids[$relcompid])) {
9c91a959
DW
628 $newcompid = $matchids[$compid]->get('id');
629 $newrelcompid = $matchids[$relcompid]->get('id');
4c4a8d41 630 if ($newcompid < $newrelcompid) {
9c91a959
DW
631 $relcomp->set('competencyid', $newcompid);
632 $relcomp->set('relatedcompetencyid', $newrelcompid);
4c4a8d41 633 } else {
9c91a959
DW
634 $relcomp->set('competencyid', $newrelcompid);
635 $relcomp->set('relatedcompetencyid', $newcompid);
4c4a8d41 636 }
9c91a959 637 $relcomp->set('id', 0);
4c4a8d41 638 $relcomp->create();
c61db7bb 639 } else {
4c4a8d41
IT
640 // Debugging message when there is no match found.
641 debugging('related competency id not found');
c61db7bb 642 }
c61db7bb 643 }
4c4a8d41
IT
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) {
20e75095 651 $transaction->rollback($e);
c61db7bb 652 }
4c4a8d41 653
a3acafa0 654 // Trigger a competency framework created event.
5a1a685c 655 \core\event\competency_framework_created::create_from_framework($framework)->trigger();
a3acafa0 656
c61db7bb
IT
657 return $framework;
658 }
659
d9a39950
DW
660 /**
661 * Delete a competency framework by id.
662 *
b90e2205 663 * Requires moodle/competency:competencymanage capability at the system context.
d9a39950
DW
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) {
90c7bdaf 669 global $DB;
338386aa 670 static::require_enabled();
2de75345 671 $framework = new competency_framework($id);
b90e2205 672 require_capability('moodle/competency:competencymanage', $framework->get_context());
a3acafa0 673
d32d64bc 674 $events = array();
90c7bdaf 675 $competenciesid = competency::get_ids_by_frameworkid($id);
9c91a959 676 $contextid = $framework->get('contextid');
90c7bdaf
IT
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.
5a1a685c 694 $event = \core\event\competency_framework_deleted::create_from_framework($framework);
90c7bdaf
IT
695 $result = $framework->delete();
696
d32d64bc 697 // Register the deleted events competencies.
5a1a685c 698 $events = \core\event\competency_deleted::create_multiple_from_competencyids($competenciesid, $contextid);
d32d64bc 699
90c7bdaf
IT
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();
a3acafa0 709
d32d64bc
IT
710 // Trigger deleted event competencies.
711 foreach ($events as $event) {
712 $event->trigger();
713 }
714
90c7bdaf 715 return $result;
d9a39950
DW
716 }
717
718 /**
719 * Update the details for a competency framework.
720 *
b90e2205 721 * Requires moodle/competency:competencymanage capability at the system context.
d9a39950
DW
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) {
338386aa 727 static::require_enabled();
2de75345 728 $framework = new competency_framework($record->id);
a54d5b04 729
2de75345 730 // Check the permissions before update.
b90e2205 731 require_capability('moodle/competency:competencymanage', $framework->get_context());
a54d5b04
FM
732
733 // Account for different formats of taxonomies.
2de75345 734 $framework->from_record($record);
a54d5b04 735 if (isset($record->taxonomies)) {
9c91a959 736 $framework->set('taxonomies', $record->taxonomies);
a54d5b04
FM
737 }
738
a3acafa0 739 // Trigger a competency framework updated event.
5a1a685c 740 \core\event\competency_framework_updated::create_from_framework($framework)->trigger();
a3acafa0 741
d9a39950
DW
742 return $framework->update();
743 }
744
745 /**
746 * Read a the details for a single competency framework and return a record.
747 *
b90e2205 748 * Requires moodle/competency:competencyview capability at the system context.
d9a39950
DW
749 *
750 * @param int $id The id of the framework to read.
55683895 751 * @return competency_framework
d9a39950
DW
752 */
753 public static function read_framework($id) {
338386aa 754 static::require_enabled();
2de75345 755 $framework = new competency_framework($id);
91e54642
FM
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', '');
d9a39950 760 }
2de75345 761 return $framework;
d9a39950
DW
762 }
763
f73a29c1
IT
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) {
338386aa 771 static::require_enabled();
f73a29c1
IT
772 $framework = $frameworkorid;
773 if (!is_object($framework)) {
774 $framework = new competency_framework($framework);
775 }
91e54642
FM
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', '');
f73a29c1 780 }
5a1a685c 781 \core\event\competency_framework_viewed::create_from_framework($framework)->trigger();
f73a29c1
IT
782 return true;
783 }
784
3e6a8e16
IT
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) {
338386aa 792 static::require_enabled();
3e6a8e16
IT
793 $competency = $competencyorid;
794 if (!is_object($competency)) {
795 $competency = new competency($competency);
796 }
797
91e54642
FM
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', '');
3e6a8e16
IT
802 }
803
5a1a685c 804 \core\event\competency_viewed::create_from_competency($competency)->trigger();
3e6a8e16
IT
805 return true;
806 }
807
d9a39950
DW
808 /**
809 * Perform a search based on the provided filters and return a paginated list of records.
810 *
b90e2205 811 * Requires moodle/competency:competencyview capability at the system context.
d9a39950 812 *
d9a39950
DW
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)
2de75345
FM
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.
964afa98 823 * @param bool $onlyvisible If true return only visible frameworks
0fe5aac7 824 * @param string $query A string to use to filter down the frameworks.
d9a39950
DW
825 * @return array of competency_framework
826 */
0fe5aac7 827 public static function list_frameworks($sort, $order, $skip, $limit, $context, $includes = 'children',
69c47866 828 $onlyvisible = false, $query = '') {
2de75345 829 global $DB;
338386aa 830 static::require_enabled();
2de75345 831
f610a957 832 // Get all the relevant contexts.
f0da26a4 833 $contexts = self::get_related_contexts($context, $includes,
b90e2205 834 array('moodle/competency:competencyview', 'moodle/competency:competencymanage'));
f610a957
FM
835
836 if (empty($contexts)) {
b90e2205 837 throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
f610a957
FM
838 }
839
840 // OK - all set.
f610a957 841 list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
964afa98
IT
842 $select = "contextid $insql";
843 if ($onlyvisible) {
844 $select .= " AND visible = :visible";
845 $inparams['visible'] = 1;
846 }
847
0fe5aac7
FM
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
964afa98 857 return competency_framework::get_records_select($select, $inparams, $sort . ' ' . $order, '*', $skip, $limit);
f610a957
FM
858 }
859
860 /**
861 * Perform a search based on the provided filters and return a paginated list of records.
862 *
b90e2205 863 * Requires moodle/competency:competencyview capability at the system context.
f610a957
FM
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;
338386aa 875 static::require_enabled();
f610a957
FM
876
877 // Get all the relevant contexts.
f0da26a4 878 $contexts = self::get_related_contexts($context, $includes,
b90e2205 879 array('moodle/competency:competencyview', 'moodle/competency:competencymanage'));
f610a957
FM
880
881 if (empty($contexts)) {
b90e2205 882 throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
f610a957
FM
883 }
884
885 // OK - all set.
f610a957 886 list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
b6070264 887 return competency_framework::count_records_select("contextid $insql", $inparams);
f610a957
FM
888 }
889
890 /**
f0da26a4
FM
891 * Fetches all the relevant contexts.
892 *
8ff9ae8d
FM
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.
f610a957
FM
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 */
f0da26a4 906 public static function get_related_contexts($context, $includes, array $hasanycapability = null) {
f610a957 907 global $DB;
338386aa 908 static::require_enabled();
f610a957 909
2de75345
FM
910 if (!in_array($includes, array('children', 'parents', 'self'))) {
911 throw new coding_exception('Invalid parameter value for \'includes\'.');
912 }
8ff9ae8d 913
554b369b
IT
914 // If context user swap it for the context_system.
915 if ($context->contextlevel == CONTEXT_USER) {
8ff9ae8d 916 $context = context_system::instance();
554b369b 917 }
2de75345 918
2de75345
FM
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
f610a957
FM
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 }
2de75345
FM
946 }
947 }
948
f610a957 949 return $contexts;
d9a39950
DW
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) {
338386aa 959 static::require_enabled();
d9a39950
DW
960
961 // OK - all set.
46fdd1d8 962 $courses = course_competency::list_courses_min($competencyid);
d9a39950 963 $count = 0;
46fdd1d8 964
d9a39950
DW
965 // Now check permissions on each course.
966 foreach ($courses as $course) {
d660824b 967 if (!self::validate_course($course, false)) {
d9a39950
DW
968 continue;
969 }
970
d660824b 971 $context = context_course::instance($course->id);
b90e2205 972 $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
d660824b 973 if (!has_any_capability($capabilities, $context)) {
d9a39950
DW
974 continue;
975 }
976
977 $count++;
978 }
979
980 return $count;
981 }
982
db650737
DW
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.
f446b2e1 988 * @return array[int] Array of course modules ids.
db650737
DW
989 */
990 public static function list_course_modules_using_competency($competencyid, $courseid) {
338386aa 991 static::require_enabled();
db650737
DW
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.
b90e2205 999 $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
db650737 1000 if (!has_any_capability($capabilities, $coursecontext)) {
b90e2205 1001 throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
db650737
DW
1002 }
1003
1004 $cmlist = course_module_competency::list_course_modules($competencyid, $courseid);
f446b2e1
DW
1005 foreach ($cmlist as $cmid) {
1006 if (self::validate_course_module($cmid, false)) {
1007 array_push($result, $cmid);
1008 }
db650737
DW
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) {
338386aa 1021 static::require_enabled();
db650737
DW
1022 $cm = $cmorid;
1023 if (!is_object($cmorid)) {
1024 $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1025 }
1026
f446b2e1
DW
1027 // Check the user have access to the course module.
1028 self::validate_course_module($cm);
db650737
DW
1029 $context = context_module::instance($cm->id);
1030
b90e2205 1031 $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
db650737 1032 if (!has_any_capability($capabilities, $context)) {
b90e2205 1033 throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
db650737
DW
1034 }
1035
1036 $result = array();
db650737
DW
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
d9a39950
DW
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) {
338386aa 1053 static::require_enabled();
d9a39950
DW
1054
1055 // OK - all set.
46fdd1d8 1056 $courses = course_competency::list_courses($competencyid);
d9a39950 1057 $result = array();
46fdd1d8 1058
d9a39950
DW
1059 // Now check permissions on each course.
1060 foreach ($courses as $id => $course) {
1061 $context = context_course::instance($course->id);
b90e2205 1062 $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
d9a39950
DW
1063 if (!has_any_capability($capabilities, $context)) {
1064 unset($courses[$id]);
1065 continue;
1066 }
d660824b 1067 if (!self::validate_course($course, false)) {
d9a39950
DW
1068 unset($courses[$id]);
1069 continue;
1070 }
d9a39950
DW
1071 array_push($result, $course);
1072 }
1073
1074 return $result;
1075 }
1076
f1979b60
DW
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
b90e2205 1092 $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
f1979b60 1093 if (!has_any_capability($capabilities, $context)) {
b90e2205 1094 throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
f1979b60
DW
1095 }
1096
1097 // OK - all set.
1098 return user_competency_course::count_proficient_competencies($courseid, $userid);
1099 }
1100
d9a39950
DW
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) {
338386aa 1108 static::require_enabled();
d4c0a2f6
SG
1109 // Check the user have access to the course.
1110 self::validate_course($courseid);
1111
d9a39950
DW
1112 // First we do a permissions check.
1113 $context = context_course::instance($courseid);
d9a39950 1114
b90e2205 1115 $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
d9a39950 1116 if (!has_any_capability($capabilities, $context)) {
b90e2205 1117 throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
d9a39950
DW
1118 }
1119
d9a39950 1120 // OK - all set.
964afa98 1121 return course_competency::count_competencies($courseid);
d9a39950
DW
1122 }
1123
1124 /**
d4c0a2f6 1125 * List the competencies associated to a course.
d9a39950 1126 *
d4c0a2f6
SG
1127 * @param mixed $courseorid The course, or its ID.
1128 * @return array( array(
67bc0eaf
FM
1129 * 'competency' => \core_competency\competency,
1130 * 'coursecompetency' => \core_competency\course_competency
d4c0a2f6 1131 * ))
d9a39950 1132 */
d4c0a2f6 1133 public static function list_course_competencies($courseorid) {
338386aa 1134 static::require_enabled();
d4c0a2f6
SG
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);
d660824b 1142 $context = context_course::instance($course->id);
d660824b 1143
b90e2205 1144 $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
d660824b 1145 if (!has_any_capability($capabilities, $context)) {
b90e2205 1146 throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
d660824b
FM
1147 }
1148
d4c0a2f6 1149 $result = array();
d660824b
FM
1150
1151 // TODO We could improve the performance of this into one single query.
964afa98
IT
1152 $coursecompetencies = course_competency::list_course_competencies($course->id);
1153 $competencies = course_competency::list_competencies($course->id);
d9a39950 1154
d4c0a2f6
SG
1155 // Build the return values.
1156 foreach ($coursecompetencies as $key => $coursecompetency) {
d4c0a2f6 1157 $result[] = array(
9c91a959 1158 'competency' => $competencies[$coursecompetency->get('competencyid')],
d4c0a2f6
SG
1159 'coursecompetency' => $coursecompetency
1160 );
d9a39950
DW
1161 }
1162
d4c0a2f6 1163 return $result;
d9a39950
DW
1164 }
1165
ad6d3004
FM
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) {
338386aa 1174 static::require_enabled();
ad6d3004
FM
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()) {
91e54642
FM
1184 throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
1185 'nopermissions', '');
ad6d3004
FM
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) {
338386aa 1197 static::require_enabled();
ad6d3004
FM
1198 $uc = new user_competency($usercompetencyid);
1199 if (!$uc->can_read()) {
91e54642
FM
1200 throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
1201 'nopermissions', '');
ad6d3004
FM
1202 }
1203 return $uc;
1204 }
1205
bde97497
DW
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
db650737
DW
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(
67bc0eaf
FM
1236 * 'competency' => \core_competency\competency,
1237 * 'coursemodulecompetency' => \core_competency\course_module_competency
db650737
DW
1238 * ))
1239 */
1240 public static function list_course_module_competencies($cmorid) {
338386aa 1241 static::require_enabled();
db650737
DW
1242 $cm = $cmorid;
1243 if (!is_object($cmorid)) {
1244 $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1245 }
1246
f446b2e1
DW
1247 // Check the user have access to the course module.
1248 self::validate_course_module($cm);
db650737
DW
1249 $context = context_module::instance($cm->id);
1250
b90e2205 1251 $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
db650737 1252 if (!has_any_capability($capabilities, $context)) {
b90e2205 1253 throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
db650737
DW
1254 }
1255
1256 $result = array();
1257
1258 // TODO We could improve the performance of this into one single query.
bde97497 1259 $coursemodulecompetencies = course_module_competency::list_course_module_competencies($cm->id);
db650737
DW
1260 $competencies = course_module_competency::list_competencies($cm->id);
1261
1262 // Build the return values.
1263 foreach ($coursemodulecompetencies as $key => $coursemodulecompetency) {
1264 $result[] = array(
9c91a959 1265 'competency' => $competencies[$coursemodulecompetency->get('competencyid')],
db650737
DW
1266 'coursemodulecompetency' => $coursemodulecompetency
1267 );
1268 }
1269
1270 return $result;
1271 }
1272
65a67e23
DW
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.
96f4d771 1279 * @return user_competency_course
65a67e23
DW
1280 */
1281 public static function get_user_competency_in_course($courseid, $userid, $competencyid) {
338386aa 1282 static::require_enabled();
65a67e23
DW
1283 // First we do a permissions check.
1284 $context = context_course::instance($courseid);
1285
b90e2205 1286 $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
65a67e23 1287 if (!has_any_capability($capabilities, $context)) {
b90e2205 1288 throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
3b11d638 1289 } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
b90e2205 1290 throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
65a67e23
DW
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
96f4d771
DW
1296 $params = array('courseid' => $courseid, 'userid' => $userid, 'competencyid' => $competencyid);
1297 $exists = user_competency_course::get_record($params);
65a67e23 1298 // Create missing.
96f4d771
DW
1299 if ($exists) {
1300 $ucc = $exists;
65a67e23 1301 } else {
9c91a959 1302 $ucc = user_competency_course::create_relation($userid, $competency->get('id'), $courseid);
96f4d771 1303 $ucc->create();
65a67e23
DW
1304 }
1305
96f4d771 1306 return $ucc;
65a67e23
DW
1307 }
1308
bf780fbf
DW
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.
96f4d771 1314 * @return array of user_competency_course objects
bf780fbf
DW
1315 */
1316 public static function list_user_competencies_in_course($courseid, $userid) {
338386aa 1317 static::require_enabled();
bf780fbf
DW
1318 // First we do a permissions check.
1319 $context = context_course::instance($courseid);
1320 $onlyvisible = 1;
1321
b90e2205 1322 $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
bf780fbf 1323 if (!has_any_capability($capabilities, $context)) {
b90e2205 1324 throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
943989c2 1325 } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
b90e2205 1326 throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
bf780fbf
DW
1327 }
1328
bf780fbf 1329 // OK - all set.
65a67e23 1330 $competencylist = course_competency::list_competencies($courseid, false);
bf780fbf 1331
96f4d771 1332 $existing = user_competency_course::get_multiple($userid, $courseid, $competencylist);
02f9db88 1333 // Create missing.
96f4d771 1334 $orderedusercompetencycourses = array();
02f9db88
DW
1335
1336 $somemissing = false;
1337 foreach ($competencylist as $coursecompetency) {
1338 $found = false;
96f4d771 1339 foreach ($existing as $usercompetencycourse) {
9c91a959 1340 if ($usercompetencycourse->get('competencyid') == $coursecompetency->get('id')) {
02f9db88 1341 $found = true;
9c91a959 1342 $orderedusercompetencycourses[$usercompetencycourse->get('id')] = $usercompetencycourse;
65a67e23 1343 break;
02f9db88
DW
1344 }
1345 }
1346 if (!$found) {
9c91a959 1347 $ucc = user_competency_course::create_relation($userid, $coursecompetency->get('id'), $courseid);
96f4d771 1348 $ucc->create();
9c91a959 1349 $orderedusercompetencycourses[$ucc->get('id')] = $ucc;
02f9db88
DW
1350 }
1351 }
02f9db88 1352
96f4d771 1353 return $orderedusercompetencycourses;
bf780fbf
DW
1354 }
1355
7bb5ccbe
FM
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;
338386aa 1380 static::require_enabled();
7bb5ccbe
FM
1381 if ($userid === null) {
1382 $userid = $USER->id;
1383 }
1384
b90e2205 1385 $capability = 'moodle/competency:usercompetencyreview';
bbf727f1
FM
1386 $ucfields = user_competency::get_sql_fields('uc', 'uc_');
1387 $compfields = competency::get_sql_fields('c', 'c_');
7bb5ccbe 1388 $usercols = array('id') + get_user_fieldnames();
d58eabed
FM
1389 $userfields = array();
1390 foreach ($usercols as $field) {
1391 $userfields[] = "u." . $field . " AS usr_" . $field;
1392 }
1393 $userfields = implode(',', $userfields);
7bb5ccbe
FM
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
8c13028c
SL
1403 OR (uc.status = :inreview AND uc.reviewerid = :reviewerid))
1404 AND u.deleted = 0";
7bb5ccbe
FM
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.
81de839f 1420 list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql(
7bb5ccbe
FM
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(
bbf727f1
FM
1431 'usercompetency' => new user_competency(0, user_competency::extract_record($record, 'uc_')),
1432 'competency' => new competency(0, competency::extract_record($record, 'c_')),
7bb5ccbe
FM
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
db650737
DW
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) {
338386aa 1453 static::require_enabled();
db650737
DW
1454 $cm = $cmorid;
1455 if (!is_object($cmorid)) {
1456 $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1457 }
1458
f446b2e1
DW
1459 // Check the user have access to the course module.
1460 self::validate_course_module($cm);
db650737
DW
1461
1462 // First we do a permissions check.
1463 $context = context_module::instance($cm->id);
1464
b90e2205 1465 require_capability('moodle/competency:coursecompetencymanage', $context);
db650737
DW
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
db650737
DW
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) {
338386aa 1496 static::require_enabled();
db650737
DW
1497 $cm = $cmorid;
1498 if (!is_object($cmorid)) {
1499 $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1500 }
f446b2e1
DW
1501 // Check the user have access to the course module.
1502 self::validate_course_module($cm);
db650737
DW
1503
1504 // First we do a permissions check.
1505 $context = context_module::instance($cm->id);
1506
b90e2205 1507 require_capability('moodle/competency:coursecompetencymanage', $context);
db650737
DW
1508
1509 $record = new stdClass();
1510 $record->cmid = $cm->id;
1511 $record->competencyid = $competencyid;
1512
1513 $competency = new competency($competencyid);
f446b2e1 1514 $exists = course_module_competency::get_record(array('cmid' => $cm->id, 'competencyid' => $competencyid));
db650737 1515 if ($exists) {
f446b2e1 1516 return $exists->delete();
db650737
DW
1517 }
1518 return false;
1519 }
1520
1521 /**
1522 * Move the course module competency up or down in the display list.
1523 *
b90e2205 1524 * Requires moodle/competency:coursecompetencymanage capability at the course module context.
db650737
DW
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) {
338386aa 1532 static::require_enabled();
db650737
DW
1533 $cm = $cmorid;
1534 if (!is_object($cmorid)) {
1535 $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1536 }
f446b2e1
DW
1537 // Check the user have access to the course module.
1538 self::validate_course_module($cm);
db650737
DW
1539
1540 // First we do a permissions check.
1541 $context = context_module::instance($cm->id);
1542
b90e2205 1543 require_capability('moodle/competency:coursecompetencymanage', $context);
db650737
DW
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
9c91a959 1561 if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
db650737
DW
1562 // We are moving up, so put it before the "to" item.
1563 $down = false;
1564 }
1565
1566 foreach ($all as $id => $coursemodulecompetency) {
9c91a959
DW
1567 $sort = $coursemodulecompetency->get('sortorder');
1568 if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
1569 $coursemodulecompetency->set('sortorder', $coursemodulecompetency->get('sortorder') - 1);
db650737 1570 $coursemodulecompetency->update();
9c91a959
DW
1571 } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
1572 $coursemodulecompetency->set('sortorder', $coursemodulecompetency->get('sortorder') + 1);
db650737
DW
1573 $coursemodulecompetency->update();
1574 }
1575 }
9c91a959 1576 $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
db650737
DW
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) {
338386aa 1588 static::require_enabled();
db650737
DW
1589 $coursemodulecompetency = $coursemodulecompetencyorid;
1590 if (!is_object($coursemodulecompetency)) {
1591 $coursemodulecompetency = new course_module_competency($coursemodulecompetencyorid);
1592 }
1593
9c91a959 1594 $cm = get_coursemodule_from_id('', $coursemodulecompetency->get('cmid'), 0, true, MUST_EXIST);
db650737 1595
f446b2e1 1596 self::validate_course_module($cm);
db650737
DW
1597 $context = context_module::instance($cm->id);
1598
b90e2205 1599 require_capability('moodle/competency:coursecompetencymanage', $context);
db650737 1600
9c91a959 1601 $coursemodulecompetency->set('ruleoutcome', $ruleoutcome);
db650737
DW
1602 return $coursemodulecompetency->update();
1603 }
1604
d9a39950
DW
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) {
338386aa 1613 static::require_enabled();
d4c0a2f6
SG
1614 // Check the user have access to the course.
1615 self::validate_course($courseid);
1616
d9a39950
DW
1617 // First we do a permissions check.
1618 $context = context_course::instance($courseid);
1619
b90e2205 1620 require_capability('moodle/competency:coursecompetencymanage', $context);
e90c24d0 1621
d9a39950
DW
1622 $record = new stdClass();
1623 $record->courseid = $courseid;
1624 $record->competencyid = $competencyid;
1625
964afa98
IT
1626 $competency = new competency($competencyid);
1627
1628 // Can not add a competency that belong to a hidden framework.
9c91a959 1629 if ($competency->get_framework()->get('visible') == false) {
964afa98 1630 throw new coding_exception('A competency belonging to hidden framework can not be linked to course');
d9a39950
DW
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) {
338386aa 1652 static::require_enabled();
d4c0a2f6
SG
1653 // Check the user have access to the course.
1654 self::validate_course($courseid);
1655
d9a39950
DW
1656 // First we do a permissions check.
1657 $context = context_course::instance($courseid);
1658
b90e2205 1659 require_capability('moodle/competency:coursecompetencymanage', $context);
e90c24d0 1660
d9a39950
DW
1661 $record = new stdClass();
1662 $record->courseid = $courseid;
1663 $record->competencyid = $competencyid;
1664
d9a39950 1665 $coursecompetency = new course_competency();
d7891585 1666 $exists = course_competency::get_record(array('courseid' => $courseid, 'competencyid' => $competencyid));
d9a39950 1667 if ($exists) {
f446b2e1 1668 // Delete all course_module_competencies for this competency in this course.
d7891585 1669 $cmcs = course_module_competency::get_records_by_competencyid_in_course($competencyid, $courseid);
f446b2e1
DW
1670 foreach ($cmcs as $cmc) {
1671 $cmc->delete();
1672 }
1673 return $exists->delete();
d9a39950
DW
1674 }
1675 return false;
1676 }
1677
1678 /**
1679 * Move the course competency up or down in the display list.
1680 *
b90e2205 1681 * Requires moodle/competency:coursecompetencymanage capability at the course context.
d9a39950
DW
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) {
338386aa 1689 static::require_enabled();
d4c0a2f6
SG
1690 // Check the user have access to the course.
1691 self::validate_course($courseid);
1692
d9a39950
DW
1693 // First we do a permissions check.
1694 $context = context_course::instance($courseid);
1695
b90e2205 1696 require_capability('moodle/competency:coursecompetencymanage', $context);
d9a39950
DW
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
9c91a959 1715 if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
d9a39950
DW
1716 // We are moving up, so put it before the "to" item.
1717 $down = false;
1718 }
1719
1720 foreach ($all as $id => $coursecompetency) {
9c91a959
DW
1721 $sort = $coursecompetency->get('sortorder');
1722 if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
1723 $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') - 1);
d9a39950 1724 $coursecompetency->update();
9c91a959
DW
1725 } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
1726 $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') + 1);
d9a39950
DW
1727 $coursecompetency->update();
1728 }
1729 }
9c91a959 1730 $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
d9a39950
DW
1731 return $competencyfrom->update();
1732 }
1733
d660824b
FM
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) {
338386aa 1742 static::require_enabled();
d660824b
FM
1743 $coursecompetency = $coursecompetencyorid;
1744 if (!is_object($coursecompetency)) {
1745 $coursecompetency = new course_competency($coursecompetencyorid);
1746 }
1747
9c91a959 1748 $courseid = $coursecompetency->get('courseid');
d660824b
FM
1749 self::validate_course($courseid);
1750 $coursecontext = context_course::instance($courseid);
1751
b90e2205 1752 require_capability('moodle/competency:coursecompetencymanage', $coursecontext);
d660824b 1753
9c91a959 1754 $coursecompetency->set('ruleoutcome', $ruleoutcome);
d660824b
FM
1755 return $coursecompetency->update();
1756 }
1757
d9a39950
DW
1758 /**
1759 * Create a learning plan template from a record containing all the data for the class.
1760 *
b90e2205 1761 * Requires moodle/competency:templatemanage capability.
d9a39950
DW
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) {
338386aa 1767 static::require_enabled();
f0da26a4
FM
1768 $template = new template(0, $record);
1769
d9a39950 1770 // First we do a permissions check.
f85ea576 1771 if (!$template->can_manage()) {
91e54642
FM
1772 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1773 'nopermissions', '');
f85ea576 1774 }
d9a39950
DW
1775
1776 // OK - all set.
85e45ff8
SG
1777 $template = $template->create();
1778
1779 // Trigger a template created event.
5a1a685c 1780 \core\event\competency_template_created::create_from_template($template)->trigger();
85e45ff8 1781
d9a39950
DW
1782 return $template;
1783 }
1784
76c107ea
IT
1785 /**
1786 * Duplicate a learning plan template.
1787 *
b90e2205 1788 * Requires moodle/competency:templatemanage capability at the template context.
76c107ea
IT
1789 *
1790 * @param int $id the template id.
1791 * @return template
1792 */
1793 public static function duplicate_template($id) {
338386aa 1794 static::require_enabled();
76c107ea
IT
1795 $template = new template($id);
1796
1797 // First we do a permissions check.
f85ea576 1798 if (!$template->can_manage()) {
91e54642
FM
1799 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1800 'nopermissions', '');
f85ea576 1801 }
76c107ea
IT
1802
1803 // OK - all set.
14d9abc2 1804 $competencies = template_competency::list_competencies($id, false);
76c107ea
IT
1805
1806 // Adding the suffix copy.
9c91a959
DW
1807 $template->set('shortname', get_string('duplicateditemname', 'core_competency', $template->get('shortname')));
1808 $template->set('id', 0);
76c107ea
IT
1809
1810 $duplicatedtemplate = $template->create();
1811
1812 // Associate each competency for the duplicated template.
1813 foreach ($competencies as $competency) {
9c91a959 1814 self::add_competency_to_template($duplicatedtemplate->get('id'), $competency->get('id'));
76c107ea 1815 }
85e45ff8
SG
1816
1817 // Trigger a template created event.
5a1a685c 1818 \core\event\competency_template_created::create_from_template($duplicatedtemplate)->trigger();
85e45ff8 1819
76c107ea
IT
1820 return $duplicatedtemplate;
1821 }
1822
d9a39950
DW
1823 /**
1824 * Delete a learning plan template by id.
5a4ee001 1825 * If the learning plan template has associated cohorts they will be deleted.
d9a39950 1826 *
b90e2205 1827 * Requires moodle/competency:templatemanage capability.
d9a39950
DW
1828 *
1829 * @param int $id The record to delete.
8fabc738 1830 * @param boolean $deleteplans True to delete plans associaated to template, false to unlink them.
d9a39950
DW
1831 * @return boolean
1832 */
8fabc738 1833 public static function delete_template($id, $deleteplans = true) {
5a4ee001 1834 global $DB;
338386aa 1835 static::require_enabled();
f0da26a4
FM
1836 $template = new template($id);
1837
d9a39950 1838 // First we do a permissions check.
f85ea576 1839 if (!$template->can_manage()) {
91e54642
FM
1840 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1841 'nopermissions', '');
f85ea576 1842 }
d9a39950 1843
5a4ee001
IT
1844 $transaction = $DB->start_delegated_transaction();
1845 $success = true;
1846
1847 // Check if there are cohorts associated.
9c91a959 1848 $templatecohorts = template_cohort::get_relations_by_templateid($template->get('id'));
5a4ee001
IT
1849 foreach ($templatecohorts as $templatecohort) {
1850 $success = $templatecohort->delete();
1851 if (!$success) {
1852 break;
1853 }
1854 }
1855
8fabc738
SG
1856 // Still OK, delete or unlink the plans from the template.
1857 if ($success) {
9c91a959 1858 $plans = plan::get_records(array('templateid' => $template->get('id')));
8fabc738 1859 foreach ($plans as $plan) {
9c91a959 1860 $success = $deleteplans ? self::delete_plan($plan->get('id')) : self::unlink_plan_from_template($plan);
8fabc738
SG
1861 if (!$success) {
1862 break;
1863 }
1864 }
1865 }
1866
1867 // Still OK, delete the template comptencies.
1868 if ($success) {
9c91a959 1869 $success = template_competency::delete_by_templateid($template->get('id'));
8fabc738
SG
1870 }
1871
d9a39950 1872 // OK - all set.
5a4ee001 1873 if ($success) {
85e45ff8 1874 // Create a template deleted event.
5a1a685c 1875 $event = \core\event\competency_template_deleted::create_from_template($template);
85e45ff8 1876
5a4ee001
IT
1877 $success = $template->delete();
1878 }
1879
1880 if ($success) {
85e45ff8
SG
1881 // Trigger a template deleted event.
1882 $event->trigger();
1883
5a4ee001
IT
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;
d9a39950
DW
1891 }
1892
1893 /**
1894 * Update the details for a learning plan template.
1895 *
b90e2205 1896 * Requires moodle/competency:templatemanage capability.
d9a39950
DW
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) {
c7999f6d 1902 global $DB;
338386aa 1903 static::require_enabled();
f0da26a4
FM
1904 $template = new template($record->id);
1905
d9a39950 1906 // First we do a permissions check.
f85ea576 1907 if (!$template->can_manage()) {
91e54642
FM
1908 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1909 'nopermissions', '');
f85ea576 1910
9c91a959 1911 } else if (isset($record->contextid) && $record->contextid != $template->get('contextid')) {
91e54642 1912 // We can never change the context of a template.
f0da26a4 1913 throw new coding_exception('Changing the context of an existing tempalte is forbidden.');
f85ea576 1914
f0da26a4 1915 }
d9a39950 1916
c7999f6d
FM
1917 $updateplans = false;
1918 $before = $template->to_record();
1919
f0da26a4 1920 $template->from_record($record);
c7999f6d
FM
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
85e45ff8 1939 // Trigger a template updated event.
5a1a685c 1940 \core\event\competency_template_updated::create_from_template($template)->trigger();
85e45ff8 1941
c7999f6d
FM
1942 if ($updateplans) {
1943 plan::update_multiple_from_template($template);
1944 }
1945
1946 $transaction->allow_commit();
1947
1948 return $success;
d9a39950
DW
1949 }
1950
1951 /**
1952 * Read a the details for a single learning plan template and return a record.
1953 *
b90e2205 1954 * Requires moodle/competency:templateview capability at the system context.
d9a39950
DW
1955 *
1956 * @param int $id The id of the template to read.
55683895 1957 * @return template
d9a39950
DW
1958 */
1959 public static function read_template($id) {
338386aa 1960 static::require_enabled();
f0da26a4
FM
1961 $template = new template($id);
1962 $context = $template->get_context();
1963
d9a39950 1964 // First we do a permissions check.
f85ea576 1965 if (!$template->can_read()) {
91e54642
FM
1966 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
1967 'nopermissions', '');
d9a39950
DW
1968 }
1969
1970 // OK - all set.
f0da26a4 1971 return $template;
d9a39950
DW
1972 }
1973
d9a39950
DW
1974 /**
1975 * Perform a search based on the provided filters and return a paginated list of records.
1976 *
b90e2205 1977 * Requires moodle/competency:templateview capability at the system context.
d9a39950 1978 *
d9a39950
DW
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)
f0da26a4
FM
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.
964afa98 1989 * @param bool $onlyvisible If should list only visible templates
d9a39950
DW
1990 * @return array of competency_framework
1991 */
964afa98 1992 public static function list_templates($sort, $order, $skip, $limit, $context, $includes = 'children', $onlyvisible = false) {
f0da26a4 1993 global $DB;
338386aa 1994 static::require_enabled();
f0da26a4
FM
1995
1996 // Get all the relevant contexts.
1997 $contexts = self::get_related_contexts($context, $includes,
b90e2205 1998 array('moodle/competency:templateview', 'moodle/competency:templatemanage'));
f0da26a4 1999
d9a39950 2000 // First we do a permissions check.
f0da26a4 2001 if (empty($contexts)) {
b90e2205 2002 throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
d9a39950
DW
2003 }
2004
f0da26a4
FM
2005 // Make the order by.
2006 $orderby = '';
2007 if (!empty($sort)) {
2008 $orderby = $sort . ' ' . $order;
2009 }
2010
d9a39950
DW
2011 // OK - all set.
2012 $template = new template();
964afa98
IT
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);
d9a39950
DW
2021 }
2022
2023 /**
2024 * Perform a search based on the provided filters and return how many results there are.
2025 *
b90e2205 2026 * Requires moodle/competency:templateview capability at the system context.
d9a39950 2027 *
f0da26a4
FM
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.
d9a39950
DW
2034 * @return int
2035 */
f0da26a4
FM
2036 public static function count_templates($context, $includes) {
2037 global $DB;
338386aa 2038 static::require_enabled();
f0da26a4 2039
d9a39950 2040 // First we do a permissions check.
f0da26a4 2041 $contexts = self::get_related_contexts($context, $includes,
b90e2205 2042 array('moodle/competency:templateview', 'moodle/competency:templatemanage'));
f0da26a4
FM
2043
2044 if (empty($contexts)) {
b90e2205 2045 throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
d9a39950
DW
2046 }
2047
2048 // OK - all set.
2049 $template = new template();
f0da26a4
FM
2050 list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
2051 return $template->count_records_select("contextid $insql", $inparams);
d9a39950
DW
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) {
338386aa 2061 static::require_enabled();
d9a39950
DW
2062 // First we do a permissions check.
2063 $context = context_system::instance();
2064 $onlyvisible = 1;
2065
b90e2205 2066 $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
d9a39950 2067 if (!has_any_capability($capabilities, $context)) {
b90e2205 2068 throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
d9a39950
DW
2069 }
2070
b90e2205 2071 if (has_capability('moodle/competency:templatemanage', $context)) {
d9a39950
DW
2072 $onlyvisible = 0;
2073 }
2074
2075 // OK - all set.
cc643d16 2076 return template_competency::count_templates($competencyid, $onlyvisible);
d9a39950
DW
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) {
338386aa 2086 static::require_enabled();
d9a39950
DW
2087 // First we do a permissions check.
2088 $context = context_system::instance();
2089 $onlyvisible = 1;
2090
b90e2205 2091 $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
d9a39950 2092 if (!has_any_capability($capabilities, $context)) {
b90e2205 2093 throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
d9a39950
DW
2094 }
2095
b90e2205 2096 if (has_capability('moodle/competency:templatemanage', $context)) {
d9a39950
DW
2097 $onlyvisible = 0;
2098 }
2099
2100 // OK - all set.
cc643d16 2101 return template_competency::list_templates($competencyid, $onlyvisible);
d9a39950
DW
2102
2103 }
2104
2105 /**
2106 * Count all the competencies in a learning plan template.
2107 *
3edcd295 2108 * @param template|int $templateorid The template or its ID.
d9a39950
DW
2109 * @return int
2110 */
3edcd295 2111 public static function count_competencies_in_template($templateorid) {
338386aa 2112 static::require_enabled();
d9a39950 2113 // First we do a permissions check.
3edcd295
DW
2114 $template = $templateorid;
2115 if (!is_object($template)) {
2116 $template = new template($template);
2117 }
2118
2119 if (!$template->can_read()) {
91e54642
FM
2120 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2121 'nopermissions', '');
3edcd295
DW
2122 }
2123
2124 // OK - all set.
9c91a959 2125 return template_competency::count_competencies($template->get('id'));
3edcd295
DW
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 }
d9a39950 2140
f85ea576 2141 if (!$template->can_read()) {
91e54642
FM
2142 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2143 'nopermissions', '');
d9a39950
DW
2144 }
2145
d9a39950 2146 // OK - all set.
9c91a959 2147 return template_competency::count_competencies_with_no_courses($template->get('id'));
d9a39950
DW
2148 }
2149
2150 /**
2151 * List all the competencies in a template.
2152 *
3edcd295 2153 * @param template|int $templateorid The template or its ID.
d9a39950
DW
2154 * @return array of competencies
2155 */
3edcd295 2156 public static function list_competencies_in_template($templateorid) {
338386aa 2157 static::require_enabled();
d9a39950 2158 // First we do a permissions check.
3edcd295
DW
2159 $template = $templateorid;
2160 if (!is_object($template)) {
2161 $template = new template($template);
2162 }
d9a39950 2163
f85ea576 2164 if (!$template->can_read()) {
91e54642
FM
2165 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2166 'nopermissions', '');
d9a39950
DW
2167 }
2168
d9a39950 2169 // OK - all set.
9c91a959 2170 return template_competency::list_competencies($template->get('id'));
d9a39950
DW
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) {
338386aa 2181 static::require_enabled();
d9a39950 2182 // First we do a permissions check.
f0da26a4 2183 $template = new template($templateid);
f85ea576 2184 if (!$template->can_manage()) {
91e54642
FM
2185 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
2186 'nopermissions', '');
f85ea576 2187 }
d9a39950
DW
2188
2189 $record = new stdClass();
2190 $record->templateid = $templateid;
2191 $record->competencyid = $competencyid;
2192
964afa98
IT
2193 $competency = new competency($competencyid);
2194
2195 // Can not add a competency that belong to a hidden framework.
9c91a959 2196 if ($competency->get_framework()->get('visible') == false) {
964afa98 2197 throw new coding_exception('A competency belonging to hidden framework can not be added');
d9a39950
DW
2198 }
2199
cc643d16 2200 $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
d9a39950 2201 if (!$exists) {
cc643d16
FM
2202 $templatecompetency = new template_competency(0, $record);
2203 $templatecompetency->create();
2204 return true;
d9a39950
DW
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) {
338386aa 2217 static::require_enabled();
d9a39950 2218 // First we do a permissions check.
f0da26a4 2219 $template = new template($templateid);
f85ea576 2220 if (!$template->can_manage()) {
91e54642
FM
2221 throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
2222 'nopermissions', '');
f85ea576 2223 }
d9a39950
DW
2224
2225 $record = new stdClass();
2226 $record->templateid = $templateid;
2227 $record->competencyid = $competencyid;
2228
f0da26a4 2229 $competency = new competency($competencyid);
d9a39950 2230
cc643d16 2231 $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
d9a39950
DW
2232 if ($exists) {
2233 $link = array_pop($exists);
2234 return $link->delete();
2235 }
2236 return false;
2237 }
4db373d5 2238
55683895
MN
2239 /**
2240 * Move the template competency up or down in the display list.
2241 *
b90e2205 2242 * Requires moodle/competency:templatemanage capability at the system context.
55683895
MN
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) {
338386aa 2250 static::require_enabled();
fd8043b2 2251 $template = new template($templateid);
55683895 2252
fd8043b2
IT
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 }
55683895
MN
2258
2259 $down = true;
cc643d16 2260 $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidfrom));
55683895
MN
2261 if (count($matches) == 0) {
2262 throw new coding_exception('The link does not exist');
2263 }
2264
2265 $competencyfrom = array_pop($matches);
cc643d16 2266 $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidto));
55683895
MN
2267 if (count($matches) == 0) {
2268 throw new coding_exception('The link does not exist');
2269 }
2270
2271 $competencyto = array_pop($matches);
2272
cc643d16 2273 $all = template_competency::get_records(array('templateid' => $templateid), 'sortorder', 'ASC', 0, 0);
55683895 2274
9c91a959 2275 if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
55683895
MN
2276 // We are moving up, so put it before the "to" item.
2277 $down = false;
2278 }
2279
2280 foreach ($all as $id => $templatecompetency) {
9c91a959
DW
2281 $sort = $templatecompetency->get('sortorder');
2282 if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
2283 $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') - 1);
55683895 2284 $templatecompetency->update();
9c91a959
DW
2285 } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
2286 $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') + 1);
55683895
MN
2287 $templatecompetency->update();
2288 }
2289 }
9c91a959 2290 $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
55683895
MN
2291 return $competencyfrom->update();
2292 }
2293
bee480a4
FM
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.
96c2b847 2300 * @param stdClass|int $cohortorid The cohort ot its ID.
bee480a4
FM
2301 * @return template_cohort
2302 */
2303 public static function create_template_cohort($templateorid, $cohortorid) {
2304 global $DB;
338386aa 2305 static::require_enabled();
bee480a4
FM
2306
2307 $template = $templateorid;
2308 if (!is_object($template)) {
2309 $template = new template($template);
2310 }
b90e2205 2311 require_capability('moodle/competency:templatemanage', $template->get_context());
bee480a4
FM
2312
2313 $cohort = $cohortorid;
2314 if (!is_object($cohort)) {
2315 $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
2316 }
2317
6f0c979f 2318 // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
bee480a4 2319 $cohortcontext = context::instance_by_id($cohort->contextid);
6f0c979f 2320 if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
bee480a4
FM
2321 throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
2322 }
2323
9c91a959
DW
2324 $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id);
2325 if (!$tplcohort->get('id')) {
bee480a4
FM
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.
96c2b847 2336 * @param stdClass|int $cohortorid The cohort ot its ID.
bee480a4
FM
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;
338386aa 2341 static::require_enabled();
bee480a4
FM
2342
2343 $template = $templateorid;
2344 if (!is_object($template)) {
2345 $template = new template($template);
2346 }
b90e2205 2347 require_capability('moodle/competency:templatemanage', $template->get_context());
bee480a4
FM
2348
2349 $cohort = $cohortorid;
2350 if (!is_object($cohort)) {
2351 $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
2352 }
2353
9c91a959
DW
2354 $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id);
2355 if (!$tplcohort->get('id')) {
bee480a4
FM
2356 return true;
2357 }
2358
2359 return $tplcohort->delete();
2360 }
2361
4db373d5
DM
2362 /**
2363 * Lists user plans.
2364 *
55683895 2365 * @param int $userid
67bc0eaf 2366 * @return \core_competency\plan[]
4db373d5
DM
2367 */
2368 public static function list_user_plans($userid) {
3c230247 2369 global $DB, $USER;
338386aa 2370 static::require_enabled();
4db373d5
DM
2371 $select = 'userid = :userid';
2372 $params = array('userid' => $userid);
4db373d5
DM
2373 $context = context_user::instance($userid);
2374
5159d679
FM
2375 // Check that we can read something here.
2376 if (!plan::can_read_user($userid) && !plan::can_read_user_draft($userid)) {
b90e2205 2377 throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
4db373d5
DM
2378 }
2379
5159d679
FM
2380 // The user cannot view the drafts.
2381 if (!plan::can_read_user_draft($userid)) {
3c230247
FM
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;
4db373d5 2385 }
5159d679
FM
2386 // The user cannot view the non-drafts.
2387 if (!plan::can_read_user($userid)) {
3c230247
FM
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;
5159d679 2392 }
4db373d5 2393
52eda876 2394 return plan::get_records_select($select, $params, 'name ASC');
4db373d5
DM
2395 }
2396
7bb5ccbe
FM
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;
338386aa 2421 static::require_enabled();
7bb5ccbe
FM
2422
2423 if ($userid === null) {
2424 $userid = $USER->id;
2425 }
2426
bbf727f1
FM
2427 $planfields = plan::get_sql_fields('p', 'plan_');
2428 $tplfields = template::get_sql_fields('t', 'tpl_');
7bb5ccbe 2429 $usercols = array('id') + get_user_fieldnames();
d58eabed
FM
2430 $userfields = array();
2431 foreach ($usercols as $field) {
2432 $userfields[] = "u." . $field . " AS usr_" . $field;
2433 }
2434 $userfields = implode(',', $userfields);
7bb5ccbe
FM
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.
81de839f 2462 list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql('moodle/competency:planreview',
96c2b847 2463 $userid, SQL_PARAMS_NAMED);
7bb5ccbe
FM
2464 $sql .= " AND p.userid $insql";
2465 $params += $inparams;
2466
d309f3e2
FM
2467 // Order by ID just to have some ordering in place.
2468 $ordersql = " ORDER BY p.id ASC";
c07712db 2469
7bb5ccbe 2470 $plans = array();
d309f3e2 2471 $records = $DB->get_recordset_sql($select . $sql . $ordersql, $params, $skip, $limit);
7bb5ccbe 2472 foreach ($records as $record) {
bbf727f1 2473 $plan = new plan(0, plan::extract_record($record, 'plan_'));
7bb5ccbe
FM
2474 $template = null;
2475
2476 if ($plan->is_based_on_template()) {
bbf727f1 2477 $template = new template(0, template::extract_record($record, 'tpl_'));
7bb5ccbe
FM
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
4db373d5
DM
2494 /**
2495 * Creates a learning plan based on the provided data.
2496 *
2497 * @param stdClass $record
67bc0eaf 2498 * @return \core_competency\plan
4db373d5
DM
2499 */
2500 public static function create_plan(stdClass $record) {
2501 global $USER;
338386aa 2502 static::require_enabled();
5159d679 2503 $plan = new plan(0, $record);
4db373d5 2504
2388d46d
FM
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().');
9c91a959 2507 } else if ($plan->get('status') == plan::STATUS_COMPLETE) {
3c230247 2508 throw new coding_exception('A plan cannot be created as complete.');
2388d46d
FM
2509 }
2510
5159d679 2511 if (!$plan->can_manage()) {
9c91a959 2512 $context = context_user::instance($plan->get('userid'));
b90e2205 2513 throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
4db373d5
DM
2514 }
2515
5af3778e 2516 $plan->create();
4ed524b7
IT
2517
2518 // Trigger created event.
5a1a685c 2519 \core\event\competency_plan_created::create_from_plan($plan)->trigger();
4db373d5
DM
2520 return $plan;
2521 }
2522
2388d46d
FM
2523 /**
2524 * Create a learning plan from a template.
2525 *
2526 * @param mixed $templateorid The template object or ID.
2527 * @param int $userid
67bc0eaf 2528 * @return false|\core_competency\plan Returns false when the plan already exists.
2388d46d
FM
2529 */
2530 public static function create_plan_from_template($templateorid, $userid) {
338386aa 2531 static::require_enabled();
2388d46d
FM
2532 $template = $templateorid;
2533 if (!is_object($template)) {
2534 $template = new template($template);
2535 }
f85ea576
FM
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()) {
91e54642
FM
2539 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2540 'nopermissions', '');
f85ea576 2541 }
964afa98 2542 // Can not create plan from a hidden template.
9c91a959 2543 if ($template->get('visible') == false) {
964afa98
IT
2544 throw new coding_exception('A plan can not be created from a hidden template');
2545 }
2388d46d
FM
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()) {
91e54642
FM
2569 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage',
2570 'nopermissions', '');
2388d46d
FM
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(
9c91a959 2576 'templateid' => $template->get('id'), 'userid' => $userid))) {
2388d46d
FM
2577 return false;
2578 }
2579
2580 $plan->create();
4ed524b7
IT
2581
2582 // Trigger created event.
5a1a685c 2583 \core\event\competency_plan_created::create_from_plan($plan)->trigger();
2388d46d
FM
2584 return $plan;
2585 }
2586
dd1df082
FM
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;
338386aa 2597 static::require_enabled();
dd1df082
FM
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()) {
91e54642
FM
2607 throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2608 'nopermissions', '');
dd1df082
FM
2609 }
2610
964afa98 2611 // Can not create plan from a hidden template.
9c91a959 2612 if ($template->get('visible') == false) {
964afa98
IT
2613 throw new coding_exception('A plan can not be created from a hidden template');
2614 }
2615
6f0c979f 2616 // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
dd1df082 2617 $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
6f0c979f
FM
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', '');
dd1df082
FM
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;
9c91a959 2644 $userids = template_cohort::get_missing_plans($template->get('id'), $cohortid, $recreateunlinked);
dd1df082
FM
2645 foreach ($userids as $userid) {
2646 $record = (object) (array) $recordbase;
2647 $record->userid = $userid;
2648
9031be44
FM
2649 $plan = new plan(0, $record);
2650 if (!$plan->can_manage()) {
dd1df082
FM
2651 // Silently skip members where permissions are lacking.
2652 continue;
2653 }
2654
2655 $plan->create();
4ed524b7 2656 // Trigger created event.
5a1a685c 2657 \core\event\competency_plan_created::create_from_plan($plan)->trigger();
dd1df082
FM
2658 $created++;
2659 }
2660
2661 return $created;
2662 }
2663
6d2c2e86
FM
2664 /**
2665 * Unlink a plan from its template.
2666 *
67bc0eaf 2667 * @param \core_competency\plan|int $planorid The plan or its ID.
6d2c2e86
FM
2668 * @return bool
2669 */
2670 public static function unlink_plan_from_template($planorid) {
2671 global $DB;
338386aa 2672 static::require_enabled();
6d2c2e86
FM
2673
2674 $plan = $planorid;
2675 if (!is_object($planorid)) {
2676 $plan = new plan($planorid);
2677 }
2678
f85ea576 2679 // The user must be allowed to manage the plans of the user, nothing about the template.
6d2c2e86 2680 if (!$plan->can_manage()) {
b90e2205 2681 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
6d2c2e86
FM
2682 }
2683
a8902ee2 2684 // Only plan with status DRAFT or ACTIVE can be unliked..
9c91a959 2685 if ($plan->get('status') == plan::STATUS_COMPLETE) {
a8902ee2
SG
2686 throw new coding_exception('Only draft or active plan can be unliked from a template');
2687 }
2688
6d2c2e86
FM
2689 // Early exit, it's already done...
2690 if (!$plan->is_based_on_template()) {
2691 return true;
2692 }
2693
2694 // Fetch the template.
9c91a959 2695 $template = new template($plan->get('templateid'));
6d2c2e86
FM
2696
2697 // Now, proceed by copying all competencies to the plan, then update the plan.
2698 $transaction = $DB->start_delegated_transaction();
9c91a959 2699 $competencies = template_competency::list_competencies($template->get('id'), false);
6d2c2e86
FM
2700 $i = 0;
2701 foreach ($competencies as $competency) {
2702 $record = (object) array(
9c91a959
DW
2703 'planid' => $plan->get('id'),
2704 'competencyid' => $competency->get('id'),
6d2c2e86
FM
2705 'sortorder' => $i++
2706 );
2707 $pc = new plan_competency(null, $record);
2708 $pc->create();
2709 }
9c91a959
DW
2710 $plan->set('origtemplateid', $template->get('id'));
2711 $plan->set('templateid', null);
6d2c2e86
FM
2712 $success = $plan->update();
2713 $transaction->allow_commit();
2714
25deee17 2715 // Trigger unlinked event.
5a1a685c 2716 \core\event\competency_plan_unlinked::create_from_plan($plan)->trigger();
4ed524b7 2717
6d2c2e86
FM
2718 return $success;
2719 }
2720
4db373d5
DM
2721 /**
2722 * Updates a plan.
2723 *
2724 * @param stdClass $record
67bc0eaf 2725 * @return \core_competency\plan
4db373d5
DM
2726 */
2727 public static function update_plan(stdClass $record) {
338386aa 2728 static::require_enabled();
192569ed 2729
5159d679 2730 $plan = new plan($record->id);
4db373d5 2731
5159d679
FM
2732 // Validate that the plan as it is can be managed.
2733 if (!$plan->can_manage()) {
b90e2205 2734 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
4de877eb 2735
9c91a959 2736 } else if ($plan->get('status') == plan::STATUS_COMPLETE) {
3c230247
FM
2737 // A completed plan cannot be edited.
2738 throw new coding_exception('Completed plan cannot be edited.');
2739
4de877eb 2740 } else if ($plan->is_based_on_template()) {
d805cc37 2741 // Prevent a plan based on a template to be edited.
4de877eb 2742 throw new coding_exception('Cannot update a plan that is based on a template.');
4db373d5 2743
9c91a959 2744 } else if (isset($record->templateid) && $plan->get('templateid') != $record->templateid) {
3c230247
FM
2745 // Prevent a plan to be based on a template.
2746 throw new coding_exception('Cannot base a plan on a template.');
2747
9c91a959 2748 } else if (isset($record->userid) && $plan->get('userid') != $record->userid) {
d805cc37 2749 // Prevent change of ownership as the capabilities are checked against that.
5159d679 2750 throw new coding_exception('A plan cannot be transfered to another user');
192569ed 2751
9c91a959 2752 } else if (isset($record->status) && $plan->get('status') != $record->status) {
3c230247
FM
2753 // Prevent change of status.
2754 throw new coding_exception('To change the status of a plan use the appropriate methods.');
4db373d5 2755
d805cc37 2756 }
192569ed 2757
3c230247 2758 $plan->from_record($record);
d805cc37 2759 $plan->update();
192569ed 2760
4ed524b7 2761 // Trigger updated event.
5a1a685c 2762 \core\event\competency_plan_updated::create_from_plan($plan)->trigger();
4ed524b7 2763
5159d679 2764 return $plan;
4db373d5
DM
2765 }
2766
2767 /**
2768 * Returns a plan data.
2769 *
2770 * @param int $id
67bc0eaf 2771 * @return \core_competency\plan
4db373d5
DM
2772 */
2773 public static function read_plan($id) {
338386aa 2774 static::require_enabled();
4db373d5 2775 $plan = new plan($id);
4db373d5 2776
5159d679 2777 if (!$plan->can_read()) {
9c91a959 2778 $context = context_user::instance($plan->get('userid'));
b90e2205 2779 throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
4db373d5
DM
2780 }
2781
4db373d5
DM
2782 return $plan;
2783 }
2784
4ed524b7
IT
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) {
338386aa 2792 static::require_enabled();
4ed524b7
IT
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()) {
9c91a959 2800 $context = context_user::instance($plan->get('userid'));
b90e2205 2801 throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
4ed524b7
IT
2802 }
2803
2804 // Trigger a template viewed event.
5a1a685c 2805 \core\event\competency_plan_viewed::create_from_plan($plan)->trigger();
4ed524b7
IT
2806
2807 return true;
2808 }
2809
4db373d5
DM
2810 /**
2811 * Deletes a plan.
2812 *
f85ea576
FM
2813 * Plans based on a template can be removed just like any other one.
2814 *
4db373d5
DM
2815 * @param int $id
2816 * @return bool Success?
2817 */
2818 public static function delete_plan($id) {
192569ed 2819 global $DB;
338386aa 2820 static::require_enabled();
192569ed 2821
4db373d5
DM
2822 $plan = new plan($id);
2823
5159d679 2824 if (!$plan->can_manage()) {
9c91a959 2825 $context = context_user::instance($plan->get('userid'));
b90e2205 2826 throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
4db373d5
DM
2827 }
2828
192569ed
JPG
2829 // Wrap the suppression in a DB transaction.
2830 $transaction = $DB->start_delegated_transaction();
2831
357d3862 2832 // Delete plan competencies.
9c91a959 2833 $plancomps = plan_competency::get_records(array('planid' => $plan->get('id')));
357d3862
FM
2834 foreach ($plancomps as $plancomp) {
2835 $plancomp->delete();
2836 }
2837
192569ed 2838 // Delete archive user competencies if the status of the plan is complete.
9c91a959 2839 if ($plan->get('status') == plan::STATUS_COMPLETE) {
192569ed
JPG
2840 self::remove_archived_user_competencies_in_plan($plan);
2841 }
5a1a685c 2842 $event = \core\event\competency_plan_deleted::create_from_plan($plan);
192569ed
JPG
2843 $success = $plan->delete();
2844
2845 $transaction->allow_commit();
2846
4ed524b7
IT
2847 // Trigger deleted event.
2848 $event->trigger();
2849
192569ed 2850 return $success;
4db373d5 2851 }
4f815459 2852
3c230247
FM
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) {
338386aa 2860 static::require_enabled();
3c230247
FM
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()) {
b90e2205 2868 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
3c230247
FM
2869 }
2870
2871 if ($plan->is_based_on_template()) {
2872 throw new coding_exception('Template plans cannot be reviewed.'); // This should never happen.
9c91a959 2873 } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) {
3c230247
FM
2874 throw new coding_exception('The plan review cannot be cancelled at this stage.');
2875 } else if (!$plan->can_request_review()) {
b90e2205 2876 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3c230247
FM
2877 }
2878
9c91a959 2879 $plan->set('status', plan::STATUS_DRAFT);
4ed524b7
IT
2880 $result = $plan->update();
2881
25deee17 2882 // Trigger review request cancelled event.
5a1a685c 2883 \core\event\competency_plan_review_request_cancelled::create_from_plan($plan)->trigger();
4ed524b7
IT
2884
2885 return $result;
3c230247
FM
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) {
338386aa 2895 static::require_enabled();
3c230247
FM
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()) {
b90e2205 2903 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
3c230247
FM
2904 }
2905
2906 if ($plan->is_based_on_template()) {
2907 throw new coding_exception('Template plans cannot be reviewed.'); // This should never happen.
9c91a959 2908 } else if ($plan->get('status') != plan::STATUS_DRAFT) {
3c230247
FM
2909 throw new coding_exception('The plan cannot be sent for review at this stage.');
2910 } else if (!$plan->can_request_review()) {
b90e2205 2911 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3c230247
FM
2912 }
2913
9c91a959 2914 $plan->set('status', plan::STATUS_WAITING_FOR_REVIEW);
4ed524b7
IT
2915 $result = $plan->update();
2916
25deee17 2917 // Trigger review requested event.
5a1a685c 2918 \core\event\competency_plan_review_requested::create_from_plan($plan)->trigger();
4ed524b7
IT
2919
2920 return $result;
3c230247
FM
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;
338386aa 2931 static::require_enabled();
3c230247
FM
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()) {
b90e2205 2939 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
3c230247
FM
2940 }
2941
2942 if ($plan->is_based_on_template()) {
2943 throw new coding_exception('Template plans cannot be reviewed.'); // This should never happen.
9c91a959 2944 } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) {
3c230247
FM
2945 throw new coding_exception('The plan review cannot be started at this stage.');
2946 } else if (!$plan->can_review()) {
b90e2205 2947 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3c230247
FM
2948 }
2949
9c91a959
DW
2950 $plan->set('status', plan::STATUS_IN_REVIEW);
2951 $plan->set('reviewerid', $USER->id);
4ed524b7
IT
2952 $result = $plan->update();
2953
25deee17 2954 // Trigger review started event.
5a1a685c 2955 \core\event\competency_plan_review_started::create_from_plan($plan)->trigger();
4ed524b7
IT
2956
2957 return $result;
3c230247
FM
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) {
338386aa 2967 static::require_enabled();
3c230247
FM
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()) {
b90e2205 2975 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
3c230247
FM
2976 }
2977
2978 if ($plan->is_based_on_template()) {
2979 throw new coding_exception('Template plans cannot be reviewed.'); // This should never happen.
9c91a959 2980 } else if ($plan->get('status') != plan::STATUS_IN_REVIEW) {
3c230247
FM
2981 throw new coding_exception('The plan review cannot be stopped at this stage.');
2982 } else if (!$plan->can_review()) {
b90e2205 2983 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3c230247
FM
2984 }
2985
9c91a959
DW
2986 $plan->set('status', plan::STATUS_DRAFT);
2987 $plan->set('reviewerid', null);
4ed524b7
IT
2988 $result = $plan->update();
2989
25deee17 2990 // Trigger review stopped event.
5a1a685c 2991 \core\event\competency_plan_review_stopped::create_from_plan($plan)->trigger();
4ed524b7
IT
2992
2993 return $result;
3c230247
FM
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) {
338386aa 3005 static::require_enabled();
3c230247
FM
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()) {
b90e2205 3013 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
3c230247
FM
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()) {
b90e2205 3022 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3c230247
FM
3023 }
3024
9c91a959
DW
3025 $plan->set('status', plan::STATUS_ACTIVE);
3026 $plan->set('reviewerid', null);
4ed524b7
IT
3027 $result = $plan->update();
3028
25deee17 3029 // Trigger approved event.
5a1a685c 3030 \core\event\competency_plan_approved::create_from_plan($plan)->trigger();
4ed524b7
IT
3031
3032 return $result;
3c230247
FM
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) {
338386aa 3044 static::require_enabled();
3c230247
FM
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()) {
b90e2205 3052 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
3c230247
FM
3053 }
3054
3055 if ($plan->is_based_on_template()) {
3056 throw new coding_exception('Template plans are always approved.'); // This should never happen.
9c91a959 3057 } else if ($plan->get('status') != plan::STATUS_ACTIVE) {
3c230247
FM
3058 throw new coding_exception('The plan cannot be sent back to draft at this stage.');
3059 } else if (!$plan->can_review()) {
b90e2205 3060 throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3c230247
FM
3061 }
3062
9c91a959 3063 $plan->set('status', plan::STATUS_DRAFT);
4ed524b7
IT
3064 $result = $plan->update();
3065
25deee17 3066 // Trigger unapproved event.
5a1a685c 3067 \core\event\competency_plan_unapproved::create_from_plan($plan)->trigger();
4ed524b7
IT
3068
3069 return $result;
3c230247
FM
3070 }
3071
5bfab685
FM
3072 /**
3073 * Complete a plan.