MDL-55043 core_competency: Sort by id to ensure we get expected result.
[moodle.git] / competency / classes / api.php
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/>.
17 /**
18  * Class for loading/storing competency frameworks from the DB.
19  *
20  * @package    core_competency
21  * @copyright  2015 Damyon Wiese
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 namespace core_competency;
25 defined('MOODLE_INTERNAL') || die();
27 use stdClass;
28 use cm_info;
29 use context;
30 use context_helper;
31 use context_system;
32 use context_course;
33 use context_module;
34 use context_user;
35 use coding_exception;
36 use require_login_exception;
37 use moodle_exception;
38 use moodle_url;
39 use required_capability_exception;
41 /**
42  * Class for doing things with competency frameworks.
43  *
44  * @copyright  2015 Damyon Wiese
45  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46  */
47 class api {
49     /**
50      * Returns whether competencies are enabled.
51      *
52      * This method should never do more than checking the config setting, the reason
53      * being that some other code could be checking the config value directly
54      * to avoid having to load this entire file into memory.
55      *
56      * @return boolean True when enabled.
57      */
58     public static function is_enabled() {
59         return get_config('core_competency', 'enabled');
60     }
62     /**
63      * Throws an exception if competencies are not enabled.
64      *
65      * @return void
66      * @throws moodle_exception
67      */
68     public static function require_enabled() {
69         if (!static::is_enabled()) {
70             throw new moodle_exception('competenciesarenotenabled', 'core_competency');
71         }
72     }
74     /**
75      * Checks whether a scale is used anywhere in the plugin.
76      *
77      * This public API has two exceptions:
78      * - It MUST NOT perform any capability checks.
79      * - It MUST ignore whether competencies are enabled or not ({@link self::is_enabled()}).
80      *
81      * @param int $scaleid The scale ID.
82      * @return bool
83      */
84     public static function is_scale_used_anywhere($scaleid) {
85         global $DB;
86         $sql = "SELECT s.id
87                   FROM {scale} s
88              LEFT JOIN {" . competency_framework::TABLE ."} f
89                     ON f.scaleid = :scaleid1
90              LEFT JOIN {" . competency::TABLE ."} c
91                     ON c.scaleid = :scaleid2
92                  WHERE f.id IS NOT NULL
93                     OR c.id IS NOT NULL";
94         return $DB->record_exists_sql($sql, ['scaleid1' => $scaleid, 'scaleid2' => $scaleid]);
95     }
97     /**
98      * Validate if current user have acces to the course_module if hidden.
99      *
100      * @param mixed $cmmixed The cm_info class, course module record or its ID.
101      * @param bool $throwexception Throw an exception or not.
102      * @return bool
103      */
104     protected static function validate_course_module($cmmixed, $throwexception = true) {
105         $cm = $cmmixed;
106         if (!is_object($cm)) {
107             $cmrecord = get_coursemodule_from_id(null, $cmmixed);
108             $modinfo = get_fast_modinfo($cmrecord->course);
109             $cm = $modinfo->get_cm($cmmixed);
110         } else if (!$cm instanceof cm_info) {
111             // Assume we got a course module record.
112             $modinfo = get_fast_modinfo($cm->course);
113             $cm = $modinfo->get_cm($cm->id);
114         }
116         if (!$cm->uservisible) {
117             if ($throwexception) {
118                 throw new require_login_exception('Course module is hidden');
119             } else {
120                 return false;
121             }
122         }
124         return true;
125     }
127     /**
128      * Validate if current user have acces to the course if hidden.
129      *
130      * @param mixed $courseorid The course or it ID.
131      * @param bool $throwexception Throw an exception or not.
132      * @return bool
133      */
134     protected static function validate_course($courseorid, $throwexception = true) {
135         $course = $courseorid;
136         if (!is_object($course)) {
137             $course = get_course($course);
138         }
140         $coursecontext = context_course::instance($course->id);
141         if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
142             if ($throwexception) {
143                 throw new require_login_exception('Course is hidden');
144             } else {
145                 return false;
146             }
147         }
149         return true;
150     }
152     /**
153      * Create a competency from a record containing all the data for the class.
154      *
155      * Requires moodle/competency:competencymanage capability at the system context.
156      *
157      * @param stdClass $record Record containing all the data for an instance of the class.
158      * @return competency
159      */
160     public static function create_competency(stdClass $record) {
161         static::require_enabled();
162         $competency = new competency(0, $record);
164         // First we do a permissions check.
165         require_capability('moodle/competency:competencymanage', $competency->get_context());
167         // Reset the sortorder, use reorder instead.
168         $competency->set_sortorder(null);
169         $competency->create();
171         \core\event\competency_created::create_from_competency($competency)->trigger();
173         // Reset the rule of the parent.
174         $parent = $competency->get_parent();
175         if ($parent) {
176             $parent->reset_rule();
177             $parent->update();
178         }
180         return $competency;
181     }
183     /**
184      * Delete a competency by id.
185      *
186      * Requires moodle/competency:competencymanage capability at the system context.
187      *
188      * @param int $id The record to delete. This will delete alot of related data - you better be sure.
189      * @return boolean
190      */
191     public static function delete_competency($id) {
192         global $DB;
193         static::require_enabled();
194         $competency = new competency($id);
196         // First we do a permissions check.
197         require_capability('moodle/competency:competencymanage', $competency->get_context());
199         $events = array();
200         $competencyids = array(intval($competency->get_id()));
201         $contextid = $competency->get_context()->id;
202         $competencyids = array_merge(competency::get_descendants_ids($competency), $competencyids);
203         if (!competency::can_all_be_deleted($competencyids)) {
204             return false;
205         }
206         $transaction = $DB->start_delegated_transaction();
208         try {
210             // Reset the rule of the parent.
211             $parent = $competency->get_parent();
212             if ($parent) {
213                 $parent->reset_rule();
214                 $parent->update();
215             }
217             // Delete the competency separately so the after_delete event can be triggered.
218             $competency->delete();
220             // Delete the competencies.
221             competency::delete_multiple($competencyids);
223             // Delete the competencies relation.
224             related_competency::delete_multiple_relations($competencyids);
226             // Delete competency evidences.
227             user_evidence_competency::delete_by_competencyids($competencyids);
229             // Register the competencies deleted events.
230             $events = \core\event\competency_deleted::create_multiple_from_competencyids($competencyids, $contextid);
232         } catch (\Exception $e) {
233             $transaction->rollback($e);
234         }
236         $transaction->allow_commit();
237         // Trigger events.
238         foreach ($events as $event) {
239             $event->trigger();
240         }
242         return true;
243     }
245     /**
246      * Reorder this competency.
247      *
248      * Requires moodle/competency:competencymanage capability at the system context.
249      *
250      * @param int $id The id of the competency to move.
251      * @return boolean
252      */
253     public static function move_down_competency($id) {
254         static::require_enabled();
255         $current = new competency($id);
257         // First we do a permissions check.
258         require_capability('moodle/competency:competencymanage', $current->get_context());
260         $max = self::count_competencies(array('parentid' => $current->get_parentid(),
261                                               'competencyframeworkid' => $current->get_competencyframeworkid()));
262         if ($max > 0) {
263             $max--;
264         }
266         $sortorder = $current->get_sortorder();
267         if ($sortorder >= $max) {
268             return false;
269         }
270         $sortorder = $sortorder + 1;
271         $current->set_sortorder($sortorder);
273         $filters = array('parentid' => $current->get_parentid(),
274                          'competencyframeworkid' => $current->get_competencyframeworkid(),
275                          'sortorder' => $sortorder);
276         $children = self::list_competencies($filters, 'id');
277         foreach ($children as $needtoswap) {
278             $needtoswap->set_sortorder($sortorder - 1);
279             $needtoswap->update();
280         }
282         // OK - all set.
283         $result = $current->update();
285         return $result;
286     }
288     /**
289      * Reorder this competency.
290      *
291      * Requires moodle/competency:competencymanage capability at the system context.
292      *
293      * @param int $id The id of the competency to move.
294      * @return boolean
295      */
296     public static function move_up_competency($id) {
297         static::require_enabled();
298         $current = new competency($id);
300         // First we do a permissions check.
301         require_capability('moodle/competency:competencymanage', $current->get_context());
303         $sortorder = $current->get_sortorder();
304         if ($sortorder == 0) {
305             return false;
306         }
308         $sortorder = $sortorder - 1;
309         $current->set_sortorder($sortorder);
311         $filters = array('parentid' => $current->get_parentid(),
312                          'competencyframeworkid' => $current->get_competencyframeworkid(),
313                          'sortorder' => $sortorder);
314         $children = self::list_competencies($filters, 'id');
315         foreach ($children as $needtoswap) {
316             $needtoswap->set_sortorder($sortorder + 1);
317             $needtoswap->update();
318         }
320         // OK - all set.
321         $result = $current->update();
323         return $result;
324     }
326     /**
327      * Move this competency so it sits in a new parent.
328      *
329      * Requires moodle/competency:competencymanage capability at the system context.
330      *
331      * @param int $id The id of the competency to move.
332      * @param int $newparentid The new parent id for the competency.
333      * @return boolean
334      */
335     public static function set_parent_competency($id, $newparentid) {
336         global $DB;
337         static::require_enabled();
338         $current = new competency($id);
340         // First we do a permissions check.
341         require_capability('moodle/competency:competencymanage', $current->get_context());
342         if ($id == $newparentid) {
343             throw new coding_exception('Can not set a competency as a parent of itself.');
344         } if ($newparentid == $current->get_parentid()) {
345             throw new coding_exception('Can not move a competency to the same location.');
346         }
348         // Some great variable assignment right here.
349         $currentparent = $current->get_parent();
350         $parent = !empty($newparentid) ? new competency($newparentid) : null;
351         $parentpath = !empty($parent) ? $parent->get_path() : '/0/';
353         // We're going to change quite a few things.
354         $transaction = $DB->start_delegated_transaction();
356         // If we are moving a node to a child of itself:
357         // - promote all the child nodes by one level.
358         // - remove the rule on self.
359         // - re-read the parent.
360         $newparents = explode('/', $parentpath);
361         if (in_array($current->get_id(), $newparents)) {
362             $children = competency::get_records(array('parentid' => $current->get_id()), 'id');
363             foreach ($children as $child) {
364                 $child->set_parentid($current->get_parentid());
365                 $child->update();
366             }
368             // Reset the rule on self as our children have changed.
369             $current->reset_rule();
371             // The destination parent is one of our descendants, we need to re-fetch its values (path, parentid).
372             $parent->read();
373         }
375         // Reset the rules of initial parent and destination.
376         if (!empty($currentparent)) {
377             $currentparent->reset_rule();
378             $currentparent->update();
379         }
380         if (!empty($parent)) {
381             $parent->reset_rule();
382             $parent->update();
383         }
385         // Do the actual move.
386         $current->set_parentid($newparentid);
387         $result = $current->update();
389         // All right, let's commit this.
390         $transaction->allow_commit();
392         return $result;
393     }
395     /**
396      * Update the details for a competency.
397      *
398      * Requires moodle/competency:competencymanage capability at the system context.
399      *
400      * @param stdClass $record The new details for the competency.
401      *                         Note - must contain an id that points to the competency to update.
402      *
403      * @return boolean
404      */
405     public static function update_competency($record) {
406         static::require_enabled();
407         $competency = new competency($record->id);
409         // First we do a permissions check.
410         require_capability('moodle/competency:competencymanage', $competency->get_context());
412         // Some things should not be changed in an update - they should use a more specific method.
413         $record->sortorder = $competency->get_sortorder();
414         $record->parentid = $competency->get_parentid();
415         $record->competencyframeworkid = $competency->get_competencyframeworkid();
417         $competency->from_record($record);
418         require_capability('moodle/competency:competencymanage', $competency->get_context());
420         // OK - all set.
421         $result = $competency->update();
423         // Trigger the update event.
424         \core\event\competency_updated::create_from_competency($competency)->trigger();
426         return $result;
427     }
429     /**
430      * Read a the details for a single competency and return a record.
431      *
432      * Requires moodle/competency:competencyview capability at the system context.
433      *
434      * @param int $id The id of the competency to read.
435      * @param bool $includerelated Include related tags or not.
436      * @return stdClass
437      */
438     public static function read_competency($id, $includerelated = false) {
439         static::require_enabled();
440         $competency = new competency($id);
442         // First we do a permissions check.
443         $context = $competency->get_context();
444         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
445              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
446         }
448         // OK - all set.
449         if ($includerelated) {
450             $relatedcompetency = new related_competency();
451             if ($related = $relatedcompetency->list_relations($id)) {
452                 $competency->relatedcompetencies = $related;
453             }
454         }
456         return $competency;
457     }
459     /**
460      * Perform a text search based and return all results and their parents.
461      *
462      * Requires moodle/competency:competencyview capability at the framework context.
463      *
464      * @param string $textsearch A string to search for.
465      * @param int $competencyframeworkid The id of the framework to limit the search.
466      * @return array of competencies
467      */
468     public static function search_competencies($textsearch, $competencyframeworkid) {
469         static::require_enabled();
470         $framework = new competency_framework($competencyframeworkid);
472         // First we do a permissions check.
473         $context = $framework->get_context();
474         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
475              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
476         }
478         // OK - all set.
479         $competencies = competency::search($textsearch, $competencyframeworkid);
480         return $competencies;
481     }
483     /**
484      * Perform a search based on the provided filters and return a paginated list of records.
485      *
486      * Requires moodle/competency:competencyview capability at some context.
487      *
488      * @param array $filters A list of filters to apply to the list.
489      * @param string $sort The column to sort on
490      * @param string $order ('ASC' or 'DESC')
491      * @param int $skip Number of records to skip (pagination)
492      * @param int $limit Max of records to return (pagination)
493      * @return array of competencies
494      */
495     public static function list_competencies($filters, $sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
496         static::require_enabled();
497         if (!isset($filters['competencyframeworkid'])) {
498             $context = context_system::instance();
499         } else {
500             $framework = new competency_framework($filters['competencyframeworkid']);
501             $context = $framework->get_context();
502         }
504         // First we do a permissions check.
505         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
506              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
507         }
509         // OK - all set.
510         return competency::get_records($filters, $sort, $order, $skip, $limit);
511     }
513     /**
514      * Perform a search based on the provided filters and return a paginated list of records.
515      *
516      * Requires moodle/competency:competencyview capability at some context.
517      *
518      * @param array $filters A list of filters to apply to the list.
519      * @return int
520      */
521     public static function count_competencies($filters) {
522         static::require_enabled();
523         if (!isset($filters['competencyframeworkid'])) {
524             $context = context_system::instance();
525         } else {
526             $framework = new competency_framework($filters['competencyframeworkid']);
527             $context = $framework->get_context();
528         }
530         // First we do a permissions check.
531         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
532              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
533         }
535         // OK - all set.
536         return competency::count_records($filters);
537     }
539     /**
540      * Create a competency framework from a record containing all the data for the class.
541      *
542      * Requires moodle/competency:competencymanage capability at the system context.
543      *
544      * @param stdClass $record Record containing all the data for an instance of the class.
545      * @return competency_framework
546      */
547     public static function create_framework(stdClass $record) {
548         static::require_enabled();
549         $framework = new competency_framework(0, $record);
550         require_capability('moodle/competency:competencymanage', $framework->get_context());
552         // Account for different formats of taxonomies.
553         if (isset($record->taxonomies)) {
554             $framework->set_taxonomies($record->taxonomies);
555         }
557         $framework = $framework->create();
559         // Trigger a competency framework created event.
560         \core\event\competency_framework_created::create_from_framework($framework)->trigger();
562         return $framework;
563     }
565     /**
566      * Duplicate a competency framework by id.
567      *
568      * Requires moodle/competency:competencymanage capability at the system context.
569      *
570      * @param int $id The record to duplicate. All competencies associated and related will be duplicated.
571      * @return competency_framework the framework duplicated
572      */
573     public static function duplicate_framework($id) {
574         global $DB;
575         static::require_enabled();
577         $framework = new competency_framework($id);
578         require_capability('moodle/competency:competencymanage', $framework->get_context());
579         // Starting transaction.
580         $transaction = $DB->start_delegated_transaction();
582         try {
583             // Get a uniq idnumber based on the origin framework.
584             $idnumber = competency_framework::get_unused_idnumber($framework->get_idnumber());
585             $framework->set_idnumber($idnumber);
586             // Adding the suffix copy to the shortname.
587             $framework->set_shortname(get_string('duplicateditemname', 'core_competency', $framework->get_shortname()));
588             $framework->set_id(0);
589             $framework = $framework->create();
591             // Array that match the old competencies ids with the new one to use when copying related competencies.
592             $frameworkcompetency = competency::get_framework_tree($id);
593             $matchids = self::duplicate_competency_tree($framework->get_id(), $frameworkcompetency, 0, 0);
595             // Copy the related competencies.
596             $relcomps = related_competency::get_multiple_relations(array_keys($matchids));
598             foreach ($relcomps as $relcomp) {
599                 $compid = $relcomp->get_competencyid();
600                 $relcompid = $relcomp->get_relatedcompetencyid();
601                 if (isset($matchids[$compid]) && isset($matchids[$relcompid])) {
602                     $newcompid = $matchids[$compid]->get_id();
603                     $newrelcompid = $matchids[$relcompid]->get_id();
604                     if ($newcompid < $newrelcompid) {
605                         $relcomp->set_competencyid($newcompid);
606                         $relcomp->set_relatedcompetencyid($newrelcompid);
607                     } else {
608                         $relcomp->set_competencyid($newrelcompid);
609                         $relcomp->set_relatedcompetencyid($newcompid);
610                     }
611                     $relcomp->set_id(0);
612                     $relcomp->create();
613                 } else {
614                     // Debugging message when there is no match found.
615                     debugging('related competency id not found');
616                 }
617             }
619             // Setting rules on duplicated competencies.
620             self::migrate_competency_tree_rules($frameworkcompetency, $matchids);
622             $transaction->allow_commit();
624         } catch (\Exception $e) {
625             $transaction->rollback($e);
626         }
628         // Trigger a competency framework created event.
629         \core\event\competency_framework_created::create_from_framework($framework)->trigger();
631         return $framework;
632     }
634     /**
635      * Delete a competency framework by id.
636      *
637      * Requires moodle/competency:competencymanage capability at the system context.
638      *
639      * @param int $id The record to delete. This will delete alot of related data - you better be sure.
640      * @return boolean
641      */
642     public static function delete_framework($id) {
643         global $DB;
644         static::require_enabled();
645         $framework = new competency_framework($id);
646         require_capability('moodle/competency:competencymanage', $framework->get_context());
648         $events = array();
649         $competenciesid = competency::get_ids_by_frameworkid($id);
650         $contextid = $framework->get_contextid();
651         if (!competency::can_all_be_deleted($competenciesid)) {
652             return false;
653         }
654         $transaction = $DB->start_delegated_transaction();
655         try {
656             if (!empty($competenciesid)) {
657                 // Delete competencies.
658                 competency::delete_by_frameworkid($id);
660                 // Delete the related competencies.
661                 related_competency::delete_multiple_relations($competenciesid);
663                 // Delete the evidences for competencies.
664                 user_evidence_competency::delete_by_competencyids($competenciesid);
665             }
667             // Create a competency framework deleted event.
668             $event = \core\event\competency_framework_deleted::create_from_framework($framework);
669             $result = $framework->delete();
671             // Register the deleted events competencies.
672             $events = \core\event\competency_deleted::create_multiple_from_competencyids($competenciesid, $contextid);
674         } catch (\Exception $e) {
675             $transaction->rollback($e);
676         }
678         // Commit the transaction.
679         $transaction->allow_commit();
681         // If all operations are successfull then trigger the delete event.
682         $event->trigger();
684         // Trigger deleted event competencies.
685         foreach ($events as $event) {
686             $event->trigger();
687         }
689         return $result;
690     }
692     /**
693      * Update the details for a competency framework.
694      *
695      * Requires moodle/competency:competencymanage capability at the system context.
696      *
697      * @param stdClass $record The new details for the framework. Note - must contain an id that points to the framework to update.
698      * @return boolean
699      */
700     public static function update_framework($record) {
701         static::require_enabled();
702         $framework = new competency_framework($record->id);
704         // Check the permissions before update.
705         require_capability('moodle/competency:competencymanage', $framework->get_context());
707         // Account for different formats of taxonomies.
708         $framework->from_record($record);
709         if (isset($record->taxonomies)) {
710             $framework->set_taxonomies($record->taxonomies);
711         }
713         // Trigger a competency framework updated event.
714         \core\event\competency_framework_updated::create_from_framework($framework)->trigger();
716         return $framework->update();
717     }
719     /**
720      * Read a the details for a single competency framework and return a record.
721      *
722      * Requires moodle/competency:competencyview capability at the system context.
723      *
724      * @param int $id The id of the framework to read.
725      * @return competency_framework
726      */
727     public static function read_framework($id) {
728         static::require_enabled();
729         $framework = new competency_framework($id);
730         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
731                 $framework->get_context())) {
732             throw new required_capability_exception($framework->get_context(), 'moodle/competency:competencyview',
733                 'nopermissions', '');
734         }
735         return $framework;
736     }
738     /**
739      * Logg the competency framework viewed event.
740      *
741      * @param competency_framework|int $frameworkorid The competency_framework object or competency framework id
742      * @return bool
743      */
744     public static function competency_framework_viewed($frameworkorid) {
745         static::require_enabled();
746         $framework = $frameworkorid;
747         if (!is_object($framework)) {
748             $framework = new competency_framework($framework);
749         }
750         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
751                 $framework->get_context())) {
752             throw new required_capability_exception($framework->get_context(), 'moodle/competency:competencyview',
753                 'nopermissions', '');
754         }
755         \core\event\competency_framework_viewed::create_from_framework($framework)->trigger();
756         return true;
757     }
759     /**
760      * Logg the competency viewed event.
761      *
762      * @param competency|int $competencyorid The competency object or competency id
763      * @return bool
764      */
765     public static function competency_viewed($competencyorid) {
766         static::require_enabled();
767         $competency = $competencyorid;
768         if (!is_object($competency)) {
769             $competency = new competency($competency);
770         }
772         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
773                 $competency->get_context())) {
774             throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview',
775                 'nopermissions', '');
776         }
778         \core\event\competency_viewed::create_from_competency($competency)->trigger();
779         return true;
780     }
782     /**
783      * Perform a search based on the provided filters and return a paginated list of records.
784      *
785      * Requires moodle/competency:competencyview capability at the system context.
786      *
787      * @param string $sort The column to sort on
788      * @param string $order ('ASC' or 'DESC')
789      * @param int $skip Number of records to skip (pagination)
790      * @param int $limit Max of records to return (pagination)
791      * @param context $context The parent context of the frameworks.
792      * @param string $includes Defines what other contexts to fetch frameworks from.
793      *                         Accepted values are:
794      *                          - children: All descendants
795      *                          - parents: All parents, grand parents, etc...
796      *                          - self: Context passed only.
797      * @param bool $onlyvisible If true return only visible frameworks
798      * @param string $query A string to use to filter down the frameworks.
799      * @return array of competency_framework
800      */
801     public static function list_frameworks($sort, $order, $skip, $limit, $context, $includes = 'children',
802                                            $onlyvisible = false, $query = '') {
803         global $DB;
804         static::require_enabled();
806         // Get all the relevant contexts.
807         $contexts = self::get_related_contexts($context, $includes,
808             array('moodle/competency:competencyview', 'moodle/competency:competencymanage'));
810         if (empty($contexts)) {
811             throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
812         }
814         // OK - all set.
815         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
816         $select = "contextid $insql";
817         if ($onlyvisible) {
818             $select .= " AND visible = :visible";
819             $inparams['visible'] = 1;
820         }
822         if (!empty($query) || is_numeric($query)) {
823             $sqlnamelike = $DB->sql_like('shortname', ':namelike', false);
824             $sqlidnlike = $DB->sql_like('idnumber', ':idnlike', false);
826             $select .= " AND ($sqlnamelike OR $sqlidnlike) ";
827             $inparams['namelike'] = '%' . $DB->sql_like_escape($query) . '%';
828             $inparams['idnlike'] = '%' . $DB->sql_like_escape($query) . '%';
829         }
831         return competency_framework::get_records_select($select, $inparams, $sort . ' ' . $order, '*', $skip, $limit);
832     }
834     /**
835      * Perform a search based on the provided filters and return a paginated list of records.
836      *
837      * Requires moodle/competency:competencyview capability at the system context.
838      *
839      * @param context $context The parent context of the frameworks.
840      * @param string $includes Defines what other contexts to fetch frameworks from.
841      *                         Accepted values are:
842      *                          - children: All descendants
843      *                          - parents: All parents, grand parents, etc...
844      *                          - self: Context passed only.
845      * @return int
846      */
847     public static function count_frameworks($context, $includes) {
848         global $DB;
849         static::require_enabled();
851         // Get all the relevant contexts.
852         $contexts = self::get_related_contexts($context, $includes,
853             array('moodle/competency:competencyview', 'moodle/competency:competencymanage'));
855         if (empty($contexts)) {
856             throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
857         }
859         // OK - all set.
860         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
861         return competency_framework::count_records_select("contextid $insql", $inparams);
862     }
864     /**
865      * Fetches all the relevant contexts.
866      *
867      * Note: This currently only supports system, category and user contexts. However user contexts
868      * behave a bit differently and will fallback on the system context. This is what makes the most
869      * sense because a user context does not have descendants, and only has system as a parent.
870      *
871      * @param context $context The context to start from.
872      * @param string $includes Defines what other contexts to find.
873      *                         Accepted values are:
874      *                          - children: All descendants
875      *                          - parents: All parents, grand parents, etc...
876      *                          - self: Context passed only.
877      * @param array $hasanycapability Array of capabilities passed to {@link has_any_capability()} in each context.
878      * @return context[] An array of contexts where keys are context IDs.
879      */
880     public static function get_related_contexts($context, $includes, array $hasanycapability = null) {
881         global $DB;
882         static::require_enabled();
884         if (!in_array($includes, array('children', 'parents', 'self'))) {
885             throw new coding_exception('Invalid parameter value for \'includes\'.');
886         }
888         // If context user swap it for the context_system.
889         if ($context->contextlevel == CONTEXT_USER) {
890             $context = context_system::instance();
891         }
893         $contexts = array($context->id => $context);
895         if ($includes == 'children') {
896             $params = array('coursecatlevel' => CONTEXT_COURSECAT, 'path' => $context->path . '/%');
897             $pathlike = $DB->sql_like('path', ':path');
898             $sql = "contextlevel = :coursecatlevel AND $pathlike";
899             $rs = $DB->get_recordset_select('context', $sql, $params);
900             foreach ($rs as $record) {
901                 $ctxid = $record->id;
902                 context_helper::preload_from_record($record);
903                 $contexts[$ctxid] = context::instance_by_id($ctxid);
904             }
905             $rs->close();
907         } else if ($includes == 'parents') {
908             $children = $context->get_parent_contexts();
909             foreach ($children as $ctx) {
910                 $contexts[$ctx->id] = $ctx;
911             }
912         }
914         // Filter according to the capabilities required.
915         if (!empty($hasanycapability)) {
916             foreach ($contexts as $key => $ctx) {
917                 if (!has_any_capability($hasanycapability, $ctx)) {
918                     unset($contexts[$key]);
919                 }
920             }
921         }
923         return $contexts;
924     }
926     /**
927      * Count all the courses using a competency.
928      *
929      * @param int $competencyid The id of the competency to check.
930      * @return int
931      */
932     public static function count_courses_using_competency($competencyid) {
933         static::require_enabled();
935         // OK - all set.
936         $courses = course_competency::list_courses_min($competencyid);
937         $count = 0;
939         // Now check permissions on each course.
940         foreach ($courses as $course) {
941             if (!self::validate_course($course, false)) {
942                 continue;
943             }
945             $context = context_course::instance($course->id);
946             $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
947             if (!has_any_capability($capabilities, $context)) {
948                 continue;
949             }
951             $count++;
952         }
954         return $count;
955     }
957     /**
958      * List all the courses modules using a competency in a course.
959      *
960      * @param int $competencyid The id of the competency to check.
961      * @param int $courseid The id of the course to check.
962      * @return array[int] Array of course modules ids.
963      */
964     public static function list_course_modules_using_competency($competencyid, $courseid) {
965         static::require_enabled();
967         $result = array();
968         self::validate_course($courseid);
970         $coursecontext = context_course::instance($courseid);
972         // We will not check each module - course permissions should be enough.
973         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
974         if (!has_any_capability($capabilities, $coursecontext)) {
975             throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
976         }
978         $cmlist = course_module_competency::list_course_modules($competencyid, $courseid);
979         foreach ($cmlist as $cmid) {
980             if (self::validate_course_module($cmid, false)) {
981                 array_push($result, $cmid);
982             }
983         }
985         return $result;
986     }
988     /**
989      * List all the competencies linked to a course module.
990      *
991      * @param mixed $cmorid The course module, or its ID.
992      * @return array[competency] Array of competency records.
993      */
994     public static function list_course_module_competencies_in_course_module($cmorid) {
995         static::require_enabled();
996         $cm = $cmorid;
997         if (!is_object($cmorid)) {
998             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
999         }
1001         // Check the user have access to the course module.
1002         self::validate_course_module($cm);
1003         $context = context_module::instance($cm->id);
1005         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1006         if (!has_any_capability($capabilities, $context)) {
1007             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1008         }
1010         $result = array();
1012         $cmclist = course_module_competency::list_course_module_competencies($cm->id);
1013         foreach ($cmclist as $id => $cmc) {
1014             array_push($result, $cmc);
1015         }
1017         return $result;
1018     }
1020     /**
1021      * List all the courses using a competency.
1022      *
1023      * @param int $competencyid The id of the competency to check.
1024      * @return array[stdClass] Array of stdClass containing id and shortname.
1025      */
1026     public static function list_courses_using_competency($competencyid) {
1027         static::require_enabled();
1029         // OK - all set.
1030         $courses = course_competency::list_courses($competencyid);
1031         $result = array();
1033         // Now check permissions on each course.
1034         foreach ($courses as $id => $course) {
1035             $context = context_course::instance($course->id);
1036             $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1037             if (!has_any_capability($capabilities, $context)) {
1038                 unset($courses[$id]);
1039                 continue;
1040             }
1041             if (!self::validate_course($course, false)) {
1042                 unset($courses[$id]);
1043                 continue;
1044             }
1045             array_push($result, $course);
1046         }
1048         return $result;
1049     }
1051     /**
1052      * Count the proficient competencies in a course for one user.
1053      *
1054      * @param int $courseid The id of the course to check.
1055      * @param int $userid The id of the user to check.
1056      * @return int
1057      */
1058     public static function count_proficient_competencies_in_course_for_user($courseid, $userid) {
1059         static::require_enabled();
1060         // Check the user have access to the course.
1061         self::validate_course($courseid);
1063         // First we do a permissions check.
1064         $context = context_course::instance($courseid);
1066         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1067         if (!has_any_capability($capabilities, $context)) {
1068              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1069         }
1071         // OK - all set.
1072         return user_competency_course::count_proficient_competencies($courseid, $userid);
1073     }
1075     /**
1076      * Count all the competencies in a course.
1077      *
1078      * @param int $courseid The id of the course to check.
1079      * @return int
1080      */
1081     public static function count_competencies_in_course($courseid) {
1082         static::require_enabled();
1083         // Check the user have access to the course.
1084         self::validate_course($courseid);
1086         // First we do a permissions check.
1087         $context = context_course::instance($courseid);
1089         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1090         if (!has_any_capability($capabilities, $context)) {
1091              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1092         }
1094         // OK - all set.
1095         return course_competency::count_competencies($courseid);
1096     }
1098     /**
1099      * List the competencies associated to a course.
1100      *
1101      * @param mixed $courseorid The course, or its ID.
1102      * @return array( array(
1103      *                   'competency' => \core_competency\competency,
1104      *                   'coursecompetency' => \core_competency\course_competency
1105      *              ))
1106      */
1107     public static function list_course_competencies($courseorid) {
1108         static::require_enabled();
1109         $course = $courseorid;
1110         if (!is_object($courseorid)) {
1111             $course = get_course($courseorid);
1112         }
1114         // Check the user have access to the course.
1115         self::validate_course($course);
1116         $context = context_course::instance($course->id);
1118         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1119         if (!has_any_capability($capabilities, $context)) {
1120             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1121         }
1123         $result = array();
1125         // TODO We could improve the performance of this into one single query.
1126         $coursecompetencies = course_competency::list_course_competencies($course->id);
1127         $competencies = course_competency::list_competencies($course->id);
1129         // Build the return values.
1130         foreach ($coursecompetencies as $key => $coursecompetency) {
1131             $result[] = array(
1132                 'competency' => $competencies[$coursecompetency->get_competencyid()],
1133                 'coursecompetency' => $coursecompetency
1134             );
1135         }
1137         return $result;
1138     }
1140     /**
1141      * Get a user competency.
1142      *
1143      * @param int $userid The user ID.
1144      * @param int $competencyid The competency ID.
1145      * @return user_competency
1146      */
1147     public static function get_user_competency($userid, $competencyid) {
1148         static::require_enabled();
1149         $existing = user_competency::get_multiple($userid, array($competencyid));
1150         $uc = array_pop($existing);
1152         if (!$uc) {
1153             $uc = user_competency::create_relation($userid, $competencyid);
1154             $uc->create();
1155         }
1157         if (!$uc->can_read()) {
1158             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
1159                 'nopermissions', '');
1160         }
1161         return $uc;
1162     }
1164     /**
1165      * Get a user competency by ID.
1166      *
1167      * @param int $usercompetencyid The user competency ID.
1168      * @return user_competency
1169      */
1170     public static function get_user_competency_by_id($usercompetencyid) {
1171         static::require_enabled();
1172         $uc = new user_competency($usercompetencyid);
1173         if (!$uc->can_read()) {
1174             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
1175                 'nopermissions', '');
1176         }
1177         return $uc;
1178     }
1180     /**
1181      * List the competencies associated to a course module.
1182      *
1183      * @param mixed $cmorid The course module, or its ID.
1184      * @return array( array(
1185      *                   'competency' => \core_competency\competency,
1186      *                   'coursemodulecompetency' => \core_competency\course_module_competency
1187      *              ))
1188      */
1189     public static function list_course_module_competencies($cmorid) {
1190         static::require_enabled();
1191         $cm = $cmorid;
1192         if (!is_object($cmorid)) {
1193             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1194         }
1196         // Check the user have access to the course module.
1197         self::validate_course_module($cm);
1198         $context = context_module::instance($cm->id);
1200         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1201         if (!has_any_capability($capabilities, $context)) {
1202             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1203         }
1205         $result = array();
1207         // TODO We could improve the performance of this into one single query.
1208         $coursemodulecompetencies = course_competency::list_course_module_competencies($cm->id);
1209         $competencies = course_module_competency::list_competencies($cm->id);
1211         // Build the return values.
1212         foreach ($coursemodulecompetencies as $key => $coursemodulecompetency) {
1213             $result[] = array(
1214                 'competency' => $competencies[$coursemodulecompetency->get_competencyid()],
1215                 'coursemodulecompetency' => $coursemodulecompetency
1216             );
1217         }
1219         return $result;
1220     }
1222     /**
1223      * Get a user competency in a course.
1224      *
1225      * @param int $courseid The id of the course to check.
1226      * @param int $userid The id of the course to check.
1227      * @param int $competencyid The id of the competency.
1228      * @return user_competency_course
1229      */
1230     public static function get_user_competency_in_course($courseid, $userid, $competencyid) {
1231         static::require_enabled();
1232         // First we do a permissions check.
1233         $context = context_course::instance($courseid);
1235         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1236         if (!has_any_capability($capabilities, $context)) {
1237             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1238         } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
1239             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
1240         }
1242         // This will throw an exception if the competency does not belong to the course.
1243         $competency = course_competency::get_competency($courseid, $competencyid);
1245         $params = array('courseid' => $courseid, 'userid' => $userid, 'competencyid' => $competencyid);
1246         $exists = user_competency_course::get_record($params);
1247         // Create missing.
1248         if ($exists) {
1249             $ucc = $exists;
1250         } else {
1251             $ucc = user_competency_course::create_relation($userid, $competency->get_id(), $courseid);
1252             $ucc->create();
1253         }
1255         return $ucc;
1256     }
1258     /**
1259      * List all the user competencies in a course.
1260      *
1261      * @param int $courseid The id of the course to check.
1262      * @param int $userid The id of the course to check.
1263      * @return array of user_competency_course objects
1264      */
1265     public static function list_user_competencies_in_course($courseid, $userid) {
1266         static::require_enabled();
1267         // First we do a permissions check.
1268         $context = context_course::instance($courseid);
1269         $onlyvisible = 1;
1271         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1272         if (!has_any_capability($capabilities, $context)) {
1273             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1274         } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
1275             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
1276         }
1278         // OK - all set.
1279         $competencylist = course_competency::list_competencies($courseid, false);
1281         $existing = user_competency_course::get_multiple($userid, $courseid, $competencylist);
1282         // Create missing.
1283         $orderedusercompetencycourses = array();
1285         $somemissing = false;
1286         foreach ($competencylist as $coursecompetency) {
1287             $found = false;
1288             foreach ($existing as $usercompetencycourse) {
1289                 if ($usercompetencycourse->get_competencyid() == $coursecompetency->get_id()) {
1290                     $found = true;
1291                     $orderedusercompetencycourses[$usercompetencycourse->get_id()] = $usercompetencycourse;
1292                     break;
1293                 }
1294             }
1295             if (!$found) {
1296                 $ucc = user_competency_course::create_relation($userid, $coursecompetency->get_id(), $courseid);
1297                 $ucc->create();
1298                 $orderedusercompetencycourses[$ucc->get_id()] = $ucc;
1299             }
1300         }
1302         return $orderedusercompetencycourses;
1303     }
1305     /**
1306      * List the user competencies to review.
1307      *
1308      * The method returns values in this format:
1309      *
1310      * array(
1311      *     'competencies' => array(
1312      *         (stdClass)(
1313      *             'usercompetency' => (user_competency),
1314      *             'competency' => (competency),
1315      *             'user' => (user)
1316      *         )
1317      *     ),
1318      *     'count' => (int)
1319      * )
1320      *
1321      * @param int $skip The number of records to skip.
1322      * @param int $limit The number of results to return.
1323      * @param int $userid The user we're getting the competencies to review for.
1324      * @return array Containing the keys 'count', and 'competencies'. The 'competencies' key contains an object
1325      *               which contains 'competency', 'usercompetency' and 'user'.
1326      */
1327     public static function list_user_competencies_to_review($skip = 0, $limit = 50, $userid = null) {
1328         global $DB, $USER;
1329         static::require_enabled();
1330         if ($userid === null) {
1331             $userid = $USER->id;
1332         }
1334         $capability = 'moodle/competency:usercompetencyreview';
1335         $ucfields = user_competency::get_sql_fields('uc', 'uc_');
1336         $compfields = competency::get_sql_fields('c', 'c_');
1337         $usercols = array('id') + get_user_fieldnames();
1338         $userfields = array();
1339         foreach ($usercols as $field) {
1340             $userfields[] = "u." . $field . " AS usr_" . $field;
1341         }
1342         $userfields = implode(',', $userfields);
1344         $select = "SELECT $ucfields, $compfields, $userfields";
1345         $countselect = "SELECT COUNT('x')";
1346         $sql = "  FROM {" . user_competency::TABLE . "} uc
1347                   JOIN {" . competency::TABLE . "} c
1348                     ON c.id = uc.competencyid
1349                   JOIN {user} u
1350                     ON u.id = uc.userid
1351                  WHERE (uc.status = :waitingforreview
1352                     OR (uc.status = :inreview AND uc.reviewerid = :reviewerid))";
1353         $ordersql = " ORDER BY c.shortname ASC";
1354         $params = array(
1355             'inreview' => user_competency::STATUS_IN_REVIEW,
1356             'reviewerid' => $userid,
1357             'waitingforreview' => user_competency::STATUS_WAITING_FOR_REVIEW,
1358         );
1359         $countsql = $countselect . $sql;
1361         // Primary check to avoid the hard work of getting the users in which the user has permission.
1362         $count = $DB->count_records_sql($countselect . $sql, $params);
1363         if ($count < 1) {
1364             return array('count' => 0, 'competencies' => array());
1365         }
1367         // TODO MDL-52243 Use core function.
1368         list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql(
1369             $capability, $userid, SQL_PARAMS_NAMED);
1370         $params += $inparams;
1371         $countsql = $countselect . $sql . " AND uc.userid $insql";
1372         $getsql = $select . $sql . " AND uc.userid $insql " . $ordersql;
1374         // Extracting the results.
1375         $competencies = array();
1376         $records = $DB->get_recordset_sql($getsql, $params, $skip, $limit);
1377         foreach ($records as $record) {
1378             $objects = (object) array(
1379                 'usercompetency' => new user_competency(0, user_competency::extract_record($record, 'uc_')),
1380                 'competency' => new competency(0, competency::extract_record($record, 'c_')),
1381                 'user' => persistent::extract_record($record, 'usr_'),
1382             );
1383             $competencies[] = $objects;
1384         }
1385         $records->close();
1387         return array(
1388             'count' => $DB->count_records_sql($countsql, $params),
1389             'competencies' => $competencies
1390         );
1391     }
1393     /**
1394      * Add a competency to this course module.
1395      *
1396      * @param mixed $cmorid The course module, or id of the course module
1397      * @param int $competencyid The id of the competency
1398      * @return bool
1399      */
1400     public static function add_competency_to_course_module($cmorid, $competencyid) {
1401         static::require_enabled();
1402         $cm = $cmorid;
1403         if (!is_object($cmorid)) {
1404             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1405         }
1407         // Check the user have access to the course module.
1408         self::validate_course_module($cm);
1410         // First we do a permissions check.
1411         $context = context_module::instance($cm->id);
1413         require_capability('moodle/competency:coursecompetencymanage', $context);
1415         // Check that the competency belongs to the course.
1416         $exists = course_competency::get_records(array('courseid' => $cm->course, 'competencyid' => $competencyid));
1417         if (!$exists) {
1418             throw new coding_exception('Cannot add a competency to a module if it does not belong to the course');
1419         }
1421         $record = new stdClass();
1422         $record->cmid = $cm->id;
1423         $record->competencyid = $competencyid;
1425         $coursemodulecompetency = new course_module_competency();
1426         $exists = $coursemodulecompetency->get_records(array('cmid' => $cm->id, 'competencyid' => $competencyid));
1427         if (!$exists) {
1428             $coursemodulecompetency->from_record($record);
1429             if ($coursemodulecompetency->create()) {
1430                 return true;
1431             }
1432         }
1433         return false;
1434     }
1436     /**
1437      * Remove a competency from this course module.
1438      *
1439      * @param mixed $cmorid The course module, or id of the course module
1440      * @param int $competencyid The id of the competency
1441      * @return bool
1442      */
1443     public static function remove_competency_from_course_module($cmorid, $competencyid) {
1444         static::require_enabled();
1445         $cm = $cmorid;
1446         if (!is_object($cmorid)) {
1447             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1448         }
1449         // Check the user have access to the course module.
1450         self::validate_course_module($cm);
1452         // First we do a permissions check.
1453         $context = context_module::instance($cm->id);
1455         require_capability('moodle/competency:coursecompetencymanage', $context);
1457         $record = new stdClass();
1458         $record->cmid = $cm->id;
1459         $record->competencyid = $competencyid;
1461         $competency = new competency($competencyid);
1462         $exists = course_module_competency::get_record(array('cmid' => $cm->id, 'competencyid' => $competencyid));
1463         if ($exists) {
1464             return $exists->delete();
1465         }
1466         return false;
1467     }
1469     /**
1470      * Move the course module competency up or down in the display list.
1471      *
1472      * Requires moodle/competency:coursecompetencymanage capability at the course module context.
1473      *
1474      * @param mixed $cmorid The course module, or id of the course module
1475      * @param int $competencyidfrom The id of the competency we are moving.
1476      * @param int $competencyidto The id of the competency we are moving to.
1477      * @return boolean
1478      */
1479     public static function reorder_course_module_competency($cmorid, $competencyidfrom, $competencyidto) {
1480         static::require_enabled();
1481         $cm = $cmorid;
1482         if (!is_object($cmorid)) {
1483             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1484         }
1485         // Check the user have access to the course module.
1486         self::validate_course_module($cm);
1488         // First we do a permissions check.
1489         $context = context_module::instance($cm->id);
1491         require_capability('moodle/competency:coursecompetencymanage', $context);
1493         $down = true;
1494         $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidfrom));
1495         if (count($matches) == 0) {
1496              throw new coding_exception('The link does not exist');
1497         }
1499         $competencyfrom = array_pop($matches);
1500         $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidto));
1501         if (count($matches) == 0) {
1502              throw new coding_exception('The link does not exist');
1503         }
1505         $competencyto = array_pop($matches);
1507         $all = course_module_competency::get_records(array('cmid' => $cm->id), 'sortorder', 'ASC', 0, 0);
1509         if ($competencyfrom->get_sortorder() > $competencyto->get_sortorder()) {
1510             // We are moving up, so put it before the "to" item.
1511             $down = false;
1512         }
1514         foreach ($all as $id => $coursemodulecompetency) {
1515             $sort = $coursemodulecompetency->get_sortorder();
1516             if ($down && $sort > $competencyfrom->get_sortorder() && $sort <= $competencyto->get_sortorder()) {
1517                 $coursemodulecompetency->set_sortorder($coursemodulecompetency->get_sortorder() - 1);
1518                 $coursemodulecompetency->update();
1519             } else if (!$down && $sort >= $competencyto->get_sortorder() && $sort < $competencyfrom->get_sortorder()) {
1520                 $coursemodulecompetency->set_sortorder($coursemodulecompetency->get_sortorder() + 1);
1521                 $coursemodulecompetency->update();
1522             }
1523         }
1524         $competencyfrom->set_sortorder($competencyto->get_sortorder());
1525         return $competencyfrom->update();
1526     }
1528     /**
1529      * Update ruleoutcome value for a course module competency.
1530      *
1531      * @param int|course_module_competency $coursemodulecompetencyorid The course_module_competency, or its ID.
1532      * @param int $ruleoutcome The value of ruleoutcome.
1533      * @return bool True on success.
1534      */
1535     public static function set_course_module_competency_ruleoutcome($coursemodulecompetencyorid, $ruleoutcome) {
1536         static::require_enabled();
1537         $coursemodulecompetency = $coursemodulecompetencyorid;
1538         if (!is_object($coursemodulecompetency)) {
1539             $coursemodulecompetency = new course_module_competency($coursemodulecompetencyorid);
1540         }
1542         $cm = get_coursemodule_from_id('', $coursemodulecompetency->get_cmid(), 0, true, MUST_EXIST);
1544         self::validate_course_module($cm);
1545         $context = context_module::instance($cm->id);
1547         require_capability('moodle/competency:coursecompetencymanage', $context);
1549         $coursemodulecompetency->set_ruleoutcome($ruleoutcome);
1550         return $coursemodulecompetency->update();
1551     }
1553     /**
1554      * Add a competency to this course.
1555      *
1556      * @param int $courseid The id of the course
1557      * @param int $competencyid The id of the competency
1558      * @return bool
1559      */
1560     public static function add_competency_to_course($courseid, $competencyid) {
1561         static::require_enabled();
1562         // Check the user have access to the course.
1563         self::validate_course($courseid);
1565         // First we do a permissions check.
1566         $context = context_course::instance($courseid);
1568         require_capability('moodle/competency:coursecompetencymanage', $context);
1570         $record = new stdClass();
1571         $record->courseid = $courseid;
1572         $record->competencyid = $competencyid;
1574         $competency = new competency($competencyid);
1576         // Can not add a competency that belong to a hidden framework.
1577         if ($competency->get_framework()->get_visible() == false) {
1578             throw new coding_exception('A competency belonging to hidden framework can not be linked to course');
1579         }
1581         $coursecompetency = new course_competency();
1582         $exists = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyid));
1583         if (!$exists) {
1584             $coursecompetency->from_record($record);
1585             if ($coursecompetency->create()) {
1586                 return true;
1587             }
1588         }
1589         return false;
1590     }
1592     /**
1593      * Remove a competency from this course.
1594      *
1595      * @param int $courseid The id of the course
1596      * @param int $competencyid The id of the competency
1597      * @return bool
1598      */
1599     public static function remove_competency_from_course($courseid, $competencyid) {
1600         static::require_enabled();
1601         // Check the user have access to the course.
1602         self::validate_course($courseid);
1604         // First we do a permissions check.
1605         $context = context_course::instance($courseid);
1607         require_capability('moodle/competency:coursecompetencymanage', $context);
1609         $record = new stdClass();
1610         $record->courseid = $courseid;
1611         $record->competencyid = $competencyid;
1613         $coursecompetency = new course_competency();
1614         $exists = course_competency::get_record(array('courseid' => $courseid, 'competencyid' => $competencyid));
1615         if ($exists) {
1616             // Delete all course_module_competencies for this competency in this course.
1617             $cmcs = course_module_competency::get_records_by_competencyid_in_course($competencyid, $courseid);
1618             foreach ($cmcs as $cmc) {
1619                 $cmc->delete();
1620             }
1621             return $exists->delete();
1622         }
1623         return false;
1624     }
1626     /**
1627      * Move the course competency up or down in the display list.
1628      *
1629      * Requires moodle/competency:coursecompetencymanage capability at the course context.
1630      *
1631      * @param int $courseid The course
1632      * @param int $competencyidfrom The id of the competency we are moving.
1633      * @param int $competencyidto The id of the competency we are moving to.
1634      * @return boolean
1635      */
1636     public static function reorder_course_competency($courseid, $competencyidfrom, $competencyidto) {
1637         static::require_enabled();
1638         // Check the user have access to the course.
1639         self::validate_course($courseid);
1641         // First we do a permissions check.
1642         $context = context_course::instance($courseid);
1644         require_capability('moodle/competency:coursecompetencymanage', $context);
1646         $down = true;
1647         $coursecompetency = new course_competency();
1648         $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidfrom));
1649         if (count($matches) == 0) {
1650              throw new coding_exception('The link does not exist');
1651         }
1653         $competencyfrom = array_pop($matches);
1654         $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidto));
1655         if (count($matches) == 0) {
1656              throw new coding_exception('The link does not exist');
1657         }
1659         $competencyto = array_pop($matches);
1661         $all = $coursecompetency->get_records(array('courseid' => $courseid), 'sortorder', 'ASC', 0, 0);
1663         if ($competencyfrom->get_sortorder() > $competencyto->get_sortorder()) {
1664             // We are moving up, so put it before the "to" item.
1665             $down = false;
1666         }
1668         foreach ($all as $id => $coursecompetency) {
1669             $sort = $coursecompetency->get_sortorder();
1670             if ($down && $sort > $competencyfrom->get_sortorder() && $sort <= $competencyto->get_sortorder()) {
1671                 $coursecompetency->set_sortorder($coursecompetency->get_sortorder() - 1);
1672                 $coursecompetency->update();
1673             } else if (!$down && $sort >= $competencyto->get_sortorder() && $sort < $competencyfrom->get_sortorder()) {
1674                 $coursecompetency->set_sortorder($coursecompetency->get_sortorder() + 1);
1675                 $coursecompetency->update();
1676             }
1677         }
1678         $competencyfrom->set_sortorder($competencyto->get_sortorder());
1679         return $competencyfrom->update();
1680     }
1682     /**
1683      * Update ruleoutcome value for a course competency.
1684      *
1685      * @param int|course_competency $coursecompetencyorid The course_competency, or its ID.
1686      * @param int $ruleoutcome The value of ruleoutcome.
1687      * @return bool True on success.
1688      */
1689     public static function set_course_competency_ruleoutcome($coursecompetencyorid, $ruleoutcome) {
1690         static::require_enabled();
1691         $coursecompetency = $coursecompetencyorid;
1692         if (!is_object($coursecompetency)) {
1693             $coursecompetency = new course_competency($coursecompetencyorid);
1694         }
1696         $courseid = $coursecompetency->get_courseid();
1697         self::validate_course($courseid);
1698         $coursecontext = context_course::instance($courseid);
1700         require_capability('moodle/competency:coursecompetencymanage', $coursecontext);
1702         $coursecompetency->set_ruleoutcome($ruleoutcome);
1703         return $coursecompetency->update();
1704     }
1706     /**
1707      * Create a learning plan template from a record containing all the data for the class.
1708      *
1709      * Requires moodle/competency:templatemanage capability.
1710      *
1711      * @param stdClass $record Record containing all the data for an instance of the class.
1712      * @return template
1713      */
1714     public static function create_template(stdClass $record) {
1715         static::require_enabled();
1716         $template = new template(0, $record);
1718         // First we do a permissions check.
1719         if (!$template->can_manage()) {
1720             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1721                 'nopermissions', '');
1722         }
1724         // OK - all set.
1725         $template = $template->create();
1727         // Trigger a template created event.
1728         \core\event\competency_template_created::create_from_template($template)->trigger();
1730         return $template;
1731     }
1733     /**
1734      * Duplicate a learning plan template.
1735      *
1736      * Requires moodle/competency:templatemanage capability at the template context.
1737      *
1738      * @param int $id the template id.
1739      * @return template
1740      */
1741     public static function duplicate_template($id) {
1742         static::require_enabled();
1743         $template = new template($id);
1745         // First we do a permissions check.
1746         if (!$template->can_manage()) {
1747             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1748                 'nopermissions', '');
1749         }
1751         // OK - all set.
1752         $competencies = template_competency::list_competencies($id, false);
1754         // Adding the suffix copy.
1755         $template->set_shortname(get_string('duplicateditemname', 'core_competency', $template->get_shortname()));
1756         $template->set_id(0);
1758         $duplicatedtemplate = $template->create();
1760         // Associate each competency for the duplicated template.
1761         foreach ($competencies as $competency) {
1762             self::add_competency_to_template($duplicatedtemplate->get_id(), $competency->get_id());
1763         }
1765         // Trigger a template created event.
1766         \core\event\competency_template_created::create_from_template($duplicatedtemplate)->trigger();
1768         return $duplicatedtemplate;
1769     }
1771     /**
1772      * Delete a learning plan template by id.
1773      * If the learning plan template has associated cohorts they will be deleted.
1774      *
1775      * Requires moodle/competency:templatemanage capability.
1776      *
1777      * @param int $id The record to delete.
1778      * @param boolean $deleteplans True to delete plans associaated to template, false to unlink them.
1779      * @return boolean
1780      */
1781     public static function delete_template($id, $deleteplans = true) {
1782         global $DB;
1783         static::require_enabled();
1784         $template = new template($id);
1786         // First we do a permissions check.
1787         if (!$template->can_manage()) {
1788             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1789                 'nopermissions', '');
1790         }
1792         $transaction = $DB->start_delegated_transaction();
1793         $success = true;
1795         // Check if there are cohorts associated.
1796         $templatecohorts = template_cohort::get_relations_by_templateid($template->get_id());
1797         foreach ($templatecohorts as $templatecohort) {
1798             $success = $templatecohort->delete();
1799             if (!$success) {
1800                 break;
1801             }
1802         }
1804         // Still OK, delete or unlink the plans from the template.
1805         if ($success) {
1806             $plans = plan::get_records(array('templateid' => $template->get_id()));
1807             foreach ($plans as $plan) {
1808                 $success = $deleteplans ? self::delete_plan($plan->get_id()) : self::unlink_plan_from_template($plan);
1809                 if (!$success) {
1810                     break;
1811                 }
1812             }
1813         }
1815         // Still OK, delete the template comptencies.
1816         if ($success) {
1817             $success = template_competency::delete_by_templateid($template->get_id());
1818         }
1820         // OK - all set.
1821         if ($success) {
1822             // Create a template deleted event.
1823             $event = \core\event\competency_template_deleted::create_from_template($template);
1825             $success = $template->delete();
1826         }
1828         if ($success) {
1829             // Trigger a template deleted event.
1830             $event->trigger();
1832             // Commit the transaction.
1833             $transaction->allow_commit();
1834         } else {
1835             $transaction->rollback(new moodle_exception('Error while deleting the template.'));
1836         }
1838         return $success;
1839     }
1841     /**
1842      * Update the details for a learning plan template.
1843      *
1844      * Requires moodle/competency:templatemanage capability.
1845      *
1846      * @param stdClass $record The new details for the template. Note - must contain an id that points to the template to update.
1847      * @return boolean
1848      */
1849     public static function update_template($record) {
1850         global $DB;
1851         static::require_enabled();
1852         $template = new template($record->id);
1854         // First we do a permissions check.
1855         if (!$template->can_manage()) {
1856             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1857                 'nopermissions', '');
1859         } else if (isset($record->contextid) && $record->contextid != $template->get_contextid()) {
1860             // We can never change the context of a template.
1861             throw new coding_exception('Changing the context of an existing tempalte is forbidden.');
1863         }
1865         $updateplans = false;
1866         $before = $template->to_record();
1868         $template->from_record($record);
1869         $after = $template->to_record();
1871         // Should we update the related plans?
1872         if ($before->duedate != $after->duedate ||
1873                 $before->shortname != $after->shortname ||
1874                 $before->description != $after->description ||
1875                 $before->descriptionformat != $after->descriptionformat) {
1876             $updateplans = true;
1877         }
1879         $transaction = $DB->start_delegated_transaction();
1880         $success = $template->update();
1882         if (!$success) {
1883             $transaction->rollback(new moodle_exception('Error while updating the template.'));
1884             return $success;
1885         }
1887         // Trigger a template updated event.
1888         \core\event\competency_template_updated::create_from_template($template)->trigger();
1890         if ($updateplans) {
1891             plan::update_multiple_from_template($template);
1892         }
1894         $transaction->allow_commit();
1896         return $success;
1897     }
1899     /**
1900      * Read a the details for a single learning plan template and return a record.
1901      *
1902      * Requires moodle/competency:templateview capability at the system context.
1903      *
1904      * @param int $id The id of the template to read.
1905      * @return template
1906      */
1907     public static function read_template($id) {
1908         static::require_enabled();
1909         $template = new template($id);
1910         $context = $template->get_context();
1912         // First we do a permissions check.
1913         if (!$template->can_read()) {
1914              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
1915                 'nopermissions', '');
1916         }
1918         // OK - all set.
1919         return $template;
1920     }
1922     /**
1923      * Perform a search based on the provided filters and return a paginated list of records.
1924      *
1925      * Requires moodle/competency:templateview capability at the system context.
1926      *
1927      * @param string $sort The column to sort on
1928      * @param string $order ('ASC' or 'DESC')
1929      * @param int $skip Number of records to skip (pagination)
1930      * @param int $limit Max of records to return (pagination)
1931      * @param context $context The parent context of the frameworks.
1932      * @param string $includes Defines what other contexts to fetch frameworks from.
1933      *                         Accepted values are:
1934      *                          - children: All descendants
1935      *                          - parents: All parents, grand parents, etc...
1936      *                          - self: Context passed only.
1937      * @param bool $onlyvisible If should list only visible templates
1938      * @return array of competency_framework
1939      */
1940     public static function list_templates($sort, $order, $skip, $limit, $context, $includes = 'children', $onlyvisible = false) {
1941         global $DB;
1942         static::require_enabled();
1944         // Get all the relevant contexts.
1945         $contexts = self::get_related_contexts($context, $includes,
1946             array('moodle/competency:templateview', 'moodle/competency:templatemanage'));
1948         // First we do a permissions check.
1949         if (empty($contexts)) {
1950              throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
1951         }
1953         // Make the order by.
1954         $orderby = '';
1955         if (!empty($sort)) {
1956             $orderby = $sort . ' ' . $order;
1957         }
1959         // OK - all set.
1960         $template = new template();
1961         list($insql, $params) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
1962         $select = "contextid $insql";
1964         if ($onlyvisible) {
1965             $select .= " AND visible = :visible";
1966             $params['visible'] = 1;
1967         }
1968         return $template->get_records_select($select, $params, $orderby, '*', $skip, $limit);
1969     }
1971     /**
1972      * Perform a search based on the provided filters and return how many results there are.
1973      *
1974      * Requires moodle/competency:templateview capability at the system context.
1975      *
1976      * @param context $context The parent context of the frameworks.
1977      * @param string $includes Defines what other contexts to fetch frameworks from.
1978      *                         Accepted values are:
1979      *                          - children: All descendants
1980      *                          - parents: All parents, grand parents, etc...
1981      *                          - self: Context passed only.
1982      * @return int
1983      */
1984     public static function count_templates($context, $includes) {
1985         global $DB;
1986         static::require_enabled();
1988         // First we do a permissions check.
1989         $contexts = self::get_related_contexts($context, $includes,
1990             array('moodle/competency:templateview', 'moodle/competency:templatemanage'));
1992         if (empty($contexts)) {
1993              throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
1994         }
1996         // OK - all set.
1997         $template = new template();
1998         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
1999         return $template->count_records_select("contextid $insql", $inparams);
2000     }
2002     /**
2003      * Count all the templates using a competency.
2004      *
2005      * @param int $competencyid The id of the competency to check.
2006      * @return int
2007      */
2008     public static function count_templates_using_competency($competencyid) {
2009         static::require_enabled();
2010         // First we do a permissions check.
2011         $context = context_system::instance();
2012         $onlyvisible = 1;
2014         $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
2015         if (!has_any_capability($capabilities, $context)) {
2016              throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
2017         }
2019         if (has_capability('moodle/competency:templatemanage', $context)) {
2020             $onlyvisible = 0;
2021         }
2023         // OK - all set.
2024         return template_competency::count_templates($competencyid, $onlyvisible);
2025     }
2027     /**
2028      * List all the learning plan templatesd using a competency.
2029      *
2030      * @param int $competencyid The id of the competency to check.
2031      * @return array[stdClass] Array of stdClass containing id and shortname.
2032      */
2033     public static function list_templates_using_competency($competencyid) {
2034         static::require_enabled();
2035         // First we do a permissions check.
2036         $context = context_system::instance();
2037         $onlyvisible = 1;
2039         $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
2040         if (!has_any_capability($capabilities, $context)) {
2041              throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
2042         }
2044         if (has_capability('moodle/competency:templatemanage', $context)) {
2045             $onlyvisible = 0;
2046         }
2048         // OK - all set.
2049         return template_competency::list_templates($competencyid, $onlyvisible);
2051     }
2053     /**
2054      * Count all the competencies in a learning plan template.
2055      *
2056      * @param  template|int $templateorid The template or its ID.
2057      * @return int
2058      */
2059     public static function count_competencies_in_template($templateorid) {
2060         static::require_enabled();
2061         // First we do a permissions check.
2062         $template = $templateorid;
2063         if (!is_object($template)) {
2064             $template = new template($template);
2065         }
2067         if (!$template->can_read()) {
2068             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2069                 'nopermissions', '');
2070         }
2072         // OK - all set.
2073         return template_competency::count_competencies($template->get_id());
2074     }
2076     /**
2077      * Count all the competencies in a learning plan template with no linked courses.
2078      *
2079      * @param  template|int $templateorid The template or its ID.
2080      * @return int
2081      */
2082     public static function count_competencies_in_template_with_no_courses($templateorid) {
2083         // First we do a permissions check.
2084         $template = $templateorid;
2085         if (!is_object($template)) {
2086             $template = new template($template);
2087         }
2089         if (!$template->can_read()) {
2090             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2091                 'nopermissions', '');
2092         }
2094         // OK - all set.
2095         return template_competency::count_competencies_with_no_courses($template->get_id());
2096     }
2098     /**
2099      * List all the competencies in a template.
2100      *
2101      * @param  template|int $templateorid The template or its ID.
2102      * @return array of competencies
2103      */
2104     public static function list_competencies_in_template($templateorid) {
2105         static::require_enabled();
2106         // First we do a permissions check.
2107         $template = $templateorid;
2108         if (!is_object($template)) {
2109             $template = new template($template);
2110         }
2112         if (!$template->can_read()) {
2113             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2114                 'nopermissions', '');
2115         }
2117         // OK - all set.
2118         return template_competency::list_competencies($template->get_id());
2119     }
2121     /**
2122      * Add a competency to this template.
2123      *
2124      * @param int $templateid The id of the template
2125      * @param int $competencyid The id of the competency
2126      * @return bool
2127      */
2128     public static function add_competency_to_template($templateid, $competencyid) {
2129         static::require_enabled();
2130         // First we do a permissions check.
2131         $template = new template($templateid);
2132         if (!$template->can_manage()) {
2133             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
2134                 'nopermissions', '');
2135         }
2137         $record = new stdClass();
2138         $record->templateid = $templateid;
2139         $record->competencyid = $competencyid;
2141         $competency = new competency($competencyid);
2143         // Can not add a competency that belong to a hidden framework.
2144         if ($competency->get_framework()->get_visible() == false) {
2145             throw new coding_exception('A competency belonging to hidden framework can not be added');
2146         }
2148         $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
2149         if (!$exists) {
2150             $templatecompetency = new template_competency(0, $record);
2151             $templatecompetency->create();
2152             return true;
2153         }
2154         return false;
2155     }
2157     /**
2158      * Remove a competency from this template.
2159      *
2160      * @param int $templateid The id of the template
2161      * @param int $competencyid The id of the competency
2162      * @return bool
2163      */
2164     public static function remove_competency_from_template($templateid, $competencyid) {
2165         static::require_enabled();
2166         // First we do a permissions check.
2167         $template = new template($templateid);
2168         if (!$template->can_manage()) {
2169             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
2170                 'nopermissions', '');
2171         }
2173         $record = new stdClass();
2174         $record->templateid = $templateid;
2175         $record->competencyid = $competencyid;
2177         $competency = new competency($competencyid);
2179         $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
2180         if ($exists) {
2181             $link = array_pop($exists);
2182             return $link->delete();
2183         }
2184         return false;
2185     }
2187     /**
2188      * Move the template competency up or down in the display list.
2189      *
2190      * Requires moodle/competency:templatemanage capability at the system context.
2191      *
2192      * @param int $templateid The template id
2193      * @param int $competencyidfrom The id of the competency we are moving.
2194      * @param int $competencyidto The id of the competency we are moving to.
2195      * @return boolean
2196      */
2197     public static function reorder_template_competency($templateid, $competencyidfrom, $competencyidto) {
2198         static::require_enabled();
2199         // First we do a permissions check.
2200         $context = context_system::instance();
2202         require_capability('moodle/competency:templatemanage', $context);
2204         $down = true;
2205         $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidfrom));
2206         if (count($matches) == 0) {
2207             throw new coding_exception('The link does not exist');
2208         }
2210         $competencyfrom = array_pop($matches);
2211         $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidto));
2212         if (count($matches) == 0) {
2213             throw new coding_exception('The link does not exist');
2214         }
2216         $competencyto = array_pop($matches);
2218         $all = template_competency::get_records(array('templateid' => $templateid), 'sortorder', 'ASC', 0, 0);
2220         if ($competencyfrom->get_sortorder() > $competencyto->get_sortorder()) {
2221             // We are moving up, so put it before the "to" item.
2222             $down = false;
2223         }
2225         foreach ($all as $id => $templatecompetency) {
2226             $sort = $templatecompetency->get_sortorder();
2227             if ($down && $sort > $competencyfrom->get_sortorder() && $sort <= $competencyto->get_sortorder()) {
2228                 $templatecompetency->set_sortorder($templatecompetency->get_sortorder() - 1);
2229                 $templatecompetency->update();
2230             } else if (!$down && $sort >= $competencyto->get_sortorder() && $sort < $competencyfrom->get_sortorder()) {
2231                 $templatecompetency->set_sortorder($templatecompetency->get_sortorder() + 1);
2232                 $templatecompetency->update();
2233             }
2234         }
2235         $competencyfrom->set_sortorder($competencyto->get_sortorder());
2236         return $competencyfrom->update();
2237     }
2239     /**
2240      * Create a relation between a template and a cohort.
2241      *
2242      * This silently ignores when the relation already existed.
2243      *
2244      * @param  template|int $templateorid The template or its ID.
2245      * @param  stdClass|int $cohortorid   The cohort ot its ID.
2246      * @return template_cohort
2247      */
2248     public static function create_template_cohort($templateorid, $cohortorid) {
2249         global $DB;
2250         static::require_enabled();
2252         $template = $templateorid;
2253         if (!is_object($template)) {
2254             $template = new template($template);
2255         }
2256         require_capability('moodle/competency:templatemanage', $template->get_context());
2258         $cohort = $cohortorid;
2259         if (!is_object($cohort)) {
2260             $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
2261         }
2263         // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
2264         $cohortcontext = context::instance_by_id($cohort->contextid);
2265         if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
2266             throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
2267         }
2269         $tplcohort = template_cohort::get_relation($template->get_id(), $cohort->id);
2270         if (!$tplcohort->get_id()) {
2271             $tplcohort->create();
2272         }
2274         return $tplcohort;
2275     }
2277     /**
2278      * Remove a relation between a template and a cohort.
2279      *
2280      * @param  template|int $templateorid The template or its ID.
2281      * @param  stdClass|int $cohortorid   The cohort ot its ID.
2282      * @return boolean True on success or when the relation did not exist.
2283      */
2284     public static function delete_template_cohort($templateorid, $cohortorid) {
2285         global $DB;
2286         static::require_enabled();
2288         $template = $templateorid;
2289         if (!is_object($template)) {
2290             $template = new template($template);
2291         }
2292         require_capability('moodle/competency:templatemanage', $template->get_context());
2294         $cohort = $cohortorid;
2295         if (!is_object($cohort)) {
2296             $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
2297         }
2299         $tplcohort = template_cohort::get_relation($template->get_id(), $cohort->id);
2300         if (!$tplcohort->get_id()) {
2301             return true;
2302         }
2304         return $tplcohort->delete();
2305     }
2307     /**
2308      * Lists user plans.
2309      *
2310      * @param int $userid
2311      * @return \core_competency\plan[]
2312      */
2313     public static function list_user_plans($userid) {
2314         global $DB, $USER;
2315         static::require_enabled();
2316         $select = 'userid = :userid';
2317         $params = array('userid' => $userid);
2318         $context = context_user::instance($userid);
2320         // Check that we can read something here.
2321         if (!plan::can_read_user($userid) && !plan::can_read_user_draft($userid)) {
2322             throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
2323         }
2325         // The user cannot view the drafts.
2326         if (!plan::can_read_user_draft($userid)) {
2327             list($insql, $inparams) = $DB->get_in_or_equal(plan::get_draft_statuses(), SQL_PARAMS_NAMED, 'param', false);
2328             $select .= " AND status $insql";
2329             $params += $inparams;
2330         }
2331         // The user cannot view the non-drafts.
2332         if (!plan::can_read_user($userid)) {
2333             list($insql, $inparams) = $DB->get_in_or_equal(array(plan::STATUS_ACTIVE, plan::STATUS_COMPLETE),
2334                 SQL_PARAMS_NAMED, 'param', false);
2335             $select .= " AND status $insql";
2336             $params += $inparams;
2337         }
2339         return plan::get_records_select($select, $params, 'name ASC');
2340     }
2342     /**
2343      * List the plans to review.
2344      *
2345      * The method returns values in this format:
2346      *
2347      * array(
2348      *     'plans' => array(
2349      *         (stdClass)(
2350      *             'plan' => (plan),
2351      *             'template' => (template),
2352      *             'owner' => (stdClass)
2353      *         )
2354      *     ),
2355      *     'count' => (int)
2356      * )
2357      *
2358      * @param int $skip The number of records to skip.
2359      * @param int $limit The number of results to return.
2360      * @param int $userid The user we're getting the plans to review for.
2361      * @return array Containing the keys 'count', and 'plans'. The 'plans' key contains an object
2362      *               which contains 'plan', 'template' and 'owner'.
2363      */
2364     public static function list_plans_to_review($skip = 0, $limit = 100, $userid = null) {
2365         global $DB, $USER;
2366         static::require_enabled();
2368         if ($userid === null) {
2369             $userid = $USER->id;
2370         }
2372         $planfields = plan::get_sql_fields('p', 'plan_');
2373         $tplfields = template::get_sql_fields('t', 'tpl_');
2374         $usercols = array('id') + get_user_fieldnames();
2375         $userfields = array();
2376         foreach ($usercols as $field) {
2377             $userfields[] = "u." . $field . " AS usr_" . $field;
2378         }
2379         $userfields = implode(',', $userfields);
2381         $select = "SELECT $planfields, $tplfields, $userfields";
2382         $countselect = "SELECT COUNT('x')";
2384         $sql = "  FROM {" . plan::TABLE . "} p
2385                   JOIN {user} u
2386                     ON u.id = p.userid
2387              LEFT JOIN {" . template::TABLE . "} t
2388                     ON t.id = p.templateid
2389                  WHERE (p.status = :waitingforreview
2390                     OR (p.status = :inreview AND p.reviewerid = :reviewerid))
2391                    AND p.userid != :userid";
2393         $params = array(
2394             'waitingforreview' => plan::STATUS_WAITING_FOR_REVIEW,
2395             'inreview' => plan::STATUS_IN_REVIEW,
2396             'reviewerid' => $userid,
2397             'userid' => $userid
2398         );
2400         // Primary check to avoid the hard work of getting the users in which the user has permission.
2401         $count = $DB->count_records_sql($countselect . $sql, $params);
2402         if ($count < 1) {
2403             return array('count' => 0, 'plans' => array());
2404         }
2406         // TODO MDL-52243 Use core function.
2407         list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql('moodle/competency:planreview',
2408             $userid, SQL_PARAMS_NAMED);
2409         $sql .= " AND p.userid $insql";
2410         $params += $inparams;
2412         // Order by ID just to have some ordering in place.
2413         $ordersql = " ORDER BY p.id ASC";
2415         $plans = array();
2416         $records = $DB->get_recordset_sql($select . $sql . $ordersql, $params, $skip, $limit);
2417         foreach ($records as $record) {
2418             $plan = new plan(0, plan::extract_record($record, 'plan_'));
2419             $template = null;
2421             if ($plan->is_based_on_template()) {
2422                 $template = new template(0, template::extract_record($record, 'tpl_'));
2423             }
2425             $plans[] = (object) array(
2426                 'plan' => $plan,
2427                 'template' => $template,
2428                 'owner' => persistent::extract_record($record, 'usr_'),
2429             );
2430         }
2431         $records->close();
2433         return array(
2434             'count' => $DB->count_records_sql($countselect . $sql, $params),
2435             'plans' => $plans
2436         );
2437     }
2439     /**
2440      * Creates a learning plan based on the provided data.
2441      *
2442      * @param stdClass $record
2443      * @return \core_competency\plan
2444      */
2445     public static function create_plan(stdClass $record) {
2446         global $USER;
2447         static::require_enabled();
2448         $plan = new plan(0, $record);
2450         if ($plan->is_based_on_template()) {
2451             throw new coding_exception('To create a plan from a template use api::create_plan_from_template().');
2452         } else if ($plan->get_status() == plan::STATUS_COMPLETE) {
2453             throw new coding_exception('A plan cannot be created as complete.');
2454         }
2456         if (!$plan->can_manage()) {
2457             $context = context_user::instance($plan->get_userid());
2458             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
2459         }
2461         $plan->create();
2463         // Trigger created event.
2464         \core\event\competency_plan_created::create_from_plan($plan)->trigger();
2465         return $plan;
2466     }
2468     /**
2469      * Create a learning plan from a template.
2470      *
2471      * @param  mixed $templateorid The template object or ID.
2472      * @param  int $userid
2473      * @return false|\core_competency\plan Returns false when the plan already exists.
2474      */
2475     public static function create_plan_from_template($templateorid, $userid) {
2476         static::require_enabled();
2477         $template = $templateorid;
2478         if (!is_object($template)) {
2479             $template = new template($template);
2480         }
2482         // The user must be able to view the template to use it as a base for a plan.
2483         if (!$template->can_read()) {
2484             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2485                 'nopermissions', '');
2486         }
2487         // Can not create plan from a hidden template.
2488         if ($template->get_visible() == false) {
2489             throw new coding_exception('A plan can not be created from a hidden template');
2490         }
2492         // Convert the template to a plan.
2493         $record = $template->to_record();
2494         $record->templateid = $record->id;
2495         $record->userid = $userid;
2496         $record->name = $record->shortname;
2497         $record->status = plan::STATUS_ACTIVE;
2499         unset($record->id);
2500         unset($record->timecreated);
2501         unset($record->timemodified);
2502         unset($record->usermodified);
2504         // Remove extra keys.
2505         $properties = plan::properties_definition();
2506         foreach ($record as $key => $value) {
2507             if (!array_key_exists($key, $properties)) {
2508                 unset($record->$key);
2509             }
2510         }
2512         $plan = new plan(0, $record);
2513         if (!$plan->can_manage()) {
2514             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage',
2515                 'nopermissions', '');
2516         }
2518         // We first apply the permission checks as we wouldn't want to leak information by returning early that
2519         // the plan already exists.
2520         if (plan::record_exists_select('templateid = :templateid AND userid = :userid', array(
2521                 'templateid' => $template->get_id(), 'userid' => $userid))) {
2522             return false;
2523         }
2525         $plan->create();
2527         // Trigger created event.
2528         \core\event\competency_plan_created::create_from_plan($plan)->trigger();
2529         return $plan;
2530     }
2532     /**
2533      * Create learning plans from a template and cohort.
2534      *
2535      * @param  mixed $templateorid The template object or ID.
2536      * @param  int $cohortid The cohort ID.
2537      * @param  bool $recreateunlinked When true the plans that were unlinked from this template will be re-created.
2538      * @return int The number of plans created.
2539      */
2540     public static function create_plans_from_template_cohort($templateorid, $cohortid, $recreateunlinked = false) {
2541         global $DB, $CFG;
2542         static::require_enabled();
2543         require_once($CFG->dirroot . '/cohort/lib.php');
2545         $template = $templateorid;
2546         if (!is_object($template)) {
2547             $template = new template($template);
2548         }
2550         // The user must be able to view the template to use it as a base for a plan.
2551         if (!$template->can_read()) {
2552             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2553                 'nopermissions', '');
2554         }
2556         // Can not create plan from a hidden template.
2557         if ($template->get_visible() == false) {
2558             throw new coding_exception('A plan can not be created from a hidden template');
2559         }
2561         // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
2562         $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
2563         $cohortcontext = context::instance_by_id($cohort->contextid);
2564         if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
2565             throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
2566         }
2568         // Convert the template to a plan.
2569         $recordbase = $template->to_record();
2570         $recordbase->templateid = $recordbase->id;
2571         $recordbase->name = $recordbase->shortname;
2572         $recordbase->status = plan::STATUS_ACTIVE;
2574         unset($recordbase->id);
2575         unset($recordbase->timecreated);
2576         unset($recordbase->timemodified);
2577         unset($recordbase->usermodified);
2579         // Remove extra keys.
2580         $properties = plan::properties_definition();
2581         foreach ($recordbase as $key => $value) {
2582             if (!array_key_exists($key, $properties)) {
2583                 unset($recordbase->$key);
2584             }
2585         }
2587         // Create the plans.
2588         $created = 0;
2589         $userids = template_cohort::get_missing_plans($template->get_id(), $cohortid, $recreateunlinked);
2590         foreach ($userids as $userid) {
2591             $record = (object) (array) $recordbase;
2592             $record->userid = $userid;
2594             $plan = new plan(0, $record);
2595             if (!$plan->can_manage()) {
2596                 // Silently skip members where permissions are lacking.
2597                 continue;
2598             }
2600             $plan->create();
2601             // Trigger created event.
2602             \core\event\competency_plan_created::create_from_plan($plan)->trigger();
2603             $created++;
2604         }
2606         return $created;
2607     }
2609     /**
2610      * Unlink a plan from its template.
2611      *
2612      * @param  \core_competency\plan|int $planorid The plan or its ID.
2613      * @return bool
2614      */
2615     public static function unlink_plan_from_template($planorid) {
2616         global $DB;
2617         static::require_enabled();
2619         $plan = $planorid;
2620         if (!is_object($planorid)) {
2621             $plan = new plan($planorid);
2622         }
2624         // The user must be allowed to manage the plans of the user, nothing about the template.
2625         if (!$plan->can_manage()) {
2626             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2627         }
2629         // Only plan with status DRAFT or ACTIVE can be unliked..
2630         if ($plan->get_status() == plan::STATUS_COMPLETE) {
2631             throw new coding_exception('Only draft or active plan can be unliked from a template');
2632         }
2634         // Early exit, it's already done...
2635         if (!$plan->is_based_on_template()) {
2636             return true;
2637         }
2639         // Fetch the template.
2640         $template = new template($plan->get_templateid());
2642         // Now, proceed by copying all competencies to the plan, then update the plan.
2643         $transaction = $DB->start_delegated_transaction();
2644         $competencies = template_competency::list_competencies($template->get_id(), false);
2645         $i = 0;
2646         foreach ($competencies as $competency) {
2647             $record = (object) array(
2648                 'planid' => $plan->get_id(),
2649                 'competencyid' => $competency->get_id(),
2650                 'sortorder' => $i++
2651             );
2652             $pc = new plan_competency(null, $record);
2653             $pc->create();
2654         }
2655         $plan->set_origtemplateid($template->get_id());
2656         $plan->set_templateid(null);
2657         $success = $plan->update();
2658         $transaction->allow_commit();
2660         // Trigger unlinked event.
2661         \core\event\competency_plan_unlinked::create_from_plan($plan)->trigger();
2663         return $success;
2664     }
2666     /**
2667      * Updates a plan.
2668      *
2669      * @param stdClass $record
2670      * @return \core_competency\plan
2671      */
2672     public static function update_plan(stdClass $record) {
2673         static::require_enabled();
2675         $plan = new plan($record->id);
2677         // Validate that the plan as it is can be managed.
2678         if (!$plan->can_manage()) {
2679             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2681         } else if ($plan->get_status() == plan::STATUS_COMPLETE) {
2682             // A completed plan cannot be edited.
2683             throw new coding_exception('Completed plan cannot be edited.');
2685         } else if ($plan->is_based_on_template()) {
2686             // Prevent a plan based on a template to be edited.
2687             throw new coding_exception('Cannot update a plan that is based on a template.');
2689         } else if (isset($record->templateid) && $plan->get_templateid() != $record->templateid) {
2690             // Prevent a plan to be based on a template.
2691             throw new coding_exception('Cannot base a plan on a template.');
2693         } else if (isset($record->userid) && $plan->get_userid() != $record->userid) {
2694             // Prevent change of ownership as the capabilities are checked against that.
2695             throw new coding_exception('A plan cannot be transfered to another user');
2697         } else if (isset($record->status) && $plan->get_status() != $record->status) {
2698             // Prevent change of status.
2699             throw new coding_exception('To change the status of a plan use the appropriate methods.');
2701         }
2703         $plan->from_record($record);
2704         $plan->update();
2706         // Trigger updated event.
2707         \core\event\competency_plan_updated::create_from_plan($plan)->trigger();
2709         return $plan;
2710     }
2712     /**
2713      * Returns a plan data.
2714      *
2715      * @param int $id
2716      * @return \core_competency\plan
2717      */
2718     public static function read_plan($id) {
2719         static::require_enabled();
2720         $plan = new plan($id);
2722         if (!$plan->can_read()) {
2723             $context = context_user::instance($plan->get_userid());
2724             throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
2725         }
2727         return $plan;
2728     }
2730     /**
2731      * Plan event viewed.
2732      *
2733      * @param mixed $planorid The id or the plan.
2734      * @return boolean
2735      */
2736     public static function plan_viewed($planorid) {
2737         static::require_enabled();
2738         $plan = $planorid;
2739         if (!is_object($plan)) {
2740             $plan = new plan($plan);
2741         }
2743         // First we do a permissions check.
2744         if (!$plan->can_read()) {
2745             $context = context_user::instance($plan->get_userid());
2746             throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
2747         }
2749         // Trigger a template viewed event.
2750         \core\event\competency_plan_viewed::create_from_plan($plan)->trigger();
2752         return true;
2753     }
2755     /**
2756      * Deletes a plan.
2757      *
2758      * Plans based on a template can be removed just like any other one.
2759      *
2760      * @param int $id
2761      * @return bool Success?
2762      */
2763     public static function delete_plan($id) {
2764         global $DB;
2765         static::require_enabled();
2767         $plan = new plan($id);
2769         if (!$plan->can_manage()) {
2770             $context = context_user::instance($plan->get_userid());
2771             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
2772         }
2774         // Wrap the suppression in a DB transaction.
2775         $transaction = $DB->start_delegated_transaction();
2777         // Delete plan competencies.
2778         $plancomps = plan_competency::get_records(array('planid' => $plan->get_id()));
2779         foreach ($plancomps as $plancomp) {
2780             $plancomp->delete();
2781         }
2783         // Delete archive user competencies if the status of the plan is complete.
2784         if ($plan->get_status() == plan::STATUS_COMPLETE) {
2785             self::remove_archived_user_competencies_in_plan($plan);
2786         }
2787         $event = \core\event\competency_plan_deleted::create_from_plan($plan);
2788         $success = $plan->delete();
2790         $transaction->allow_commit();
2792         // Trigger deleted event.
2793         $event->trigger();
2795         return $success;
2796     }
2798     /**
2799      * Cancel the review of a plan.
2800      *
2801      * @param int|plan $planorid The plan, or its ID.
2802      * @return bool
2803      */
2804     public static function plan_cancel_review_request($planorid) {
2805         static::require_enabled();
2806         $plan = $planorid;
2807         if (!is_object($plan)) {
2808             $plan = new plan($plan);
2809         }
2811         // We need to be able to view the plan at least.
2812         if (!$plan->can_read()) {
2813             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2814         }
2816         if ($plan->is_based_on_template()) {
2817             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2818         } else if ($plan->get_status() != plan::STATUS_WAITING_FOR_REVIEW) {
2819             throw new coding_exception('The plan review cannot be cancelled at this stage.');
2820         } else if (!$plan->can_request_review()) {
2821             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2822         }
2824         $plan->set_status(plan::STATUS_DRAFT);
2825         $result = $plan->update();
2827         // Trigger review request cancelled event.
2828         \core\event\competency_plan_review_request_cancelled::create_from_plan($plan)->trigger();
2830         return $result;
2831     }
2833     /**
2834      * Request the review of a plan.
2835      *
2836      * @param int|plan $planorid The plan, or its ID.
2837      * @return bool
2838      */
2839     public static function plan_request_review($planorid) {
2840         static::require_enabled();
2841         $plan = $planorid;
2842         if (!is_object($plan)) {
2843             $plan = new plan($plan);
2844         }
2846         // We need to be able to view the plan at least.
2847         if (!$plan->can_read()) {
2848             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2849         }
2851         if ($plan->is_based_on_template()) {
2852             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2853         } else if ($plan->get_status() != plan::STATUS_DRAFT) {
2854             throw new coding_exception('The plan cannot be sent for review at this stage.');
2855         } else if (!$plan->can_request_review()) {
2856             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2857         }
2859         $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW);
2860         $result = $plan->update();
2862         // Trigger review requested event.
2863         \core\event\competency_plan_review_requested::create_from_plan($plan)->trigger();
2865         return $result;
2866     }
2868     /**
2869      * Start the review of a plan.
2870      *
2871      * @param int|plan $planorid The plan, or its ID.
2872      * @return bool
2873      */
2874     public static function plan_start_review($planorid) {
2875         global $USER;
2876         static::require_enabled();
2877         $plan = $planorid;
2878         if (!is_object($plan)) {
2879             $plan = new plan($plan);
2880         }
2882         // We need to be able to view the plan at least.
2883         if (!$plan->can_read()) {
2884             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2885         }
2887         if ($plan->is_based_on_template()) {
2888             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2889         } else if ($plan->get_status() != plan::STATUS_WAITING_FOR_REVIEW) {
2890             throw new coding_exception('The plan review cannot be started at this stage.');
2891         } else if (!$plan->can_review()) {
2892             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2893         }
2895         $plan->set_status(plan::STATUS_IN_REVIEW);
2896         $plan->set_reviewerid($USER->id);
2897         $result = $plan->update();
2899         // Trigger review started event.
2900         \core\event\competency_plan_review_started::create_from_plan($plan)->trigger();
2902         return $result;
2903     }
2905     /**
2906      * Stop reviewing a plan.
2907      *
2908      * @param  int|plan $planorid The plan, or its ID.
2909      * @return bool
2910      */
2911     public static function plan_stop_review($planorid) {
2912         static::require_enabled();
2913         $plan = $planorid;
2914         if (!is_object($plan)) {
2915             $plan = new plan($plan);
2916         }
2918         // We need to be able to view the plan at least.
2919         if (!$plan->can_read()) {
2920             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2921         }
2923         if ($plan->is_based_on_template()) {
2924             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2925         } else if ($plan->get_status() != plan::STATUS_IN_REVIEW) {
2926             throw new coding_exception('The plan review cannot be stopped at this stage.');
2927         } else if (!$plan->can_review()) {
2928             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2929         }
2931         $plan->set_status(plan::STATUS_DRAFT);
2932         $plan->set_reviewerid(null);
2933         $result = $plan->update();
2935         // Trigger review stopped event.
2936         \core\event\competency_plan_review_stopped::create_from_plan($plan)->trigger();
2938         return $result;
2939     }
2941     /**
2942      * Approve a plan.
2943      *
2944      * This means making the plan active.
2945      *
2946      * @param  int|plan $planorid The plan, or its ID.
2947      * @return bool
2948      */
2949     public static function approve_plan($planorid) {
2950         static::require_enabled();
2951         $plan = $planorid;
2952         if (!is_object($plan)) {
2953             $plan = new plan($plan);
2954         }
2956         // We need to be able to view the plan at least.
2957         if (!$plan->can_read()) {
2958             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2959         }
2961         // We can approve a plan that is either a draft, in review, or waiting for review.
2962         if ($plan->is_based_on_template()) {
2963             throw new coding_exception('Template plans are already approved.');   // This should never happen.
2964         } else if (!$plan->is_draft()) {
2965             throw new coding_exception('The plan cannot be approved at this stage.');
2966         } else if (!$plan->can_review()) {
2967             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2968         }
2970         $plan->set_status(plan::STATUS_ACTIVE);
2971         $plan->set_reviewerid(null);
2972         $result = $plan->update();
2974         // Trigger approved event.
2975         \core\event\competency_plan_approved::create_from_plan($plan)->trigger();
2977         return $result;
2978     }
2980     /**
2981      * Unapprove a plan.
2982      *
2983      * This means making the plan draft.
2984      *
2985      * @param  int|plan $planorid The plan, or its ID.
2986      * @return bool
2987      */
2988     public static function unapprove_plan($planorid) {
2989         static::require_enabled();
2990         $plan = $planorid;
2991         if (!is_object($plan)) {
2992             $plan = new plan($plan);
2993         }
2995         // We need to be able to view the plan at least.
2996         if (!$plan->can_read()) {
2997             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2998         }
3000         if ($plan->is_based_on_template()) {
3001             throw new coding_exception('Template plans are always approved.');   // This should never happen.
3002         } else if ($plan->get_status() != plan::STATUS_ACTIVE) {
3003             throw new coding_exception('The plan cannot be sent back to draft at this stage.');
3004         } else if (!$plan->can_review()) {
3005             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3006         }
3008         $plan->set_status(plan::STATUS_DRAFT);
3009         $result = $plan->update();
3011         // Trigger unapproved event.
3012         \core\event\competency_plan_unapproved::create_from_plan($plan)->trigger();
3014         return $result;
3015     }
3017     /**
3018      * Complete a plan.
3019      *
3020      * @param int|plan $planorid The plan, or its ID.
3021      * @return bool
3022      */
3023     public static function complete_plan($planorid) {
3024         global $DB;
3025         static::require_enabled();
3027         $plan = $planorid;
3028         if (!is_object($planorid)) {
3029             $plan = new plan($planorid);
3030         }
3032         // Validate that the plan can be managed.
3033         if (!$plan->can_manage()) {
3034             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3035         }
3037         // Check if the plan was already completed.
3038         if ($plan->get_status() == plan::STATUS_COMPLETE) {
3039             throw new coding_exception('The plan is already completed.');
3040         }
3042         $originalstatus = $plan->get_status();
3043         $plan->set_status(plan::STATUS_COMPLETE);
3045         // The user should also be able to manage the plan when it's completed.
3046         if (!$plan->can_manage()) {
3047             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3048         }
3050         // Put back original status because archive needs it to extract competencies from the right table.
3051         $plan->set_status($originalstatus);
3053         // Do the things.
3054         $transaction = $DB->start_delegated_transaction();
3055         self::archive_user_competencies_in_plan($plan);
3056         $plan->set_status(plan::STATUS_COMPLETE);
3057         $success = $plan->update();
3059         if (!$success) {
3060             $transaction->rollback(new moodle_exception('The plan could not be updated.'));
3061             return $success;
3062         }
3064         $transaction->allow_commit();
3066         // Trigger updated event.
3067         \core\event\competency_plan_completed::create_from_plan($plan)->trigger();
3069         return $success;
3070     }
3072     /**
3073      * Reopen a plan.
3074      *
3075      * @param int|plan $planorid The plan, or its ID.
3076      * @return bool
3077      */
3078     public static function reopen_plan($planorid) {
3079         global $DB;
3080         static::require_enabled();
3082         $plan = $planorid;
3083         if (!is_object($planorid)) {
3084             $plan = new plan($planorid);
3085         }
3087         // Validate that the plan as it is can be managed.
3088         if (!$plan->can_manage()) {
3089             $context = context_user::instance($plan->get_userid());
3090             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
3091         }
3093         $beforestatus = $plan->get_status();
3094         $plan->set_status(plan::STATUS_ACTIVE);
3096         // Validate if status can be changed.
3097         if (!$plan->can_manage()) {
3098             $context = context_user::instance($plan->get_userid());
3099             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
3100         }
3102         // Wrap the updates in a DB transaction.
3103         $transaction = $DB->start_delegated_transaction();
3105         // Delete archived user competencies if the status of the plan is changed from complete to another status.
3106         $mustremovearchivedcompetencies = ($beforestatus == plan::STATUS_COMPLETE && $plan->get_status() != plan::STATUS_COMPLETE);
3107         if ($mustremovearchivedcompetencies) {
3108             self::remove_archived_user_competencies_in_plan($plan);
3109         }
3111         // If duedate less than or equal to duedate_threshold unset it.
3112         if ($plan->get_duedate() <= time() + plan::DUEDATE_THRESHOLD) {
3113             $plan->set_duedate(0);
3114         }
3116         $success = $plan->update();
3118         if (!$success) {
3119             $transaction->rollback(new moodle_exception('The plan could not be updated.'));
3120             return $success;
3121         }
3123         $transaction->allow_commit();
3125         // Trigger reopened event.
3126         \core\event\competency_plan_reopened::create_from_plan($plan)->trigger();
3128         return $success;
3129     }
3131     /**
3132      * Get a single competency from the user plan.
3133      *
3134      * @param  int $planorid The plan, or its ID.
3135      * @param  int $competencyid The competency id.
3136      * @return (object) array(
3137      *                      'competency' => \core_competency\competency,
3138      *                      'usercompetency' => \core_competency\user_competency
3139      *                      'usercompetencyplan' => \core_competency\user_competency_plan
3140      *                  )
3141      *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
3142      */
3143     public static function get_plan_competency($planorid, $competencyid) {
3144         static::require_enabled();
3145         $plan = $planorid;
3146         if (!is_object($planorid)) {
3147             $plan = new plan($planorid);
3148         }
3150         if (!user_competency::can_read_user($plan->get_userid())) {
3151             throw new required_capability_exception($plan->get_context(), 'moodle/competency:usercompetencyview',
3152                 'nopermissions', '');
3153         }
3155         $competency = $plan->get_competency($competencyid);
3157         // Get user competencies from user_competency_plan if the plan status is set to complete.
3158         $iscompletedplan = $plan->get_status() == plan::STATUS_COMPLETE;
3159         if ($iscompletedplan) {
3160             $usercompetencies = user_competency_plan::get_multiple($plan->get_userid(), $plan->get_id(), array($competencyid));
3161             $ucresultkey = 'usercompetencyplan';
3162         } else {
3163             $usercompetencies = user_competency::get_multiple($plan->get_userid(), array($competencyid));
3164             $ucresultkey = 'usercompetency';
3165         }
3167         $found = count($usercompetencies);
3169         if ($found) {
3170             $uc = array_pop($usercompetencies);
3171         } else {
3172             if ($iscompletedplan) {
3173                 throw new coding_exception('A user competency plan is missing');
3174             } else {
3175                 $uc = user_competency::create_relation($plan->get_userid(), $competency->get_id());
3176                 $uc->create();
3177             }
3178         }
3180         $plancompetency = (object) array(
3181             'competency' => $competency,
3182             'usercompetency' => null,
3183             'usercompetencyplan' => null
3184         );
3185         $plancompetency->$ucresultkey = $uc;
3187         return $plancompetency;
3188     }
3190     /**
3191      * List the competencies in a user plan.
3192      *
3193      * @param  int $planorid The plan, or its ID.
3194      * @return array((object) array(
3195      *                            'competency' => \core_competency\competency,
3196      *                            'usercompetency' => \core_competency\user_competency
3197      *                            'usercompetencyplan' => \core_competency\user_competency_plan
3198      *                        ))
3199      *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
3200      */
3201     public static function list_plan_competencies($planorid) {
3202         static::require_enabled();
3203         $plan = $planorid;
3204         if (!is_object($planorid)) {
3205             $plan = new plan($planorid);
3206         }
3208         if (!$plan->can_read()) {
3209             $context = context_user::instance($plan->get_userid());
3210             throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
3211         }
3213         $result = array();
3214         $competencies = $plan->get_competencies();
3216         // Get user competencies from user_competency_plan if the plan status is set to complete.
3217         $iscompletedplan = $plan->get_status() == plan::STATUS_COMPLETE;
3218         if ($iscompletedplan) {
3219             $usercompetencies = user_competency_plan::get_multiple($plan->get_userid(), $plan->get_id(), $competencies);
3220             $ucresultkey = 'usercompetencyplan';
3221         } else {
3222             $usercompetencies = user_competency::get_multiple($plan->get_userid(), $competencies);
3223             $ucresultkey = 'usercompetency';
3224         }
3226         // Build the return values.
3227         foreach ($competencies as $key => $competency) {
3228             $found = false;
3230             foreach ($usercompetencies as $uckey => $uc) {
3231                 if ($uc->get_competencyid() == $competency->get_id()) {
3232                     $found = true;
3233                     unset($usercompetencies[$uckey]);
3234                     break;
3235                 }
3236             }
3238             if (!$found) {
3239                 if ($iscompletedplan) {
3240                     throw new coding_exception('A user competency plan is missing');
3241                 } else {
3242                     $uc = user_competency::create_relation($plan->get_userid(), $competency->get_id());
3243                 }
3244             }
3246             $plancompetency = (object) array(
3247                 'competency' => $competency,
3248                 'usercompetency' => null,
3249                 'usercompetencyplan' => null
3250             );
3251             $plancompetency->$ucresultkey = $uc;
3252             $result[] = $plancompetency;
3253         }
3255         return $result;
3256     }
3258     /**
3259      * Add a competency to a plan.
3260      *
3261      * @param int $planid The id of the plan
3262      * @param int $competencyid The id of the competency
3263      * @return bool
3264      */
3265     public static function add_competency_to_plan($planid, $competencyid) {
3266         static::require_enabled();
3267         $plan = new plan($planid);
3269         // First we do a permissions check.
3270         if (!$plan->can_manage()) {
3271             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3273         } else if ($plan->is_based_on_template()) {
3274             throw new coding_exception('A competency can not be added to a learning plan based on a template');
3275         }
3277         if (!$plan->can_be_edited()) {
3278             throw new coding_exception('A competency can not be added to a learning plan completed');
3279         }
3281         $competency = new competency($competencyid);
3283         // Can not add a competency that belong to a hidden framework.
3284         if ($competency->get_framework()->get_visible() == false) {
3285             throw new coding_exception('A competency belonging to hidden framework can not be added');
3286         }
3288         $exists = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
3289         if (!$exists) {
3290             $record = new stdClass();
3291             $record->planid = $planid;
3292             $record->competencyid = $competencyid;
3293             $plancompetency = new plan_competency(0, $record);
3294             $plancompetency->create();
3295         }
3297         return true;
3298     }
3300     /**
3301      * Remove a competency from a plan.
3302      *
3303      * @param int $planid The plan id
3304      * @param int $competencyid The id of the competency
3305      * @return bool
3306      */
3307     public static function remove_competency_from_plan($planid, $competencyid) {
3308         static::require_enabled();
3309         $plan = new plan($planid);
3311         // First we do a permissions check.
3312         if (!$plan->can_manage()) {
3313             $context = context_user::instance($plan->get_userid());
3314             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
3316         } else if ($plan->is_based_on_template()) {
3317             throw new coding_exception('A competency can not be removed from a learning plan based on a template');
3318         }
3320         if (!$plan->can_be_edited()) {
3321             throw new coding_exception('A competency can not be removed from a learning plan completed');
3322         }
3324         $link = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
3325         if ($link) {
3326             return $link->delete();
3327         }
3328         return false;
3329     }
3331     /**
3332      * Move the plan competency up or down in the display list.
3333      *
3334      * Requires moodle/competency:planmanage capability at the system context.
3335      *
3336      * @param int $planid The plan  id
3337      * @param int $competencyidfrom The id of the competency we are moving.
3338      * @param int $competencyidto The id of the competency we are moving to.
3339      * @return boolean
3340      */
3341     public static function reorder_plan_competency($planid, $competencyidfrom, $competencyidto) {
3342         static::require_enabled();
3343         $plan = new plan($planid);
3345         // First we do a permissions check.
3346         if (!$plan->can_manage()) {
3347             $context = context_user::instance($plan->get_userid());
3348             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
3350         } else if ($plan->is_based_on_template()) {
3351             throw new coding_exception('A competency can not be reordered in a learning plan based on a template');
3352         }
3354         if (!$plan->can_be_edited()) {
3355             throw new coding_exception('A competency can not be reordered in a learning plan completed');
3356         }
3358         $down = true;
3359         $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidfrom));
3360         if (count($matches) == 0) {
3361             throw new coding_exception('The link does not exist');
3362         }
3364         $competencyfrom = array_pop($matches);
3365         $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidto));
3366         if (count($matches) == 0) {
3367             throw new coding_exception('The link does not exist');
3368         }
3370         $competencyto = array_pop($matches);
3372         $all = plan_competency::get_records(array('planid' => $planid), 'sortorder', 'ASC', 0, 0);
3374         if ($competencyfrom->get_sortorder() > $competencyto->get_sortorder()) {
3375             // We are moving up, so put it before the "to" item.
3376             $down = false;
3377         }
3379         foreach ($all as $id => $plancompetency) {
3380             $sort = $plancompetency->get_sortorder();
3381             if ($down && $sort > $competencyfrom->get_sortorder() && $sort <= $competencyto->get_sortorder()) {
3382                 $plancompetency->set_sortorder($plancompetency->get_sortorder() - 1);
3383                 $plancompetency->update();
3384             } else if (!$down && $sort >= $competencyto->get_sortorder() && $sort < $competencyfrom->get_sortorder()) {
3385                 $plancompetency->set_sortorder($plancompetency->get_sortorder() + 1);
3386                 $plancompetency->update();
3387             }
3388         }
3389         $competencyfrom->set_sortorder($competencyto->get_sortorder());
3390         return $competencyfrom->update();
3391     }
3393     /**
3394      * Cancel a user competency review request.
3395      *
3396      * @param  int $userid       The user ID.
3397      * @param  int $competencyid The competency ID.
3398      * @return bool
3399      */
3400     public static function user_competency_cancel_review_request($userid, $competencyid) {
3401         static::require_enabled();
3402         $context = context_user::instance($userid);
3403         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3404         if (!$uc || !$uc->can_read()) {
3405             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
3406         } else if ($uc->get_status() != user_competency::STATUS_WAITING_FOR_REVIEW) {
3407             throw new coding_exception('The competency can not be cancel review request at this stage.');
3408         } else if (!$uc->can_request_review()) {
3409             throw new required_capability_exception($context, 'moodle/competency:usercompetencyrequestreview', 'nopermissions', '');
3410         }
3412         $uc->set_status(user_competency::STATUS_IDLE);
3413         $result = $uc->update();
3414         if ($result) {
3415             \core\event\competency_user_competency_review_request_cancelled::create_from_user_competency($uc)->trigger();
3416         }
3417         return $result;
3418     }
3420     /**
3421      * Request a user competency review.
3422      *
3423      * @param  int $userid       The user ID.
3424      * @param  int $competencyid The competency ID.
3425      * @return bool
3426      */
3427     public static function user_competency_request_review($userid, $competencyid) {
3428         static::require_enabled();
3429         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3430         if (!$uc) {
3431             $uc = user_competency::create_relation($userid, $competencyid);
3432             $uc->create();
3433         }
3435         if (!$uc->can_read()) {
3436             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
3437                 'nopermissions', '');
3438         } else if ($uc->get_status() != user_competency::STATUS_IDLE) {
3439             throw new coding_exception('The competency can not be sent for review at this stage.');
3440         } else if (!$uc->can_request_review()) {
3441             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyrequestreview',
3442                 'nopermissions', '');
3443         }
3445         $uc->set_status(user_competency::STATUS_WAITING_FOR_REVIEW);
3446         $result = $uc->update();
3447         if ($result) {