MDL-53471 tool_lp: Hooking in to report when scale is being used
[moodle.git] / admin / tool / lp / classes / api.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Class for loading/storing competency frameworks from the DB.
19  *
20  * @package    tool_lp
21  * @copyright  2015 Damyon Wiese
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 namespace tool_lp;
25 defined('MOODLE_INTERNAL') || die();
27 use stdClass;
28 use context;
29 use context_helper;
30 use context_system;
31 use context_course;
32 use context_module;
33 use context_user;
34 use coding_exception;
35 use require_login_exception;
36 use moodle_exception;
37 use moodle_url;
38 use required_capability_exception;
40 /**
41  * Class for doing things with competency frameworks.
42  *
43  * @copyright  2015 Damyon Wiese
44  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
45  */
46 class api {
48     /**
49      * Returns whether competencies are enabled.
50      *
51      * @return boolean True when enabled.
52      */
53     public static function is_enabled() {
54         return get_config('tool_lp', 'enabled');
55     }
57     /**
58      * Throws an exception if competencies are not enabled.
59      *
60      * @return void
61      * @throws moodle_exception
62      */
63     public static function require_enabled() {
64         if (!static::is_enabled()) {
65             throw new moodle_exception('competenciesarenotenabled', 'tool_lp');
66         }
67     }
69     /**
70      * Checks whether a scale is used anywhere in the plugin.
71      *
72      * This public API has two exceptions:
73      * - It MUST NOT perform any capability checks.
74      * - It MUST ignore whether competencies are enabled or not ({@link self::is_enabled()}).
75      *
76      * @param int $scaleid The scale ID.
77      * @return bool
78      */
79     public static function is_scale_used_anywhere($scaleid) {
80         global $DB;
81         $sql = "SELECT s.id
82                   FROM {scale} s
83              LEFT JOIN {" . competency_framework::TABLE ."} f
84                     ON f.scaleid = :scaleid1
85              LEFT JOIN {" . competency::TABLE ."} c
86                     ON c.scaleid = :scaleid2
87                  WHERE f.id IS NOT NULL
88                     OR c.id IS NOT NULL";
89         return $DB->record_exists_sql($sql, ['scaleid1' => $scaleid, 'scaleid2' => $scaleid]);
90     }
92     /**
93      * Validate if current user have acces to the course_module if hidden.
94      *
95      * @param mixed $cmmixed The cm_info class, course module record or its ID.
96      * @param bool $throwexception Throw an exception or not.
97      * @return bool
98      */
99     protected static function validate_course_module($cmmixed, $throwexception = true) {
100         $cm = $cmmixed;
101         if (!is_object($cm)) {
102             $cmrecord = get_coursemodule_from_id(null, $cmmixed);
103             $modinfo = get_fast_modinfo($cmrecord->course);
104             $cm = $modinfo->get_cm($cmmixed);
105         } else if (!$cm instanceof cm_info) {
106             // Assume we got a course module record.
107             $modinfo = get_fast_modinfo($cm->course);
108             $cm = $modinfo->get_cm($cm->id);
109         }
111         if (!$cm->uservisible) {
112             if ($throwexception) {
113                 throw new require_login_exception('Course module is hidden');
114             } else {
115                 return false;
116             }
117         }
119         return true;
120     }
122     /**
123      * Validate if current user have acces to the course if hidden.
124      *
125      * @param mixed $courseorid The course or it ID.
126      * @param bool $throwexception Throw an exception or not.
127      * @return bool
128      */
129     protected static function validate_course($courseorid, $throwexception = true) {
130         $course = $courseorid;
131         if (!is_object($course)) {
132             $course = get_course($course);
133         }
135         $coursecontext = context_course::instance($course->id);
136         if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
137             if ($throwexception) {
138                 throw new require_login_exception('Course is hidden');
139             } else {
140                 return false;
141             }
142         }
144         return true;
145     }
147     /**
148      * Create a competency from a record containing all the data for the class.
149      *
150      * Requires tool/lp:competencymanage capability at the system context.
151      *
152      * @param stdClass $record Record containing all the data for an instance of the class.
153      * @return competency
154      */
155     public static function create_competency(stdClass $record) {
156         static::require_enabled();
157         $competency = new competency(0, $record);
159         // First we do a permissions check.
160         require_capability('tool/lp:competencymanage', $competency->get_context());
162         // Reset the sortorder, use reorder instead.
163         $competency->set_sortorder(null);
164         $competency->create();
166         \tool_lp\event\competency_created::create_from_competency($competency)->trigger();
168         // Reset the rule of the parent.
169         $parent = $competency->get_parent();
170         if ($parent) {
171             $parent->reset_rule();
172             $parent->update();
173         }
175         return $competency;
176     }
178     /**
179      * Delete a competency by id.
180      *
181      * Requires tool/lp:competencymanage capability at the system context.
182      *
183      * @param int $id The record to delete. This will delete alot of related data - you better be sure.
184      * @return boolean
185      */
186     public static function delete_competency($id) {
187         global $DB;
188         static::require_enabled();
189         $competency = new competency($id);
191         // First we do a permissions check.
192         require_capability('tool/lp:competencymanage', $competency->get_context());
194         $events = array();
195         $competencyids = array(intval($competency->get_id()));
196         $contextid = $competency->get_context()->id;
197         $competencyids = array_merge(competency::get_descendants_ids($competency), $competencyids);
198         if (!competency::can_all_be_deleted($competencyids)) {
199             return false;
200         }
201         $transaction = $DB->start_delegated_transaction();
203         try {
205             // Reset the rule of the parent.
206             $parent = $competency->get_parent();
207             if ($parent) {
208                 $parent->reset_rule();
209                 $parent->update();
210             }
212             // Delete the competency separately so the after_delete event can be triggered.
213             $competency->delete();
215             // Delete the competencies.
216             competency::delete_multiple($competencyids);
218             // Delete the competencies relation.
219             related_competency::delete_multiple_relations($competencyids);
221             // Delete competency evidences.
222             user_evidence_competency::delete_by_competencyids($competencyids);
224             // Register the competencies deleted events.
225             $events = \tool_lp\event\competency_deleted::create_multiple_from_competencyids($competencyids, $contextid);
227         } catch (\Exception $e) {
228             $transaction->rollback($e);
229         }
231         $transaction->allow_commit();
232         // Trigger events.
233         foreach ($events as $event) {
234             $event->trigger();
235         }
237         return true;
238     }
240     /**
241      * Reorder this competency.
242      *
243      * Requires tool/lp:competencymanage capability at the system context.
244      *
245      * @param int $id The id of the competency to move.
246      * @return boolean
247      */
248     public static function move_down_competency($id) {
249         static::require_enabled();
250         $current = new competency($id);
252         // First we do a permissions check.
253         require_capability('tool/lp:competencymanage', $current->get_context());
255         $max = self::count_competencies(array('parentid' => $current->get_parentid(),
256                                               'competencyframeworkid' => $current->get_competencyframeworkid()));
257         if ($max > 0) {
258             $max--;
259         }
261         $sortorder = $current->get_sortorder();
262         if ($sortorder >= $max) {
263             return false;
264         }
265         $sortorder = $sortorder + 1;
266         $current->set_sortorder($sortorder);
268         $filters = array('parentid' => $current->get_parentid(),
269                          'competencyframeworkid' => $current->get_competencyframeworkid(),
270                          'sortorder' => $sortorder);
271         $children = self::list_competencies($filters, 'id');
272         foreach ($children as $needtoswap) {
273             $needtoswap->set_sortorder($sortorder - 1);
274             $needtoswap->update();
275         }
277         // OK - all set.
278         $result = $current->update();
280         return $result;
281     }
283     /**
284      * Reorder this competency.
285      *
286      * Requires tool/lp:competencymanage capability at the system context.
287      *
288      * @param int $id The id of the competency to move.
289      * @return boolean
290      */
291     public static function move_up_competency($id) {
292         static::require_enabled();
293         $current = new competency($id);
295         // First we do a permissions check.
296         require_capability('tool/lp:competencymanage', $current->get_context());
298         $sortorder = $current->get_sortorder();
299         if ($sortorder == 0) {
300             return false;
301         }
303         $sortorder = $sortorder - 1;
304         $current->set_sortorder($sortorder);
306         $filters = array('parentid' => $current->get_parentid(),
307                          'competencyframeworkid' => $current->get_competencyframeworkid(),
308                          'sortorder' => $sortorder);
309         $children = self::list_competencies($filters, 'id');
310         foreach ($children as $needtoswap) {
311             $needtoswap->set_sortorder($sortorder + 1);
312             $needtoswap->update();
313         }
315         // OK - all set.
316         $result = $current->update();
318         return $result;
319     }
321     /**
322      * Move this competency so it sits in a new parent.
323      *
324      * Requires tool/lp:competencymanage capability at the system context.
325      *
326      * @param int $id The id of the competency to move.
327      * @param int $newparentid The new parent id for the competency.
328      * @return boolean
329      */
330     public static function set_parent_competency($id, $newparentid) {
331         global $DB;
332         static::require_enabled();
333         $current = new competency($id);
335         // First we do a permissions check.
336         require_capability('tool/lp:competencymanage', $current->get_context());
337         if ($id == $newparentid) {
338             throw new coding_exception('Can not set a competency as a parent of itself.');
339         } if ($newparentid == $current->get_parentid()) {
340             throw new coding_exception('Can not move a competency to the same location.');
341         }
343         // Some great variable assignment right here.
344         $currentparent = $current->get_parent();
345         $parent = !empty($newparentid) ? new competency($newparentid) : null;
346         $parentpath = !empty($parent) ? $parent->get_path() : '/0/';
348         // We're going to change quite a few things.
349         $transaction = $DB->start_delegated_transaction();
351         // If we are moving a node to a child of itself:
352         // - promote all the child nodes by one level.
353         // - remove the rule on self.
354         // - re-read the parent.
355         $newparents = explode('/', $parentpath);
356         if (in_array($current->get_id(), $newparents)) {
357             $children = competency::get_records(array('parentid' => $current->get_id()), 'id');
358             foreach ($children as $child) {
359                 $child->set_parentid($current->get_parentid());
360                 $child->update();
361             }
363             // Reset the rule on self as our children have changed.
364             $current->reset_rule();
366             // The destination parent is one of our descendants, we need to re-fetch its values (path, parentid).
367             $parent->read();
368         }
370         // Reset the rules of initial parent and destination.
371         if (!empty($currentparent)) {
372             $currentparent->reset_rule();
373             $currentparent->update();
374         }
375         if (!empty($parent)) {
376             $parent->reset_rule();
377             $parent->update();
378         }
380         // Do the actual move.
381         $current->set_parentid($newparentid);
382         $result = $current->update();
384         // All right, let's commit this.
385         $transaction->allow_commit();
387         return $result;
388     }
390     /**
391      * Update the details for a competency.
392      *
393      * Requires tool/lp:competencymanage capability at the system context.
394      *
395      * @param stdClass $record The new details for the competency.
396      *                         Note - must contain an id that points to the competency to update.
397      *
398      * @return boolean
399      */
400     public static function update_competency($record) {
401         static::require_enabled();
402         $competency = new competency($record->id);
404         // First we do a permissions check.
405         require_capability('tool/lp:competencymanage', $competency->get_context());
407         // Some things should not be changed in an update - they should use a more specific method.
408         $record->sortorder = $competency->get_sortorder();
409         $record->parentid = $competency->get_parentid();
410         $record->competencyframeworkid = $competency->get_competencyframeworkid();
412         $competency->from_record($record);
413         require_capability('tool/lp:competencymanage', $competency->get_context());
415         // OK - all set.
416         $result = $competency->update();
418         // Trigger the update event.
419         \tool_lp\event\competency_updated::create_from_competency($competency)->trigger();
421         return $result;
422     }
424     /**
425      * Read a the details for a single competency and return a record.
426      *
427      * Requires tool/lp:competencyview capability at the system context.
428      *
429      * @param int $id The id of the competency to read.
430      * @param bool $includerelated Include related tags or not.
431      * @return stdClass
432      */
433     public static function read_competency($id, $includerelated = false) {
434         static::require_enabled();
435         $competency = new competency($id);
437         // First we do a permissions check.
438         $context = $competency->get_context();
439         if (!has_any_capability(array('tool/lp:competencyview', 'tool/lp:competencymanage'), $context)) {
440              throw new required_capability_exception($context, 'tool/lp:competencyview', 'nopermissions', '');
441         }
443         // OK - all set.
444         if ($includerelated) {
445             $relatedcompetency = new related_competency();
446             if ($related = $relatedcompetency->list_relations($id)) {
447                 $competency->relatedcompetencies = $related;
448             }
449         }
451         return $competency;
452     }
454     /**
455      * Perform a text search based and return all results and their parents.
456      *
457      * Requires tool/lp:competencyview capability at the framework context.
458      *
459      * @param string $textsearch A string to search for.
460      * @param int $competencyframeworkid The id of the framework to limit the search.
461      * @return array of competencies
462      */
463     public static function search_competencies($textsearch, $competencyframeworkid) {
464         static::require_enabled();
465         $framework = new competency_framework($competencyframeworkid);
467         // First we do a permissions check.
468         $context = $framework->get_context();
469         if (!has_any_capability(array('tool/lp:competencyview', 'tool/lp:competencymanage'), $context)) {
470              throw new required_capability_exception($context, 'tool/lp:competencyview', 'nopermissions', '');
471         }
473         // OK - all set.
474         $competencies = competency::search($textsearch, $competencyframeworkid);
475         return $competencies;
476     }
478     /**
479      * Perform a search based on the provided filters and return a paginated list of records.
480      *
481      * Requires tool/lp:competencyview capability at some context.
482      *
483      * @param array $filters A list of filters to apply to the list.
484      * @param string $sort The column to sort on
485      * @param string $order ('ASC' or 'DESC')
486      * @param int $skip Number of records to skip (pagination)
487      * @param int $limit Max of records to return (pagination)
488      * @return array of competencies
489      */
490     public static function list_competencies($filters, $sort = '', $order = 'ASC', $skip = 0, $limit = 0) {
491         static::require_enabled();
492         if (!isset($filters['competencyframeworkid'])) {
493             $context = context_system::instance();
494         } else {
495             $framework = new competency_framework($filters['competencyframeworkid']);
496             $context = $framework->get_context();
497         }
499         // First we do a permissions check.
500         if (!has_any_capability(array('tool/lp:competencyview', 'tool/lp:competencymanage'), $context)) {
501              throw new required_capability_exception($context, 'tool/lp:competencyview', 'nopermissions', '');
502         }
504         // OK - all set.
505         return competency::get_records($filters, $sort, $order, $skip, $limit);
506     }
508     /**
509      * Perform a search based on the provided filters and return a paginated list of records.
510      *
511      * Requires tool/lp:competencyview capability at some context.
512      *
513      * @param array $filters A list of filters to apply to the list.
514      * @return int
515      */
516     public static function count_competencies($filters) {
517         static::require_enabled();
518         if (!isset($filters['competencyframeworkid'])) {
519             $context = context_system::instance();
520         } else {
521             $framework = new competency_framework($filters['competencyframeworkid']);
522             $context = $framework->get_context();
523         }
525         // First we do a permissions check.
526         if (!has_any_capability(array('tool/lp:competencyview', 'tool/lp:competencymanage'), $context)) {
527              throw new required_capability_exception($context, 'tool/lp:competencyview', 'nopermissions', '');
528         }
530         // OK - all set.
531         return competency::count_records($filters);
532     }
534     /**
535      * Create a competency framework from a record containing all the data for the class.
536      *
537      * Requires tool/lp:competencymanage capability at the system context.
538      *
539      * @param stdClass $record Record containing all the data for an instance of the class.
540      * @return competency_framework
541      */
542     public static function create_framework(stdClass $record) {
543         static::require_enabled();
544         $framework = new competency_framework(0, $record);
545         require_capability('tool/lp:competencymanage', $framework->get_context());
547         // Account for different formats of taxonomies.
548         if (isset($record->taxonomies)) {
549             $framework->set_taxonomies($record->taxonomies);
550         }
552         $framework = $framework->create();
554         // Trigger a competency framework created event.
555         \tool_lp\event\competency_framework_created::create_from_framework($framework)->trigger();
557         return $framework;
558     }
560     /**
561      * Duplicate a competency framework by id.
562      *
563      * Requires tool/lp:competencymanage capability at the system context.
564      *
565      * @param int $id The record to duplicate. All competencies associated and related will be duplicated.
566      * @return competency_framework the framework duplicated
567      */
568     public static function duplicate_framework($id) {
569         global $DB;
570         static::require_enabled();
572         $framework = new competency_framework($id);
573         require_capability('tool/lp:competencymanage', $framework->get_context());
574         // Starting transaction.
575         $transaction = $DB->start_delegated_transaction();
577         try {
578             // Get a uniq idnumber based on the origin framework.
579             $idnumber = competency_framework::get_unused_idnumber($framework->get_idnumber());
580             $framework->set_idnumber($idnumber);
581             // Adding the suffix copy to the shortname.
582             $framework->set_shortname(get_string('duplicateditemname', 'tool_lp', $framework->get_shortname()));
583             $framework->set_id(0);
584             $framework = $framework->create();
586             // Array that match the old competencies ids with the new one to use when copying related competencies.
587             $frameworkcompetency = competency::get_framework_tree($id);
588             $matchids = self::duplicate_competency_tree($framework->get_id(), $frameworkcompetency, 0, 0);
590             // Copy the related competencies.
591             $relcomps = related_competency::get_multiple_relations(array_keys($matchids));
593             foreach ($relcomps as $relcomp) {
594                 $compid = $relcomp->get_competencyid();
595                 $relcompid = $relcomp->get_relatedcompetencyid();
596                 if (isset($matchids[$compid]) && isset($matchids[$relcompid])) {
597                     $newcompid = $matchids[$compid]->get_id();
598                     $newrelcompid = $matchids[$relcompid]->get_id();
599                     if ($newcompid < $newrelcompid) {
600                         $relcomp->set_competencyid($newcompid);
601                         $relcomp->set_relatedcompetencyid($newrelcompid);
602                     } else {
603                         $relcomp->set_competencyid($newrelcompid);
604                         $relcomp->set_relatedcompetencyid($newcompid);
605                     }
606                     $relcomp->set_id(0);
607                     $relcomp->create();
608                 } else {
609                     // Debugging message when there is no match found.
610                     debugging('related competency id not found');
611                 }
612             }
614             // Setting rules on duplicated competencies.
615             self::migrate_competency_tree_rules($frameworkcompetency, $matchids);
617             $transaction->allow_commit();
619         } catch (\Exception $e) {
620             $transaction->rollback($e);
621         }
623         // Trigger a competency framework created event.
624         \tool_lp\event\competency_framework_created::create_from_framework($framework)->trigger();
626         return $framework;
627     }
629     /**
630      * Delete a competency framework by id.
631      *
632      * Requires tool/lp:competencymanage capability at the system context.
633      *
634      * @param int $id The record to delete. This will delete alot of related data - you better be sure.
635      * @return boolean
636      */
637     public static function delete_framework($id) {
638         global $DB;
639         static::require_enabled();
640         $framework = new competency_framework($id);
641         require_capability('tool/lp:competencymanage', $framework->get_context());
643         $events = array();
644         $competenciesid = competency::get_ids_by_frameworkid($id);
645         $contextid = $framework->get_contextid();
646         if (!competency::can_all_be_deleted($competenciesid)) {
647             return false;
648         }
649         $transaction = $DB->start_delegated_transaction();
650         try {
651             if (!empty($competenciesid)) {
652                 // Delete competencies.
653                 competency::delete_by_frameworkid($id);
655                 // Delete the related competencies.
656                 related_competency::delete_multiple_relations($competenciesid);
658                 // Delete the evidences for competencies.
659                 user_evidence_competency::delete_by_competencyids($competenciesid);
660             }
662             // Create a competency framework deleted event.
663             $event = \tool_lp\event\competency_framework_deleted::create_from_framework($framework);
664             $result = $framework->delete();
666             // Register the deleted events competencies.
667             $events = \tool_lp\event\competency_deleted::create_multiple_from_competencyids($competenciesid, $contextid);
669         } catch (\Exception $e) {
670             $transaction->rollback($e);
671         }
673         // Commit the transaction.
674         $transaction->allow_commit();
676         // If all operations are successfull then trigger the delete event.
677         $event->trigger();
679         // Trigger deleted event competencies.
680         foreach ($events as $event) {
681             $event->trigger();
682         }
684         return $result;
685     }
687     /**
688      * Update the details for a competency framework.
689      *
690      * Requires tool/lp:competencymanage capability at the system context.
691      *
692      * @param stdClass $record The new details for the framework. Note - must contain an id that points to the framework to update.
693      * @return boolean
694      */
695     public static function update_framework($record) {
696         static::require_enabled();
697         $framework = new competency_framework($record->id);
699         // Check the permissions before update.
700         require_capability('tool/lp:competencymanage', $framework->get_context());
702         // Account for different formats of taxonomies.
703         $framework->from_record($record);
704         if (isset($record->taxonomies)) {
705             $framework->set_taxonomies($record->taxonomies);
706         }
708         // Trigger a competency framework updated event.
709         \tool_lp\event\competency_framework_updated::create_from_framework($framework)->trigger();
711         return $framework->update();
712     }
714     /**
715      * Read a the details for a single competency framework and return a record.
716      *
717      * Requires tool/lp:competencyview capability at the system context.
718      *
719      * @param int $id The id of the framework to read.
720      * @return competency_framework
721      */
722     public static function read_framework($id) {
723         static::require_enabled();
724         $framework = new competency_framework($id);
725         if (!has_any_capability(array('tool/lp:competencyview', 'tool/lp:competencymanage'), $framework->get_context())) {
726              throw new required_capability_exception($framework->get_context(), 'tool/lp:competencyview', 'nopermissions', '');
727         }
728         return $framework;
729     }
731     /**
732      * Logg the competency framework viewed event.
733      *
734      * @param competency_framework|int $frameworkorid The competency_framework object or competency framework id
735      * @return bool
736      */
737     public static function competency_framework_viewed($frameworkorid) {
738         static::require_enabled();
739         $framework = $frameworkorid;
740         if (!is_object($framework)) {
741             $framework = new competency_framework($framework);
742         }
743         if (!has_any_capability(array('tool/lp:competencyview', 'tool/lp:competencymanage'), $framework->get_context())) {
744              throw new required_capability_exception($framework->get_context(), 'tool/lp:competencyview', 'nopermissions', '');
745         }
746         \tool_lp\event\competency_framework_viewed::create_from_framework($framework)->trigger();
747         return true;
748     }
750     /**
751      * Logg the competency viewed event.
752      *
753      * @param competency|int $competencyorid The competency object or competency id
754      * @return bool
755      */
756     public static function competency_viewed($competencyorid) {
757         static::require_enabled();
758         $competency = $competencyorid;
759         if (!is_object($competency)) {
760             $competency = new competency($competency);
761         }
763         if (!has_any_capability(array('tool/lp:competencyview', 'tool/lp:competencymanage'), $competency->get_context())) {
764              throw new required_capability_exception($competency->get_context(), 'tool/lp:competencyview', 'nopermissions', '');
765         }
767         \tool_lp\event\competency_viewed::create_from_competency($competency)->trigger();
768         return true;
769     }
771     /**
772      * Perform a search based on the provided filters and return a paginated list of records.
773      *
774      * Requires tool/lp:competencyview capability at the system context.
775      *
776      * @param string $sort The column to sort on
777      * @param string $order ('ASC' or 'DESC')
778      * @param int $skip Number of records to skip (pagination)
779      * @param int $limit Max of records to return (pagination)
780      * @param context $context The parent context of the frameworks.
781      * @param string $includes Defines what other contexts to fetch frameworks from.
782      *                         Accepted values are:
783      *                          - children: All descendants
784      *                          - parents: All parents, grand parents, etc...
785      *                          - self: Context passed only.
786      * @param bool $onlyvisible If true return only visible frameworks
787      * @param string $query A string to use to filter down the frameworks.
788      * @return array of competency_framework
789      */
790     public static function list_frameworks($sort, $order, $skip, $limit, $context, $includes = 'children',
791             $onlyvisible = false, $query = '') {
792         global $DB;
793         static::require_enabled();
795         // Get all the relevant contexts.
796         $contexts = self::get_related_contexts($context, $includes,
797             array('tool/lp:competencyview', 'tool/lp:competencymanage'));
799         if (empty($contexts)) {
800             throw new required_capability_exception($context, 'tool/lp:competencyview', 'nopermissions', '');
801         }
803         // OK - all set.
804         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
805         $select = "contextid $insql";
806         if ($onlyvisible) {
807             $select .= " AND visible = :visible";
808             $inparams['visible'] = 1;
809         }
811         if (!empty($query) || is_numeric($query)) {
812             $sqlnamelike = $DB->sql_like('shortname', ':namelike', false);
813             $sqlidnlike = $DB->sql_like('idnumber', ':idnlike', false);
815             $select .= " AND ($sqlnamelike OR $sqlidnlike) ";
816             $inparams['namelike'] = '%' . $DB->sql_like_escape($query) . '%';
817             $inparams['idnlike'] = '%' . $DB->sql_like_escape($query) . '%';
818         }
820         return competency_framework::get_records_select($select, $inparams, $sort . ' ' . $order, '*', $skip, $limit);
821     }
823     /**
824      * Perform a search based on the provided filters and return a paginated list of records.
825      *
826      * Requires tool/lp:competencyview capability at the system context.
827      *
828      * @param context $context The parent context of the frameworks.
829      * @param string $includes Defines what other contexts to fetch frameworks from.
830      *                         Accepted values are:
831      *                          - children: All descendants
832      *                          - parents: All parents, grand parents, etc...
833      *                          - self: Context passed only.
834      * @return int
835      */
836     public static function count_frameworks($context, $includes) {
837         global $DB;
838         static::require_enabled();
840         // Get all the relevant contexts.
841         $contexts = self::get_related_contexts($context, $includes,
842             array('tool/lp:competencyview', 'tool/lp:competencymanage'));
844         if (empty($contexts)) {
845             throw new required_capability_exception($context, 'tool/lp:competencyview', 'nopermissions', '');
846         }
848         // OK - all set.
849         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
850         return competency_framework::count_records_select("contextid $insql", $inparams);
851     }
853     /**
854      * Fetches all the relevant contexts.
855      *
856      * Note: This currently only supports system, category and user contexts. However user contexts
857      * behave a bit differently and will fallback on the system context. This is what makes the most
858      * sense because a user context does not have descendants, and only has system as a parent.
859      *
860      * @param context $context The context to start from.
861      * @param string $includes Defines what other contexts to find.
862      *                         Accepted values are:
863      *                          - children: All descendants
864      *                          - parents: All parents, grand parents, etc...
865      *                          - self: Context passed only.
866      * @param array $hasanycapability Array of capabilities passed to {@link has_any_capability()} in each context.
867      * @return context[] An array of contexts where keys are context IDs.
868      */
869     public static function get_related_contexts($context, $includes, array $hasanycapability = null) {
870         global $DB;
871         static::require_enabled();
873         if (!in_array($includes, array('children', 'parents', 'self'))) {
874             throw new coding_exception('Invalid parameter value for \'includes\'.');
875         }
877         // If context user swap it for the context_system.
878         if ($context->contextlevel == CONTEXT_USER) {
879             $context = context_system::instance();
880         }
882         $contexts = array($context->id => $context);
884         if ($includes == 'children') {
885             $params = array('coursecatlevel' => CONTEXT_COURSECAT, 'path' => $context->path . '/%');
886             $pathlike = $DB->sql_like('path', ':path');
887             $sql = "contextlevel = :coursecatlevel AND $pathlike";
888             $rs = $DB->get_recordset_select('context', $sql, $params);
889             foreach ($rs as $record) {
890                 $ctxid = $record->id;
891                 context_helper::preload_from_record($record);
892                 $contexts[$ctxid] = context::instance_by_id($ctxid);
893             }
894             $rs->close();
896         } else if ($includes == 'parents') {
897             $children = $context->get_parent_contexts();
898             foreach ($children as $ctx) {
899                 $contexts[$ctx->id] = $ctx;
900             }
901         }
903         // Filter according to the capabilities required.
904         if (!empty($hasanycapability)) {
905             foreach ($contexts as $key => $ctx) {
906                 if (!has_any_capability($hasanycapability, $ctx)) {
907                     unset($contexts[$key]);
908                 }
909             }
910         }
912         return $contexts;
913     }
915     /**
916      * Count all the courses using a competency.
917      *
918      * @param int $competencyid The id of the competency to check.
919      * @return int
920      */
921     public static function count_courses_using_competency($competencyid) {
922         static::require_enabled();
924         // OK - all set.
925         $courses = course_competency::list_courses_min($competencyid);
926         $count = 0;
928         // Now check permissions on each course.
929         foreach ($courses as $course) {
930             if (!self::validate_course($course, false)) {
931                 continue;
932             }
934             $context = context_course::instance($course->id);
935             $capabilities = array('tool/lp:coursecompetencyview', 'tool/lp:coursecompetencymanage');
936             if (!has_any_capability($capabilities, $context)) {
937                 continue;
938             }
940             $count++;
941         }
943         return $count;
944     }
946     /**
947      * List all the courses modules using a competency in a course.
948      *
949      * @param int $competencyid The id of the competency to check.
950      * @param int $courseid The id of the course to check.
951      * @return array[int] Array of course modules ids.
952      */
953     public static function list_course_modules_using_competency($competencyid, $courseid) {
954         static::require_enabled();
956         $result = array();
957         self::validate_course($courseid);
959         $coursecontext = context_course::instance($courseid);
961         // We will not check each module - course permissions should be enough.
962         $capabilities = array('tool/lp:coursecompetencyview', 'tool/lp:coursecompetencymanage');
963         if (!has_any_capability($capabilities, $coursecontext)) {
964             throw new required_capability_exception($coursecontext, 'tool/lp:coursecompetencyview', 'nopermissions', '');
965         }
967         $cmlist = course_module_competency::list_course_modules($competencyid, $courseid);
968         foreach ($cmlist as $cmid) {
969             if (self::validate_course_module($cmid, false)) {
970                 array_push($result, $cmid);
971             }
972         }
974         return $result;
975     }
977     /**
978      * List all the competencies linked to a course module.
979      *
980      * @param mixed $cmorid The course module, or its ID.
981      * @return array[competency] Array of competency records.
982      */
983     public static function list_course_module_competencies_in_course_module($cmorid) {
984         static::require_enabled();
985         $cm = $cmorid;
986         if (!is_object($cmorid)) {
987             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
988         }
990         // Check the user have access to the course module.
991         self::validate_course_module($cm);
992         $context = context_module::instance($cm->id);
994         $capabilities = array('tool/lp:coursecompetencyview', 'tool/lp:coursecompetencymanage');
995         if (!has_any_capability($capabilities, $context)) {
996             throw new required_capability_exception($context, 'tool/lp:coursecompetencyview', 'nopermissions', '');
997         }
999         $result = array();
1001         $cmclist = course_module_competency::list_course_module_competencies($cm->id);
1002         foreach ($cmclist as $id => $cmc) {
1003             array_push($result, $cmc);
1004         }
1006         return $result;
1007     }
1009     /**
1010      * List all the courses using a competency.
1011      *
1012      * @param int $competencyid The id of the competency to check.
1013      * @return array[stdClass] Array of stdClass containing id and shortname.
1014      */
1015     public static function list_courses_using_competency($competencyid) {
1016         static::require_enabled();
1018         // OK - all set.
1019         $courses = course_competency::list_courses($competencyid);
1020         $result = array();
1022         // Now check permissions on each course.
1023         foreach ($courses as $id => $course) {
1024             $context = context_course::instance($course->id);
1025             $capabilities = array('tool/lp:coursecompetencyview', 'tool/lp:coursecompetencymanage');
1026             if (!has_any_capability($capabilities, $context)) {
1027                 unset($courses[$id]);
1028                 continue;
1029             }
1030             if (!self::validate_course($course, false)) {
1031                 unset($courses[$id]);
1032                 continue;
1033             }
1034             array_push($result, $course);
1035         }
1037         return $result;
1038     }
1040     /**
1041      * Count all the competencies in a course.
1042      *
1043      * @param int $courseid The id of the course to check.
1044      * @return int
1045      */
1046     public static function count_competencies_in_course($courseid) {
1047         static::require_enabled();
1048         // Check the user have access to the course.
1049         self::validate_course($courseid);
1051         // First we do a permissions check.
1052         $context = context_course::instance($courseid);
1054         $capabilities = array('tool/lp:coursecompetencyview', 'tool/lp:coursecompetencymanage');
1055         if (!has_any_capability($capabilities, $context)) {
1056              throw new required_capability_exception($context, 'tool/lp:coursecompetencyview', 'nopermissions', '');
1057         }
1059         // OK - all set.
1060         return course_competency::count_competencies($courseid);
1061     }
1063     /**
1064      * List the competencies associated to a course.
1065      *
1066      * @param mixed $courseorid The course, or its ID.
1067      * @return array( array(
1068      *                   'competency' => \tool_lp\competency,
1069      *                   'coursecompetency' => \tool_lp\course_competency
1070      *              ))
1071      */
1072     public static function list_course_competencies($courseorid) {
1073         static::require_enabled();
1074         $course = $courseorid;
1075         if (!is_object($courseorid)) {
1076             $course = get_course($courseorid);
1077         }
1079         // Check the user have access to the course.
1080         self::validate_course($course);
1081         $context = context_course::instance($course->id);
1083         $capabilities = array('tool/lp:coursecompetencyview', 'tool/lp:coursecompetencymanage');
1084         if (!has_any_capability($capabilities, $context)) {
1085             throw new required_capability_exception($context, 'tool/lp:coursecompetencyview', 'nopermissions', '');
1086         }
1088         $result = array();
1090         // TODO We could improve the performance of this into one single query.
1091         $coursecompetencies = course_competency::list_course_competencies($course->id);
1092         $competencies = course_competency::list_competencies($course->id);
1094         // Build the return values.
1095         foreach ($coursecompetencies as $key => $coursecompetency) {
1096             $result[] = array(
1097                 'competency' => $competencies[$coursecompetency->get_competencyid()],
1098                 'coursecompetency' => $coursecompetency
1099             );
1100         }
1102         return $result;
1103     }
1105     /**
1106      * Get a user competency.
1107      *
1108      * @param int $userid The user ID.
1109      * @param int $competencyid The competency ID.
1110      * @return user_competency
1111      */
1112     public static function get_user_competency($userid, $competencyid) {
1113         static::require_enabled();
1114         $existing = user_competency::get_multiple($userid, array($competencyid));
1115         $uc = array_pop($existing);
1117         if (!$uc) {
1118             $uc = user_competency::create_relation($userid, $competencyid);
1119             $uc->create();
1120         }
1122         if (!$uc->can_read()) {
1123             throw new required_capability_exception($uc->get_context(), 'tool/lp:usercompetencyview', 'nopermissions', '');
1124         }
1125         return $uc;
1126     }
1128     /**
1129      * Get a user competency by ID.
1130      *
1131      * @param int $usercompetencyid The user competency ID.
1132      * @return user_competency
1133      */
1134     public static function get_user_competency_by_id($usercompetencyid) {
1135         static::require_enabled();
1136         $uc = new user_competency($usercompetencyid);
1137         if (!$uc->can_read()) {
1138             throw new required_capability_exception($uc->get_context(), 'tool/lp:usercompetencyview', 'nopermissions', '');
1139         }
1140         return $uc;
1141     }
1143     /**
1144      * List the competencies associated to a course module.
1145      *
1146      * @param mixed $cmorid The course module, or its ID.
1147      * @return array( array(
1148      *                   'competency' => \tool_lp\competency,
1149      *                   'coursemodulecompetency' => \tool_lp\course_module_competency
1150      *              ))
1151      */
1152     public static function list_course_module_competencies($cmorid) {
1153         static::require_enabled();
1154         $cm = $cmorid;
1155         if (!is_object($cmorid)) {
1156             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1157         }
1159         // Check the user have access to the course module.
1160         self::validate_course_module($cm);
1161         $context = context_module::instance($cm->id);
1163         $capabilities = array('tool/lp:coursecompetencyview', 'tool/lp:coursecompetencymanage');
1164         if (!has_any_capability($capabilities, $context)) {
1165             throw new required_capability_exception($context, 'tool/lp:coursecompetencyview', 'nopermissions', '');
1166         }
1168         $result = array();
1170         // TODO We could improve the performance of this into one single query.
1171         $coursemodulecompetencies = course_competency::list_course_module_competencies($cm->id);
1172         $competencies = course_module_competency::list_competencies($cm->id);
1174         // Build the return values.
1175         foreach ($coursemodulecompetencies as $key => $coursemodulecompetency) {
1176             $result[] = array(
1177                 'competency' => $competencies[$coursemodulecompetency->get_competencyid()],
1178                 'coursemodulecompetency' => $coursemodulecompetency
1179             );
1180         }
1182         return $result;
1183     }
1185     /**
1186      * Get a user competency in a course.
1187      *
1188      * @param int $courseid The id of the course to check.
1189      * @param int $userid The id of the course to check.
1190      * @param int $competencyid The id of the competency.
1191      * @return user_competency
1192      */
1193     public static function get_user_competency_in_course($courseid, $userid, $competencyid) {
1194         static::require_enabled();
1195         // First we do a permissions check.
1196         $context = context_course::instance($courseid);
1198         $capabilities = array('tool/lp:coursecompetencyview', 'tool/lp:coursecompetencymanage');
1199         if (!has_any_capability($capabilities, $context)) {
1200             throw new required_capability_exception($context, 'tool/lp:coursecompetencyview', 'nopermissions', '');
1201         } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
1202             throw new required_capability_exception($context, 'tool/lp:usercompetencyview', 'nopermissions', '');
1203         }
1205         // This will throw an exception if the competency does not belong to the course.
1206         $competency = course_competency::get_competency($courseid, $competencyid);
1208         $existing = user_competency::get_multiple($userid, array($competencyid));
1209         // Create missing.
1210         $found = count($existing);
1211         if ($found) {
1212             $uc = array_pop($existing);
1213         } else {
1214             $uc = user_competency::create_relation($userid, $competency->get_id());
1215             $uc->create();
1216         }
1218         return $uc;
1219     }
1221     /**
1222      * List all the user competencies in a course.
1223      *
1224      * @param int $courseid The id of the course to check.
1225      * @param int $userid The id of the course to check.
1226      * @return array of competencies
1227      */
1228     public static function list_user_competencies_in_course($courseid, $userid) {
1229         static::require_enabled();
1230         // First we do a permissions check.
1231         $context = context_course::instance($courseid);
1232         $onlyvisible = 1;
1234         $capabilities = array('tool/lp:coursecompetencyview', 'tool/lp:coursecompetencymanage');
1235         if (!has_any_capability($capabilities, $context)) {
1236             throw new required_capability_exception($context, 'tool/lp:coursecompetencyview', 'nopermissions', '');
1237         } else if (!user_competency::can_read_user_in_course($userid, $courseid)) {
1238             throw new required_capability_exception($context, 'tool/lp:usercompetencyview', 'nopermissions', '');
1239         }
1241         // OK - all set.
1242         $competencylist = course_competency::list_competencies($courseid, false);
1244         $existing = user_competency::get_multiple($userid, $competencylist);
1245         // Create missing.
1246         $orderedusercompetencies = array();
1248         $somemissing = false;
1249         foreach ($competencylist as $coursecompetency) {
1250             $found = false;
1251             foreach ($existing as $usercompetency) {
1252                 if ($usercompetency->get_competencyid() == $coursecompetency->get_id()) {
1253                     $found = true;
1254                     $orderedusercompetencies[$usercompetency->get_id()] = $usercompetency;
1255                     break;
1256                 }
1257             }
1258             if (!$found) {
1259                 $uc = user_competency::create_relation($userid, $coursecompetency->get_id());
1260                 $uc->create();
1261                 $orderedusercompetencies[$uc->get_id()] = $uc;
1262             }
1263         }
1265         return $orderedusercompetencies;
1266     }
1268     /**
1269      * List the user competencies to review.
1270      *
1271      * The method returns values in this format:
1272      *
1273      * array(
1274      *     'competencies' => array(
1275      *         (stdClass)(
1276      *             'usercompetency' => (user_competency),
1277      *             'competency' => (competency),
1278      *             'user' => (user)
1279      *         )
1280      *     ),
1281      *     'count' => (int)
1282      * )
1283      *
1284      * @param int $skip The number of records to skip.
1285      * @param int $limit The number of results to return.
1286      * @param int $userid The user we're getting the competencies to review for.
1287      * @return array Containing the keys 'count', and 'competencies'. The 'competencies' key contains an object
1288      *               which contains 'competency', 'usercompetency' and 'user'.
1289      */
1290     public static function list_user_competencies_to_review($skip = 0, $limit = 50, $userid = null) {
1291         global $DB, $USER;
1292         static::require_enabled();
1293         if ($userid === null) {
1294             $userid = $USER->id;
1295         }
1297         $capability = 'tool/lp:usercompetencyreview';
1298         $ucfields = user_competency::get_sql_fields('uc');
1299         $compfields = competency::get_sql_fields('c');
1300         $usercols = array('id') + get_user_fieldnames();
1301         $userfields = array();
1302         foreach ($usercols as $field) {
1303             $userfields[] = "u." . $field . " AS usr_" . $field;
1304         }
1305         $userfields = implode(',', $userfields);
1307         $select = "SELECT $ucfields, $compfields, $userfields";
1308         $countselect = "SELECT COUNT('x')";
1309         $sql = "  FROM {" . user_competency::TABLE . "} uc
1310                   JOIN {" . competency::TABLE . "} c
1311                     ON c.id = uc.competencyid
1312                   JOIN {user} u
1313                     ON u.id = uc.userid
1314                  WHERE (uc.status = :waitingforreview
1315                     OR (uc.status = :inreview AND uc.reviewerid = :reviewerid))";
1316         $ordersql = " ORDER BY c.shortname ASC";
1317         $params = array(
1318             'inreview' => user_competency::STATUS_IN_REVIEW,
1319             'reviewerid' => $userid,
1320             'waitingforreview' => user_competency::STATUS_WAITING_FOR_REVIEW,
1321         );
1322         $countsql = $countselect . $sql;
1324         // Primary check to avoid the hard work of getting the users in which the user has permission.
1325         $count = $DB->count_records_sql($countselect . $sql, $params);
1326         if ($count < 1) {
1327             return array('count' => 0, 'competencies' => array());
1328         }
1330         // TODO MDL-52243 Use core function.
1331         list($insql, $inparams) = external::filter_users_with_capability_on_user_context_sql(
1332             $capability, $userid, SQL_PARAMS_NAMED);
1333         $params += $inparams;
1334         $countsql = $countselect . $sql . " AND uc.userid $insql";
1335         $getsql = $select . $sql . " AND uc.userid $insql " . $ordersql;
1337         // Extracting the results.
1338         $competencies = array();
1339         $records = $DB->get_recordset_sql($getsql, $params, $skip, $limit);
1340         foreach ($records as $record) {
1341             $objects = (object) array(
1342                 'usercompetency' => new user_competency(0, user_competency::extract_record($record)),
1343                 'competency' => new competency(0, competency::extract_record($record)),
1344                 'user' => persistent::extract_record($record, 'usr_'),
1345             );
1346             $competencies[] = $objects;
1347         }
1348         $records->close();
1350         return array(
1351             'count' => $DB->count_records_sql($countsql, $params),
1352             'competencies' => $competencies
1353         );
1354     }
1356     /**
1357      * Add a competency to this course module.
1358      *
1359      * @param mixed $cmorid The course module, or id of the course module
1360      * @param int $competencyid The id of the competency
1361      * @return bool
1362      */
1363     public static function add_competency_to_course_module($cmorid, $competencyid) {
1364         static::require_enabled();
1365         $cm = $cmorid;
1366         if (!is_object($cmorid)) {
1367             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1368         }
1370         // Check the user have access to the course module.
1371         self::validate_course_module($cm);
1373         // First we do a permissions check.
1374         $context = context_module::instance($cm->id);
1376         require_capability('tool/lp:coursecompetencymanage', $context);
1378         // Check that the competency belongs to the course.
1379         $exists = course_competency::get_records(array('courseid' => $cm->course, 'competencyid' => $competencyid));
1380         if (!$exists) {
1381             throw new coding_exception('Cannot add a competency to a module if it does not belong to the course');
1382         }
1384         $record = new stdClass();
1385         $record->cmid = $cm->id;
1386         $record->competencyid = $competencyid;
1388         $coursemodulecompetency = new course_module_competency();
1389         $exists = $coursemodulecompetency->get_records(array('cmid' => $cm->id, 'competencyid' => $competencyid));
1390         if (!$exists) {
1391             $coursemodulecompetency->from_record($record);
1392             if ($coursemodulecompetency->create()) {
1393                 return true;
1394             }
1395         }
1396         return false;
1397     }
1399     /**
1400      * Remove a competency from this course module.
1401      *
1402      * @param mixed $cmorid The course module, or id of the course module
1403      * @param int $competencyid The id of the competency
1404      * @return bool
1405      */
1406     public static function remove_competency_from_course_module($cmorid, $competencyid) {
1407         static::require_enabled();
1408         $cm = $cmorid;
1409         if (!is_object($cmorid)) {
1410             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1411         }
1412         // Check the user have access to the course module.
1413         self::validate_course_module($cm);
1415         // First we do a permissions check.
1416         $context = context_module::instance($cm->id);
1418         require_capability('tool/lp:coursecompetencymanage', $context);
1420         $record = new stdClass();
1421         $record->cmid = $cm->id;
1422         $record->competencyid = $competencyid;
1424         $competency = new competency($competencyid);
1425         $exists = course_module_competency::get_record(array('cmid' => $cm->id, 'competencyid' => $competencyid));
1426         if ($exists) {
1427             return $exists->delete();
1428         }
1429         return false;
1430     }
1432     /**
1433      * Move the course module competency up or down in the display list.
1434      *
1435      * Requires tool/lp:coursecompetencymanage capability at the course module context.
1436      *
1437      * @param mixed $cmorid The course module, or id of the course module
1438      * @param int $competencyidfrom The id of the competency we are moving.
1439      * @param int $competencyidto The id of the competency we are moving to.
1440      * @return boolean
1441      */
1442     public static function reorder_course_module_competency($cmorid, $competencyidfrom, $competencyidto) {
1443         static::require_enabled();
1444         $cm = $cmorid;
1445         if (!is_object($cmorid)) {
1446             $cm = get_coursemodule_from_id('', $cmorid, 0, true, MUST_EXIST);
1447         }
1448         // Check the user have access to the course module.
1449         self::validate_course_module($cm);
1451         // First we do a permissions check.
1452         $context = context_module::instance($cm->id);
1454         require_capability('tool/lp:coursecompetencymanage', $context);
1456         $down = true;
1457         $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidfrom));
1458         if (count($matches) == 0) {
1459              throw new coding_exception('The link does not exist');
1460         }
1462         $competencyfrom = array_pop($matches);
1463         $matches = course_module_competency::get_records(array('cmid' => $cm->id, 'competencyid' => $competencyidto));
1464         if (count($matches) == 0) {
1465              throw new coding_exception('The link does not exist');
1466         }
1468         $competencyto = array_pop($matches);
1470         $all = course_module_competency::get_records(array('cmid' => $cm->id), 'sortorder', 'ASC', 0, 0);
1472         if ($competencyfrom->get_sortorder() > $competencyto->get_sortorder()) {
1473             // We are moving up, so put it before the "to" item.
1474             $down = false;
1475         }
1477         foreach ($all as $id => $coursemodulecompetency) {
1478             $sort = $coursemodulecompetency->get_sortorder();
1479             if ($down && $sort > $competencyfrom->get_sortorder() && $sort <= $competencyto->get_sortorder()) {
1480                 $coursemodulecompetency->set_sortorder($coursemodulecompetency->get_sortorder() - 1);
1481                 $coursemodulecompetency->update();
1482             } else if (!$down && $sort >= $competencyto->get_sortorder() && $sort < $competencyfrom->get_sortorder()) {
1483                 $coursemodulecompetency->set_sortorder($coursemodulecompetency->get_sortorder() + 1);
1484                 $coursemodulecompetency->update();
1485             }
1486         }
1487         $competencyfrom->set_sortorder($competencyto->get_sortorder());
1488         return $competencyfrom->update();
1489     }
1491     /**
1492      * Update ruleoutcome value for a course module competency.
1493      *
1494      * @param int|course_module_competency $coursemodulecompetencyorid The course_module_competency, or its ID.
1495      * @param int $ruleoutcome The value of ruleoutcome.
1496      * @return bool True on success.
1497      */
1498     public static function set_course_module_competency_ruleoutcome($coursemodulecompetencyorid, $ruleoutcome) {
1499         static::require_enabled();
1500         $coursemodulecompetency = $coursemodulecompetencyorid;
1501         if (!is_object($coursemodulecompetency)) {
1502             $coursemodulecompetency = new course_module_competency($coursemodulecompetencyorid);
1503         }
1505         $cm = get_coursemodule_from_id('', $coursemodulecompetency->get_cmid(), 0, true, MUST_EXIST);
1507         self::validate_course_module($cm);
1508         $context = context_module::instance($cm->id);
1510         require_capability('tool/lp:coursecompetencymanage', $context);
1512         $coursemodulecompetency->set_ruleoutcome($ruleoutcome);
1513         return $coursemodulecompetency->update();
1514     }
1516     /**
1517      * Add a competency to this course.
1518      *
1519      * @param int $courseid The id of the course
1520      * @param int $competencyid The id of the competency
1521      * @return bool
1522      */
1523     public static function add_competency_to_course($courseid, $competencyid) {
1524         static::require_enabled();
1525         // Check the user have access to the course.
1526         self::validate_course($courseid);
1528         // First we do a permissions check.
1529         $context = context_course::instance($courseid);
1531         require_capability('tool/lp:coursecompetencymanage', $context);
1533         $record = new stdClass();
1534         $record->courseid = $courseid;
1535         $record->competencyid = $competencyid;
1537         $competency = new competency($competencyid);
1539         // Can not add a competency that belong to a hidden framework.
1540         if ($competency->get_framework()->get_visible() == false) {
1541             throw new coding_exception('A competency belonging to hidden framework can not be linked to course');
1542         }
1544         $coursecompetency = new course_competency();
1545         $exists = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyid));
1546         if (!$exists) {
1547             $coursecompetency->from_record($record);
1548             if ($coursecompetency->create()) {
1549                 return true;
1550             }
1551         }
1552         return false;
1553     }
1555     /**
1556      * Remove a competency from this course.
1557      *
1558      * @param int $courseid The id of the course
1559      * @param int $competencyid The id of the competency
1560      * @return bool
1561      */
1562     public static function remove_competency_from_course($courseid, $competencyid) {
1563         static::require_enabled();
1564         // Check the user have access to the course.
1565         self::validate_course($courseid);
1567         // First we do a permissions check.
1568         $context = context_course::instance($courseid);
1570         require_capability('tool/lp:coursecompetencymanage', $context);
1572         $record = new stdClass();
1573         $record->courseid = $courseid;
1574         $record->competencyid = $competencyid;
1576         $competency = new competency($competencyid);
1577         $coursecompetency = new course_competency();
1578         $exists = $coursecompetency->get_record(array('courseid' => $courseid, 'competencyid' => $competencyid));
1579         if ($exists) {
1580             // Delete all course_module_competencies for this competency in this course.
1581             $cmcs = course_module_competency::list_course_module_competencies($competencyid, $courseid);
1582             foreach ($cmcs as $cmc) {
1583                 $cmc->delete();
1584             }
1585             return $exists->delete();
1586         }
1587         return false;
1588     }
1590     /**
1591      * Move the course competency up or down in the display list.
1592      *
1593      * Requires tool/lp:coursecompetencymanage capability at the course context.
1594      *
1595      * @param int $courseid The course
1596      * @param int $competencyidfrom The id of the competency we are moving.
1597      * @param int $competencyidto The id of the competency we are moving to.
1598      * @return boolean
1599      */
1600     public static function reorder_course_competency($courseid, $competencyidfrom, $competencyidto) {
1601         static::require_enabled();
1602         // Check the user have access to the course.
1603         self::validate_course($courseid);
1605         // First we do a permissions check.
1606         $context = context_course::instance($courseid);
1608         require_capability('tool/lp:coursecompetencymanage', $context);
1610         $down = true;
1611         $coursecompetency = new course_competency();
1612         $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidfrom));
1613         if (count($matches) == 0) {
1614              throw new coding_exception('The link does not exist');
1615         }
1617         $competencyfrom = array_pop($matches);
1618         $matches = $coursecompetency->get_records(array('courseid' => $courseid, 'competencyid' => $competencyidto));
1619         if (count($matches) == 0) {
1620              throw new coding_exception('The link does not exist');
1621         }
1623         $competencyto = array_pop($matches);
1625         $all = $coursecompetency->get_records(array('courseid' => $courseid), 'sortorder', 'ASC', 0, 0);
1627         if ($competencyfrom->get_sortorder() > $competencyto->get_sortorder()) {
1628             // We are moving up, so put it before the "to" item.
1629             $down = false;
1630         }
1632         foreach ($all as $id => $coursecompetency) {
1633             $sort = $coursecompetency->get_sortorder();
1634             if ($down && $sort > $competencyfrom->get_sortorder() && $sort <= $competencyto->get_sortorder()) {
1635                 $coursecompetency->set_sortorder($coursecompetency->get_sortorder() - 1);
1636                 $coursecompetency->update();
1637             } else if (!$down && $sort >= $competencyto->get_sortorder() && $sort < $competencyfrom->get_sortorder()) {
1638                 $coursecompetency->set_sortorder($coursecompetency->get_sortorder() + 1);
1639                 $coursecompetency->update();
1640             }
1641         }
1642         $competencyfrom->set_sortorder($competencyto->get_sortorder());
1643         return $competencyfrom->update();
1644     }
1646     /**
1647      * Update ruleoutcome value for a course competency.
1648      *
1649      * @param int|course_competency $coursecompetencyorid The course_competency, or its ID.
1650      * @param int $ruleoutcome The value of ruleoutcome.
1651      * @return bool True on success.
1652      */
1653     public static function set_course_competency_ruleoutcome($coursecompetencyorid, $ruleoutcome) {
1654         static::require_enabled();
1655         $coursecompetency = $coursecompetencyorid;
1656         if (!is_object($coursecompetency)) {
1657             $coursecompetency = new course_competency($coursecompetencyorid);
1658         }
1660         $courseid = $coursecompetency->get_courseid();
1661         self::validate_course($courseid);
1662         $coursecontext = context_course::instance($courseid);
1664         require_capability('tool/lp:coursecompetencymanage', $coursecontext);
1666         $coursecompetency->set_ruleoutcome($ruleoutcome);
1667         return $coursecompetency->update();
1668     }
1670     /**
1671      * Create a learning plan template from a record containing all the data for the class.
1672      *
1673      * Requires tool/lp:templatemanage capability.
1674      *
1675      * @param stdClass $record Record containing all the data for an instance of the class.
1676      * @return template
1677      */
1678     public static function create_template(stdClass $record) {
1679         static::require_enabled();
1680         $template = new template(0, $record);
1682         // First we do a permissions check.
1683         if (!$template->can_manage()) {
1684             throw new required_capability_exception($template->get_context(), 'tool/lp:templatemanage', 'nopermissions', '');
1685         }
1687         // OK - all set.
1688         $template = $template->create();
1690         // Trigger a template created event.
1691         \tool_lp\event\template_created::create_from_template($template)->trigger();
1693         return $template;
1694     }
1696     /**
1697      * Duplicate a learning plan template.
1698      *
1699      * Requires tool/lp:templatemanage capability at the template context.
1700      *
1701      * @param int $id the template id.
1702      * @return template
1703      */
1704     public static function duplicate_template($id) {
1705         static::require_enabled();
1706         $template = new template($id);
1708         // First we do a permissions check.
1709         if (!$template->can_manage()) {
1710             throw new required_capability_exception($template->get_context(), 'tool/lp:templatemanage', 'nopermissions', '');
1711         }
1713         // OK - all set.
1714         $competencies = template_competency::list_competencies($id, false);
1716         // Adding the suffix copy.
1717         $template->set_shortname(get_string('duplicateditemname', 'tool_lp', $template->get_shortname()));
1718         $template->set_id(0);
1720         $duplicatedtemplate = $template->create();
1722         // Associate each competency for the duplicated template.
1723         foreach ($competencies as $competency) {
1724             self::add_competency_to_template($duplicatedtemplate->get_id(), $competency->get_id());
1725         }
1727         // Trigger a template created event.
1728         \tool_lp\event\template_created::create_from_template($duplicatedtemplate)->trigger();
1730         return $duplicatedtemplate;
1731     }
1733     /**
1734      * Delete a learning plan template by id.
1735      * If the learning plan template has associated cohorts they will be deleted.
1736      *
1737      * Requires tool/lp:templatemanage capability.
1738      *
1739      * @param int $id The record to delete.
1740      * @param boolean $deleteplans True to delete plans associaated to template, false to unlink them.
1741      * @return boolean
1742      */
1743     public static function delete_template($id, $deleteplans = true) {
1744         global $DB;
1745         static::require_enabled();
1746         $template = new template($id);
1748         // First we do a permissions check.
1749         if (!$template->can_manage()) {
1750             throw new required_capability_exception($template->get_context(), 'tool/lp:templatemanage', 'nopermissions', '');
1751         }
1753         $transaction = $DB->start_delegated_transaction();
1754         $success = true;
1756         // Check if there are cohorts associated.
1757         $templatecohorts = template_cohort::get_relations_by_templateid($template->get_id());
1758         foreach ($templatecohorts as $templatecohort) {
1759             $success = $templatecohort->delete();
1760             if (!$success) {
1761                 break;
1762             }
1763         }
1765         // Still OK, delete or unlink the plans from the template.
1766         if ($success) {
1767             $plans = plan::get_records(array('templateid' => $template->get_id()));
1768             foreach ($plans as $plan) {
1769                 $success = $deleteplans ? self::delete_plan($plan->get_id()) : self::unlink_plan_from_template($plan);
1770                 if (!$success) {
1771                     break;
1772                 }
1773             }
1774         }
1776         // Still OK, delete the template comptencies.
1777         if ($success) {
1778             $success = template_competency::delete_by_templateid($template->get_id());
1779         }
1781         // OK - all set.
1782         if ($success) {
1783             // Create a template deleted event.
1784             $event = \tool_lp\event\template_deleted::create_from_template($template);
1786             $success = $template->delete();
1787         }
1789         if ($success) {
1790             // Trigger a template deleted event.
1791             $event->trigger();
1793             // Commit the transaction.
1794             $transaction->allow_commit();
1795         } else {
1796             $transaction->rollback(new moodle_exception('Error while deleting the template.'));
1797         }
1799         return $success;
1800     }
1802     /**
1803      * Update the details for a learning plan template.
1804      *
1805      * Requires tool/lp:templatemanage capability.
1806      *
1807      * @param stdClass $record The new details for the template. Note - must contain an id that points to the template to update.
1808      * @return boolean
1809      */
1810     public static function update_template($record) {
1811         global $DB;
1812         static::require_enabled();
1813         $template = new template($record->id);
1815         // First we do a permissions check.
1816         if (!$template->can_manage()) {
1817             throw new required_capability_exception($template->get_context(), 'tool/lp:templatemanage', 'nopermissions', '');
1819         } else if (isset($record->contextid) && $record->contextid != $template->get_contextid()) {
1820              // We can never change the context of a template.
1821             throw new coding_exception('Changing the context of an existing tempalte is forbidden.');
1823         }
1825         $updateplans = false;
1826         $before = $template->to_record();
1828         $template->from_record($record);
1829         $after = $template->to_record();
1831         // Should we update the related plans?
1832         if ($before->duedate != $after->duedate ||
1833                 $before->shortname != $after->shortname ||
1834                 $before->description != $after->description ||
1835                 $before->descriptionformat != $after->descriptionformat) {
1836             $updateplans = true;
1837         }
1839         $transaction = $DB->start_delegated_transaction();
1840         $success = $template->update();
1842         if (!$success) {
1843             $transaction->rollback(new moodle_exception('Error while updating the template.'));
1844             return $success;
1845         }
1847         // Trigger a template updated event.
1848         \tool_lp\event\template_updated::create_from_template($template)->trigger();
1850         if ($updateplans) {
1851             plan::update_multiple_from_template($template);
1852         }
1854         $transaction->allow_commit();
1856         return $success;
1857     }
1859     /**
1860      * Read a the details for a single learning plan template and return a record.
1861      *
1862      * Requires tool/lp:templateview capability at the system context.
1863      *
1864      * @param int $id The id of the template to read.
1865      * @return template
1866      */
1867     public static function read_template($id) {
1868         static::require_enabled();
1869         $template = new template($id);
1870         $context = $template->get_context();
1872         // First we do a permissions check.
1873         if (!$template->can_read()) {
1874              throw new required_capability_exception($template->get_context(), 'tool/lp:templateview', 'nopermissions', '');
1875         }
1877         // OK - all set.
1878         return $template;
1879     }
1881     /**
1882      * Perform a search based on the provided filters and return a paginated list of records.
1883      *
1884      * Requires tool/lp:templateview capability at the system context.
1885      *
1886      * @param string $sort The column to sort on
1887      * @param string $order ('ASC' or 'DESC')
1888      * @param int $skip Number of records to skip (pagination)
1889      * @param int $limit Max of records to return (pagination)
1890      * @param context $context The parent context of the frameworks.
1891      * @param string $includes Defines what other contexts to fetch frameworks from.
1892      *                         Accepted values are:
1893      *                          - children: All descendants
1894      *                          - parents: All parents, grand parents, etc...
1895      *                          - self: Context passed only.
1896      * @param bool $onlyvisible If should list only visible templates
1897      * @return array of competency_framework
1898      */
1899     public static function list_templates($sort, $order, $skip, $limit, $context, $includes = 'children', $onlyvisible = false) {
1900         global $DB;
1901         static::require_enabled();
1903         // Get all the relevant contexts.
1904         $contexts = self::get_related_contexts($context, $includes,
1905             array('tool/lp:templateview', 'tool/lp:templatemanage'));
1907         // First we do a permissions check.
1908         if (empty($contexts)) {
1909              throw new required_capability_exception($context, 'tool/lp:templateview', 'nopermissions', '');
1910         }
1912         // Make the order by.
1913         $orderby = '';
1914         if (!empty($sort)) {
1915             $orderby = $sort . ' ' . $order;
1916         }
1918         // OK - all set.
1919         $template = new template();
1920         list($insql, $params) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
1921         $select = "contextid $insql";
1923         if ($onlyvisible) {
1924             $select .= " AND visible = :visible";
1925             $params['visible'] = 1;
1926         }
1927         return $template->get_records_select($select, $params, $orderby, '*', $skip, $limit);
1928     }
1930     /**
1931      * Perform a search based on the provided filters and return how many results there are.
1932      *
1933      * Requires tool/lp:templateview capability at the system context.
1934      *
1935      * @param context $context The parent context of the frameworks.
1936      * @param string $includes Defines what other contexts to fetch frameworks from.
1937      *                         Accepted values are:
1938      *                          - children: All descendants
1939      *                          - parents: All parents, grand parents, etc...
1940      *                          - self: Context passed only.
1941      * @return int
1942      */
1943     public static function count_templates($context, $includes) {
1944         global $DB;
1945         static::require_enabled();
1947         // First we do a permissions check.
1948         $contexts = self::get_related_contexts($context, $includes,
1949             array('tool/lp:templateview', 'tool/lp:templatemanage'));
1951         if (empty($contexts)) {
1952              throw new required_capability_exception($context, 'tool/lp:templateview', 'nopermissions', '');
1953         }
1955         // OK - all set.
1956         $template = new template();
1957         list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contexts), SQL_PARAMS_NAMED);
1958         return $template->count_records_select("contextid $insql", $inparams);
1959     }
1961     /**
1962      * Count all the templates using a competency.
1963      *
1964      * @param int $competencyid The id of the competency to check.
1965      * @return int
1966      */
1967     public static function count_templates_using_competency($competencyid) {
1968         static::require_enabled();
1969         // First we do a permissions check.
1970         $context = context_system::instance();
1971         $onlyvisible = 1;
1973         $capabilities = array('tool/lp:templateview', 'tool/lp:templatemanage');
1974         if (!has_any_capability($capabilities, $context)) {
1975              throw new required_capability_exception($context, 'tool/lp:templateview', 'nopermissions', '');
1976         }
1978         if (has_capability('tool/lp:templatemanage', $context)) {
1979             $onlyvisible = 0;
1980         }
1982         // OK - all set.
1983         return template_competency::count_templates($competencyid, $onlyvisible);
1984     }
1986     /**
1987      * List all the learning plan templatesd using a competency.
1988      *
1989      * @param int $competencyid The id of the competency to check.
1990      * @return array[stdClass] Array of stdClass containing id and shortname.
1991      */
1992     public static function list_templates_using_competency($competencyid) {
1993         static::require_enabled();
1994         // First we do a permissions check.
1995         $context = context_system::instance();
1996         $onlyvisible = 1;
1998         $capabilities = array('tool/lp:templateview', 'tool/lp:templatemanage');
1999         if (!has_any_capability($capabilities, $context)) {
2000              throw new required_capability_exception($context, 'tool/lp:templateview', 'nopermissions', '');
2001         }
2003         if (has_capability('tool/lp:templatemanage', $context)) {
2004             $onlyvisible = 0;
2005         }
2007         // OK - all set.
2008         return template_competency::list_templates($competencyid, $onlyvisible);
2010     }
2012     /**
2013      * Count all the competencies in a learning plan template.
2014      *
2015      * @param  template|int $templateorid The template or its ID.
2016      * @return int
2017      */
2018     public static function count_competencies_in_template($templateorid) {
2019         static::require_enabled();
2020         // First we do a permissions check.
2021         $template = $templateorid;
2022         if (!is_object($template)) {
2023             $template = new template($template);
2024         }
2026         if (!$template->can_read()) {
2027             throw new required_capability_exception($template->get_context(), 'tool/lp:templateview', 'nopermissions', '');
2028         }
2030         // OK - all set.
2031         return template_competency::count_competencies($template->get_id());
2032     }
2034     /**
2035      * Count all the competencies in a learning plan template with no linked courses.
2036      *
2037      * @param  template|int $templateorid The template or its ID.
2038      * @return int
2039      */
2040     public static function count_competencies_in_template_with_no_courses($templateorid) {
2041         // First we do a permissions check.
2042         $template = $templateorid;
2043         if (!is_object($template)) {
2044             $template = new template($template);
2045         }
2047         if (!$template->can_read()) {
2048             throw new required_capability_exception($template->get_context(), 'tool/lp:templateview', 'nopermissions', '');
2049         }
2051         // OK - all set.
2052         return template_competency::count_competencies_with_no_courses($template->get_id());
2053     }
2055     /**
2056      * List all the competencies in a template.
2057      *
2058      * @param  template|int $templateorid The template or its ID.
2059      * @return array of competencies
2060      */
2061     public static function list_competencies_in_template($templateorid) {
2062         static::require_enabled();
2063         // First we do a permissions check.
2064         $template = $templateorid;
2065         if (!is_object($template)) {
2066             $template = new template($template);
2067         }
2069         if (!$template->can_read()) {
2070             throw new required_capability_exception($template->get_context(), 'tool/lp:templateview', 'nopermissions', '');
2071         }
2073         // OK - all set.
2074         return template_competency::list_competencies($template->get_id());
2075     }
2077     /**
2078      * Add a competency to this template.
2079      *
2080      * @param int $templateid The id of the template
2081      * @param int $competencyid The id of the competency
2082      * @return bool
2083      */
2084     public static function add_competency_to_template($templateid, $competencyid) {
2085         static::require_enabled();
2086         // First we do a permissions check.
2087         $template = new template($templateid);
2088         if (!$template->can_manage()) {
2089             throw new required_capability_exception($template->get_context(), 'tool/lp:templatemanage', 'nopermissions', '');
2090         }
2092         $record = new stdClass();
2093         $record->templateid = $templateid;
2094         $record->competencyid = $competencyid;
2096         $competency = new competency($competencyid);
2098         // Can not add a competency that belong to a hidden framework.
2099         if ($competency->get_framework()->get_visible() == false) {
2100             throw new coding_exception('A competency belonging to hidden framework can not be added');
2101         }
2103         $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
2104         if (!$exists) {
2105             $templatecompetency = new template_competency(0, $record);
2106             $templatecompetency->create();
2107             return true;
2108         }
2109         return false;
2110     }
2112     /**
2113      * Remove a competency from this template.
2114      *
2115      * @param int $templateid The id of the template
2116      * @param int $competencyid The id of the competency
2117      * @return bool
2118      */
2119     public static function remove_competency_from_template($templateid, $competencyid) {
2120         static::require_enabled();
2121         // First we do a permissions check.
2122         $template = new template($templateid);
2123         if (!$template->can_manage()) {
2124             throw new required_capability_exception($template->get_context(), 'tool/lp:templatemanage', 'nopermissions', '');
2125         }
2127         $record = new stdClass();
2128         $record->templateid = $templateid;
2129         $record->competencyid = $competencyid;
2131         $competency = new competency($competencyid);
2133         $exists = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyid));
2134         if ($exists) {
2135             $link = array_pop($exists);
2136             return $link->delete();
2137         }
2138         return false;
2139     }
2141     /**
2142      * Move the template competency up or down in the display list.
2143      *
2144      * Requires tool/lp:templatemanage capability at the system context.
2145      *
2146      * @param int $templateid The template id
2147      * @param int $competencyidfrom The id of the competency we are moving.
2148      * @param int $competencyidto The id of the competency we are moving to.
2149      * @return boolean
2150      */
2151     public static function reorder_template_competency($templateid, $competencyidfrom, $competencyidto) {
2152         static::require_enabled();
2153         // First we do a permissions check.
2154         $context = context_system::instance();
2156         require_capability('tool/lp:templatemanage', $context);
2158         $down = true;
2159         $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidfrom));
2160         if (count($matches) == 0) {
2161             throw new coding_exception('The link does not exist');
2162         }
2164         $competencyfrom = array_pop($matches);
2165         $matches = template_competency::get_records(array('templateid' => $templateid, 'competencyid' => $competencyidto));
2166         if (count($matches) == 0) {
2167             throw new coding_exception('The link does not exist');
2168         }
2170         $competencyto = array_pop($matches);
2172         $all = template_competency::get_records(array('templateid' => $templateid), 'sortorder', 'ASC', 0, 0);
2174         if ($competencyfrom->get_sortorder() > $competencyto->get_sortorder()) {
2175             // We are moving up, so put it before the "to" item.
2176             $down = false;
2177         }
2179         foreach ($all as $id => $templatecompetency) {
2180             $sort = $templatecompetency->get_sortorder();
2181             if ($down && $sort > $competencyfrom->get_sortorder() && $sort <= $competencyto->get_sortorder()) {
2182                 $templatecompetency->set_sortorder($templatecompetency->get_sortorder() - 1);
2183                 $templatecompetency->update();
2184             } else if (!$down && $sort >= $competencyto->get_sortorder() && $sort < $competencyfrom->get_sortorder()) {
2185                 $templatecompetency->set_sortorder($templatecompetency->get_sortorder() + 1);
2186                 $templatecompetency->update();
2187             }
2188         }
2189         $competencyfrom->set_sortorder($competencyto->get_sortorder());
2190         return $competencyfrom->update();
2191     }
2193     /**
2194      * Create a relation between a template and a cohort.
2195      *
2196      * This silently ignores when the relation already existed.
2197      *
2198      * @param  template|int $templateorid The template or its ID.
2199      * @param  stdClass|int $cohortorid   The cohort ot its ID.
2200      * @return template_cohort
2201      */
2202     public static function create_template_cohort($templateorid, $cohortorid) {
2203         global $DB;
2204         static::require_enabled();
2206         $template = $templateorid;
2207         if (!is_object($template)) {
2208             $template = new template($template);
2209         }
2210         require_capability('tool/lp:templatemanage', $template->get_context());
2212         $cohort = $cohortorid;
2213         if (!is_object($cohort)) {
2214             $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
2215         }
2217         // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
2218         $cohortcontext = context::instance_by_id($cohort->contextid);
2219         if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
2220             throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
2221         }
2223         $tplcohort = template_cohort::get_relation($template->get_id(), $cohort->id);
2224         if (!$tplcohort->get_id()) {
2225             $tplcohort->create();
2226         }
2228         return $tplcohort;
2229     }
2231     /**
2232      * Remove a relation between a template and a cohort.
2233      *
2234      * @param  template|int $templateorid The template or its ID.
2235      * @param  stdClass|int $cohortorid   The cohort ot its ID.
2236      * @return boolean True on success or when the relation did not exist.
2237      */
2238     public static function delete_template_cohort($templateorid, $cohortorid) {
2239         global $DB;
2240         static::require_enabled();
2242         $template = $templateorid;
2243         if (!is_object($template)) {
2244             $template = new template($template);
2245         }
2246         require_capability('tool/lp:templatemanage', $template->get_context());
2248         $cohort = $cohortorid;
2249         if (!is_object($cohort)) {
2250             $cohort = $DB->get_record('cohort', array('id' => $cohort), '*', MUST_EXIST);
2251         }
2253         $tplcohort = template_cohort::get_relation($template->get_id(), $cohort->id);
2254         if (!$tplcohort->get_id()) {
2255             return true;
2256         }
2258         return $tplcohort->delete();
2259     }
2261     /**
2262      * Lists user plans.
2263      *
2264      * @param int $userid
2265      * @return \tool_lp\plan[]
2266      */
2267     public static function list_user_plans($userid) {
2268         global $DB, $USER;
2269         static::require_enabled();
2270         $select = 'userid = :userid';
2271         $params = array('userid' => $userid);
2272         $context = context_user::instance($userid);
2274         // Check that we can read something here.
2275         if (!plan::can_read_user($userid) && !plan::can_read_user_draft($userid)) {
2276             throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
2277         }
2279         // The user cannot view the drafts.
2280         if (!plan::can_read_user_draft($userid)) {
2281             list($insql, $inparams) = $DB->get_in_or_equal(plan::get_draft_statuses(), SQL_PARAMS_NAMED, 'param', false);
2282             $select .= " AND status $insql";
2283             $params += $inparams;
2284         }
2285         // The user cannot view the non-drafts.
2286         if (!plan::can_read_user($userid)) {
2287             list($insql, $inparams) = $DB->get_in_or_equal(array(plan::STATUS_ACTIVE, plan::STATUS_COMPLETE),
2288                 SQL_PARAMS_NAMED, 'param', false);
2289             $select .= " AND status $insql";
2290             $params += $inparams;
2291         }
2293         return plan::get_records_select($select, $params, 'name ASC');
2294     }
2296     /**
2297      * List the plans to review.
2298      *
2299      * The method returns values in this format:
2300      *
2301      * array(
2302      *     'plans' => array(
2303      *         (stdClass)(
2304      *             'plan' => (plan),
2305      *             'template' => (template),
2306      *             'owner' => (stdClass)
2307      *         )
2308      *     ),
2309      *     'count' => (int)
2310      * )
2311      *
2312      * @param int $skip The number of records to skip.
2313      * @param int $limit The number of results to return.
2314      * @param int $userid The user we're getting the plans to review for.
2315      * @return array Containing the keys 'count', and 'plans'. The 'plans' key contains an object
2316      *               which contains 'plan', 'template' and 'owner'.
2317      */
2318     public static function list_plans_to_review($skip = 0, $limit = 100, $userid = null) {
2319         global $DB, $USER;
2320         static::require_enabled();
2322         if ($userid === null) {
2323             $userid = $USER->id;
2324         }
2326         $planfields = plan::get_sql_fields('p');
2327         $tplfields = template::get_sql_fields('t');
2328         $usercols = array('id') + get_user_fieldnames();
2329         $userfields = array();
2330         foreach ($usercols as $field) {
2331             $userfields[] = "u." . $field . " AS usr_" . $field;
2332         }
2333         $userfields = implode(',', $userfields);
2335         $select = "SELECT $planfields, $tplfields, $userfields";
2336         $countselect = "SELECT COUNT('x')";
2338         $sql = "  FROM {" . plan::TABLE . "} p
2339                   JOIN {user} u
2340                     ON u.id = p.userid
2341              LEFT JOIN {" . template::TABLE . "} t
2342                     ON t.id = p.templateid
2343                  WHERE (p.status = :waitingforreview
2344                     OR (p.status = :inreview AND p.reviewerid = :reviewerid))
2345                    AND p.userid != :userid";
2347         $params = array(
2348             'waitingforreview' => plan::STATUS_WAITING_FOR_REVIEW,
2349             'inreview' => plan::STATUS_IN_REVIEW,
2350             'reviewerid' => $userid,
2351             'userid' => $userid
2352         );
2354         // Primary check to avoid the hard work of getting the users in which the user has permission.
2355         $count = $DB->count_records_sql($countselect . $sql, $params);
2356         if ($count < 1) {
2357             return array('count' => 0, 'plans' => array());
2358         }
2360         // TODO MDL-52243 Use core function.
2361         list($insql, $inparams) = external::filter_users_with_capability_on_user_context_sql('tool/lp:planreview',
2362             $userid, SQL_PARAMS_NAMED);
2363         $sql .= " AND p.userid $insql";
2364         $params += $inparams;
2366         $plans = array();
2367         $records = $DB->get_recordset_sql($select . $sql, $params, $skip, $limit);
2368         foreach ($records as $record) {
2369             $plan = new plan(0, plan::extract_record($record));
2370             $template = null;
2372             if ($plan->is_based_on_template()) {
2373                 $template = new template(0, template::extract_record($record));
2374             }
2376             $plans[] = (object) array(
2377                 'plan' => $plan,
2378                 'template' => $template,
2379                 'owner' => persistent::extract_record($record, 'usr_'),
2380             );
2381         }
2382         $records->close();
2384         return array(
2385             'count' => $DB->count_records_sql($countselect . $sql, $params),
2386             'plans' => $plans
2387         );
2388     }
2390     /**
2391      * Creates a learning plan based on the provided data.
2392      *
2393      * @param stdClass $record
2394      * @return \tool_lp\plan
2395      */
2396     public static function create_plan(stdClass $record) {
2397         global $USER;
2398         static::require_enabled();
2399         $plan = new plan(0, $record);
2401         if ($plan->is_based_on_template()) {
2402             throw new coding_exception('To create a plan from a template use api::create_plan_from_template().');
2403         } else if ($plan->get_status() == plan::STATUS_COMPLETE) {
2404             throw new coding_exception('A plan cannot be created as complete.');
2405         }
2407         if (!$plan->can_manage()) {
2408             $context = context_user::instance($plan->get_userid());
2409             throw new required_capability_exception($context, 'tool/lp:planmanage', 'nopermissions', '');
2410         }
2412         $plan->create();
2414         // Trigger created event.
2415         \tool_lp\event\plan_created::create_from_plan($plan)->trigger();
2416         return $plan;
2417     }
2419     /**
2420      * Create a learning plan from a template.
2421      *
2422      * @param  mixed $templateorid The template object or ID.
2423      * @param  int $userid
2424      * @return false|\tool_lp\plan Returns false when the plan already exists.
2425      */
2426     public static function create_plan_from_template($templateorid, $userid) {
2427         static::require_enabled();
2428         $template = $templateorid;
2429         if (!is_object($template)) {
2430             $template = new template($template);
2431         }
2433         // The user must be able to view the template to use it as a base for a plan.
2434         if (!$template->can_read()) {
2435             throw new required_capability_exception($template->get_context(), 'tool/lp:templateview', 'nopermissions', '');
2436         }
2437         // Can not create plan from a hidden template.
2438         if ($template->get_visible() == false) {
2439             throw new coding_exception('A plan can not be created from a hidden template');
2440         }
2442         // Convert the template to a plan.
2443         $record = $template->to_record();
2444         $record->templateid = $record->id;
2445         $record->userid = $userid;
2446         $record->name = $record->shortname;
2447         $record->status = plan::STATUS_ACTIVE;
2449         unset($record->id);
2450         unset($record->timecreated);
2451         unset($record->timemodified);
2452         unset($record->usermodified);
2454         // Remove extra keys.
2455         $properties = plan::properties_definition();
2456         foreach ($record as $key => $value) {
2457             if (!array_key_exists($key, $properties)) {
2458                 unset($record->$key);
2459             }
2460         }
2462         $plan = new plan(0, $record);
2463         if (!$plan->can_manage()) {
2464             throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', '');
2465         }
2467         // We first apply the permission checks as we wouldn't want to leak information by returning early that
2468         // the plan already exists.
2469         if (plan::record_exists_select('templateid = :templateid AND userid = :userid', array(
2470                 'templateid' => $template->get_id(), 'userid' => $userid))) {
2471             return false;
2472         }
2474         $plan->create();
2476         // Trigger created event.
2477         \tool_lp\event\plan_created::create_from_plan($plan)->trigger();
2478         return $plan;
2479     }
2481     /**
2482      * Create learning plans from a template and cohort.
2483      *
2484      * @param  mixed $templateorid The template object or ID.
2485      * @param  int $cohortid The cohort ID.
2486      * @param  bool $recreateunlinked When true the plans that were unlinked from this template will be re-created.
2487      * @return int The number of plans created.
2488      */
2489     public static function create_plans_from_template_cohort($templateorid, $cohortid, $recreateunlinked = false) {
2490         global $DB, $CFG;
2491         static::require_enabled();
2492         require_once($CFG->dirroot . '/cohort/lib.php');
2494         $template = $templateorid;
2495         if (!is_object($template)) {
2496             $template = new template($template);
2497         }
2499         // The user must be able to view the template to use it as a base for a plan.
2500         if (!$template->can_read()) {
2501             throw new required_capability_exception($template->get_context(), 'tool/lp:templateview', 'nopermissions', '');
2502         }
2504         // Can not create plan from a hidden template.
2505         if ($template->get_visible() == false) {
2506             throw new coding_exception('A plan can not be created from a hidden template');
2507         }
2509         // Replicate logic in cohort_can_view_cohort() because we can't use it directly as we don't have a course context.
2510         $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
2511         $cohortcontext = context::instance_by_id($cohort->contextid);
2512         if (!$cohort->visible && !has_capability('moodle/cohort:view', $cohortcontext)) {
2513             throw new required_capability_exception($cohortcontext, 'moodle/cohort:view', 'nopermissions', '');
2514         }
2516         // Convert the template to a plan.
2517         $recordbase = $template->to_record();
2518         $recordbase->templateid = $recordbase->id;
2519         $recordbase->name = $recordbase->shortname;
2520         $recordbase->status = plan::STATUS_ACTIVE;
2522         unset($recordbase->id);
2523         unset($recordbase->timecreated);
2524         unset($recordbase->timemodified);
2525         unset($recordbase->usermodified);
2527         // Remove extra keys.
2528         $properties = plan::properties_definition();
2529         foreach ($recordbase as $key => $value) {
2530             if (!array_key_exists($key, $properties)) {
2531                 unset($recordbase->$key);
2532             }
2533         }
2535         // Create the plans.
2536         $created = 0;
2537         $userids = template_cohort::get_missing_plans($template->get_id(), $cohortid, $recreateunlinked);
2538         foreach ($userids as $userid) {
2539             $record = (object) (array) $recordbase;
2540             $record->userid = $userid;
2542             $plan = new plan(0, $record);
2543             if (!$plan->can_manage()) {
2544                 // Silently skip members where permissions are lacking.
2545                 continue;
2546             }
2548             $plan->create();
2549             // Trigger created event.
2550             \tool_lp\event\plan_created::create_from_plan($plan)->trigger();
2551             $created++;
2552         }
2554         return $created;
2555     }
2557     /**
2558      * Unlink a plan from its template.
2559      *
2560      * @param  \tool_lp\plan|int $planorid The plan or its ID.
2561      * @return bool
2562      */
2563     public static function unlink_plan_from_template($planorid) {
2564         global $DB;
2565         static::require_enabled();
2567         $plan = $planorid;
2568         if (!is_object($planorid)) {
2569             $plan = new plan($planorid);
2570         }
2572         // The user must be allowed to manage the plans of the user, nothing about the template.
2573         if (!$plan->can_manage()) {
2574             throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', '');
2575         }
2577         // Only plan with status DRAFT or ACTIVE can be unliked..
2578         if ($plan->get_status() == plan::STATUS_COMPLETE) {
2579             throw new coding_exception('Only draft or active plan can be unliked from a template');
2580         }
2582         // Early exit, it's already done...
2583         if (!$plan->is_based_on_template()) {
2584             return true;
2585         }
2587         // Fetch the template.
2588         $template = new template($plan->get_templateid());
2590         // Now, proceed by copying all competencies to the plan, then update the plan.
2591         $transaction = $DB->start_delegated_transaction();
2592         $competencies = template_competency::list_competencies($template->get_id(), false);
2593         $i = 0;
2594         foreach ($competencies as $competency) {
2595             $record = (object) array(
2596                 'planid' => $plan->get_id(),
2597                 'competencyid' => $competency->get_id(),
2598                 'sortorder' => $i++
2599             );
2600             $pc = new plan_competency(null, $record);
2601             $pc->create();
2602         }
2603         $plan->set_origtemplateid($template->get_id());
2604         $plan->set_templateid(null);
2605         $success = $plan->update();
2606         $transaction->allow_commit();
2608         // Trigger unlinked event.
2609         \tool_lp\event\plan_unlinked::create_from_plan($plan)->trigger();
2611         return $success;
2612     }
2614     /**
2615      * Updates a plan.
2616      *
2617      * @param stdClass $record
2618      * @return \tool_lp\plan
2619      */
2620     public static function update_plan(stdClass $record) {
2621         static::require_enabled();
2623         $plan = new plan($record->id);
2625         // Validate that the plan as it is can be managed.
2626         if (!$plan->can_manage()) {
2627             throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', '');
2629         } else if ($plan->get_status() == plan::STATUS_COMPLETE) {
2630             // A completed plan cannot be edited.
2631             throw new coding_exception('Completed plan cannot be edited.');
2633         } else if ($plan->is_based_on_template()) {
2634             // Prevent a plan based on a template to be edited.
2635             throw new coding_exception('Cannot update a plan that is based on a template.');
2637         } else if (isset($record->templateid) && $plan->get_templateid() != $record->templateid) {
2638             // Prevent a plan to be based on a template.
2639             throw new coding_exception('Cannot base a plan on a template.');
2641         } else if (isset($record->userid) && $plan->get_userid() != $record->userid) {
2642             // Prevent change of ownership as the capabilities are checked against that.
2643             throw new coding_exception('A plan cannot be transfered to another user');
2645         } else if (isset($record->status) && $plan->get_status() != $record->status) {
2646             // Prevent change of status.
2647             throw new coding_exception('To change the status of a plan use the appropriate methods.');
2649         }
2651         $plan->from_record($record);
2652         $plan->update();
2654         // Trigger updated event.
2655         \tool_lp\event\plan_updated::create_from_plan($plan)->trigger();
2657         return $plan;
2658     }
2660     /**
2661      * Returns a plan data.
2662      *
2663      * @param int $id
2664      * @return \tool_lp\plan
2665      */
2666     public static function read_plan($id) {
2667         static::require_enabled();
2668         $plan = new plan($id);
2670         if (!$plan->can_read()) {
2671             $context = context_user::instance($plan->get_userid());
2672             throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
2673         }
2675         return $plan;
2676     }
2678     /**
2679      * Plan event viewed.
2680      *
2681      * @param mixed $planorid The id or the plan.
2682      * @return boolean
2683      */
2684     public static function plan_viewed($planorid) {
2685         static::require_enabled();
2686         $plan = $planorid;
2687         if (!is_object($plan)) {
2688             $plan = new plan($plan);
2689         }
2691         // First we do a permissions check.
2692         if (!$plan->can_read()) {
2693             $context = context_user::instance($plan->get_userid());
2694             throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
2695         }
2697         // Trigger a template viewed event.
2698         \tool_lp\event\plan_viewed::create_from_plan($plan)->trigger();
2700         return true;
2701     }
2703     /**
2704      * Deletes a plan.
2705      *
2706      * Plans based on a template can be removed just like any other one.
2707      *
2708      * @param int $id
2709      * @return bool Success?
2710      */
2711     public static function delete_plan($id) {
2712         global $DB;
2713         static::require_enabled();
2715         $plan = new plan($id);
2717         if (!$plan->can_manage()) {
2718             $context = context_user::instance($plan->get_userid());
2719             throw new required_capability_exception($context, 'tool/lp:planmanage', 'nopermissions', '');
2720         }
2722         // Wrap the suppression in a DB transaction.
2723         $transaction = $DB->start_delegated_transaction();
2725         // Delete plan competencies.
2726         $plancomps = plan_competency::get_records(array('planid' => $plan->get_id()));
2727         foreach ($plancomps as $plancomp) {
2728             $plancomp->delete();
2729         }
2731         // Delete archive user competencies if the status of the plan is complete.
2732         if ($plan->get_status() == plan::STATUS_COMPLETE) {
2733             self::remove_archived_user_competencies_in_plan($plan);
2734         }
2735         $event = \tool_lp\event\plan_deleted::create_from_plan($plan);
2736         $success = $plan->delete();
2738         $transaction->allow_commit();
2740         // Trigger deleted event.
2741         $event->trigger();
2743         return $success;
2744     }
2746     /**
2747      * Cancel the review of a plan.
2748      *
2749      * @param int|plan $planorid The plan, or its ID.
2750      * @return bool
2751      */
2752     public static function plan_cancel_review_request($planorid) {
2753         static::require_enabled();
2754         $plan = $planorid;
2755         if (!is_object($plan)) {
2756             $plan = new plan($plan);
2757         }
2759         // We need to be able to view the plan at least.
2760         if (!$plan->can_read()) {
2761             throw new required_capability_exception($plan->get_context(), 'tool/lp:planview', 'nopermissions', '');
2762         }
2764         if ($plan->is_based_on_template()) {
2765             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2766         } else if ($plan->get_status() != plan::STATUS_WAITING_FOR_REVIEW) {
2767             throw new coding_exception('The plan review cannot be cancelled at this stage.');
2768         } else if (!$plan->can_request_review()) {
2769             throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', '');
2770         }
2772         $plan->set_status(plan::STATUS_DRAFT);
2773         $result = $plan->update();
2775         // Trigger review request cancelled event.
2776         \tool_lp\event\plan_review_request_cancelled::create_from_plan($plan)->trigger();
2778         return $result;
2779     }
2781     /**
2782      * Request the review of a plan.
2783      *
2784      * @param int|plan $planorid The plan, or its ID.
2785      * @return bool
2786      */
2787     public static function plan_request_review($planorid) {
2788         static::require_enabled();
2789         $plan = $planorid;
2790         if (!is_object($plan)) {
2791             $plan = new plan($plan);
2792         }
2794         // We need to be able to view the plan at least.
2795         if (!$plan->can_read()) {
2796             throw new required_capability_exception($plan->get_context(), 'tool/lp:planview', 'nopermissions', '');
2797         }
2799         if ($plan->is_based_on_template()) {
2800             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2801         } else if ($plan->get_status() != plan::STATUS_DRAFT) {
2802             throw new coding_exception('The plan cannot be sent for review at this stage.');
2803         } else if (!$plan->can_request_review()) {
2804             throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', '');
2805         }
2807         $plan->set_status(plan::STATUS_WAITING_FOR_REVIEW);
2808         $result = $plan->update();
2810         // Trigger review requested event.
2811         \tool_lp\event\plan_review_requested::create_from_plan($plan)->trigger();
2813         return $result;
2814     }
2816     /**
2817      * Start the review of a plan.
2818      *
2819      * @param int|plan $planorid The plan, or its ID.
2820      * @return bool
2821      */
2822     public static function plan_start_review($planorid) {
2823         global $USER;
2824         static::require_enabled();
2825         $plan = $planorid;
2826         if (!is_object($plan)) {
2827             $plan = new plan($plan);
2828         }
2830         // We need to be able to view the plan at least.
2831         if (!$plan->can_read()) {
2832             throw new required_capability_exception($plan->get_context(), 'tool/lp:planview', 'nopermissions', '');
2833         }
2835         if ($plan->is_based_on_template()) {
2836             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2837         } else if ($plan->get_status() != plan::STATUS_WAITING_FOR_REVIEW) {
2838             throw new coding_exception('The plan review cannot be started at this stage.');
2839         } else if (!$plan->can_review()) {
2840             throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', '');
2841         }
2843         $plan->set_status(plan::STATUS_IN_REVIEW);
2844         $plan->set_reviewerid($USER->id);
2845         $result = $plan->update();
2847         // Trigger review started event.
2848         \tool_lp\event\plan_review_started::create_from_plan($plan)->trigger();
2850         return $result;
2851     }
2853     /**
2854      * Stop reviewing a plan.
2855      *
2856      * @param  int|plan $planorid The plan, or its ID.
2857      * @return bool
2858      */
2859     public static function plan_stop_review($planorid) {
2860         static::require_enabled();
2861         $plan = $planorid;
2862         if (!is_object($plan)) {
2863             $plan = new plan($plan);
2864         }
2866         // We need to be able to view the plan at least.
2867         if (!$plan->can_read()) {
2868             throw new required_capability_exception($plan->get_context(), 'tool/lp:planview', 'nopermissions', '');
2869         }
2871         if ($plan->is_based_on_template()) {
2872             throw new coding_exception('Template plans cannot be reviewed.');   // This should never happen.
2873         } else if ($plan->get_status() != plan::STATUS_IN_REVIEW) {
2874             throw new coding_exception('The plan review cannot be stopped at this stage.');
2875         } else if (!$plan->can_review()) {
2876             throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', '');
2877         }
2879         $plan->set_status(plan::STATUS_DRAFT);
2880         $plan->set_reviewerid(null);
2881         $result = $plan->update();
2883         // Trigger review stopped event.
2884         \tool_lp\event\plan_review_stopped::create_from_plan($plan)->trigger();
2886         return $result;
2887     }
2889     /**
2890      * Approve a plan.
2891      *
2892      * This means making the plan active.
2893      *
2894      * @param  int|plan $planorid The plan, or its ID.
2895      * @return bool
2896      */
2897     public static function approve_plan($planorid) {
2898         static::require_enabled();
2899         $plan = $planorid;
2900         if (!is_object($plan)) {
2901             $plan = new plan($plan);
2902         }
2904         // We need to be able to view the plan at least.
2905         if (!$plan->can_read()) {
2906             throw new required_capability_exception($plan->get_context(), 'tool/lp:planview', 'nopermissions', '');
2907         }
2909         // We can approve a plan that is either a draft, in review, or waiting for review.
2910         if ($plan->is_based_on_template()) {
2911             throw new coding_exception('Template plans are already approved.');   // This should never happen.
2912         } else if (!$plan->is_draft()) {
2913             throw new coding_exception('The plan cannot be approved at this stage.');
2914         } else if (!$plan->can_review()) {
2915             throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', '');
2916         }
2918         $plan->set_status(plan::STATUS_ACTIVE);
2919         $plan->set_reviewerid(null);
2920         $result = $plan->update();
2922         // Trigger approved event.
2923         \tool_lp\event\plan_approved::create_from_plan($plan)->trigger();
2925         return $result;
2926     }
2928     /**
2929      * Unapprove a plan.
2930      *
2931      * This means making the plan draft.
2932      *
2933      * @param  int|plan $planorid The plan, or its ID.
2934      * @return bool
2935      */
2936     public static function unapprove_plan($planorid) {
2937         static::require_enabled();
2938         $plan = $planorid;
2939         if (!is_object($plan)) {
2940             $plan = new plan($plan);
2941         }
2943         // We need to be able to view the plan at least.
2944         if (!$plan->can_read()) {
2945             throw new required_capability_exception($plan->get_context(), 'tool/lp:planview', 'nopermissions', '');
2946         }
2948         if ($plan->is_based_on_template()) {
2949             throw new coding_exception('Template plans are always approved.');   // This should never happen.
2950         } else if ($plan->get_status() != plan::STATUS_ACTIVE) {
2951             throw new coding_exception('The plan cannot be sent back to draft at this stage.');
2952         } else if (!$plan->can_review()) {
2953             throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', '');
2954         }
2956         $plan->set_status(plan::STATUS_DRAFT);
2957         $result = $plan->update();
2959         // Trigger unapproved event.
2960         \tool_lp\event\plan_unapproved::create_from_plan($plan)->trigger();
2962         return $result;
2963     }
2965     /**
2966      * Complete a plan.
2967      *
2968      * @param int|plan $planorid The plan, or its ID.
2969      * @return bool
2970      */
2971     public static function complete_plan($planorid) {
2972         global $DB;
2973         static::require_enabled();
2975         $plan = $planorid;
2976         if (!is_object($planorid)) {
2977             $plan = new plan($planorid);
2978         }
2980         // Validate that the plan can be managed.
2981         if (!$plan->can_manage()) {
2982             throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', '');
2983         }
2985         // Check if the plan was already completed.
2986         if ($plan->get_status() == plan::STATUS_COMPLETE) {
2987             throw new coding_exception('The plan is already completed.');
2988         }
2990         $originalstatus = $plan->get_status();
2991         $plan->set_status(plan::STATUS_COMPLETE);
2993         // The user should also be able to manage the plan when it's completed.
2994         if (!$plan->can_manage()) {
2995             throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', '');
2996         }
2998         // Put back original status because archive needs it to extract competencies from the right table.
2999         $plan->set_status($originalstatus);
3001         // Do the things.
3002         $transaction = $DB->start_delegated_transaction();
3003         self::archive_user_competencies_in_plan($plan);
3004         $plan->set_status(plan::STATUS_COMPLETE);
3005         $success = $plan->update();
3007         if (!$success) {
3008             $transaction->rollback(new moodle_exception('The plan could not be updated.'));
3009             return $success;
3010         }
3012         $transaction->allow_commit();
3014         // Trigger updated event.
3015         \tool_lp\event\plan_completed::create_from_plan($plan)->trigger();
3017         return $success;
3018     }
3020     /**
3021      * Reopen a plan.
3022      *
3023      * @param int|plan $planorid The plan, or its ID.
3024      * @return bool
3025      */
3026     public static function reopen_plan($planorid) {
3027         global $DB;
3028         static::require_enabled();
3030         $plan = $planorid;
3031         if (!is_object($planorid)) {
3032             $plan = new plan($planorid);
3033         }
3035         // Validate that the plan as it is can be managed.
3036         if (!$plan->can_manage()) {
3037             $context = context_user::instance($plan->get_userid());
3038             throw new required_capability_exception($context, 'tool/lp:planmanage', 'nopermissions', '');
3039         }
3041         $beforestatus = $plan->get_status();
3042         $plan->set_status(plan::STATUS_ACTIVE);
3044         // Validate if status can be changed.
3045         if (!$plan->can_manage()) {
3046             $context = context_user::instance($plan->get_userid());
3047             throw new required_capability_exception($context, 'tool/lp:planmanage', 'nopermissions', '');
3048         }
3050         // Wrap the updates in a DB transaction.
3051         $transaction = $DB->start_delegated_transaction();
3053         // Delete archived user competencies if the status of the plan is changed from complete to another status.
3054         $mustremovearchivedcompetencies = ($beforestatus == plan::STATUS_COMPLETE && $plan->get_status() != plan::STATUS_COMPLETE);
3055         if ($mustremovearchivedcompetencies) {
3056             self::remove_archived_user_competencies_in_plan($plan);
3057         }
3059         // If duedate less than or equal to duedate_threshold unset it.
3060         if ($plan->get_duedate() <= time() + plan::DUEDATE_THRESHOLD) {
3061             $plan->set_duedate(0);
3062         }
3064         $success = $plan->update();
3066         if (!$success) {
3067             $transaction->rollback(new moodle_exception('The plan could not be updated.'));
3068             return $success;
3069         }
3071         $transaction->allow_commit();
3073         // Trigger reopened event.
3074         \tool_lp\event\plan_reopened::create_from_plan($plan)->trigger();
3076         return $success;
3077     }
3079     /**
3080      * Get a single competency from the user plan.
3081      *
3082      * @param  int $planorid The plan, or its ID.
3083      * @param  int $competencyid The competency id.
3084      * @return (object) array(
3085      *                      'competency' => \tool_lp\competency,
3086      *                      'usercompetency' => \tool_lp\user_competency
3087      *                      'usercompetencyplan' => \tool_lp\user_competency_plan
3088      *                  )
3089      *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
3090      */
3091     public static function get_plan_competency($planorid, $competencyid) {
3092         static::require_enabled();
3093         $plan = $planorid;
3094         if (!is_object($planorid)) {
3095             $plan = new plan($planorid);
3096         }
3098         if (!user_competency::can_read_user($plan->get_userid())) {
3099             throw new required_capability_exception($plan->get_context(), 'tool/lp:usercompetencyview', 'nopermissions', '');
3100         }
3102         $competency = $plan->get_competency($competencyid);
3104         // Get user competencies from user_competency_plan if the plan status is set to complete.
3105         $iscompletedplan = $plan->get_status() == plan::STATUS_COMPLETE;
3106         if ($iscompletedplan) {
3107             $usercompetencies = user_competency_plan::get_multiple($plan->get_userid(), $plan->get_id(), array($competencyid));
3108             $ucresultkey = 'usercompetencyplan';
3109         } else {
3110             $usercompetencies = user_competency::get_multiple($plan->get_userid(), array($competencyid));
3111             $ucresultkey = 'usercompetency';
3112         }
3114         $found = count($usercompetencies);
3116         if ($found) {
3117             $uc = array_pop($usercompetencies);
3118         } else {
3119             if ($iscompletedplan) {
3120                 throw new coding_exception('A user competency plan is missing');
3121             } else {
3122                 $uc = user_competency::create_relation($plan->get_userid(), $competency->get_id());
3123                 $uc->create();
3124             }
3125         }
3127         $plancompetency = (object) array(
3128             'competency' => $competency,
3129             'usercompetency' => null,
3130             'usercompetencyplan' => null
3131         );
3132         $plancompetency->$ucresultkey = $uc;
3134         return $plancompetency;
3135     }
3137     /**
3138      * List the competencies in a user plan.
3139      *
3140      * @param  int $planorid The plan, or its ID.
3141      * @return array((object) array(
3142      *                            'competency' => \tool_lp\competency,
3143      *                            'usercompetency' => \tool_lp\user_competency
3144      *                            'usercompetencyplan' => \tool_lp\user_competency_plan
3145      *                        ))
3146      *         The values of of keys usercompetency and usercompetencyplan cannot be defined at the same time.
3147      */
3148     public static function list_plan_competencies($planorid) {
3149         static::require_enabled();
3150         $plan = $planorid;
3151         if (!is_object($planorid)) {
3152             $plan = new plan($planorid);
3153         }
3155         if (!$plan->can_read()) {
3156             $context = context_user::instance($plan->get_userid());
3157             throw new required_capability_exception($context, 'tool/lp:planview', 'nopermissions', '');
3158         }
3160         $result = array();
3161         $competencies = $plan->get_competencies();
3163         // Get user competencies from user_competency_plan if the plan status is set to complete.
3164         $iscompletedplan = $plan->get_status() == plan::STATUS_COMPLETE;
3165         if ($iscompletedplan) {
3166             $usercompetencies = user_competency_plan::get_multiple($plan->get_userid(), $plan->get_id(), $competencies);
3167             $ucresultkey = 'usercompetencyplan';
3168         } else {
3169             $usercompetencies = user_competency::get_multiple($plan->get_userid(), $competencies);
3170             $ucresultkey = 'usercompetency';
3171         }
3173         // Build the return values.
3174         foreach ($competencies as $key => $competency) {
3175             $found = false;
3177             foreach ($usercompetencies as $uckey => $uc) {
3178                 if ($uc->get_competencyid() == $competency->get_id()) {
3179                     $found = true;
3180                     unset($usercompetencies[$uckey]);
3181                     break;
3182                 }
3183             }
3185             if (!$found) {
3186                 if ($iscompletedplan) {
3187                     throw new coding_exception('A user competency plan is missing');
3188                 } else {
3189                     $uc = user_competency::create_relation($plan->get_userid(), $competency->get_id());
3190                 }
3191             }
3193             $plancompetency = (object) array(
3194                 'competency' => $competency,
3195                 'usercompetency' => null,
3196                 'usercompetencyplan' => null
3197             );
3198             $plancompetency->$ucresultkey = $uc;
3199             $result[] = $plancompetency;
3200         }
3202         return $result;
3203     }
3205     /**
3206      * Add a competency to a plan.
3207      *
3208      * @param int $planid The id of the plan
3209      * @param int $competencyid The id of the competency
3210      * @return bool
3211      */
3212     public static function add_competency_to_plan($planid, $competencyid) {
3213         static::require_enabled();
3214         $plan = new plan($planid);
3216         // First we do a permissions check.
3217         if (!$plan->can_manage()) {
3218             throw new required_capability_exception($plan->get_context(), 'tool/lp:planmanage', 'nopermissions', '');
3220         } else if ($plan->is_based_on_template()) {
3221             throw new coding_exception('A competency can not be added to a learning plan based on a template');
3222         }
3224         if (!$plan->can_be_edited()) {
3225             throw new coding_exception('A competency can not be added to a learning plan completed');
3226         }
3228         $competency = new competency($competencyid);
3230         // Can not add a competency that belong to a hidden framework.
3231         if ($competency->get_framework()->get_visible() == false) {
3232             throw new coding_exception('A competency belonging to hidden framework can not be added');
3233         }
3235         $exists = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
3236         if (!$exists) {
3237             $record = new stdClass();
3238             $record->planid = $planid;
3239             $record->competencyid = $competencyid;
3240             $plancompetency = new plan_competency(0, $record);
3241             $plancompetency->create();
3242         }
3244         return true;
3245     }
3247     /**
3248      * Remove a competency from a plan.
3249      *
3250      * @param int $planid The plan id
3251      * @param int $competencyid The id of the competency
3252      * @return bool
3253      */
3254     public static function remove_competency_from_plan($planid, $competencyid) {
3255         static::require_enabled();
3256         $plan = new plan($planid);
3258         // First we do a permissions check.
3259         if (!$plan->can_manage()) {
3260             $context = context_user::instance($plan->get_userid());
3261             throw new required_capability_exception($context, 'tool/lp:planmanage', 'nopermissions', '');
3263         } else if ($plan->is_based_on_template()) {
3264             throw new coding_exception('A competency can not be removed from a learning plan based on a template');
3265         }
3267         if (!$plan->can_be_edited()) {
3268             throw new coding_exception('A competency can not be removed from a learning plan completed');
3269         }
3271         $link = plan_competency::get_record(array('planid' => $planid, 'competencyid' => $competencyid));
3272         if ($link) {
3273             return $link->delete();
3274         }
3275         return false;
3276     }
3278     /**
3279      * Move the plan competency up or down in the display list.
3280      *
3281      * Requires tool/lp:planmanage capability at the system context.
3282      *
3283      * @param int $planid The plan  id
3284      * @param int $competencyidfrom The id of the competency we are moving.
3285      * @param int $competencyidto The id of the competency we are moving to.
3286      * @return boolean
3287      */
3288     public static function reorder_plan_competency($planid, $competencyidfrom, $competencyidto) {
3289         static::require_enabled();
3290         $plan = new plan($planid);
3292         // First we do a permissions check.
3293         if (!$plan->can_manage()) {
3294             $context = context_user::instance($plan->get_userid());
3295             throw new required_capability_exception($context, 'tool/lp:planmanage', 'nopermissions', '');
3297         } else if ($plan->is_based_on_template()) {
3298             throw new coding_exception('A competency can not be reordered in a learning plan based on a template');
3299         }
3301         if (!$plan->can_be_edited()) {
3302             throw new coding_exception('A competency can not be reordered in a learning plan completed');
3303         }
3305         $down = true;
3306         $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidfrom));
3307         if (count($matches) == 0) {
3308             throw new coding_exception('The link does not exist');
3309         }
3311         $competencyfrom = array_pop($matches);
3312         $matches = plan_competency::get_records(array('planid' => $planid, 'competencyid' => $competencyidto));
3313         if (count($matches) == 0) {
3314             throw new coding_exception('The link does not exist');
3315         }
3317         $competencyto = array_pop($matches);
3319         $all = plan_competency::get_records(array('planid' => $planid), 'sortorder', 'ASC', 0, 0);
3321         if ($competencyfrom->get_sortorder() > $competencyto->get_sortorder()) {
3322             // We are moving up, so put it before the "to" item.
3323             $down = false;
3324         }
3326         foreach ($all as $id => $plancompetency) {
3327             $sort = $plancompetency->get_sortorder();
3328             if ($down && $sort > $competencyfrom->get_sortorder() && $sort <= $competencyto->get_sortorder()) {
3329                 $plancompetency->set_sortorder($plancompetency->get_sortorder() - 1);
3330                 $plancompetency->update();
3331             } else if (!$down && $sort >= $competencyto->get_sortorder() && $sort < $competencyfrom->get_sortorder()) {
3332                 $plancompetency->set_sortorder($plancompetency->get_sortorder() + 1);
3333                 $plancompetency->update();
3334             }
3335         }
3336         $competencyfrom->set_sortorder($competencyto->get_sortorder());
3337         return $competencyfrom->update();
3338     }
3340     /**
3341      * Cancel a user competency review request.
3342      *
3343      * @param  int $userid       The user ID.
3344      * @param  int $competencyid The competency ID.
3345      * @return bool
3346      */
3347     public static function user_competency_cancel_review_request($userid, $competencyid) {
3348         static::require_enabled();
3349         $context = context_user::instance($userid);
3350         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3351         if (!$uc || !$uc->can_read()) {
3352             throw new required_capability_exception($context, 'tool/lp:usercompetencyview', 'nopermissions', '');
3353         } else if ($uc->get_status() != user_competency::STATUS_WAITING_FOR_REVIEW) {
3354             throw new coding_exception('The competency can not be cancel review request at this stage.');
3355         } else if (!$uc->can_request_review()) {
3356             throw new required_capability_exception($context, 'tool/lp:usercompetencyrequestreview', 'nopermissions', '');
3357         }
3359         $uc->set_status(user_competency::STATUS_IDLE);
3360         $result = $uc->update();
3361         if ($result) {
3362             \tool_lp\event\user_competency_review_request_cancelled::create_from_user_competency($uc)->trigger();
3363         }
3364         return $result;
3365     }
3367     /**
3368      * Request a user competency review.
3369      *
3370      * @param  int $userid       The user ID.
3371      * @param  int $competencyid The competency ID.
3372      * @return bool
3373      */
3374     public static function user_competency_request_review($userid, $competencyid) {
3375         static::require_enabled();
3376         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3377         if (!$uc) {
3378             $uc = user_competency::create_relation($userid, $competencyid);
3379             $uc->create();
3380         }
3382         if (!$uc->can_read()) {
3383             throw new required_capability_exception($uc->get_context(), 'tool/lp:usercompetencyview', 'nopermissions', '');
3384         } else if ($uc->get_status() != user_competency::STATUS_IDLE) {
3385             throw new coding_exception('The competency can not be sent for review at this stage.');
3386         } else if (!$uc->can_request_review()) {
3387             throw new required_capability_exception($uc->get_context(), 'tool/lp:usercompetencyrequestreview', 'nopermissions', '');
3388         }
3390         $uc->set_status(user_competency::STATUS_WAITING_FOR_REVIEW);
3391         $result = $uc->update();
3392         if ($result) {
3393             \tool_lp\event\user_competency_review_requested::create_from_user_competency($uc)->trigger();
3394         }
3395         return $result;
3396     }
3398     /**
3399      * Start a user competency review.
3400      *
3401      * @param  int $userid       The user ID.
3402      * @param  int $competencyid The competency ID.
3403      * @return bool
3404      */
3405     public static function user_competency_start_review($userid, $competencyid) {
3406         global $USER;
3407         static::require_enabled();
3409         $context = context_user::instance($userid);
3410         $uc = user_competency::get_record(array('userid' => $userid, 'competencyid' => $competencyid));
3411         if (!$uc || !$uc->can_read()) {
3412             throw new required_capability_exception($context, 'tool/lp:usercompetencyview', 'nopermissions', '');
3413         } else if ($uc->get_status() != user_competency::STATUS_WAITING_FOR_REVIEW) {
3414             throw new coding_exception('The competency review can not be started at this stage.');
3415         } else if (!$uc->can_review()) {
3416             throw new required_capability_exception($context, 'tool/lp:usercompetencyreview', 'nopermissions', '');
3417         }
3419         $uc->set_status(user_competency::STATUS_IN_REVIEW);
3420         $uc->set_reviewerid($USER->id);
3421         $result = $uc->update();