Proyectos de Subversion Moodle

Rev

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