ba2266228fc79dfc3177fae7a72018979cf5fd0b
[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     /** @var boolean Allow api functions even if competencies are not enabled for the site. */
50     private static $skipenabled = false;
52     /**
53      * Returns whether competencies are enabled.
54      *
55      * This method should never do more than checking the config setting, the reason
56      * being that some other code could be checking the config value directly
57      * to avoid having to load this entire file into memory.
58      *
59      * @return boolean True when enabled.
60      */
61     public static function is_enabled() {
62         return self::$skipenabled || get_config('core_competency', 'enabled');
63     }
65     /**
66      * When competencies used to be enabled, we can show the text but do not include links.
67      *
68      * @return boolean True means show links.
69      */
70     public static function show_links() {
71         return isloggedin() && !isguestuser() && get_config('core_competency', 'enabled');
72     }
74     /**
75      * Allow calls to competency api functions even if competencies are not currently enabled.
76      */
77     public static function skip_enabled() {
78         self::$skipenabled = true;
79     }
81     /**
82      * Restore the checking that competencies are enabled with any api function.
83      */
84     public static function check_enabled() {
85         self::$skipenabled = false;
86     }
88     /**
89      * Throws an exception if competencies are not enabled.
90      *
91      * @return void
92      * @throws moodle_exception
93      */
94     public static function require_enabled() {
95         if (!static::is_enabled()) {
96             throw new moodle_exception('competenciesarenotenabled', 'core_competency');
97         }
98     }
100     /**
101      * Checks whether a scale is used anywhere in the plugin.
102      *
103      * This public API has two exceptions:
104      * - It MUST NOT perform any capability checks.
105      * - It MUST ignore whether competencies are enabled or not ({@link self::is_enabled()}).
106      *
107      * @param int $scaleid The scale ID.
108      * @return bool
109      */
110     public static function is_scale_used_anywhere($scaleid) {
111         global $DB;
112         $sql = "SELECT s.id
113                   FROM {scale} s
114              LEFT JOIN {" . competency_framework::TABLE ."} f
115                     ON f.scaleid = :scaleid1
116              LEFT JOIN {" . competency::TABLE ."} c
117                     ON c.scaleid = :scaleid2
118                  WHERE f.id IS NOT NULL
119                     OR c.id IS NOT NULL";
120         return $DB->record_exists_sql($sql, ['scaleid1' => $scaleid, 'scaleid2' => $scaleid]);
121     }
123     /**
124      * Validate if current user have acces to the course_module if hidden.
125      *
126      * @param mixed $cmmixed The cm_info class, course module record or its ID.
127      * @param bool $throwexception Throw an exception or not.
128      * @return bool
129      */
130     protected static function validate_course_module($cmmixed, $throwexception = true) {
131         $cm = $cmmixed;
132         if (!is_object($cm)) {
133             $cmrecord = get_coursemodule_from_id(null, $cmmixed);
134             $modinfo = get_fast_modinfo($cmrecord->course);
135             $cm = $modinfo->get_cm($cmmixed);
136         } else if (!$cm instanceof cm_info) {
137             // Assume we got a course module record.
138             $modinfo = get_fast_modinfo($cm->course);
139             $cm = $modinfo->get_cm($cm->id);
140         }
142         if (!$cm->uservisible) {
143             if ($throwexception) {
144                 throw new require_login_exception('Course module is hidden');
145             } else {
146                 return false;
147             }
148         }
150         return true;
151     }
153     /**
154      * Validate if current user have acces to the course if hidden.
155      *
156      * @param mixed $courseorid The course or it ID.
157      * @param bool $throwexception Throw an exception or not.
158      * @return bool
159      */
160     protected static function validate_course($courseorid, $throwexception = true) {
161         $course = $courseorid;
162         if (!is_object($course)) {
163             $course = get_course($course);
164         }
166         $coursecontext = context_course::instance($course->id);
167         if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
168             if ($throwexception) {
169                 throw new require_login_exception('Course is hidden');
170             } else {
171                 return false;
172             }
173         }
175         return true;
176     }
178     /**
179      * Create a competency from a record containing all the data for the class.
180      *
181      * Requires moodle/competency:competencymanage capability at the system context.
182      *
183      * @param stdClass $record Record containing all the data for an instance of the class.
184      * @return competency
185      */
186     public static function create_competency(stdClass $record) {
187         static::require_enabled();
188         $competency = new competency(0, $record);
190         // First we do a permissions check.
191         require_capability('moodle/competency:competencymanage', $competency->get_context());
193         // Reset the sortorder, use reorder instead.
194         $competency->set('sortorder', 0);
195         $competency->create();
197         \core\event\competency_created::create_from_competency($competency)->trigger();
199         // Reset the rule of the parent.
200         $parent = $competency->get_parent();
201         if ($parent) {
202             $parent->reset_rule();
203             $parent->update();
204         }
206         return $competency;
207     }
209     /**
210      * Delete a competency by id.
211      *
212      * Requires moodle/competency:competencymanage capability at the system context.
213      *
214      * @param int $id The record to delete. This will delete alot of related data - you better be sure.
215      * @return boolean
216      */
217     public static function delete_competency($id) {
218         global $DB;
219         static::require_enabled();
220         $competency = new competency($id);
222         // First we do a permissions check.
223         require_capability('moodle/competency:competencymanage', $competency->get_context());
225         $events = array();
226         $competencyids = array(intval($competency->get('id')));
227         $contextid = $competency->get_context()->id;
228         $competencyids = array_merge(competency::get_descendants_ids($competency), $competencyids);
229         if (!competency::can_all_be_deleted($competencyids)) {
230             return false;
231         }
232         $transaction = $DB->start_delegated_transaction();
234         try {
236             // Reset the rule of the parent.
237             $parent = $competency->get_parent();
238             if ($parent) {
239                 $parent->reset_rule();
240                 $parent->update();
241             }
243             // Delete the competency separately so the after_delete event can be triggered.
244             $competency->delete();
246             // Delete the competencies.
247             competency::delete_multiple($competencyids);
249             // Delete the competencies relation.
250             related_competency::delete_multiple_relations($competencyids);
252             // Delete competency evidences.
253             user_evidence_competency::delete_by_competencyids($competencyids);
255             // Register the competencies deleted events.
256             $events = \core\event\competency_deleted::create_multiple_from_competencyids($competencyids, $contextid);
258         } catch (\Exception $e) {
259             $transaction->rollback($e);
260         }
262         $transaction->allow_commit();
263         // Trigger events.
264         foreach ($events as $event) {
265             $event->trigger();
266         }
268         return true;
269     }
271     /**
272      * Reorder this competency.
273      *
274      * Requires moodle/competency:competencymanage capability at the system context.
275      *
276      * @param int $id The id of the competency to move.
277      * @return boolean
278      */
279     public static function move_down_competency($id) {
280         static::require_enabled();
281         $current = new competency($id);
283         // First we do a permissions check.
284         require_capability('moodle/competency:competencymanage', $current->get_context());
286         $max = self::count_competencies(array('parentid' => $current->get('parentid'),
287                                               'competencyframeworkid' => $current->get('competencyframeworkid')));
288         if ($max > 0) {
289             $max--;
290         }
292         $sortorder = $current->get('sortorder');
293         if ($sortorder >= $max) {
294             return false;
295         }
296         $sortorder = $sortorder + 1;
297         $current->set('sortorder', $sortorder);
299         $filters = array('parentid' => $current->get('parentid'),
300                          'competencyframeworkid' => $current->get('competencyframeworkid'),
301                          'sortorder' => $sortorder);
302         $children = self::list_competencies($filters, 'id');
303         foreach ($children as $needtoswap) {
304             $needtoswap->set('sortorder', $sortorder - 1);
305             $needtoswap->update();
306         }
308         // OK - all set.
309         $result = $current->update();
311         return $result;
312     }
314     /**
315      * Reorder this competency.
316      *
317      * Requires moodle/competency:competencymanage capability at the system context.
318      *
319      * @param int $id The id of the competency to move.
320      * @return boolean
321      */
322     public static function move_up_competency($id) {
323         static::require_enabled();
324         $current = new competency($id);
326         // First we do a permissions check.
327         require_capability('moodle/competency:competencymanage', $current->get_context());
329         $sortorder = $current->get('sortorder');
330         if ($sortorder == 0) {
331             return false;
332         }
334         $sortorder = $sortorder - 1;
335         $current->set('sortorder', $sortorder);
337         $filters = array('parentid' => $current->get('parentid'),
338                          'competencyframeworkid' => $current->get('competencyframeworkid'),
339                          'sortorder' => $sortorder);
340         $children = self::list_competencies($filters, 'id');
341         foreach ($children as $needtoswap) {
342             $needtoswap->set('sortorder', $sortorder + 1);
343             $needtoswap->update();
344         }
346         // OK - all set.
347         $result = $current->update();
349         return $result;
350     }
352     /**
353      * Move this competency so it sits in a new parent.
354      *
355      * Requires moodle/competency:competencymanage capability at the system context.
356      *
357      * @param int $id The id of the competency to move.
358      * @param int $newparentid The new parent id for the competency.
359      * @return boolean
360      */
361     public static function set_parent_competency($id, $newparentid) {
362         global $DB;
363         static::require_enabled();
364         $current = new competency($id);
366         // First we do a permissions check.
367         require_capability('moodle/competency:competencymanage', $current->get_context());
368         if ($id == $newparentid) {
369             throw new coding_exception('Can not set a competency as a parent of itself.');
370         } if ($newparentid == $current->get('parentid')) {
371             throw new coding_exception('Can not move a competency to the same location.');
372         }
374         // Some great variable assignment right here.
375         $currentparent = $current->get_parent();
376         $parent = !empty($newparentid) ? new competency($newparentid) : null;
377         $parentpath = !empty($parent) ? $parent->get('path') : '/0/';
379         // We're going to change quite a few things.
380         $transaction = $DB->start_delegated_transaction();
382         // If we are moving a node to a child of itself:
383         // - promote all the child nodes by one level.
384         // - remove the rule on self.
385         // - re-read the parent.
386         $newparents = explode('/', $parentpath);
387         if (in_array($current->get('id'), $newparents)) {
388             $children = competency::get_records(array('parentid' => $current->get('id')), 'id');
389             foreach ($children as $child) {
390                 $child->set('parentid', $current->get('parentid'));
391                 $child->update();
392             }
394             // Reset the rule on self as our children have changed.
395             $current->reset_rule();
397             // The destination parent is one of our descendants, we need to re-fetch its values (path, parentid).
398             $parent->read();
399         }
401         // Reset the rules of initial parent and destination.
402         if (!empty($currentparent)) {
403             $currentparent->reset_rule();
404             $currentparent->update();
405         }
406         if (!empty($parent)) {
407             $parent->reset_rule();
408             $parent->update();
409         }
411         // Do the actual move.
412         $current->set('parentid', $newparentid);
413         $result = $current->update();
415         // All right, let's commit this.
416         $transaction->allow_commit();
418         return $result;
419     }
421     /**
422      * Update the details for a competency.
423      *
424      * Requires moodle/competency:competencymanage capability at the system context.
425      *
426      * @param stdClass $record The new details for the competency.
427      *                         Note - must contain an id that points to the competency to update.
428      *
429      * @return boolean
430      */
431     public static function update_competency($record) {
432         static::require_enabled();
433         $competency = new competency($record->id);
435         // First we do a permissions check.
436         require_capability('moodle/competency:competencymanage', $competency->get_context());
438         // Some things should not be changed in an update - they should use a more specific method.
439         $record->sortorder = $competency->get('sortorder');
440         $record->parentid = $competency->get('parentid');
441         $record->competencyframeworkid = $competency->get('competencyframeworkid');
443         $competency->from_record($record);
444         require_capability('moodle/competency:competencymanage', $competency->get_context());
446         // OK - all set.
447         $result = $competency->update();
449         // Trigger the update event.
450         \core\event\competency_updated::create_from_competency($competency)->trigger();
452         return $result;
453     }
455     /**
456      * Read a the details for a single competency and return a record.
457      *
458      * Requires moodle/competency:competencyview capability at the system context.
459      *
460      * @param int $id The id of the competency to read.
461      * @param bool $includerelated Include related tags or not.
462      * @return stdClass
463      */
464     public static function read_competency($id, $includerelated = false) {
465         static::require_enabled();
466         $competency = new competency($id);
468         // First we do a permissions check.
469         $context = $competency->get_context();
470         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
471              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
472         }
474         // OK - all set.
475         if ($includerelated) {
476             $relatedcompetency = new related_competency();
477             if ($related = $relatedcompetency->list_relations($id)) {
478                 $competency->relatedcompetencies = $related;
479             }
480         }
482         return $competency;
483     }
485     /**
486      * Perform a text search based and return all results and their parents.
487      *
488      * Requires moodle/competency:competencyview capability at the framework context.
489      *
490      * @param string $textsearch A string to search for.
491      * @param int $competencyframeworkid The id of the framework to limit the search.
492      * @return array of competencies
493      */
494     public static function search_competencies($textsearch, $competencyframeworkid) {
495         static::require_enabled();
496         $framework = new competency_framework($competencyframeworkid);
498         // First we do a permissions check.
499         $context = $framework->get_context();
500         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
501              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
502         }
504         // OK - all set.
505         $competencies = competency::search($textsearch, $competencyframeworkid);
506         return $competencies;
507     }
509     /**
510      * Perform a search based on the provided filters and return a paginated list of records.
511      *
512      * Requires moodle/competency:competencyview capability at some context.
513      *
514      * @param array $filters A list of filters to apply to the list.
515      * @param string $sort The column to sort on
516      * @param string $order ('ASC' or 'DESC')
517      * @param int $skip Number of records to skip (pagination)
518      * @param int $limit Max of records to return (pagination)
519      * @return array of competencies
520      */
521     public static function list_competencies($filters, $sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
522         static::require_enabled();
523         if (!isset($filters['competencyframeworkid'])) {
524             $context = context_system::instance();
525         } else {
526             $framework = new competency_framework($filters['competencyframeworkid']);
527             $context = $framework->get_context();
528         }
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::get_records($filters, $sort, $order, $skip, $limit);
537     }
539     /**
540      * Perform a search based on the provided filters and return a paginated list of records.
541      *
542      * Requires moodle/competency:competencyview capability at some context.
543      *
544      * @param array $filters A list of filters to apply to the list.
545      * @return int
546      */
547     public static function count_competencies($filters) {
548         static::require_enabled();
549         if (!isset($filters['competencyframeworkid'])) {
550             $context = context_system::instance();
551         } else {
552             $framework = new competency_framework($filters['competencyframeworkid']);
553             $context = $framework->get_context();
554         }
556         // First we do a permissions check.
557         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'), $context)) {
558              throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
559         }
561         // OK - all set.
562         return competency::count_records($filters);
563     }
565     /**
566      * Create a competency framework from a record containing all the data for the class.
567      *
568      * Requires moodle/competency:competencymanage capability at the system context.
569      *
570      * @param stdClass $record Record containing all the data for an instance of the class.
571      * @return competency_framework
572      */
573     public static function create_framework(stdClass $record) {
574         static::require_enabled();
575         $framework = new competency_framework(0, $record);
576         require_capability('moodle/competency:competencymanage', $framework->get_context());
578         // Account for different formats of taxonomies.
579         if (isset($record->taxonomies)) {
580             $framework->set('taxonomies', $record->taxonomies);
581         }
583         $framework = $framework->create();
585         // Trigger a competency framework created event.
586         \core\event\competency_framework_created::create_from_framework($framework)->trigger();
588         return $framework;
589     }
591     /**
592      * Duplicate a competency framework by id.
593      *
594      * Requires moodle/competency:competencymanage capability at the system context.
595      *
596      * @param int $id The record to duplicate. All competencies associated and related will be duplicated.
597      * @return competency_framework the framework duplicated
598      */
599     public static function duplicate_framework($id) {
600         global $DB;
601         static::require_enabled();
603         $framework = new competency_framework($id);
604         require_capability('moodle/competency:competencymanage', $framework->get_context());
605         // Starting transaction.
606         $transaction = $DB->start_delegated_transaction();
608         try {
609             // Get a uniq idnumber based on the origin framework.
610             $idnumber = competency_framework::get_unused_idnumber($framework->get('idnumber'));
611             $framework->set('idnumber', $idnumber);
612             // Adding the suffix copy to the shortname.
613             $framework->set('shortname', get_string('duplicateditemname', 'core_competency', $framework->get('shortname')));
614             $framework->set('id', 0);
615             $framework = $framework->create();
617             // Array that match the old competencies ids with the new one to use when copying related competencies.
618             $frameworkcompetency = competency::get_framework_tree($id);
619             $matchids = self::duplicate_competency_tree($framework->get('id'), $frameworkcompetency, 0, 0);
621             // Copy the related competencies.
622             $relcomps = related_competency::get_multiple_relations(array_keys($matchids));
624             foreach ($relcomps as $relcomp) {
625                 $compid = $relcomp->get('competencyid');
626                 $relcompid = $relcomp->get('relatedcompetencyid');
627                 if (isset($matchids[$compid]) && isset($matchids[$relcompid])) {
628                     $newcompid = $matchids[$compid]->get('id');
629                     $newrelcompid = $matchids[$relcompid]->get('id');
630                     if ($newcompid < $newrelcompid) {
631                         $relcomp->set('competencyid', $newcompid);
632                         $relcomp->set('relatedcompetencyid', $newrelcompid);
633                     } else {
634                         $relcomp->set('competencyid', $newrelcompid);
635                         $relcomp->set('relatedcompetencyid', $newcompid);
636                     }
637                     $relcomp->set('id', 0);
638                     $relcomp->create();
639                 } else {
640                     // Debugging message when there is no match found.
641                     debugging('related competency id not found');
642                 }
643             }
645             // Setting rules on duplicated competencies.
646             self::migrate_competency_tree_rules($frameworkcompetency, $matchids);
648             $transaction->allow_commit();
650         } catch (\Exception $e) {
651             $transaction->rollback($e);
652         }
654         // Trigger a competency framework created event.
655         \core\event\competency_framework_created::create_from_framework($framework)->trigger();
657         return $framework;
658     }
660     /**
661      * Delete a competency framework by id.
662      *
663      * Requires moodle/competency:competencymanage capability at the system context.
664      *
665      * @param int $id The record to delete. This will delete alot of related data - you better be sure.
666      * @return boolean
667      */
668     public static function delete_framework($id) {
669         global $DB;
670         static::require_enabled();
671         $framework = new competency_framework($id);
672         require_capability('moodle/competency:competencymanage', $framework->get_context());
674         $events = array();
675         $competenciesid = competency::get_ids_by_frameworkid($id);
676         $contextid = $framework->get('contextid');
677         if (!competency::can_all_be_deleted($competenciesid)) {
678             return false;
679         }
680         $transaction = $DB->start_delegated_transaction();
681         try {
682             if (!empty($competenciesid)) {
683                 // Delete competencies.
684                 competency::delete_by_frameworkid($id);
686                 // Delete the related competencies.
687                 related_competency::delete_multiple_relations($competenciesid);
689                 // Delete the evidences for competencies.
690                 user_evidence_competency::delete_by_competencyids($competenciesid);
691             }
693             // Create a competency framework deleted event.
694             $event = \core\event\competency_framework_deleted::create_from_framework($framework);
695             $result = $framework->delete();
697             // Register the deleted events competencies.
698             $events = \core\event\competency_deleted::create_multiple_from_competencyids($competenciesid, $contextid);
700         } catch (\Exception $e) {
701             $transaction->rollback($e);
702         }
704         // Commit the transaction.
705         $transaction->allow_commit();
707         // If all operations are successfull then trigger the delete event.
708         $event->trigger();
710         // Trigger deleted event competencies.
711         foreach ($events as $event) {
712             $event->trigger();
713         }
715         return $result;
716     }
718     /**
719      * Update the details for a competency framework.
720      *
721      * Requires moodle/competency:competencymanage capability at the system context.
722      *
723      * @param stdClass $record The new details for the framework. Note - must contain an id that points to the framework to update.
724      * @return boolean
725      */
726     public static function update_framework($record) {
727         static::require_enabled();
728         $framework = new competency_framework($record->id);
730         // Check the permissions before update.
731         require_capability('moodle/competency:competencymanage', $framework->get_context());
733         // Account for different formats of taxonomies.
734         $framework->from_record($record);
735         if (isset($record->taxonomies)) {
736             $framework->set('taxonomies', $record->taxonomies);
737         }
739         // Trigger a competency framework updated event.
740         \core\event\competency_framework_updated::create_from_framework($framework)->trigger();
742         return $framework->update();
743     }
745     /**
746      * Read a the details for a single competency framework and return a record.
747      *
748      * Requires moodle/competency:competencyview capability at the system context.
749      *
750      * @param int $id The id of the framework to read.
751      * @return competency_framework
752      */
753     public static function read_framework($id) {
754         static::require_enabled();
755         $framework = new competency_framework($id);
756         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
757                 $framework->get_context())) {
758             throw new required_capability_exception($framework->get_context(), 'moodle/competency:competencyview',
759                 'nopermissions', '');
760         }
761         return $framework;
762     }
764     /**
765      * Logg the competency framework viewed event.
766      *
767      * @param competency_framework|int $frameworkorid The competency_framework object or competency framework id
768      * @return bool
769      */
770     public static function competency_framework_viewed($frameworkorid) {
771         static::require_enabled();
772         $framework = $frameworkorid;
773         if (!is_object($framework)) {
774             $framework = new competency_framework($framework);
775         }
776         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
777                 $framework->get_context())) {
778             throw new required_capability_exception($framework->get_context(), 'moodle/competency:competencyview',
779                 'nopermissions', '');
780         }
781         \core\event\competency_framework_viewed::create_from_framework($framework)->trigger();
782         return true;
783     }
785     /**
786      * Logg the competency viewed event.
787      *
788      * @param competency|int $competencyorid The competency object or competency id
789      * @return bool
790      */
791     public static function competency_viewed($competencyorid) {
792         static::require_enabled();
793         $competency = $competencyorid;
794         if (!is_object($competency)) {
795             $competency = new competency($competency);
796         }
798         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
799                 $competency->get_context())) {
800             throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview',
801                 'nopermissions', '');
802         }
804         \core\event\competency_viewed::create_from_competency($competency)->trigger();
805         return true;
806     }
808     /**
809      * Perform a search based on the provided filters and return a paginated list of records.
810      *
811      * Requires moodle/competency:competencyview capability at the system context.
812      *
813      * @param string $sort The column to sort on
814      * @param string $order ('ASC' or 'DESC')
815      * @param int $skip Number of records to skip (pagination)
816      * @param int $limit Max of records to return (pagination)
817      * @param context $context The parent context of the frameworks.
818      * @param string $includes Defines what other contexts to fetch frameworks from.
819      *                         Accepted values are:
820      *                          - children: All descendants
821      *                          - parents: All parents, grand parents, etc...
822      *                          - self: Context passed only.
823      * @param bool $onlyvisible If true return only visible frameworks
824      * @param string $query A string to use to filter down the frameworks.
825      * @return array of competency_framework
826      */
827     public static function list_frameworks($sort, $order, $skip, $limit, $context, $includes = 'children',
828                                            $onlyvisible = false, $query = '') {
829         global $DB;
830         static::require_enabled();
832         // Get all the relevant contexts.
833         $contexts = self::get_related_contexts($context, $includes,
834             array('moodle/competency:competencyview', 'moodle/competency:competencymanage'));
836         if (empty($contexts)) {
837             throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
838         }
840         // OK - all set.
841         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
842         $select = "contextid $insql";
843         if ($onlyvisible) {
844             $select .= " AND visible = :visible";
845             $inparams['visible'] = 1;
846         }
848         if (!empty($query) || is_numeric($query)) {
849             $sqlnamelike = $DB->sql_like('shortname', ':namelike', false);
850             $sqlidnlike = $DB->sql_like('idnumber', ':idnlike', false);
852             $select .= " AND ($sqlnamelike OR $sqlidnlike) ";
853             $inparams['namelike'] = '%' . $DB->sql_like_escape($query) . '%';
854             $inparams['idnlike'] = '%' . $DB->sql_like_escape($query) . '%';
855         }
857         return competency_framework::get_records_select($select, $inparams, $sort . ' ' . $order, '*', $skip, $limit);
858     }
860     /**
861      * Perform a search based on the provided filters and return a paginated list of records.
862      *
863      * Requires moodle/competency:competencyview capability at the system context.
864      *
865      * @param context $context The parent context of the frameworks.
866      * @param string $includes Defines what other contexts to fetch frameworks from.
867      *                         Accepted values are:
868      *                          - children: All descendants
869      *                          - parents: All parents, grand parents, etc...
870      *                          - self: Context passed only.
871      * @return int
872      */
873     public static function count_frameworks($context, $includes) {
874         global $DB;
875         static::require_enabled();
877         // Get all the relevant contexts.
878         $contexts = self::get_related_contexts($context, $includes,
879             array('moodle/competency:competencyview', 'moodle/competency:competencymanage'));
881         if (empty($contexts)) {
882             throw new required_capability_exception($context, 'moodle/competency:competencyview', 'nopermissions', '');
883         }
885         // OK - all set.
886         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
887         return competency_framework::count_records_select("contextid $insql", $inparams);
888     }
890     /**
891      * Fetches all the relevant contexts.
892      *
893      * Note: This currently only supports system, category and user contexts. However user contexts
894      * behave a bit differently and will fallback on the system context. This is what makes the most
895      * sense because a user context does not have descendants, and only has system as a parent.
896      *
897      * @param context $context The context to start from.
898      * @param string $includes Defines what other contexts to find.
899      *                         Accepted values are:
900      *                          - children: All descendants
901      *                          - parents: All parents, grand parents, etc...
902      *                          - self: Context passed only.
903      * @param array $hasanycapability Array of capabilities passed to {@link has_any_capability()} in each context.
904      * @return context[] An array of contexts where keys are context IDs.
905      */
906     public static function get_related_contexts($context, $includes, array $hasanycapability = null) {
907         global $DB;
908         static::require_enabled();
910         if (!in_array($includes, array('children', 'parents', 'self'))) {
911             throw new coding_exception('Invalid parameter value for \'includes\'.');
912         }
914         // If context user swap it for the context_system.
915         if ($context->contextlevel == CONTEXT_USER) {
916             $context = context_system::instance();
917         }
919         $contexts = array($context->id => $context);
921         if ($includes == 'children') {
922             $params = array('coursecatlevel' => CONTEXT_COURSECAT, 'path' => $context->path . '/%');
923             $pathlike = $DB->sql_like('path', ':path');
924             $sql = "contextlevel = :coursecatlevel AND $pathlike";
925             $rs = $DB->get_recordset_select('context', $sql, $params);
926             foreach ($rs as $record) {
927                 $ctxid = $record->id;
928                 context_helper::preload_from_record($record);
929                 $contexts[$ctxid] = context::instance_by_id($ctxid);
930             }
931             $rs->close();
933         } else if ($includes == 'parents') {
934             $children = $context->get_parent_contexts();
935             foreach ($children as $ctx) {
936                 $contexts[$ctx->id] = $ctx;
937             }
938         }
940         // Filter according to the capabilities required.
941         if (!empty($hasanycapability)) {
942             foreach ($contexts as $key => $ctx) {
943                 if (!has_any_capability($hasanycapability, $ctx)) {
944                     unset($contexts[$key]);
945                 }
946             }
947         }
949         return $contexts;
950     }
952     /**
953      * Count all the courses using a competency.
954      *
955      * @param int $competencyid The id of the competency to check.
956      * @return int
957      */
958     public static function count_courses_using_competency($competencyid) {
959         static::require_enabled();
961         // OK - all set.
962         $courses = course_competency::list_courses_min($competencyid);
963         $count = 0;
965         // Now check permissions on each course.
966         foreach ($courses as $course) {
967             if (!self::validate_course($course, false)) {
968                 continue;
969             }
971             $context = context_course::instance($course->id);
972             $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
973             if (!has_any_capability($capabilities, $context)) {
974                 continue;
975             }
977             $count++;
978         }
980         return $count;
981     }
983     /**
984      * List all the courses modules using a competency in a course.
985      *
986      * @param int $competencyid The id of the competency to check.
987      * @param int $courseid The id of the course to check.
988      * @return array[int] Array of course modules ids.
989      */
990     public static function list_course_modules_using_competency($competencyid, $courseid) {
991         static::require_enabled();
993         $result = array();
994         self::validate_course($courseid);
996         $coursecontext = context_course::instance($courseid);
998         // We will not check each module - course permissions should be enough.
999         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1000         if (!has_any_capability($capabilities, $coursecontext)) {
1001             throw new required_capability_exception($coursecontext, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1002         }
1004         $cmlist = course_module_competency::list_course_modules($competencyid, $courseid);
1005         foreach ($cmlist as $cmid) {
1006             if (self::validate_course_module($cmid, false)) {
1007                 array_push($result, $cmid);
1008             }
1009         }
1011         return $result;
1012     }
1014     /**
1015      * List all the competencies linked to a course module.
1016      *
1017      * @param mixed $cmorid The course module, or its ID.
1018      * @return array[competency] Array of competency records.
1019      */
1020     public static function list_course_module_competencies_in_course_module($cmorid) {
1021         static::require_enabled();
1022         $cm = $cmorid;
1023         if (!is_object($cmorid)) {
1024             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1025         }
1027         // Check the user have access to the course module.
1028         self::validate_course_module($cm);
1029         $context = context_module::instance($cm->id);
1031         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1032         if (!has_any_capability($capabilities, $context)) {
1033             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1034         }
1036         $result = array();
1038         $cmclist = course_module_competency::list_course_module_competencies($cm->id);
1039         foreach ($cmclist as $id => $cmc) {
1040             array_push($result, $cmc);
1041         }
1043         return $result;
1044     }
1046     /**
1047      * List all the courses using a competency.
1048      *
1049      * @param int $competencyid The id of the competency to check.
1050      * @return array[stdClass] Array of stdClass containing id and shortname.
1051      */
1052     public static function list_courses_using_competency($competencyid) {
1053         static::require_enabled();
1055         // OK - all set.
1056         $courses = course_competency::list_courses($competencyid);
1057         $result = array();
1059         // Now check permissions on each course.
1060         foreach ($courses as $id => $course) {
1061             $context = context_course::instance($course->id);
1062             $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1063             if (!has_any_capability($capabilities, $context)) {
1064                 unset($courses[$id]);
1065                 continue;
1066             }
1067             if (!self::validate_course($course, false)) {
1068                 unset($courses[$id]);
1069                 continue;
1070             }
1071             array_push($result, $course);
1072         }
1074         return $result;
1075     }
1077     /**
1078      * Count the proficient competencies in a course for one user.
1079      *
1080      * @param int $courseid The id of the course to check.
1081      * @param int $userid The id of the user to check.
1082      * @return int
1083      */
1084     public static function count_proficient_competencies_in_course_for_user($courseid, $userid) {
1085         static::require_enabled();
1086         // Check the user have access to the course.
1087         self::validate_course($courseid);
1089         // First we do a permissions check.
1090         $context = context_course::instance($courseid);
1092         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1093         if (!has_any_capability($capabilities, $context)) {
1094              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1095         }
1097         // OK - all set.
1098         return user_competency_course::count_proficient_competencies($courseid, $userid);
1099     }
1101     /**
1102      * Count all the competencies in a course.
1103      *
1104      * @param int $courseid The id of the course to check.
1105      * @return int
1106      */
1107     public static function count_competencies_in_course($courseid) {
1108         static::require_enabled();
1109         // Check the user have access to the course.
1110         self::validate_course($courseid);
1112         // First we do a permissions check.
1113         $context = context_course::instance($courseid);
1115         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1116         if (!has_any_capability($capabilities, $context)) {
1117              throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1118         }
1120         // OK - all set.
1121         return course_competency::count_competencies($courseid);
1122     }
1124     /**
1125      * List the competencies associated to a course.
1126      *
1127      * @param mixed $courseorid The course, or its ID.
1128      * @return array( array(
1129      *                   'competency' => \core_competency\competency,
1130      *                   'coursecompetency' => \core_competency\course_competency
1131      *              ))
1132      */
1133     public static function list_course_competencies($courseorid) {
1134         static::require_enabled();
1135         $course = $courseorid;
1136         if (!is_object($courseorid)) {
1137             $course = get_course($courseorid);
1138         }
1140         // Check the user have access to the course.
1141         self::validate_course($course);
1142         $context = context_course::instance($course->id);
1144         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1145         if (!has_any_capability($capabilities, $context)) {
1146             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1147         }
1149         $result = array();
1151         // TODO We could improve the performance of this into one single query.
1152         $coursecompetencies = course_competency::list_course_competencies($course->id);
1153         $competencies = course_competency::list_competencies($course->id);
1155         // Build the return values.
1156         foreach ($coursecompetencies as $key => $coursecompetency) {
1157             $result[] = array(
1158                 'competency' => $competencies[$coursecompetency->get('competencyid')],
1159                 'coursecompetency' => $coursecompetency
1160             );
1161         }
1163         return $result;
1164     }
1166     /**
1167      * Get a user competency.
1168      *
1169      * @param int $userid The user ID.
1170      * @param int $competencyid The competency ID.
1171      * @return user_competency
1172      */
1173     public static function get_user_competency($userid, $competencyid) {
1174         static::require_enabled();
1175         $existing = user_competency::get_multiple($userid, array($competencyid));
1176         $uc = array_pop($existing);
1178         if (!$uc) {
1179             $uc = user_competency::create_relation($userid, $competencyid);
1180             $uc->create();
1181         }
1183         if (!$uc->can_read()) {
1184             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
1185                 'nopermissions', '');
1186         }
1187         return $uc;
1188     }
1190     /**
1191      * Get a user competency by ID.
1192      *
1193      * @param int $usercompetencyid The user competency ID.
1194      * @return user_competency
1195      */
1196     public static function get_user_competency_by_id($usercompetencyid) {
1197         static::require_enabled();
1198         $uc = new user_competency($usercompetencyid);
1199         if (!$uc->can_read()) {
1200             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
1201                 'nopermissions', '');
1202         }
1203         return $uc;
1204     }
1206     /**
1207      * Count the competencies associated to a course module.
1208      *
1209      * @param mixed $cmorid The course module, or its ID.
1210      * @return int
1211      */
1212     public static function count_course_module_competencies($cmorid) {
1213         static::require_enabled();
1214         $cm = $cmorid;
1215         if (!is_object($cmorid)) {
1216             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1217         }
1219         // Check the user have access to the course module.
1220         self::validate_course_module($cm);
1221         $context = context_module::instance($cm->id);
1223         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1224         if (!has_any_capability($capabilities, $context)) {
1225             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1226         }
1228         return course_module_competency::count_competencies($cm->id);
1229     }
1231     /**
1232      * List the competencies associated to a course module.
1233      *
1234      * @param mixed $cmorid The course module, or its ID.
1235      * @return array( array(
1236      *                   'competency' => \core_competency\competency,
1237      *                   'coursemodulecompetency' => \core_competency\course_module_competency
1238      *              ))
1239      */
1240     public static function list_course_module_competencies($cmorid) {
1241         static::require_enabled();
1242         $cm = $cmorid;
1243         if (!is_object($cmorid)) {
1244             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1245         }
1247         // Check the user have access to the course module.
1248         self::validate_course_module($cm);
1249         $context = context_module::instance($cm->id);
1251         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1252         if (!has_any_capability($capabilities, $context)) {
1253             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1254         }
1256         $result = array();
1258         // TODO We could improve the performance of this into one single query.
1259         $coursemodulecompetencies = course_module_competency::list_course_module_competencies($cm->id);
1260         $competencies = course_module_competency::list_competencies($cm->id);
1262         // Build the return values.
1263         foreach ($coursemodulecompetencies as $key => $coursemodulecompetency) {
1264             $result[] = array(
1265                 'competency' => $competencies[$coursemodulecompetency->get('competencyid')],
1266                 'coursemodulecompetency' => $coursemodulecompetency
1267             );
1268         }
1270         return $result;
1271     }
1273     /**
1274      * Get a user competency in a course.
1275      *
1276      * @param int $courseid The id of the course to check.
1277      * @param int $userid The id of the course to check.
1278      * @param int $competencyid The id of the competency.
1279      * @return user_competency_course
1280      */
1281     public static function get_user_competency_in_course($courseid, $userid, $competencyid) {
1282         static::require_enabled();
1283         // First we do a permissions check.
1284         $context = context_course::instance($courseid);
1286         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1287         if (!has_any_capability($capabilities, $context)) {
1288             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1289         } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
1290             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
1291         }
1293         // This will throw an exception if the competency does not belong to the course.
1294         $competency = course_competency::get_competency($courseid, $competencyid);
1296         $params = array('courseid' => $courseid, 'userid' => $userid, 'competencyid' => $competencyid);
1297         $exists = user_competency_course::get_record($params);
1298         // Create missing.
1299         if ($exists) {
1300             $ucc = $exists;
1301         } else {
1302             $ucc = user_competency_course::create_relation($userid, $competency->get('id'), $courseid);
1303             $ucc->create();
1304         }
1306         return $ucc;
1307     }
1309     /**
1310      * List all the user competencies in a course.
1311      *
1312      * @param int $courseid The id of the course to check.
1313      * @param int $userid The id of the course to check.
1314      * @return array of user_competency_course objects
1315      */
1316     public static function list_user_competencies_in_course($courseid, $userid) {
1317         static::require_enabled();
1318         // First we do a permissions check.
1319         $context = context_course::instance($courseid);
1320         $onlyvisible = 1;
1322         $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
1323         if (!has_any_capability($capabilities, $context)) {
1324             throw new required_capability_exception($context, 'moodle/competency:coursecompetencyview', 'nopermissions', '');
1325         } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
1326             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
1327         }
1329         // OK - all set.
1330         $competencylist = course_competency::list_competencies($courseid, false);
1332         $existing = user_competency_course::get_multiple($userid, $courseid, $competencylist);
1333         // Create missing.
1334         $orderedusercompetencycourses = array();
1336         $somemissing = false;
1337         foreach ($competencylist as $coursecompetency) {
1338             $found = false;
1339             foreach ($existing as $usercompetencycourse) {
1340                 if ($usercompetencycourse->get('competencyid') == $coursecompetency->get('id')) {
1341                     $found = true;
1342                     $orderedusercompetencycourses[$usercompetencycourse->get('id')] = $usercompetencycourse;
1343                     break;
1344                 }
1345             }
1346             if (!$found) {
1347                 $ucc = user_competency_course::create_relation($userid, $coursecompetency->get('id'), $courseid);
1348                 $ucc->create();
1349                 $orderedusercompetencycourses[$ucc->get('id')] = $ucc;
1350             }
1351         }
1353         return $orderedusercompetencycourses;
1354     }
1356     /**
1357      * List the user competencies to review.
1358      *
1359      * The method returns values in this format:
1360      *
1361      * array(
1362      *     'competencies' => array(
1363      *         (stdClass)(
1364      *             'usercompetency' => (user_competency),
1365      *             'competency' => (competency),
1366      *             'user' => (user)
1367      *         )
1368      *     ),
1369      *     'count' => (int)
1370      * )
1371      *
1372      * @param int $skip The number of records to skip.
1373      * @param int $limit The number of results to return.
1374      * @param int $userid The user we're getting the competencies to review for.
1375      * @return array Containing the keys 'count', and 'competencies'. The 'competencies' key contains an object
1376      *               which contains 'competency', 'usercompetency' and 'user'.
1377      */
1378     public static function list_user_competencies_to_review($skip = 0, $limit = 50, $userid = null) {
1379         global $DB, $USER;
1380         static::require_enabled();
1381         if ($userid === null) {
1382             $userid = $USER->id;
1383         }
1385         $capability = 'moodle/competency:usercompetencyreview';
1386         $ucfields = user_competency::get_sql_fields('uc', 'uc_');
1387         $compfields = competency::get_sql_fields('c', 'c_');
1388         $usercols = array('id') + get_user_fieldnames();
1389         $userfields = array();
1390         foreach ($usercols as $field) {
1391             $userfields[] = "u." . $field . " AS usr_" . $field;
1392         }
1393         $userfields = implode(',', $userfields);
1395         $select = "SELECT $ucfields, $compfields, $userfields";
1396         $countselect = "SELECT COUNT('x')";
1397         $sql = "  FROM {" . user_competency::TABLE . "} uc
1398                   JOIN {" . competency::TABLE . "} c
1399                     ON c.id = uc.competencyid
1400                   JOIN {user} u
1401                     ON u.id = uc.userid
1402                  WHERE (uc.status = :waitingforreview
1403                     OR (uc.status = :inreview AND uc.reviewerid = :reviewerid))
1404                    AND u.deleted = 0";
1405         $ordersql = " ORDER BY c.shortname ASC";
1406         $params = array(
1407             'inreview' => user_competency::STATUS_IN_REVIEW,
1408             'reviewerid' => $userid,
1409             'waitingforreview' => user_competency::STATUS_WAITING_FOR_REVIEW,
1410         );
1411         $countsql = $countselect . $sql;
1413         // Primary check to avoid the hard work of getting the users in which the user has permission.
1414         $count = $DB->count_records_sql($countselect . $sql, $params);
1415         if ($count < 1) {
1416             return array('count' => 0, 'competencies' => array());
1417         }
1419         // TODO MDL-52243 Use core function.
1420         list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql(
1421             $capability, $userid, SQL_PARAMS_NAMED);
1422         $params += $inparams;
1423         $countsql = $countselect . $sql . " AND uc.userid $insql";
1424         $getsql = $select . $sql . " AND uc.userid $insql " . $ordersql;
1426         // Extracting the results.
1427         $competencies = array();
1428         $records = $DB->get_recordset_sql($getsql, $params, $skip, $limit);
1429         foreach ($records as $record) {
1430             $objects = (object) array(
1431                 'usercompetency' => new user_competency(0, user_competency::extract_record($record, 'uc_')),
1432                 'competency' => new competency(0, competency::extract_record($record, 'c_')),
1433                 'user' => persistent::extract_record($record, 'usr_'),
1434             );
1435             $competencies[] = $objects;
1436         }
1437         $records->close();
1439         return array(
1440             'count' => $DB->count_records_sql($countsql, $params),
1441             'competencies' => $competencies
1442         );
1443     }
1445     /**
1446      * Add a competency to this course module.
1447      *
1448      * @param mixed $cmorid The course module, or id of the course module
1449      * @param int $competencyid The id of the competency
1450      * @return bool
1451      */
1452     public static function add_competency_to_course_module($cmorid, $competencyid) {
1453         static::require_enabled();
1454         $cm = $cmorid;
1455         if (!is_object($cmorid)) {
1456             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1457         }
1459         // Check the user have access to the course module.
1460         self::validate_course_module($cm);
1462         // First we do a permissions check.
1463         $context = context_module::instance($cm->id);
1465         require_capability('moodle/competency:coursecompetencymanage', $context);
1467         // Check that the competency belongs to the course.
1468         $exists = course_competency::get_records(array('courseid' => $cm->course, 'competencyid' => $competencyid));
1469         if (!$exists) {
1470             throw new coding_exception('Cannot add a competency to a module if it does not belong to the course');
1471         }
1473         $record = new stdClass();
1474         $record->cmid = $cm->id;
1475         $record->competencyid = $competencyid;
1477         $coursemodulecompetency = new course_module_competency();
1478         $exists = $coursemodulecompetency->get_records(array('cmid' => $cm->id, 'competencyid' => $competencyid));
1479         if (!$exists) {
1480             $coursemodulecompetency->from_record($record);
1481             if ($coursemodulecompetency->create()) {
1482                 return true;
1483             }
1484         }
1485         return false;
1486     }
1488     /**
1489      * Remove a competency from this course module.
1490      *
1491      * @param mixed $cmorid The course module, or id of the course module
1492      * @param int $competencyid The id of the competency
1493      * @return bool
1494      */
1495     public static function remove_competency_from_course_module($cmorid, $competencyid) {
1496         static::require_enabled();
1497         $cm = $cmorid;
1498         if (!is_object($cmorid)) {
1499             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1500         }
1501         // Check the user have access to the course module.
1502         self::validate_course_module($cm);
1504         // First we do a permissions check.
1505         $context = context_module::instance($cm->id);
1507         require_capability('moodle/competency:coursecompetencymanage', $context);
1509         $record = new stdClass();
1510         $record->cmid = $cm->id;
1511         $record->competencyid = $competencyid;
1513         $competency = new competency($competencyid);
1514         $exists = course_module_competency::get_record(array('cmid' => $cm->id, 'competencyid' => $competencyid));
1515         if ($exists) {
1516             return $exists->delete();
1517         }
1518         return false;
1519     }
1521     /**
1522      * Move the course module competency up or down in the display list.
1523      *
1524      * Requires moodle/competency:coursecompetencymanage capability at the course module context.
1525      *
1526      * @param mixed $cmorid The course module, or id of the course module
1527      * @param int $competencyidfrom The id of the competency we are moving.
1528      * @param int $competencyidto The id of the competency we are moving to.
1529      * @return boolean
1530      */
1531     public static function reorder_course_module_competency($cmorid, $competencyidfrom, $competencyidto) {
1532         static::require_enabled();
1533         $cm = $cmorid;
1534         if (!is_object($cmorid)) {
1535             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1536         }
1537         // Check the user have access to the course module.
1538         self::validate_course_module($cm);
1540         // First we do a permissions check.
1541         $context = context_module::instance($cm->id);
1543         require_capability('moodle/competency:coursecompetencymanage', $context);
1545         $down = true;
1546         $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidfrom));
1547         if (count($matches) == 0) {
1548              throw new coding_exception('The link does not exist');
1549         }
1551         $competencyfrom = array_pop($matches);
1552         $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidto));
1553         if (count($matches) == 0) {
1554              throw new coding_exception('The link does not exist');
1555         }
1557         $competencyto = array_pop($matches);
1559         $all = course_module_competency::get_records(array('cmid' => $cm->id), 'sortorder', 'ASC', 0, 0);
1561         if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
1562             // We are moving up, so put it before the "to" item.
1563             $down = false;
1564         }
1566         foreach ($all as $id => $coursemodulecompetency) {
1567             $sort = $coursemodulecompetency->get('sortorder');
1568             if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
1569                 $coursemodulecompetency->set('sortorder', $coursemodulecompetency->get('sortorder') - 1);
1570                 $coursemodulecompetency->update();
1571             } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
1572                 $coursemodulecompetency->set('sortorder', $coursemodulecompetency->get('sortorder') + 1);
1573                 $coursemodulecompetency->update();
1574             }
1575         }
1576         $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
1577         return $competencyfrom->update();
1578     }
1580     /**
1581      * Update ruleoutcome value for a course module competency.
1582      *
1583      * @param int|course_module_competency $coursemodulecompetencyorid The course_module_competency, or its ID.
1584      * @param int $ruleoutcome The value of ruleoutcome.
1585      * @return bool True on success.
1586      */
1587     public static function set_course_module_competency_ruleoutcome($coursemodulecompetencyorid, $ruleoutcome) {
1588         static::require_enabled();
1589         $coursemodulecompetency = $coursemodulecompetencyorid;
1590         if (!is_object($coursemodulecompetency)) {
1591             $coursemodulecompetency = new course_module_competency($coursemodulecompetencyorid);
1592         }
1594         $cm = get_coursemodule_from_id('', $coursemodulecompetency->get('cmid'), 0, true, MUST_EXIST);
1596         self::validate_course_module($cm);
1597         $context = context_module::instance($cm->id);
1599         require_capability('moodle/competency:coursecompetencymanage', $context);
1601         $coursemodulecompetency->set('ruleoutcome', $ruleoutcome);
1602         return $coursemodulecompetency->update();
1603     }
1605     /**
1606      * Add a competency to this course.
1607      *
1608      * @param int $courseid The id of the course
1609      * @param int $competencyid The id of the competency
1610      * @return bool
1611      */
1612     public static function add_competency_to_course($courseid, $competencyid) {
1613         static::require_enabled();
1614         // Check the user have access to the course.
1615         self::validate_course($courseid);
1617         // First we do a permissions check.
1618         $context = context_course::instance($courseid);
1620         require_capability('moodle/competency:coursecompetencymanage', $context);
1622         $record = new stdClass();
1623         $record->courseid = $courseid;
1624         $record->competencyid = $competencyid;
1626         $competency = new competency($competencyid);
1628         // Can not add a competency that belong to a hidden framework.
1629         if ($competency->get_framework()->get('visible') == false) {
1630             throw new coding_exception('A competency belonging to hidden framework can not be linked to course');
1631         }
1633         $coursecompetency = new course_competency();
1634         $exists = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyid));
1635         if (!$exists) {
1636             $coursecompetency->from_record($record);
1637             if ($coursecompetency->create()) {
1638                 return true;
1639             }
1640         }
1641         return false;
1642     }
1644     /**
1645      * Remove a competency from this course.
1646      *
1647      * @param int $courseid The id of the course
1648      * @param int $competencyid The id of the competency
1649      * @return bool
1650      */
1651     public static function remove_competency_from_course($courseid, $competencyid) {
1652         static::require_enabled();
1653         // Check the user have access to the course.
1654         self::validate_course($courseid);
1656         // First we do a permissions check.
1657         $context = context_course::instance($courseid);
1659         require_capability('moodle/competency:coursecompetencymanage', $context);
1661         $record = new stdClass();
1662         $record->courseid = $courseid;
1663         $record->competencyid = $competencyid;
1665         $coursecompetency = new course_competency();
1666         $exists = course_competency::get_record(array('courseid' => $courseid, 'competencyid' => $competencyid));
1667         if ($exists) {
1668             // Delete all course_module_competencies for this competency in this course.
1669             $cmcs = course_module_competency::get_records_by_competencyid_in_course($competencyid, $courseid);
1670             foreach ($cmcs as $cmc) {
1671                 $cmc->delete();
1672             }
1673             return $exists->delete();
1674         }
1675         return false;
1676     }
1678     /**
1679      * Move the course competency up or down in the display list.
1680      *
1681      * Requires moodle/competency:coursecompetencymanage capability at the course context.
1682      *
1683      * @param int $courseid The course
1684      * @param int $competencyidfrom The id of the competency we are moving.
1685      * @param int $competencyidto The id of the competency we are moving to.
1686      * @return boolean
1687      */
1688     public static function reorder_course_competency($courseid, $competencyidfrom, $competencyidto) {
1689         static::require_enabled();
1690         // Check the user have access to the course.
1691         self::validate_course($courseid);
1693         // First we do a permissions check.
1694         $context = context_course::instance($courseid);
1696         require_capability('moodle/competency:coursecompetencymanage', $context);
1698         $down = true;
1699         $coursecompetency = new course_competency();
1700         $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidfrom));
1701         if (count($matches) == 0) {
1702              throw new coding_exception('The link does not exist');
1703         }
1705         $competencyfrom = array_pop($matches);
1706         $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidto));
1707         if (count($matches) == 0) {
1708              throw new coding_exception('The link does not exist');
1709         }
1711         $competencyto = array_pop($matches);
1713         $all = $coursecompetency->get_records(array('courseid' => $courseid), 'sortorder', 'ASC', 0, 0);
1715         if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
1716             // We are moving up, so put it before the "to" item.
1717             $down = false;
1718         }
1720         foreach ($all as $id => $coursecompetency) {
1721             $sort = $coursecompetency->get('sortorder');
1722             if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
1723                 $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') - 1);
1724                 $coursecompetency->update();
1725             } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
1726                 $coursecompetency->set('sortorder', $coursecompetency->get('sortorder') + 1);
1727                 $coursecompetency->update();
1728             }
1729         }
1730         $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
1731         return $competencyfrom->update();
1732     }
1734     /**
1735      * Update ruleoutcome value for a course competency.
1736      *
1737      * @param int|course_competency $coursecompetencyorid The course_competency, or its ID.
1738      * @param int $ruleoutcome The value of ruleoutcome.
1739      * @return bool True on success.
1740      */
1741     public static function set_course_competency_ruleoutcome($coursecompetencyorid, $ruleoutcome) {
1742         static::require_enabled();
1743         $coursecompetency = $coursecompetencyorid;
1744         if (!is_object($coursecompetency)) {
1745             $coursecompetency = new course_competency($coursecompetencyorid);
1746         }
1748         $courseid = $coursecompetency->get('courseid');
1749         self::validate_course($courseid);
1750         $coursecontext = context_course::instance($courseid);
1752         require_capability('moodle/competency:coursecompetencymanage', $coursecontext);
1754         $coursecompetency->set('ruleoutcome', $ruleoutcome);
1755         return $coursecompetency->update();
1756     }
1758     /**
1759      * Create a learning plan template from a record containing all the data for the class.
1760      *
1761      * Requires moodle/competency:templatemanage capability.
1762      *
1763      * @param stdClass $record Record containing all the data for an instance of the class.
1764      * @return template
1765      */
1766     public static function create_template(stdClass $record) {
1767         static::require_enabled();
1768         $template = new template(0, $record);
1770         // First we do a permissions check.
1771         if (!$template->can_manage()) {
1772             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1773                 'nopermissions', '');
1774         }
1776         // OK - all set.
1777         $template = $template->create();
1779         // Trigger a template created event.
1780         \core\event\competency_template_created::create_from_template($template)->trigger();
1782         return $template;
1783     }
1785     /**
1786      * Duplicate a learning plan template.
1787      *
1788      * Requires moodle/competency:templatemanage capability at the template context.
1789      *
1790      * @param int $id the template id.
1791      * @return template
1792      */
1793     public static function duplicate_template($id) {
1794         static::require_enabled();
1795         $template = new template($id);
1797         // First we do a permissions check.
1798         if (!$template->can_manage()) {
1799             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1800                 'nopermissions', '');
1801         }
1803         // OK - all set.
1804         $competencies = template_competency::list_competencies($id, false);
1806         // Adding the suffix copy.
1807         $template->set('shortname', get_string('duplicateditemname', 'core_competency', $template->get('shortname')));
1808         $template->set('id', 0);
1810         $duplicatedtemplate = $template->create();
1812         // Associate each competency for the duplicated template.
1813         foreach ($competencies as $competency) {
1814             self::add_competency_to_template($duplicatedtemplate->get('id'), $competency->get('id'));
1815         }
1817         // Trigger a template created event.
1818         \core\event\competency_template_created::create_from_template($duplicatedtemplate)->trigger();
1820         return $duplicatedtemplate;
1821     }
1823     /**
1824      * Delete a learning plan template by id.
1825      * If the learning plan template has associated cohorts they will be deleted.
1826      *
1827      * Requires moodle/competency:templatemanage capability.
1828      *
1829      * @param int $id The record to delete.
1830      * @param boolean $deleteplans True to delete plans associaated to template, false to unlink them.
1831      * @return boolean
1832      */
1833     public static function delete_template($id, $deleteplans = true) {
1834         global $DB;
1835         static::require_enabled();
1836         $template = new template($id);
1838         // First we do a permissions check.
1839         if (!$template->can_manage()) {
1840             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1841                 'nopermissions', '');
1842         }
1844         $transaction = $DB->start_delegated_transaction();
1845         $success = true;
1847         // Check if there are cohorts associated.
1848         $templatecohorts = template_cohort::get_relations_by_templateid($template->get('id'));
1849         foreach ($templatecohorts as $templatecohort) {
1850             $success = $templatecohort->delete();
1851             if (!$success) {
1852                 break;
1853             }
1854         }
1856         // Still OK, delete or unlink the plans from the template.
1857         if ($success) {
1858             $plans = plan::get_records(array('templateid' => $template->get('id')));
1859             foreach ($plans as $plan) {
1860                 $success = $deleteplans ? self::delete_plan($plan->get('id')) : self::unlink_plan_from_template($plan);
1861                 if (!$success) {
1862                     break;
1863                 }
1864             }
1865         }
1867         // Still OK, delete the template comptencies.
1868         if ($success) {
1869             $success = template_competency::delete_by_templateid($template->get('id'));
1870         }
1872         // OK - all set.
1873         if ($success) {
1874             // Create a template deleted event.
1875             $event = \core\event\competency_template_deleted::create_from_template($template);
1877             $success = $template->delete();
1878         }
1880         if ($success) {
1881             // Trigger a template deleted event.
1882             $event->trigger();
1884             // Commit the transaction.
1885             $transaction->allow_commit();
1886         } else {
1887             $transaction->rollback(new moodle_exception('Error while deleting the template.'));
1888         }
1890         return $success;
1891     }
1893     /**
1894      * Update the details for a learning plan template.
1895      *
1896      * Requires moodle/competency:templatemanage capability.
1897      *
1898      * @param stdClass $record The new details for the template. Note - must contain an id that points to the template to update.
1899      * @return boolean
1900      */
1901     public static function update_template($record) {
1902         global $DB;
1903         static::require_enabled();
1904         $template = new template($record->id);
1906         // First we do a permissions check.
1907         if (!$template->can_manage()) {
1908             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
1909                 'nopermissions', '');
1911         } else if (isset($record->contextid) && $record->contextid != $template->get('contextid')) {
1912             // We can never change the context of a template.
1913             throw new coding_exception('Changing the context of an existing tempalte is forbidden.');
1915         }
1917         $updateplans = false;
1918         $before = $template->to_record();
1920         $template->from_record($record);
1921         $after = $template->to_record();
1923         // Should we update the related plans?
1924         if ($before->duedate != $after->duedate ||
1925                 $before->shortname != $after->shortname ||
1926                 $before->description != $after->description ||
1927                 $before->descriptionformat != $after->descriptionformat) {
1928             $updateplans = true;
1929         }
1931         $transaction = $DB->start_delegated_transaction();
1932         $success = $template->update();
1934         if (!$success) {
1935             $transaction->rollback(new moodle_exception('Error while updating the template.'));
1936             return $success;
1937         }
1939         // Trigger a template updated event.
1940         \core\event\competency_template_updated::create_from_template($template)->trigger();
1942         if ($updateplans) {
1943             plan::update_multiple_from_template($template);
1944         }
1946         $transaction->allow_commit();
1948         return $success;
1949     }
1951     /**
1952      * Read a the details for a single learning plan template and return a record.
1953      *
1954      * Requires moodle/competency:templateview capability at the system context.
1955      *
1956      * @param int $id The id of the template to read.
1957      * @return template
1958      */
1959     public static function read_template($id) {
1960         static::require_enabled();
1961         $template = new template($id);
1962         $context = $template->get_context();
1964         // First we do a permissions check.
1965         if (!$template->can_read()) {
1966              throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
1967                 'nopermissions', '');
1968         }
1970         // OK - all set.
1971         return $template;
1972     }
1974     /**
1975      * Perform a search based on the provided filters and return a paginated list of records.
1976      *
1977      * Requires moodle/competency:templateview capability at the system context.
1978      *
1979      * @param string $sort The column to sort on
1980      * @param string $order ('ASC' or 'DESC')
1981      * @param int $skip Number of records to skip (pagination)
1982      * @param int $limit Max of records to return (pagination)
1983      * @param context $context The parent context of the frameworks.
1984      * @param string $includes Defines what other contexts to fetch frameworks from.
1985      *                         Accepted values are:
1986      *                          - children: All descendants
1987      *                          - parents: All parents, grand parents, etc...
1988      *                          - self: Context passed only.
1989      * @param bool $onlyvisible If should list only visible templates
1990      * @return array of competency_framework
1991      */
1992     public static function list_templates($sort, $order, $skip, $limit, $context, $includes = 'children', $onlyvisible = false) {
1993         global $DB;
1994         static::require_enabled();
1996         // Get all the relevant contexts.
1997         $contexts = self::get_related_contexts($context, $includes,
1998             array('moodle/competency:templateview', 'moodle/competency:templatemanage'));
2000         // First we do a permissions check.
2001         if (empty($contexts)) {
2002              throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
2003         }
2005         // Make the order by.
2006         $orderby = '';
2007         if (!empty($sort)) {
2008             $orderby = $sort . ' ' . $order;
2009         }
2011         // OK - all set.
2012         $template = new template();
2013         list($insql, $params) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
2014         $select = "contextid $insql";
2016         if ($onlyvisible) {
2017             $select .= " AND visible = :visible";
2018             $params['visible'] = 1;
2019         }
2020         return $template->get_records_select($select, $params, $orderby, '*', $skip, $limit);
2021     }
2023     /**
2024      * Perform a search based on the provided filters and return how many results there are.
2025      *
2026      * Requires moodle/competency:templateview capability at the system context.
2027      *
2028      * @param context $context The parent context of the frameworks.
2029      * @param string $includes Defines what other contexts to fetch frameworks from.
2030      *                         Accepted values are:
2031      *                          - children: All descendants
2032      *                          - parents: All parents, grand parents, etc...
2033      *                          - self: Context passed only.
2034      * @return int
2035      */
2036     public static function count_templates($context, $includes) {
2037         global $DB;
2038         static::require_enabled();
2040         // First we do a permissions check.
2041         $contexts = self::get_related_contexts($context, $includes,
2042             array('moodle/competency:templateview', 'moodle/competency:templatemanage'));
2044         if (empty($contexts)) {
2045              throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
2046         }
2048         // OK - all set.
2049         $template = new template();
2050         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
2051         return $template->count_records_select("contextid $insql", $inparams);
2052     }
2054     /**
2055      * Count all the templates using a competency.
2056      *
2057      * @param int $competencyid The id of the competency to check.
2058      * @return int
2059      */
2060     public static function count_templates_using_competency($competencyid) {
2061         static::require_enabled();
2062         // First we do a permissions check.
2063         $context = context_system::instance();
2064         $onlyvisible = 1;
2066         $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
2067         if (!has_any_capability($capabilities, $context)) {
2068              throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
2069         }
2071         if (has_capability('moodle/competency:templatemanage', $context)) {
2072             $onlyvisible = 0;
2073         }
2075         // OK - all set.
2076         return template_competency::count_templates($competencyid, $onlyvisible);
2077     }
2079     /**
2080      * List all the learning plan templatesd using a competency.
2081      *
2082      * @param int $competencyid The id of the competency to check.
2083      * @return array[stdClass] Array of stdClass containing id and shortname.
2084      */
2085     public static function list_templates_using_competency($competencyid) {
2086         static::require_enabled();
2087         // First we do a permissions check.
2088         $context = context_system::instance();
2089         $onlyvisible = 1;
2091         $capabilities = array('moodle/competency:templateview', 'moodle/competency:templatemanage');
2092         if (!has_any_capability($capabilities, $context)) {
2093              throw new required_capability_exception($context, 'moodle/competency:templateview', 'nopermissions', '');
2094         }
2096         if (has_capability('moodle/competency:templatemanage', $context)) {
2097             $onlyvisible = 0;
2098         }
2100         // OK - all set.
2101         return template_competency::list_templates($competencyid, $onlyvisible);
2103     }
2105     /**
2106      * Count all the competencies in a learning plan template.
2107      *
2108      * @param  template|int $templateorid The template or its ID.
2109      * @return int
2110      */
2111     public static function count_competencies_in_template($templateorid) {
2112         static::require_enabled();
2113         // First we do a permissions check.
2114         $template = $templateorid;
2115         if (!is_object($template)) {
2116             $template = new template($template);
2117         }
2119         if (!$template->can_read()) {
2120             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2121                 'nopermissions', '');
2122         }
2124         // OK - all set.
2125         return template_competency::count_competencies($template->get('id'));
2126     }
2128     /**
2129      * Count all the competencies in a learning plan template with no linked courses.
2130      *
2131      * @param  template|int $templateorid The template or its ID.
2132      * @return int
2133      */
2134     public static function count_competencies_in_template_with_no_courses($templateorid) {
2135         // First we do a permissions check.
2136         $template = $templateorid;
2137         if (!is_object($template)) {
2138             $template = new template($template);
2139         }
2141         if (!$template->can_read()) {
2142             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2143                 'nopermissions', '');
2144         }
2146         // OK - all set.
2147         return template_competency::count_competencies_with_no_courses($template->get('id'));
2148     }
2150     /**
2151      * List all the competencies in a template.
2152      *
2153      * @param  template|int $templateorid The template or its ID.
2154      * @return array of competencies
2155      */
2156     public static function list_competencies_in_template($templateorid) {
2157         static::require_enabled();
2158         // First we do a permissions check.
2159         $template = $templateorid;
2160         if (!is_object($template)) {
2161             $template = new template($template);
2162         }
2164         if (!$template->can_read()) {
2165             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2166                 'nopermissions', '');
2167         }
2169         // OK - all set.
2170         return template_competency::list_competencies($template->get('id'));
2171     }
2173     /**
2174      * Add a competency to this template.
2175      *
2176      * @param int $templateid The id of the template
2177      * @param int $competencyid The id of the competency
2178      * @return bool
2179      */
2180     public static function add_competency_to_template($templateid, $competencyid) {
2181         static::require_enabled();
2182         // First we do a permissions check.
2183         $template = new template($templateid);
2184         if (!$template->can_manage()) {
2185             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
2186                 'nopermissions', '');
2187         }
2189         $record = new stdClass();
2190         $record->templateid = $templateid;
2191         $record->competencyid = $competencyid;
2193         $competency = new competency($competencyid);
2195         // Can not add a competency that belong to a hidden framework.
2196         if ($competency->get_framework()->get('visible') == false) {
2197             throw new coding_exception('A competency belonging to hidden framework can not be added');
2198         }
2200         $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
2201         if (!$exists) {
2202             $templatecompetency = new template_competency(0, $record);
2203             $templatecompetency->create();
2204             return true;
2205         }
2206         return false;
2207     }
2209     /**
2210      * Remove a competency from this template.
2211      *
2212      * @param int $templateid The id of the template
2213      * @param int $competencyid The id of the competency
2214      * @return bool
2215      */
2216     public static function remove_competency_from_template($templateid, $competencyid) {
2217         static::require_enabled();
2218         // First we do a permissions check.
2219         $template = new template($templateid);
2220         if (!$template->can_manage()) {
2221             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
2222                 'nopermissions', '');
2223         }
2225         $record = new stdClass();
2226         $record->templateid = $templateid;
2227         $record->competencyid = $competencyid;
2229         $competency = new competency($competencyid);
2231         $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
2232         if ($exists) {
2233             $link = array_pop($exists);
2234             return $link->delete();
2235         }
2236         return false;
2237     }
2239     /**
2240      * Move the template competency up or down in the display list.
2241      *
2242      * Requires moodle/competency:templatemanage capability at the system context.
2243      *
2244      * @param int $templateid The template id
2245      * @param int $competencyidfrom The id of the competency we are moving.
2246      * @param int $competencyidto The id of the competency we are moving to.
2247      * @return boolean
2248      */
2249     public static function reorder_template_competency($templateid, $competencyidfrom, $competencyidto) {
2250         static::require_enabled();
2251         $template = new template($templateid);
2253         // First we do a permissions check.
2254         if (!$template->can_manage()) {
2255             throw new required_capability_exception($template->get_context(), 'moodle/competency:templatemanage',
2256                 'nopermissions', '');
2257         }
2259         $down = true;
2260         $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidfrom));
2261         if (count($matches) == 0) {
2262             throw new coding_exception('The link does not exist');
2263         }
2265         $competencyfrom = array_pop($matches);
2266         $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidto));
2267         if (count($matches) == 0) {
2268             throw new coding_exception('The link does not exist');
2269         }
2271         $competencyto = array_pop($matches);
2273         $all = template_competency::get_records(array('templateid' => $templateid), 'sortorder', 'ASC', 0, 0);
2275         if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
2276             // We are moving up, so put it before the "to" item.
2277             $down = false;
2278         }
2280         foreach ($all as $id => $templatecompetency) {
2281             $sort = $templatecompetency->get('sortorder');
2282             if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
2283                 $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') - 1);
2284                 $templatecompetency->update();
2285             } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
2286                 $templatecompetency->set('sortorder', $templatecompetency->get('sortorder') + 1);
2287                 $templatecompetency->update();
2288             }
2289         }
2290         $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
2291         return $competencyfrom->update();
2292     }
2294     /**
2295      * Create a relation between a template and a cohort.
2296      *
2297      * This silently ignores when the relation already existed.
2298      *
2299      * @param  template|int $templateorid The template or its ID.
2300      * @param  stdClass|int $cohortorid   The cohort ot its ID.
2301      * @return template_cohort
2302      */
2303     public static function create_template_cohort($templateorid, $cohortorid) {
2304         global $DB;
2305         static::require_enabled();
2307         $template = $templateorid;
2308         if (!is_object($template)) {
2309             $template = new template($template);
2310         }
2311         require_capability('moodle/competency:templatemanage', $template->get_context());
2313         $cohort = $cohortorid;
2314         if (!is_object($cohort)) {
2315             $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
2316         }
2318         // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
2319         $cohortcontext = context::instance_by_id($cohort->contextid);
2320         if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
2321             throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
2322         }
2324         $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id);
2325         if (!$tplcohort->get('id')) {
2326             $tplcohort->create();
2327         }
2329         return $tplcohort;
2330     }
2332     /**
2333      * Remove a relation between a template and a cohort.
2334      *
2335      * @param  template|int $templateorid The template or its ID.
2336      * @param  stdClass|int $cohortorid   The cohort ot its ID.
2337      * @return boolean True on success or when the relation did not exist.
2338      */
2339     public static function delete_template_cohort($templateorid, $cohortorid) {
2340         global $DB;
2341         static::require_enabled();
2343         $template = $templateorid;
2344         if (!is_object($template)) {
2345             $template = new template($template);
2346         }
2347         require_capability('moodle/competency:templatemanage', $template->get_context());
2349         $cohort = $cohortorid;
2350         if (!is_object($cohort)) {
2351             $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
2352         }
2354         $tplcohort = template_cohort::get_relation($template->get('id'), $cohort->id);
2355         if (!$tplcohort->get('id')) {
2356             return true;
2357         }
2359         return $tplcohort->delete();
2360     }
2362     /**
2363      * Lists user plans.
2364      *
2365      * @param int $userid
2366      * @return \core_competency\plan[]
2367      */
2368     public static function list_user_plans($userid) {
2369         global $DB, $USER;
2370         static::require_enabled();
2371         $select = 'userid = :userid';
2372         $params = array('userid' => $userid);
2373         $context = context_user::instance($userid);
2375         // Check that we can read something here.
2376         if (!plan::can_read_user($userid) && !plan::can_read_user_draft($userid)) {
2377             throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
2378         }
2380         // The user cannot view the drafts.
2381         if (!plan::can_read_user_draft($userid)) {
2382             list($insql, $inparams) = $DB->get_in_or_equal(plan::get_draft_statuses(), SQL_PARAMS_NAMED, 'param', false);
2383             $select .= " AND status $insql";
2384             $params += $inparams;
2385         }
2386         // The user cannot view the non-drafts.
2387         if (!plan::can_read_user($userid)) {
2388             list($insql, $inparams) = $DB->get_in_or_equal(array(plan::STATUS_ACTIVE, plan::STATUS_COMPLETE),
2389                 SQL_PARAMS_NAMED, 'param', false);
2390             $select .= " AND status $insql";
2391             $params += $inparams;
2392         }
2394         return plan::get_records_select($select, $params, 'name ASC');
2395     }
2397     /**
2398      * List the plans to review.
2399      *
2400      * The method returns values in this format:
2401      *
2402      * array(
2403      *     'plans' => array(
2404      *         (stdClass)(
2405      *             'plan' => (plan),
2406      *             'template' => (template),
2407      *             'owner' => (stdClass)
2408      *         )
2409      *     ),
2410      *     'count' => (int)
2411      * )
2412      *
2413      * @param int $skip The number of records to skip.
2414      * @param int $limit The number of results to return.
2415      * @param int $userid The user we're getting the plans to review for.
2416      * @return array Containing the keys 'count', and 'plans'. The 'plans' key contains an object
2417      *               which contains 'plan', 'template' and 'owner'.
2418      */
2419     public static function list_plans_to_review($skip = 0, $limit = 100, $userid = null) {
2420         global $DB, $USER;
2421         static::require_enabled();
2423         if ($userid === null) {
2424             $userid = $USER->id;
2425         }
2427         $planfields = plan::get_sql_fields('p', 'plan_');
2428         $tplfields = template::get_sql_fields('t', 'tpl_');
2429         $usercols = array('id') + get_user_fieldnames();
2430         $userfields = array();
2431         foreach ($usercols as $field) {
2432             $userfields[] = "u." . $field . " AS usr_" . $field;
2433         }
2434         $userfields = implode(',', $userfields);
2436         $select = "SELECT $planfields, $tplfields, $userfields";
2437         $countselect = "SELECT COUNT('x')";
2439         $sql = "  FROM {" . plan::TABLE . "} p
2440                   JOIN {user} u
2441                     ON u.id = p.userid
2442              LEFT JOIN {" . template::TABLE . "} t
2443                     ON t.id = p.templateid
2444                  WHERE (p.status = :waitingforreview
2445                     OR (p.status = :inreview AND p.reviewerid = :reviewerid))
2446                    AND p.userid != :userid";
2448         $params = array(
2449             'waitingforreview' => plan::STATUS_WAITING_FOR_REVIEW,
2450             'inreview' => plan::STATUS_IN_REVIEW,
2451             'reviewerid' => $userid,
2452             'userid' => $userid
2453         );
2455         // Primary check to avoid the hard work of getting the users in which the user has permission.
2456         $count = $DB->count_records_sql($countselect . $sql, $params);
2457         if ($count < 1) {
2458             return array('count' => 0, 'plans' => array());
2459         }
2461         // TODO MDL-52243 Use core function.
2462         list($insql, $inparams) = self::filter_users_with_capability_on_user_context_sql('moodle/competency:planreview',
2463             $userid, SQL_PARAMS_NAMED);
2464         $sql .= " AND p.userid $insql";
2465         $params += $inparams;
2467         // Order by ID just to have some ordering in place.
2468         $ordersql = " ORDER BY p.id ASC";
2470         $plans = array();
2471         $records = $DB->get_recordset_sql($select . $sql . $ordersql, $params, $skip, $limit);
2472         foreach ($records as $record) {
2473             $plan = new plan(0, plan::extract_record($record, 'plan_'));
2474             $template = null;
2476             if ($plan->is_based_on_template()) {
2477                 $template = new template(0, template::extract_record($record, 'tpl_'));
2478             }
2480             $plans[] = (object) array(
2481                 'plan' => $plan,
2482                 'template' => $template,
2483                 'owner' => persistent::extract_record($record, 'usr_'),
2484             );
2485         }
2486         $records->close();
2488         return array(
2489             'count' => $DB->count_records_sql($countselect . $sql, $params),
2490             'plans' => $plans
2491         );
2492     }
2494     /**
2495      * Creates a learning plan based on the provided data.
2496      *
2497      * @param stdClass $record
2498      * @return \core_competency\plan
2499      */
2500     public static function create_plan(stdClass $record) {
2501         global $USER;
2502         static::require_enabled();
2503         $plan = new plan(0, $record);
2505         if ($plan->is_based_on_template()) {
2506             throw new coding_exception('To create a plan from a template use api::create_plan_from_template().');
2507         } else if ($plan->get('status') == plan::STATUS_COMPLETE) {
2508             throw new coding_exception('A plan cannot be created as complete.');
2509         }
2511         if (!$plan->can_manage()) {
2512             $context = context_user::instance($plan->get('userid'));
2513             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
2514         }
2516         $plan->create();
2518         // Trigger created event.
2519         \core\event\competency_plan_created::create_from_plan($plan)->trigger();
2520         return $plan;
2521     }
2523     /**
2524      * Create a learning plan from a template.
2525      *
2526      * @param  mixed $templateorid The template object or ID.
2527      * @param  int $userid
2528      * @return false|\core_competency\plan Returns false when the plan already exists.
2529      */
2530     public static function create_plan_from_template($templateorid, $userid) {
2531         static::require_enabled();
2532         $template = $templateorid;
2533         if (!is_object($template)) {
2534             $template = new template($template);
2535         }
2537         // The user must be able to view the template to use it as a base for a plan.
2538         if (!$template->can_read()) {
2539             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2540                 'nopermissions', '');
2541         }
2542         // Can not create plan from a hidden template.
2543         if ($template->get('visible') == false) {
2544             throw new coding_exception('A plan can not be created from a hidden template');
2545         }
2547         // Convert the template to a plan.
2548         $record = $template->to_record();
2549         $record->templateid = $record->id;
2550         $record->userid = $userid;
2551         $record->name = $record->shortname;
2552         $record->status = plan::STATUS_ACTIVE;
2554         unset($record->id);
2555         unset($record->timecreated);
2556         unset($record->timemodified);
2557         unset($record->usermodified);
2559         // Remove extra keys.
2560         $properties = plan::properties_definition();
2561         foreach ($record as $key => $value) {
2562             if (!array_key_exists($key, $properties)) {
2563                 unset($record->$key);
2564             }
2565         }
2567         $plan = new plan(0, $record);
2568         if (!$plan->can_manage()) {
2569             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage',
2570                 'nopermissions', '');
2571         }
2573         // We first apply the permission checks as we wouldn't want to leak information by returning early that
2574         // the plan already exists.
2575         if (plan::record_exists_select('templateid = :templateid AND userid = :userid', array(
2576                 'templateid' => $template->get('id'), 'userid' => $userid))) {
2577             return false;
2578         }
2580         $plan->create();
2582         // Trigger created event.
2583         \core\event\competency_plan_created::create_from_plan($plan)->trigger();
2584         return $plan;
2585     }
2587     /**
2588      * Create learning plans from a template and cohort.
2589      *
2590      * @param  mixed $templateorid The template object or ID.
2591      * @param  int $cohortid The cohort ID.
2592      * @param  bool $recreateunlinked When true the plans that were unlinked from this template will be re-created.
2593      * @return int The number of plans created.
2594      */
2595     public static function create_plans_from_template_cohort($templateorid, $cohortid, $recreateunlinked = false) {
2596         global $DB, $CFG;
2597         static::require_enabled();
2598         require_once($CFG->dirroot . '/cohort/lib.php');
2600         $template = $templateorid;
2601         if (!is_object($template)) {
2602             $template = new template($template);
2603         }
2605         // The user must be able to view the template to use it as a base for a plan.
2606         if (!$template->can_read()) {
2607             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
2608                 'nopermissions', '');
2609         }
2611         // Can not create plan from a hidden template.
2612         if ($template->get('visible') == false) {
2613             throw new coding_exception('A plan can not be created from a hidden template');
2614         }
2616         // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
2617         $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
2618         $cohortcontext = context::instance_by_id($cohort->contextid);
2619         if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
2620             throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
2621         }
2623         // Convert the template to a plan.
2624         $recordbase = $template->to_record();
2625         $recordbase->templateid = $recordbase->id;
2626         $recordbase->name = $recordbase->shortname;
2627         $recordbase->status = plan::STATUS_ACTIVE;
2629         unset($recordbase->id);
2630         unset($recordbase->timecreated);
2631         unset($recordbase->timemodified);
2632         unset($recordbase->usermodified);
2634         // Remove extra keys.
2635         $properties = plan::properties_definition();
2636         foreach ($recordbase as $key => $value) {
2637             if (!array_key_exists($key, $properties)) {
2638                 unset($recordbase->$key);
2639             }
2640         }
2642         // Create the plans.
2643         $created = 0;
2644         $userids = template_cohort::get_missing_plans($template->get('id'), $cohortid, $recreateunlinked);
2645         foreach ($userids as $userid) {
2646             $record = (object) (array) $recordbase;
2647             $record->userid = $userid;
2649             $plan = new plan(0, $record);
2650             if (!$plan->can_manage()) {
2651                 // Silently skip members where permissions are lacking.
2652                 continue;
2653             }
2655             $plan->create();
2656             // Trigger created event.
2657             \core\event\competency_plan_created::create_from_plan($plan)->trigger();
2658             $created++;
2659         }
2661         return $created;
2662     }
2664     /**
2665      * Unlink a plan from its template.
2666      *
2667      * @param  \core_competency\plan|int $planorid The plan or its ID.
2668      * @return bool
2669      */
2670     public static function unlink_plan_from_template($planorid) {
2671         global $DB;
2672         static::require_enabled();
2674         $plan = $planorid;
2675         if (!is_object($planorid)) {
2676             $plan = new plan($planorid);
2677         }
2679         // The user must be allowed to manage the plans of the user, nothing about the template.
2680         if (!$plan->can_manage()) {
2681             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2682         }
2684         // Only plan with status DRAFT or ACTIVE can be unliked..
2685         if ($plan->get('status') == plan::STATUS_COMPLETE) {
2686             throw new coding_exception('Only draft or active plan can be unliked from a template');
2687         }
2689         // Early exit, it's already done...
2690         if (!$plan->is_based_on_template()) {
2691             return true;
2692         }
2694         // Fetch the template.
2695         $template = new template($plan->get('templateid'));
2697         // Now, proceed by copying all competencies to the plan, then update the plan.
2698         $transaction = $DB->start_delegated_transaction();
2699         $competencies = template_competency::list_competencies($template->get('id'), false);
2700         $i = 0;
2701         foreach ($competencies as $competency) {
2702             $record = (object) array(
2703                 'planid' => $plan->get('id'),
2704                 'competencyid' => $competency->get('id'),
2705                 'sortorder' => $i++
2706             );
2707             $pc = new plan_competency(null, $record);
2708             $pc->create();
2709         }
2710         $plan->set('origtemplateid', $template->get('id'));
2711         $plan->set('templateid', null);
2712         $success = $plan->update();
2713         $transaction->allow_commit();
2715         // Trigger unlinked event.
2716         \core\event\competency_plan_unlinked::create_from_plan($plan)->trigger();
2718         return $success;
2719     }
2721     /**
2722      * Updates a plan.
2723      *
2724      * @param stdClass $record
2725      * @return \core_competency\plan
2726      */
2727     public static function update_plan(stdClass $record) {
2728         static::require_enabled();
2730         $plan = new plan($record->id);
2732         // Validate that the plan as it is can be managed.
2733         if (!$plan->can_manage()) {
2734             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2736         } else if ($plan->get('status') == plan::STATUS_COMPLETE) {
2737             // A completed plan cannot be edited.
2738             throw new coding_exception('Completed plan cannot be edited.');
2740         } else if ($plan->is_based_on_template()) {
2741             // Prevent a plan based on a template to be edited.
2742             throw new coding_exception('Cannot update a plan that is based on a template.');
2744         } else if (isset($record->templateid) && $plan->get('templateid') != $record->templateid) {
2745             // Prevent a plan to be based on a template.
2746             throw new coding_exception('Cannot base a plan on a template.');
2748         } else if (isset($record->userid) && $plan->get('userid') != $record->userid) {
2749             // Prevent change of ownership as the capabilities are checked against that.
2750             throw new coding_exception('A plan cannot be transfered to another user');
2752         } else if (isset($record->status) && $plan->get('status') != $record->status) {
2753             // Prevent change of status.
2754             throw new coding_exception('To change the status of a plan use the appropriate methods.');
2756         }
2758         $plan->from_record($record);
2759         $plan->update();
2761         // Trigger updated event.
2762         \core\event\competency_plan_updated::create_from_plan($plan)->trigger();
2764         return $plan;
2765     }
2767     /**
2768      * Returns a plan data.
2769      *
2770      * @param int $id
2771      * @return \core_competency\plan
2772      */
2773     public static function read_plan($id) {
2774         static::require_enabled();
2775         $plan = new plan($id);
2777         if (!$plan->can_read()) {
2778             $context = context_user::instance($plan->get('userid'));
2779             throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
2780         }
2782         return $plan;
2783     }
2785     /**
2786      * Plan event viewed.
2787      *
2788      * @param mixed $planorid The id or the plan.
2789      * @return boolean
2790      */
2791     public static function plan_viewed($planorid) {
2792         static::require_enabled();
2793         $plan = $planorid;
2794         if (!is_object($plan)) {
2795             $plan = new plan($plan);
2796         }
2798         // First we do a permissions check.
2799         if (!$plan->can_read()) {
2800             $context = context_user::instance($plan->get('userid'));
2801             throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
2802         }
2804         // Trigger a template viewed event.
2805         \core\event\competency_plan_viewed::create_from_plan($plan)->trigger();
2807         return true;
2808     }
2810     /**
2811      * Deletes a plan.
2812      *
2813      * Plans based on a template can be removed just like any other one.
2814      *
2815      * @param int $id
2816      * @return bool Success?
2817      */
2818     public static function delete_plan($id) {
2819         global $DB;
2820         static::require_enabled();
2822         $plan = new plan($id);
2824         if (!$plan->can_manage()) {
2825             $context = context_user::instance($plan->get('userid'));
2826             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
2827         }
2829         // Wrap the suppression in a DB transaction.
2830         $transaction = $DB->start_delegated_transaction();
2832         // Delete plan competencies.
2833         $plancomps = plan_competency::get_records(array('planid' => $plan->get('id')));
2834         foreach ($plancomps as $plancomp) {
2835             $plancomp->delete();
2836         }
2838         // Delete archive user competencies if the status of the plan is complete.
2839         if ($plan->get('status') == plan::STATUS_COMPLETE) {
2840             self::remove_archived_user_competencies_in_plan($plan);
2841         }
2842         $event = \core\event\competency_plan_deleted::create_from_plan($plan);
2843         $success = $plan->delete();
2845         $transaction->allow_commit();
2847         // Trigger deleted event.
2848         $event->trigger();
2850         return $success;
2851     }
2853     /**
2854      * Cancel the review of a plan.
2855      *
2856      * @param int|plan $planorid The plan, or its ID.
2857      * @return bool
2858      */
2859     public static function plan_cancel_review_request($planorid) {
2860         static::require_enabled();
2861         $plan = $planorid;
2862         if (!is_object($plan)) {
2863             $plan = new plan($plan);
2864         }
2866         // We need to be able to view the plan at least.
2867         if (!$plan->can_read()) {
2868             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2869         }
2871         if ($plan->is_based_on_template()) {
2872             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2873         } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) {
2874             throw new coding_exception('The plan review cannot be cancelled at this stage.');
2875         } else if (!$plan->can_request_review()) {
2876             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2877         }
2879         $plan->set('status', plan::STATUS_DRAFT);
2880         $result = $plan->update();
2882         // Trigger review request cancelled event.
2883         \core\event\competency_plan_review_request_cancelled::create_from_plan($plan)->trigger();
2885         return $result;
2886     }
2888     /**
2889      * Request the review of a plan.
2890      *
2891      * @param int|plan $planorid The plan, or its ID.
2892      * @return bool
2893      */
2894     public static function plan_request_review($planorid) {
2895         static::require_enabled();
2896         $plan = $planorid;
2897         if (!is_object($plan)) {
2898             $plan = new plan($plan);
2899         }
2901         // We need to be able to view the plan at least.
2902         if (!$plan->can_read()) {
2903             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2904         }
2906         if ($plan->is_based_on_template()) {
2907             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2908         } else if ($plan->get('status') != plan::STATUS_DRAFT) {
2909             throw new coding_exception('The plan cannot be sent for review at this stage.');
2910         } else if (!$plan->can_request_review()) {
2911             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2912         }
2914         $plan->set('status', plan::STATUS_WAITING_FOR_REVIEW);
2915         $result = $plan->update();
2917         // Trigger review requested event.
2918         \core\event\competency_plan_review_requested::create_from_plan($plan)->trigger();
2920         return $result;
2921     }
2923     /**
2924      * Start the review of a plan.
2925      *
2926      * @param int|plan $planorid The plan, or its ID.
2927      * @return bool
2928      */
2929     public static function plan_start_review($planorid) {
2930         global $USER;
2931         static::require_enabled();
2932         $plan = $planorid;
2933         if (!is_object($plan)) {
2934             $plan = new plan($plan);
2935         }
2937         // We need to be able to view the plan at least.
2938         if (!$plan->can_read()) {
2939             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2940         }
2942         if ($plan->is_based_on_template()) {
2943             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2944         } else if ($plan->get('status') != plan::STATUS_WAITING_FOR_REVIEW) {
2945             throw new coding_exception('The plan review cannot be started at this stage.');
2946         } else if (!$plan->can_review()) {
2947             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2948         }
2950         $plan->set('status', plan::STATUS_IN_REVIEW);
2951         $plan->set('reviewerid', $USER->id);
2952         $result = $plan->update();
2954         // Trigger review started event.
2955         \core\event\competency_plan_review_started::create_from_plan($plan)->trigger();
2957         return $result;
2958     }
2960     /**
2961      * Stop reviewing a plan.
2962      *
2963      * @param  int|plan $planorid The plan, or its ID.
2964      * @return bool
2965      */
2966     public static function plan_stop_review($planorid) {
2967         static::require_enabled();
2968         $plan = $planorid;
2969         if (!is_object($plan)) {
2970             $plan = new plan($plan);
2971         }
2973         // We need to be able to view the plan at least.
2974         if (!$plan->can_read()) {
2975             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
2976         }
2978         if ($plan->is_based_on_template()) {
2979             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2980         } else if ($plan->get('status') != plan::STATUS_IN_REVIEW) {
2981             throw new coding_exception('The plan review cannot be stopped at this stage.');
2982         } else if (!$plan->can_review()) {
2983             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
2984         }
2986         $plan->set('status', plan::STATUS_DRAFT);
2987         $plan->set('reviewerid', null);
2988         $result = $plan->update();
2990         // Trigger review stopped event.
2991         \core\event\competency_plan_review_stopped::create_from_plan($plan)->trigger();
2993         return $result;
2994     }
2996     /**
2997      * Approve a plan.
2998      *
2999      * This means making the plan active.
3000      *
3001      * @param  int|plan $planorid The plan, or its ID.
3002      * @return bool
3003      */
3004     public static function approve_plan($planorid) {
3005         static::require_enabled();
3006         $plan = $planorid;
3007         if (!is_object($plan)) {
3008             $plan = new plan($plan);
3009         }
3011         // We need to be able to view the plan at least.
3012         if (!$plan->can_read()) {
3013             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
3014         }
3016         // We can approve a plan that is either a draft, in review, or waiting for review.
3017         if ($plan->is_based_on_template()) {
3018             throw new coding_exception('Template plans are already approved.');   // This should never happen.
3019         } else if (!$plan->is_draft()) {
3020             throw new coding_exception('The plan cannot be approved at this stage.');
3021         } else if (!$plan->can_review()) {
3022             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3023         }
3025         $plan->set('status', plan::STATUS_ACTIVE);
3026         $plan->set('reviewerid', null);
3027         $result = $plan->update();
3029         // Trigger approved event.
3030         \core\event\competency_plan_approved::create_from_plan($plan)->trigger();
3032         return $result;
3033     }
3035     /**
3036      * Unapprove a plan.
3037      *
3038      * This means making the plan draft.
3039      *
3040      * @param  int|plan $planorid The plan, or its ID.
3041      * @return bool
3042      */
3043     public static function unapprove_plan($planorid) {
3044         static::require_enabled();
3045         $plan = $planorid;
3046         if (!is_object($plan)) {
3047             $plan = new plan($plan);
3048         }
3050         // We need to be able to view the plan at least.
3051         if (!$plan->can_read()) {
3052             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planview', 'nopermissions', '');
3053         }
3055         if ($plan->is_based_on_template()) {
3056             throw new coding_exception('Template plans are always approved.');   // This should never happen.
3057         } else if ($plan->get('status') != plan::STATUS_ACTIVE) {
3058             throw new coding_exception('The plan cannot be sent back to draft at this stage.');
3059         } else if (!$plan->can_review()) {
3060             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3061         }
3063         $plan->set('status', plan::STATUS_DRAFT);
3064         $result = $plan->update();
3066         // Trigger unapproved event.
3067         \core\event\competency_plan_unapproved::create_from_plan($plan)->trigger();
3069         return $result;
3070     }
3072     /**
3073      * Complete a plan.
3074      *
3075      * @param int|plan $planorid The plan, or its ID.
3076      * @return bool
3077      */
3078     public static function complete_plan($planorid) {
3079         global $DB;
3080         static::require_enabled();
3082         $plan = $planorid;
3083         if (!is_object($planorid)) {
3084             $plan = new plan($planorid);
3085         }
3087         // Validate that the plan can be managed.
3088         if (!$plan->can_manage()) {
3089             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3090         }
3092         // Check if the plan was already completed.
3093         if ($plan->get('status') == plan::STATUS_COMPLETE) {
3094             throw new coding_exception('The plan is already completed.');
3095         }
3097         $originalstatus = $plan->get('status');
3098         $plan->set('status', plan::STATUS_COMPLETE);
3100         // The user should also be able to manage the plan when it's completed.
3101         if (!$plan->can_manage()) {
3102             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3103         }
3105         // Put back original status because archive needs it to extract competencies from the right table.
3106         $plan->set('status', $originalstatus);
3108         // Do the things.
3109         $transaction = $DB->start_delegated_transaction();
3110         self::archive_user_competencies_in_plan($plan);
3111         $plan->set('status', plan::STATUS_COMPLETE);
3112         $success = $plan->update();
3114         if (!$success) {
3115             $transaction->rollback(new moodle_exception('The plan could not be updated.'));
3116             return $success;
3117         }
3119         $transaction->allow_commit();
3121         // Trigger updated event.
3122         \core\event\competency_plan_completed::create_from_plan($plan)->trigger();
3124         return $success;
3125     }
3127     /**
3128      * Reopen a plan.
3129      *
3130      * @param int|plan $planorid The plan, or its ID.
3131      * @return bool
3132      */
3133     public static function reopen_plan($planorid) {
3134         global $DB;
3135         static::require_enabled();
3137         $plan = $planorid;
3138         if (!is_object($planorid)) {
3139             $plan = new plan($planorid);
3140         }
3142         // Validate that the plan as it is can be managed.
3143         if (!$plan->can_manage()) {
3144             $context = context_user::instance($plan->get('userid'));
3145             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
3146         }
3148         $beforestatus = $plan->get('status');
3149         $plan->set('status', plan::STATUS_ACTIVE);
3151         // Validate if status can be changed.
3152         if (!$plan->can_manage()) {
3153             $context = context_user::instance($plan->get('userid'));
3154             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
3155         }
3157         // Wrap the updates in a DB transaction.
3158         $transaction = $DB->start_delegated_transaction();
3160         // Delete archived user competencies if the status of the plan is changed from complete to another status.
3161         $mustremovearchivedcompetencies = ($beforestatus == plan::STATUS_COMPLETE && $plan->get('status') != plan::STATUS_COMPLETE);
3162         if ($mustremovearchivedcompetencies) {
3163             self::remove_archived_user_competencies_in_plan($plan);
3164         }
3166         // If duedate less than or equal to duedate_threshold unset it.
3167         if ($plan->get('duedate') <= time() + plan::DUEDATE_THRESHOLD) {
3168             $plan->set('duedate', 0);
3169         }
3171         $success = $plan->update();
3173         if (!$success) {
3174             $transaction->rollback(new moodle_exception('The plan could not be updated.'));
3175             return $success;
3176         }
3178         $transaction->allow_commit();
3180         // Trigger reopened event.
3181         \core\event\competency_plan_reopened::create_from_plan($plan)->trigger();
3183         return $success;
3184     }
3186     /**
3187      * Get a single competency from the user plan.
3188      *
3189      * @param  int $planorid The plan, or its ID.
3190      * @param  int $competencyid The competency id.
3191      * @return (object) array(
3192      *                      'competency' => \core_competency\competency,
3193      *                      'usercompetency' => \core_competency\user_competency
3194      *                      'usercompetencyplan' => \core_competency\user_competency_plan
3195      *                  )
3196      *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
3197      */
3198     public static function get_plan_competency($planorid, $competencyid) {
3199         static::require_enabled();
3200         $plan = $planorid;
3201         if (!is_object($planorid)) {
3202             $plan = new plan($planorid);
3203         }
3205         if (!user_competency::can_read_user($plan->get('userid'))) {
3206             throw new required_capability_exception($plan->get_context(), 'moodle/competency:usercompetencyview',
3207                 'nopermissions', '');
3208         }
3210         $competency = $plan->get_competency($competencyid);
3212         // Get user competencies from user_competency_plan if the plan status is set to complete.
3213         $iscompletedplan = $plan->get('status') == plan::STATUS_COMPLETE;
3214         if ($iscompletedplan) {
3215             $usercompetencies = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), array($competencyid));
3216             $ucresultkey = 'usercompetencyplan';
3217         } else {
3218             $usercompetencies = user_competency::get_multiple($plan->get('userid'), array($competencyid));
3219             $ucresultkey = 'usercompetency';
3220         }
3222         $found = count($usercompetencies);
3224         if ($found) {
3225             $uc = array_pop($usercompetencies);
3226         } else {
3227             if ($iscompletedplan) {
3228                 throw new coding_exception('A user competency plan is missing');
3229             } else {
3230                 $uc = user_competency::create_relation($plan->get('userid'), $competency->get('id'));
3231                 $uc->create();
3232             }
3233         }
3235         $plancompetency = (object) array(
3236             'competency' => $competency,
3237             'usercompetency' => null,
3238             'usercompetencyplan' => null
3239         );
3240         $plancompetency->$ucresultkey = $uc;
3242         return $plancompetency;
3243     }
3245     /**
3246      * List the plans with a competency.
3247      *
3248      * @param  int $userid The user id we want the plans for.
3249      * @param  int $competencyorid The competency, or its ID.
3250      * @return array[plan] Array of learning plans.
3251      */
3252     public static function list_plans_with_competency($userid, $competencyorid) {
3253         global $USER;
3255         static::require_enabled();
3256         $competencyid = $competencyorid;
3257         $competency = null;
3258         if (is_object($competencyid)) {
3259             $competency = $competencyid;
3260             $competencyid = $competency->get('id');
3261         }
3263         $plans = plan::get_by_user_and_competency($userid, $competencyid);
3264         foreach ($plans as $index => $plan) {
3265             // Filter plans we cannot read.
3266             if (!$plan->can_read()) {
3267                 unset($plans[$index]);
3268             }
3269         }
3270         return $plans;
3271     }
3273     /**
3274      * List the competencies in a user plan.
3275      *
3276      * @param  int $planorid The plan, or its ID.
3277      * @return array((object) array(
3278      *                            'competency' => \core_competency\competency,
3279      *                            'usercompetency' => \core_competency\user_competency
3280      *                            'usercompetencyplan' => \core_competency\user_competency_plan
3281      *                        ))
3282      *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
3283      */
3284     public static function list_plan_competencies($planorid) {
3285         static::require_enabled();
3286         $plan = $planorid;
3287         if (!is_object($planorid)) {
3288             $plan = new plan($planorid);
3289         }
3291         if (!$plan->can_read()) {
3292             $context = context_user::instance($plan->get('userid'));
3293             throw new required_capability_exception($context, 'moodle/competency:planview', 'nopermissions', '');
3294         }
3296         $result = array();
3297         $competencies = $plan->get_competencies();
3299         // Get user competencies from user_competency_plan if the plan status is set to complete.
3300         $iscompletedplan = $plan->get('status') == plan::STATUS_COMPLETE;
3301         if ($iscompletedplan) {
3302             $usercompetencies = user_competency_plan::get_multiple($plan->get('userid'), $plan->get('id'), $competencies);
3303             $ucresultkey = 'usercompetencyplan';
3304         } else {
3305             $usercompetencies = user_competency::get_multiple($plan->get('userid'), $competencies);
3306             $ucresultkey = 'usercompetency';
3307         }
3309         // Build the return values.
3310         foreach ($competencies as $key => $competency) {
3311             $found = false;
3313             foreach ($usercompetencies as $uckey => $uc) {
3314                 if ($uc->get('competencyid') == $competency->get('id')) {
3315                     $found = true;
3316                     unset($usercompetencies[$uckey]);
3317                     break;
3318                 }
3319             }
3321             if (!$found) {
3322                 if ($iscompletedplan) {
3323                     throw new coding_exception('A user competency plan is missing');
3324                 } else {
3325                     $uc = user_competency::create_relation($plan->get('userid'), $competency->get('id'));
3326                 }
3327             }
3329             $plancompetency = (object) array(
3330                 'competency' => $competency,
3331                 'usercompetency' => null,
3332                 'usercompetencyplan' => null
3333             );
3334             $plancompetency->$ucresultkey = $uc;
3335             $result[] = $plancompetency;
3336         }
3338         return $result;
3339     }
3341     /**
3342      * Add a competency to a plan.
3343      *
3344      * @param int $planid The id of the plan
3345      * @param int $competencyid The id of the competency
3346      * @return bool
3347      */
3348     public static function add_competency_to_plan($planid, $competencyid) {
3349         static::require_enabled();
3350         $plan = new plan($planid);
3352         // First we do a permissions check.
3353         if (!$plan->can_manage()) {
3354             throw new required_capability_exception($plan->get_context(), 'moodle/competency:planmanage', 'nopermissions', '');
3356         } else if ($plan->is_based_on_template()) {
3357             throw new coding_exception('A competency can not be added to a learning plan based on a template');
3358         }
3360         if (!$plan->can_be_edited()) {
3361             throw new coding_exception('A competency can not be added to a learning plan completed');
3362         }
3364         $competency = new competency($competencyid);
3366         // Can not add a competency that belong to a hidden framework.
3367         if ($competency->get_framework()->get('visible') == false) {
3368             throw new coding_exception('A competency belonging to hidden framework can not be added');
3369         }
3371         $exists = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
3372         if (!$exists) {
3373             $record = new stdClass();
3374             $record->planid = $planid;
3375             $record->competencyid = $competencyid;
3376             $plancompetency = new plan_competency(0, $record);
3377             $plancompetency->create();
3378         }
3380         return true;
3381     }
3383     /**
3384      * Remove a competency from a plan.
3385      *
3386      * @param int $planid The plan id
3387      * @param int $competencyid The id of the competency
3388      * @return bool
3389      */
3390     public static function remove_competency_from_plan($planid, $competencyid) {
3391         static::require_enabled();
3392         $plan = new plan($planid);
3394         // First we do a permissions check.
3395         if (!$plan->can_manage()) {
3396             $context = context_user::instance($plan->get('userid'));
3397             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
3399         } else if ($plan->is_based_on_template()) {
3400             throw new coding_exception('A competency can not be removed from a learning plan based on a template');
3401         }
3403         if (!$plan->can_be_edited()) {
3404             throw new coding_exception('A competency can not be removed from a learning plan completed');
3405         }
3407         $link = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
3408         if ($link) {
3409             return $link->delete();
3410         }
3411         return false;
3412     }
3414     /**
3415      * Move the plan competency up or down in the display list.
3416      *
3417      * Requires moodle/competency:planmanage capability at the system context.
3418      *
3419      * @param int $planid The plan  id
3420      * @param int $competencyidfrom The id of the competency we are moving.
3421      * @param int $competencyidto The id of the competency we are moving to.
3422      * @return boolean
3423      */
3424     public static function reorder_plan_competency($planid, $competencyidfrom, $competencyidto) {
3425         static::require_enabled();
3426         $plan = new plan($planid);
3428         // First we do a permissions check.
3429         if (!$plan->can_manage()) {
3430             $context = context_user::instance($plan->get('userid'));
3431             throw new required_capability_exception($context, 'moodle/competency:planmanage', 'nopermissions', '');
3433         } else if ($plan->is_based_on_template()) {
3434             throw new coding_exception('A competency can not be reordered in a learning plan based on a template');
3435         }
3437         if (!$plan->can_be_edited()) {
3438             throw new coding_exception('A competency can not be reordered in a learning plan completed');
3439         }
3441         $down = true;
3442         $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidfrom));
3443         if (count($matches) == 0) {
3444             throw new coding_exception('The link does not exist');
3445         }
3447         $competencyfrom = array_pop($matches);
3448         $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidto));
3449         if (count($matches) == 0) {
3450             throw new coding_exception('The link does not exist');
3451         }
3453         $competencyto = array_pop($matches);
3455         $all = plan_competency::get_records(array('planid' => $planid), 'sortorder', 'ASC', 0, 0);
3457         if ($competencyfrom->get('sortorder') > $competencyto->get('sortorder')) {
3458             // We are moving up, so put it before the "to" item.
3459             $down = false;
3460         }
3462         foreach ($all as $id => $plancompetency) {
3463             $sort = $plancompetency->get('sortorder');
3464             if ($down && $sort > $competencyfrom->get('sortorder') && $sort <= $competencyto->get('sortorder')) {
3465                 $plancompetency->set('sortorder', $plancompetency->get('sortorder') - 1);
3466                 $plancompetency->update();
3467             } else if (!$down && $sort >= $competencyto->get('sortorder') && $sort < $competencyfrom->get('sortorder')) {
3468                 $plancompetency->set('sortorder', $plancompetency->get('sortorder') + 1);
3469                 $plancompetency->update();
3470             }
3471         }
3472         $competencyfrom->set('sortorder', $competencyto->get('sortorder'));
3473         return $competencyfrom->update();
3474     }
3476     /**
3477      * Cancel a user competency review request.
3478      *
3479      * @param  int $userid       The user ID.
3480      * @param  int $competencyid The competency ID.
3481      * @return bool
3482      */
3483     public static function user_competency_cancel_review_request($userid, $competencyid) {
3484         static::require_enabled();
3485         $context = context_user::instance($userid);
3486         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3487         if (!$uc || !$uc->can_read()) {
3488             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
3489         } else if ($uc->get('status') != user_competency::STATUS_WAITING_FOR_REVIEW) {
3490             throw new coding_exception('The competency can not be cancel review request at this stage.');
3491         } else if (!$uc->can_request_review()) {
3492             throw new required_capability_exception($context, 'moodle/competency:usercompetencyrequestreview', 'nopermissions', '');
3493         }
3495         $uc->set('status', user_competency::STATUS_IDLE);
3496         $result = $uc->update();
3497         if ($result) {
3498             \core\event\competency_user_competency_review_request_cancelled::create_from_user_competency($uc)->trigger();
3499         }
3500         return $result;
3501     }
3503     /**
3504      * Request a user competency review.
3505      *
3506      * @param  int $userid       The user ID.
3507      * @param  int $competencyid The competency ID.
3508      * @return bool
3509      */
3510     public static function user_competency_request_review($userid, $competencyid) {
3511         static::require_enabled();
3512         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3513         if (!$uc) {
3514             $uc = user_competency::create_relation($userid, $competencyid);
3515             $uc->create();
3516         }
3518         if (!$uc->can_read()) {
3519             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
3520                 'nopermissions', '');
3521         } else if ($uc->get('status') != user_competency::STATUS_IDLE) {
3522             throw new coding_exception('The competency can not be sent for review at this stage.');
3523         } else if (!$uc->can_request_review()) {
3524             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyrequestreview',
3525                 'nopermissions', '');
3526         }
3528         $uc->set('status', user_competency::STATUS_WAITING_FOR_REVIEW);
3529         $result = $uc->update();
3530         if ($result) {
3531             \core\event\competency_user_competency_review_requested::create_from_user_competency($uc)->trigger();
3532         }
3533         return $result;
3534     }
3536     /**
3537      * Start a user competency review.
3538      *
3539      * @param  int $userid       The user ID.
3540      * @param  int $competencyid The competency ID.
3541      * @return bool
3542      */
3543     public static function user_competency_start_review($userid, $competencyid) {
3544         global $USER;
3545         static::require_enabled();
3547         $context = context_user::instance($userid);
3548         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3549         if (!$uc || !$uc->can_read()) {
3550             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
3551         } else if ($uc->get('status') != user_competency::STATUS_WAITING_FOR_REVIEW) {
3552             throw new coding_exception('The competency review can not be started at this stage.');
3553         } else if (!$uc->can_review()) {
3554             throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', '');
3555         }
3557         $uc->set('status', user_competency::STATUS_IN_REVIEW);
3558         $uc->set('reviewerid', $USER->id);
3559         $result = $uc->update();
3560         if ($result) {
3561             \core\event\competency_user_competency_review_started::create_from_user_competency($uc)->trigger();
3562         }
3563         return $result;
3564     }
3566     /**
3567      * Stop a user competency review.
3568      *
3569      * @param  int $userid       The user ID.
3570      * @param  int $competencyid The competency ID.
3571      * @return bool
3572      */
3573     public static function user_competency_stop_review($userid, $competencyid) {
3574         static::require_enabled();
3575         $context = context_user::instance($userid);
3576         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3577         if (!$uc || !$uc->can_read()) {
3578             throw new required_capability_exception($context, 'moodle/competency:usercompetencyview', 'nopermissions', '');
3579         } else if ($uc->get('status') != user_competency::STATUS_IN_REVIEW) {
3580             throw new coding_exception('The competency review can not be stopped at this stage.');
3581         } else if (!$uc->can_review()) {
3582             throw new required_capability_exception($context, 'moodle/competency:usercompetencyreview', 'nopermissions', '');
3583         }
3585         $uc->set('status', user_competency::STATUS_IDLE);
3586         $result = $uc->update();
3587         if ($result) {
3588             \core\event\competency_user_competency_review_stopped::create_from_user_competency($uc)->trigger();
3589         }
3590         return $result;
3591     }
3593     /**
3594      * Log user competency viewed event.
3595      *
3596      * @param user_competency|int $usercompetencyorid The user competency object or user competency id
3597      * @return bool
3598      */
3599     public static function user_competency_viewed($usercompetencyorid) {
3600         static::require_enabled();
3601         $uc = $usercompetencyorid;
3602         if (!is_object($uc)) {
3603             $uc = new user_competency($uc);
3604         }
3606         if (!$uc || !$uc->can_read()) {
3607             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
3608                 'nopermissions', '');
3609         }
3611         \core\event\competency_user_competency_viewed::create_from_user_competency_viewed($uc)->trigger();
3612         return true;
3613     }
3615     /**
3616      * Log user competency viewed in plan event.
3617      *
3618      * @param user_competency|int $usercompetencyorid The user competency object or user competency id
3619      * @param int $planid The plan ID
3620      * @return bool
3621      */
3622     public static function user_competency_viewed_in_plan($usercompetencyorid, $planid) {
3623         static::require_enabled();
3624         $uc = $usercompetencyorid;
3625         if (!is_object($uc)) {
3626             $uc = new user_competency($uc);
3627         }
3629         if (!$uc || !$uc->can_read()) {
3630             throw new required_capability_exception($uc->get_context(), 'moodle/competency:usercompetencyview',
3631                 'nopermissions', '');
3632         }
3633         $plan = new plan($planid);
3634         if ($plan->get('status') == plan::STATUS_COMPLETE) {
3635             throw new coding_exception('To log the user competency in completed plan use user_competency_plan_viewed method.');
3636         }
3638         \core\event\competency_user_competency_viewed_in_plan::create_from_user_competency_viewed_in_plan($uc, $planid)->trigger();
3639         return true;
3640     }
3642     /**
3643      * Log user competency viewed in course event.
3644      *
3645      * @param user_competency_course|int $usercoursecompetencyorid The user competency course object or its ID.
3646      * @param int $courseid The course ID
3647      * @return bool
3648      */
3649     public static function user_competency_viewed_in_course($usercoursecompetencyorid) {
3650         static::require_enabled();
3651         $ucc = $usercoursecompetencyorid;
3652         if (!is_object($ucc)) {
3653             $ucc = new user_competency_course($ucc);
3654         }
3656         if (!$ucc || !user_competency::can_read_user_in_course($ucc->get('userid'), $ucc->get('courseid'))) {
3657             throw new required_capability_exception($ucc->get_context(), 'moodle/competency:usercompetencyview',
3658                 'nopermissions', '');
3659         }
3661         // Validate the course, this will throw an exception if not valid.
3662         self::validate_course($ucc->get('courseid'));
3664         \core\event\competency_user_competency_viewed_in_course::create_from_user_competency_viewed_in_course($ucc)->trigger();
3665         return true;
3666     }
3668     /**
3669      * Log user competency plan viewed event.
3670      *
3671      * @param user_competency_plan|int $usercompetencyplanorid The user competency plan object or user competency plan id
3672      * @return bool
3673      */
3674     public static function user_competency_plan_viewed($usercompetencyplanorid) {
3675         static::require_enabled();
3676         $ucp = $usercompetencyplanorid;
3677         if (!is_object($ucp)) {
3678             $ucp = new user_competency_plan($ucp);
3679         }
3681         if (!$ucp || !user_competency::can_read_user($ucp->get('userid'))) {
3682             throw new required_capability_exception($ucp->get_context(), 'moodle/competency:usercompetencyview',
3683                 'nopermissions', '');
3684         }
3685         $plan = new plan($ucp->get('planid'));
3686         if ($plan->get('status') != plan::STATUS_COMPLETE) {
3687             throw new coding_exception('To log the user competency in non-completed plan use '
3688                 . 'user_competency_viewed_in_plan method.');
3689         }
3691         \core\event\competency_user_competency_plan_viewed::create_from_user_competency_plan($ucp)->trigger();
3692         return true;
3693     }
3695     /**
3696      * Check if template has related data.
3697      *
3698      * @param int $templateid The id of the template to check.
3699      * @return boolean
3700      */
3701     public static function template_has_related_data($templateid) {
3702         static::require_enabled();
3703         // First we do a permissions check.
3704         $template = new template($templateid);
3706         if (!$template->can_read()) {
3707             throw new required_capability_exception($template->get_context(), 'moodle/competency:templateview',
3708                 'nopermissions', '');
3709         }
3711         // OK - all set.
3712         return $template->has_plans();
3713     }
3715     /**
3716      * List all the related competencies.
3717      *
3718      * @param int $competencyid The id of the competency to check.
3719      * @return competency[]
3720      */
3721     public static function list_related_competencies($competencyid) {
3722         static::require_enabled();
3723         $competency = new competency($competencyid);
3725         if (!has_any_capability(array('moodle/competency:competencyview', 'moodle/competency:competencymanage'),
3726                 $competency->get_context())) {
3727             throw new required_capability_exception($competency->get_context(), 'moodle/competency:competencyview',
3728                 'nopermissions', '');
3729         }
3731         return $competency->get_related_competencies();
3732     }
3734     /**
3735      * Add a related competency.
3736      *
3737      * @param int $competencyid The id of the competency
3738      * @param int $relatedcompetencyid The id of the related competency.
3739      * @return bool False when create failed, true on success, or if the relation already existed.
3740      */
3741     public static function add_related_competency($competencyid, $relatedcompetencyid) {
3742         static::require_enabled();
3743         $competency1 = new competency($competencyid);
3744         $competency2 = new competency($relatedcompetencyid);
3746         require_capability('moodle/competency:competencymanage', $competency1->get_context());
3748         $relatedcompetency = related_competency::get_relation($competency1->get('id'), $competency2->get('id'));
3749         if (!$relatedcompetency->get('id')) {
3750             $relatedcompetency->create();
3751             return true;
3752         }
3754         return true;
3755     }
3757     /**
3758      * Remove a related competency.
3759      *
3760      * @param int $competencyid The id of the competency.
3761      * @param int $relatedcompetencyid The id of the related competency.
3762      * @return bool True when it was deleted, false when it wasn't or the relation doesn't exist.
3763      */
3764     public static function remove_related_competency($competencyid, $relatedcompetencyid) {
3765         static::require_enabled();
3766         $competency = new competency($competencyid);
3768         // This only check if we have the permission in either competency because both competencies
3769         // should belong to the same framework.
3770         require_capability('moodle/competency:competencymanage', $competency->get_context());
3772         $relatedcompetency = related_competency::get_relation($competencyid, $relatedcompetencyid);
3773         if ($relatedcompetency->get('id')) {
3774             return $relatedcompetency->delete();
3775         }
3777         return false;
3778     }
3780     /**
3781      * Read a user evidence.
3782      *
3783      * @param int $id
3784      * @return user_evidence
3785      */
3786     public static function read_user_evidence($id) {
3787         static::require_enabled();
3788         $userevidence = new user_evidence($id);
3790         if (!$userevidence->can_read()) {
3791             $context = $userevidence->get_context();
3792             throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', '');
3793         }
3795         return $userevidence;
3796     }
3798     /**
3799      * Create a new user evidence.
3800      *
3801      * @param  object $data        The data.
3802      * @param  int    $draftitemid The draft ID in which files have been saved.
3803      * @return user_evidence
3804      */
3805     public static function create_user_evidence($data, $draftitemid = null) {
3806         static::require_enabled();
3807         $userevidence = new user_evidence(null, $data);
3808         $context = $userevidence->get_context();
3810         if (!$userevidence->can_manage()) {
3811             throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
3812         }
3814         $userevidence->create();
3815         if (!empty($draftitemid)) {
3816             $fileareaoptions = array('subdirs' => true);
3817             $itemid = $userevidence->get('id');
3818             file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions);
3819         }
3821         // Trigger an evidence of prior learning created event.
3822         \core\event\competency_user_evidence_created::create_from_user_evidence($userevidence)->trigger();
3824         return $userevidence;
3825     }
3827     /**
3828      * Create a new user evidence.
3829      *
3830      * @param  object $data        The data.
3831      * @param  int    $draftitemid The draft ID in which files have been saved.
3832      * @return user_evidence
3833      */
3834     public static function update_user_evidence($data, $draftitemid = null) {
3835         static::require_enabled();
3836         $userevidence = new user_evidence($data->id);
3837         $context = $userevidence->get_context();
3839         if (!$userevidence->can_manage()) {
3840             throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
3842         } else if (property_exists($data, 'userid') && $data->userid != $userevidence->get('userid')) {
3843             throw new coding_exception('Can not change the userid of a user evidence.');
3844         }
3846         $userevidence->from_record($data);
3847         $userevidence->update();
3849         if (!empty($draftitemid)) {
3850             $fileareaoptions = array('subdirs' => true);
3851             $itemid = $userevidence->get('id');
3852             file_save_draft_area_files($draftitemid, $context->id, 'core_competency', 'userevidence', $itemid, $fileareaoptions);
3853         }
3855         // Trigger an evidence of prior learning updated event.
3856         \core\event\competency_user_evidence_updated::create_from_user_evidence($userevidence)->trigger();
3858         return $userevidence;
3859     }
3861     /**
3862      * Delete a user evidence.
3863      *
3864      * @param  int $id The user evidence ID.
3865      * @return bool
3866      */
3867     public static function delete_user_evidence($id) {
3868         static::require_enabled();
3869         $userevidence = new user_evidence($id);
3870         $context = $userevidence->get_context();
3872         if (!$userevidence->can_manage()) {
3873             throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
3874         }
3876         // Delete the user evidence.
3877         $userevidence->delete();
3879         // Delete associated files.
3880         $fs = get_file_storage();
3881         $fs->delete_area_files($context->id, 'core_competency', 'userevidence', $id);
3883         // Delete relation between evidence and competencies.
3884         $userevidence->set('id', $id);     // Restore the ID to fully mock the object.
3885         $competencies = user_evidence_competency::get_competencies_by_userevidenceid($id);
3886         foreach ($competencies as $competency) {
3887             static::delete_user_evidence_competency($userevidence, $competency->get('id'));
3888         }
3890         // Trigger an evidence of prior learning deleted event.
3891         \core\event\competency_user_evidence_deleted::create_from_user_evidence($userevidence)->trigger();
3893         $userevidence->set('id', 0);       // Restore the object.
3895         return true;
3896     }
3898     /**
3899      * List the user evidence of a user.
3900      *
3901      * @param  int $userid The user ID.
3902      * @return user_evidence[]
3903      */
3904     public static function list_user_evidence($userid) {
3905         static::require_enabled();
3906         if (!user_evidence::can_read_user($userid)) {
3907             $context = context_user::instance($userid);
3908             throw new required_capability_exception($context, 'moodle/competency:userevidenceview', 'nopermissions', '');
3909         }
3911         $evidence = user_evidence::get_records(array('userid' => $userid), 'name');
3912         return $evidence;
3913     }
3915     /**
3916      * Link a user evidence with a competency.
3917      *
3918      * @param  user_evidence|int $userevidenceorid User evidence or its ID.
3919      * @param  int $competencyid Competency ID.
3920      * @return user_evidence_competency
3921      */
3922     public static function create_user_evidence_competency($userevidenceorid, $competencyid) {
3923         global $USER;
3924         static::require_enabled();
3926         $userevidence = $userevidenceorid;
3927         if (!is_object($userevidence)) {
3928             $userevidence = self::read_user_evidence($userevidence);
3929         }
3931         // Perform user evidence capability checks.
3932         if (!$userevidence->can_manage()) {
3933             $context = $userevidence->get_context();
3934             throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
3935         }
3937         // Perform competency capability checks.
3938         $competency = self::read_competency($competencyid);
3940         // Get (and create) the relation.
3941         $relation = user_evidence_competency::get_relation($userevidence->get('id'), $competency->get('id'));
3942         if (!$relation->get('id')) {
3943             $relation->create();
3945             $link = url::user_evidence($userevidence->get('id'));
3946             self::add_evidence(
3947                 $userevidence->get('userid'),
3948                 $competency,
3949                 $userevidence->get_context(),
3950                 evidence::ACTION_LOG,
3951                 'evidence_evidenceofpriorlearninglinked',
3952                 'core_competency',
3953                 $userevidence->get('name'),
3954                 false,
3955                 $link->out(false),
3956                 null,
3957                 $USER->id
3958             );
3959         }
3961         return $relation;
3962     }
3964     /**
3965      * Delete a relationship between a user evidence and a competency.
3966      *
3967      * @param  user_evidence|int $userevidenceorid User evidence or its ID.
3968      * @param  int $competencyid Competency ID.
3969      * @return bool
3970      */
3971     public static function delete_user_evidence_competency($userevidenceorid, $competencyid) {
3972         global $USER;
3973         static::require_enabled();
3975         $userevidence = $userevidenceorid;
3976         if (!is_object($userevidence)) {
3977             $userevidence = self::read_user_evidence($userevidence);
3978         }
3980         // Perform user evidence capability checks.
3981         if (!$userevidence->can_manage()) {
3982             $context = $userevidence->get_context();
3983             throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
3984         }
3986         // Get (and delete) the relation.
3987         $relation = user_evidence_competency::get_relation($userevidence->get('id'), $competencyid);
3988         if (!$relation->get('id')) {
3989             return true;
3990         }
3992         $success = $relation->delete();
3993         if ($success) {
3994             self::add_evidence(
3995                 $userevidence->get('userid'),
3996                 $competencyid,
3997                 $userevidence->get_context(),
3998                 evidence::ACTION_LOG,
3999                 'evidence_evidenceofpriorlearningunlinked',
4000                 'core_competency',
4001                 $userevidence->get('name'),
4002                 false,
4003                 null,
4004                 null,
4005                 $USER->id
4006             );
4007         }
4009         return $success;
4010     }
4012     /**
4013      * Send request review for user evidence competencies.
4014      *
4015      * @param  int $id The user evidence ID.
4016      * @return bool
4017      */
4018     public static function request_review_of_user_evidence_linked_competencies($id) {
4019         $userevidence = new user_evidence($id);
4020         $context = $userevidence->get_context();
4021         $userid = $userevidence->get('userid');
4023         if (!$userevidence->can_manage()) {
4024             throw new required_capability_exception($context, 'moodle/competency:userevidencemanage', 'nopermissions', '');
4025         }
4027         $usercompetencies = user_evidence_competency::get_user_competencies_by_userevidenceid($id);
4028         foreach ($usercompetencies as $usercompetency) {
4029             if ($usercompetency->get('status') == user_competency::STATUS_IDLE) {
4030                 static::user_competency_request_review($userid, $usercompetency->get('competencyid'));
4031             }
4032         }
4034         return true;
4035     }
4037     /**
4038      * Recursively duplicate competencies from a tree, we start duplicating from parents to children to have a correct path.
4039      * This method does not copy the related competencies.
4040      *
4041      * @param int $frameworkid - framework id