Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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