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