• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ewallah / moodle-enrol_coursecompleted / 30024759788

23 Jul 2026 04:21PM UTC coverage: 98.933% (-1.1%) from 100.0%
30024759788

Pull #48

github

web-flow
Merge dcc0a7164 into 9bb109224
Pull Request #48: Add optional group filter: only enrol members of selected source-course groups

35 of 39 new or added lines in 1 file covered. (89.74%)

371 of 375 relevant lines covered (98.93%)

56.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.59
/classes/plugin.php
1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16

17
/**
18
 * Enrol coursecompleted plugin
19
 *
20
 * @package   enrol_coursecompleted
21
 * @copyright eWallah (www.eWallah.net)
22
 * @author    Renaat Debleu <info@eWallah.net>
23
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25

26
declare(strict_types=1);
27

28
/**
29
 * Enrol coursecompleted plugin
30
 *
31
 * @package   enrol_coursecompleted
32
 * @copyright eWallah (www.eWallah.net)
33
 * @author    Renaat Debleu <info@eWallah.net>
34
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
36
class enrol_coursecompleted_plugin extends enrol_plugin {
37
    /**
38
     * Returns localised name of enrol instance
39
     *
40
     * @param object $instance (null is accepted too)
41
     * @return string Name of instance
42
     */
43
    public function get_instance_name($instance) {
44
        $tmp = is_null($instance) ? 'unknown' : $instance->customint1;
63✔
45
        if (
46
            $tmp !== 'unknown' &&
63✔
47
            $context = context_course::instance($tmp, IGNORE_MISSING)
63✔
48
        ) {
49
            $formatter = \core\di::get(\core\formatting::class);
63✔
50
            $name = $formatter->format_string(
63✔
51
                get_course($tmp)->shortname,
63✔
52
                context: $context,
63✔
53
                filter: false
63✔
54
            );
63✔
55
            return get_string('aftercourse', 'enrol_coursecompleted', $name);
63✔
56
        }
57

58
        return get_string('coursedeleted', '', $tmp);
7✔
59
    }
60

61
    /**
62
     * Returns optional enrolment information icons.
63
     *
64
     * @param array $instances all enrol instances of this type in one course
65
     * @return array of pix_icon
66
     */
67
    public function get_info_icons(array $instances) {
68
        $arr = [];
14✔
69
        $formatter = \core\di::get(\core\formatting::class);
14✔
70
        foreach ($instances as $instance) {
14✔
71
            if (
72
                $this->is_active($instance) &&
14✔
73
                $context = context_course::instance($instance->customint1, IGNORE_MISSING)
14✔
74
            ) {
75
                $name = $formatter->format_string(get_course($instance->customint1)->fullname, context: $context);
14✔
76
                $arr[] = new pix_icon('icon', get_string('aftercourse', 'enrol_coursecompleted', $name), 'enrol_coursecompleted');
14✔
77
            }
78
        }
79

80
        return $arr;
14✔
81
    }
82

83
    /**
84
     * Returns optional enrolment instance description text.
85
     *
86
     * @param object $instance Instance
87
     * @return string short html text
88
     */
89
    public function get_description_text($instance) {
90
        $id = $instance->customint1;
7✔
91
        return "Enrolment by completion of course with id {$id}";
7✔
92
    }
93

94
    /**
95
     * Add information for people who want to enrol.
96
     *
97
     * @param stdClass $instance Instance
98
     * @return string html text, usually a form in a text box
99
     */
100
    public function enrol_page_hook(stdClass $instance) {
101
        global $OUTPUT, $USER;
102
        if (!$this->is_active($instance)) {
21✔
103
            return '';
7✔
104
        }
105

106
        // Do not promise enrolment to users who would be skipped by the group filter.
107
        if (!$this->passes_group_filter($instance, (int)$USER->id)) {
21✔
108
            return '';
7✔
109
        }
110

111
        $data = [];
21✔
112
        $formatter = \core\di::get(\core\formatting::class);
21✔
113
        if ($this->get_config('svglearnpath')) {
21✔
114
            $items = $this->build_course_path($instance);
21✔
115
            $i = 1;
21✔
116
            foreach ($items as $item) {
21✔
117
                $name = $formatter->format_string(get_course($item)->fullname, context: context_course::instance($item));
21✔
118
                $data[] =
21✔
119
                    [
21✔
120
                        'first' => ($i === 1),
21✔
121
                        'course' => ($item == $instance->courseid),
21✔
122
                        'title' => $name,
21✔
123
                        'href' => new moodle_url('/course/view.php', ['id' => $item]),
21✔
124
                        'seqnumber' => $i,
21✔
125
                    ];
21✔
126
                $i++;
21✔
127
            }
128
        }
129

130
        $name = $formatter->format_string(
21✔
131
            get_course($instance->customint1)->fullname,
21✔
132
            context: context_course::instance($instance->customint1)
21✔
133
        );
21✔
134
        $rdata =
21✔
135
            [
21✔
136
                'coursetitle' => $name,
21✔
137
                'courseurl' => new moodle_url('/course/view.php', ['id' => $instance->customint1]),
21✔
138
                'hasdata' => count($data) > 1,
21✔
139
                'items' => $data,
21✔
140
            ];
21✔
141
        $str = $OUTPUT->render_from_template('enrol_coursecompleted/learnpath', $rdata);
21✔
142
        return $OUTPUT->box($str);
21✔
143
    }
144

145
    /**
146
     * Gets an array of the user enrolment actions
147
     *
148
     * @param \course_enrolment_manager $manager Manager
149
     * @param stdClass $ue A user enrolment object
150
     * @return array An array of user_enrolment_actions
151
     */
152
    public function get_user_enrolment_actions(\course_enrolment_manager $manager, $ue) {
153
        $actions = parent::get_user_enrolment_actions($manager, $ue);
7✔
154
        $id = $ue->enrolmentinstance->customint1;
7✔
155
        if (context_course::instance($id, IGNORE_MISSING)) {
7✔
156
            $actions[] = new user_enrolment_action(
7✔
157
                new pix_icon('a/search', ''),
7✔
158
                get_string('pluginname', 'report_completion'),
7✔
159
                new moodle_url('/report/completion/index.php', ['course' => $id]),
7✔
160
                ['class' => 'originlink', 'rel' => $ue->id]
7✔
161
            );
7✔
162
        }
163

164
        return $actions;
7✔
165
    }
166

167
    /**
168
     * Returns edit icons for the page with list of instances
169
     * @param stdClass $instance Instance
170
     * @return array of action icons
171
     */
172
    public function get_action_icons(stdClass $instance) {
173
        global $OUTPUT;
174
        if ($instance->enrol !== 'coursecompleted') {
21✔
175
            throw new coding_exception('invalid enrol instance!');
7✔
176
        }
177

178
        $context = context_course::instance($instance->courseid);
14✔
179
        $icons = [];
14✔
180
        if (has_capability('enrol/coursecompleted:enrolpast', $context)) {
14✔
181
            $managelink = new moodle_url('/enrol/coursecompleted/manage.php', ['enrolid' => $instance->id]);
7✔
182
            $icon = new pix_icon('t/enrolusers', get_string('enrolusers', 'enrol_manual'), 'core', ['class' => 'iconsmall']);
7✔
183
            $icons[] = $OUTPUT->action_icon($managelink, $icon);
7✔
184
        }
185

186
        return array_merge(parent::get_action_icons($instance), $icons);
14✔
187
    }
188

189
    /**
190
     * Restore instance and map settings.
191
     *
192
     * @param \restore_enrolments_structure_step $step Restore step
193
     * @param stdClass $data Data to restore
194
     * @param stdClass $course Course
195
     * @param int $oldid Old id
196
     */
197
    public function restore_instance(\restore_enrolments_structure_step $step, stdClass $data, $course, $oldid): void {
198
        global $DB;
199
        if ($step->get_task()->get_target() == backup::TARGET_NEW_COURSE) {
7✔
200
            $instanceid = $this->add_instance($course, (array)$data);
7✔
201
            $step->set_mapping('enrol', $oldid, $instanceid);
7✔
202
        } else {
203
            $merge = [
7✔
204
                'courseid' => $course->id,
7✔
205
                'enrol' => 'coursecompleted',
7✔
206
                'roleid' => $data->roleid,
7✔
207
                'customint1' => $data->customint1,
7✔
208
            ];
7✔
209
            if ($instances = $DB->get_records('enrol', $merge, 'id')) {
7✔
210
                $instance = reset($instances);
7✔
211
                $step->set_mapping('enrol', $oldid, $instance->id);
7✔
212
            }
213
        }
214
    }
215

216
    /**
217
     * Enrol user into course via enrol instance.
218
     *
219
     * @param stdClass $instance Instance
220
     * @param int $userid User id
221
     * @param int $roleid optional role id
222
     * @param int $timestart 0 means unknown
223
     * @param int $timeend 0 means forever
224
     * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
225
     * @param bool $recovergrades restore grade history
226
     * @return void Nothing
227
     */
228
    public function enrol_user(
229
        stdClass $instance,
230
        $userid,
231
        $roleid = null,
232
        $timestart = 0,
233
        $timeend = 0,
234
        $status = null,
235
        $recovergrades = null
236
    ): void {
237
        global $CFG, $DB;
238
        if ($this->is_active($instance)) {
252✔
239
            $userid = (int)$userid;
231✔
240

241
            // Group filter: skip users who do not belong to one of the selected source-course groups.
242
            if (!$this->passes_group_filter($instance, $userid)) {
231✔
243
                return;
7✔
244
            }
245

246
            // We ignore the role, timestart, timeend and status parameters and fall back on the instance settings.
247
            $roleid = $instance->roleid ?? $this->get_config('roleid');
231✔
248
            $context1 = context_course::instance($instance->customint1, IGNORE_MISSING);
231✔
249
            $context2 = context_course::instance($instance->courseid, IGNORE_MISSING);
231✔
250
            if ($DB->record_exists('role', ['id' => $roleid]) && $context1 && $context2) {
231✔
251
                $timestart = time();
224✔
252
                $timeend = 0;
224✔
253
                if (isset($instance->customint4) && $instance->customint4 > 0) {
224✔
254
                    $timestart = $instance->customint4;
49✔
255
                }
256

257
                if (isset($instance->enrolperiod) && $instance->enrolperiod > 0) {
224✔
258
                    $timeend = $timestart + $instance->enrolperiod;
14✔
259
                }
260

261
                parent::enrol_user($instance, $userid, $roleid, $timestart, $timeend, $status, $recovergrades);
224✔
262
                // Handle welcome message.
263
                if ($instance->customint2 != ENROL_DO_NOT_SEND_EMAIL) {
224✔
264
                    $course = get_course($instance->customint1);
203✔
265
                    $formatter = \core\di::get(\core\formatting::class);
203✔
266
                    $a = new stdClass();
203✔
267
                    $a->completed = $formatter->format_string($course->fullname, context: $context1);
203✔
268
                    if ($instance->customtext1 == '') {
203✔
269
                        $message = get_string('welcometocourse', 'enrol_coursecompleted', $a);
196✔
270
                    } else {
271
                        $key = ['{$a->completed}'];
14✔
272
                        $value = [$a->completed];
14✔
273
                        $message = str_replace($key, $value, $instance->customtext1);
14✔
274
                    }
275

276
                    $this->send_course_welcome_message_to_user(
203✔
277
                        instance: $instance,
203✔
278
                        userid: $userid,
203✔
279
                        sendoption: (int)$instance->customint2,
203✔
280
                        message: $message,
203✔
281
                        roleid: (int)$roleid,
203✔
282
                    );
203✔
283
                }
284

285
                // Keep the user in a group when needed.
286
                if ($instance->customint3 > 0) {
224✔
287
                    require_once($CFG->dirroot . '/group/lib.php');
203✔
288
                    $groups = groups_get_user_groups((int)$instance->customint1, $userid);
203✔
289
                    foreach ($groups as $group) {
203✔
290
                        foreach ($group as $sub) {
203✔
291
                            $groupnamea = groups_get_group_name($sub);
14✔
292
                            $groupnameb = groups_get_group_by_name($instance->courseid, $groupnamea);
14✔
293
                            if ($groupnameb) {
14✔
294
                                groups_add_member($groupnameb, $userid);
14✔
295
                            }
296
                        }
297
                    }
298
                }
299

300
                // Try unenrol the user from the completed course.
301
                if ($instance->customint5 > 0) {
224✔
302
                    $page = new moodle_page();
7✔
303
                    require_once($CFG->dirroot . '/enrol/locallib.php');
7✔
304
                    $course = get_course($instance->customint1);
7✔
305
                    $cem = new \course_enrolment_manager($page, $course);
7✔
306
                    $enrols = $cem->get_user_enrolments($userid);
7✔
307

308
                    foreach ($enrols as $enrol) {
7✔
309
                        $plugin = enrol_get_plugin($enrol->enrolmentinstance->enrol);
7✔
310
                        $sourceinstance = $DB->get_record('enrol', ['id' => $enrol->enrolid], '*', MUST_EXIST);
7✔
311
                        if ($plugin->allow_unenrol_user($sourceinstance, $enrol)) {
7✔
312
                            $plugin->unenrol_user($sourceinstance, $userid);
7✔
313
                        }
314
                    }
315
                }
316
            } else {
317
                debugging('Role does not exist', DEBUG_DEVELOPER);
7✔
318
            }
319
        }
320
    }
321

322
    /**
323
     * Restore user enrolment.
324
     *
325
     * @param \restore_enrolments_structure_step $step Restore step`
326
     * @param stdClass $data Ignored data
327
     * @param stdClass $instance Instance
328
     * @param int $userid User id
329
     * @param int $oldstatus Old statis
330
     */
331
    public function restore_user_enrolment(\restore_enrolments_structure_step $step, $data, $instance, $userid, $oldstatus): void {
332
        if ($step->get_task()->get_target() == backup::TARGET_NEW_COURSE) {
7✔
333
            $this->enrol_user($instance, $userid);
7✔
334
        }
335
    }
336

337
    /**
338
     * Is it possible to add enrol instance via standard UI?
339
     *
340
     * @param int $courseid id of the course to add the instance to
341
     * @return bool if user can add instance to course
342
     */
343
    public function can_add_instance($courseid) {
344
        return has_capability('enrol/coursecompleted:manage', context_course::instance($courseid));
14✔
345
    }
346

347
    /**
348
     * Is it possible to delete enrol instance via standard UI?
349
     *
350
     * @param object $instance Instance
351
     * @return bool if user can delete instance
352
     */
353
    public function can_delete_instance($instance) {
354
        return has_capability('enrol/coursecompleted:manage', context_course::instance($instance->courseid));
14✔
355
    }
356

357
    /**
358
     * Is it possible to hide/show enrol instance via standard UI?
359
     *
360
     * @param stdClass $instance Instance
361
     * @return bool True is user can enrol via UI
362
     */
363
    public function can_hide_show_instance($instance): bool {
364
        return has_capability('enrol/coursecompleted:manage', context_course::instance($instance->courseid));
14✔
365
    }
366

367
    /**
368
     * Does this plugin allow manual unenrolment of all users?
369
     *
370
     * @param stdClass $instance course enrol instance
371
     * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely
372
     */
373
    public function allow_unenrol(stdClass $instance): bool {
374
        return has_capability('enrol/coursecompleted:manage', context_course::instance($instance->courseid));
21✔
375
    }
376

377
    /**
378
     * Does this plugin allow manual changes in user_enrolments table?
379
     *
380
     * @param stdClass $instance course enrol instance
381
     * @return bool - true means it is possible to change enrol period and status in user_enrolments table
382
     */
383
    public function allow_manage(stdClass $instance): bool {
384
        return has_capability('enrol/coursecompleted:manage', context_course::instance($instance->courseid));
21✔
385
    }
386

387
    /**
388
     * Does this plugin shows enrol me?
389
     *
390
     * @param stdClass $instance course enrol instance
391
     * @return bool - true means it is possible to enrol.
392
     */
393
    public function show_enrolme_link(stdClass $instance): bool {
394
        return ($instance->status == ENROL_INSTANCE_ENABLED);
7✔
395
    }
396

397
    /**
398
     * Execute synchronisation.
399
     * @param progress_trace $trace Trace progress
400
     * @return int exit code, 0 means ok
401
     */
402
    public function sync(progress_trace $trace): int {
403
        $this->process_expirations($trace);
7✔
404
        return 0;
7✔
405
    }
406

407
    /**
408
     * We are a good plugin and don't invent our own UI/validation code path.
409
     *
410
     * @return bool True
411
     */
412
    public function use_standard_editing_ui(): bool {
413
        return true;
14✔
414
    }
415

416
    /**
417
     * Add elements to the edit instance form.
418
     *
419
     * @param stdClass $instance Instance
420
     * @param \MoodleQuickForm $mform Quick form
421
     * @param context $context Context
422
     */
423
    public function edit_instance_form($instance, \MoodleQuickForm $mform, $context): void {
424
        global $CFG;
425
        $plugin = 'enrol_coursecompleted';
14✔
426
        $options = [ENROL_INSTANCE_ENABLED => get_string('yes'), ENROL_INSTANCE_DISABLED => get_string('no')];
14✔
427
        $mform->addElement('select', 'status', get_string('enabled', 'admin'), $options);
14✔
428
        $mform->setDefault('status', $this->get_config('status'));
14✔
429

430
        $role = isset($instance->roleid) ? $instance->roleid : $this->get_config('roleid');
14✔
431
        $start = isset($instance->customint1) ? get_course($instance->customint1)->startdate : time();
14✔
432

433
        $roles = get_default_enrol_roles($context, $role);
14✔
434
        $mform->addElement('select', 'roleid', get_string('assignrole', $plugin), $roles);
14✔
435
        $mform->setDefault('roleid', $this->get_config('roleid'));
14✔
436

437
        $arr = ['optional' => true, 'defaulttime' => $start];
14✔
438
        $mform->addElement('date_time_selector', 'customint4', get_string('enroldate', $plugin), $arr);
14✔
439
        $mform->addHelpButton('customint4', 'enroldate', $plugin);
14✔
440

441
        $arr = ['optional' => true, 'defaultunit' => 86400];
14✔
442
        $mform->addElement('duration', 'enrolperiod', get_string('enrolperiod', $plugin), $arr);
14✔
443
        $mform->setDefault('enrolperiod', $this->get_config('enrolperiod'));
14✔
444
        $mform->addHelpButton('enrolperiod', 'enrolperiod', $plugin);
14✔
445

446
        $conditions = ['onlywithcompletion' => true, 'multiple' => false, 'includefrontpage' => false];
14✔
447
        $mform->addElement('course', 'customint1', get_string('course'), $conditions);
14✔
448
        $mform->addRule('customint1', get_string('required'), 'required', null, 'client');
14✔
449
        $mform->addHelpButton('customint1', 'compcourse', $plugin);
14✔
450

451
        // Group filter: restrict auto-enrolment to members of the chosen groups in the completed course.
452
        // The list is loaded by AJAX from the course selected above; already-selected groups are preloaded.
453
        $preopts = [];
14✔
454
        $selectedgroups = [];
14✔
455
        if (!empty($instance->customtext2)) {
14✔
456
            require_once($CFG->dirroot . '/group/lib.php');
14✔
457
            $selectedgroups = json_decode($instance->customtext2) ?: [];
14✔
458
            foreach ($selectedgroups as $gid) {
14✔
NEW
459
                $gname = groups_get_group_name((int)$gid);
×
NEW
460
                if ($gname !== false) {
×
NEW
461
                    $preopts[(int)$gid] = $gname;
×
462
                }
463
            }
464
        }
465
        $mform->addElement(
14✔
466
            'autocomplete',
14✔
467
            'groupfilter',
14✔
468
            get_string('groupfilter', $plugin),
14✔
469
            $preopts,
14✔
470
            [
14✔
471
                'multiple' => true,
14✔
472
                'ajax' => 'enrol_coursecompleted/groupselector',
14✔
473
                'noselectionstring' => get_string('groupfilter_all', $plugin),
14✔
474
            ]
14✔
475
        );
14✔
476
        $mform->setDefault('groupfilter', $selectedgroups);
14✔
477
        $mform->addHelpButton('groupfilter', 'groupfilter', $plugin);
14✔
478

479
        $mform->addElement('advcheckbox', 'customint5', get_string('unenrol', 'enrol'), get_string('tryunenrol', $plugin));
14✔
480
        $mform->addHelpButton('customint5', 'tryunenrol', $plugin);
14✔
481
        $mform->setDefault('customint5', $this->get_config('tryunenrol'));
14✔
482

483
        $mform->addElement('advcheckbox', 'customint3', get_string('groups'), get_string('group', $plugin));
14✔
484
        $mform->addHelpButton('customint3', 'group', $plugin);
14✔
485
        $mform->setDefault('customint3', $this->get_config('keepgroup'));
14✔
486

487
        $options = self::email_options();
14✔
488
        $mform->addElement('select', 'customint2', get_string('categoryemail', 'admin'), $options);
14✔
489

490
        $mform->addHelpButton('customint2', 'welcome', $plugin);
14✔
491
        $mform->setDefault('customint2', $this->get_config('welcome'));
14✔
492

493
        $arr = ['cols' => '60', 'rows' => '8'];
14✔
494
        $mform->addElement('textarea', 'customtext1', get_string('customwelcome', $plugin), $arr);
14✔
495
        $mform->addHelpButton('customtext1', 'customwelcome', $plugin);
14✔
496
        $mform->disabledIf('customtext1', 'customint2', 'eq', 0);
14✔
497

498
        $arr = ['optional' => true, 'defaulttime' => $start];
14✔
499
        $mform->addElement('date_time_selector', 'enrolstartdate', get_string('enrolstartdate', $plugin), $arr);
14✔
500
        $mform->addHelpButton('enrolstartdate', 'enrolstartdate', $plugin);
14✔
501

502
        $arr['defaulttime'] = $start + get_config('moodlecourse', 'courseduration');
14✔
503
        $mform->addElement('date_time_selector', 'enrolenddate', get_string('enrolenddate', $plugin), $arr);
14✔
504
        $mform->addHelpButton('enrolenddate', 'enrolenddate', 'enrol_coursecompleted');
14✔
505
    }
506

507
    /**
508
     * Get email options.
509
     * @return array of options
510
     */
511
    public static function email_options(): array {
512
        $options = enrol_send_welcome_email_options();
28✔
513
        unset($options[ENROL_SEND_EMAIL_FROM_KEY_HOLDER]);
28✔
514
        return $options;
28✔
515
    }
516

517
    /**
518
     * Add new instance of enrol plugin.
519
     * @param object $course Course
520
     * @param array|null $fields Fields
521
     * @return int id of new instance, null if can not be created
522
     */
523
    public function add_instance($course, ?array $fields = null): int {
524
        if ($fields !== null) {
392✔
525
            if (!isset($fields['customint2'])) {
392✔
526
                $fields['customint2'] = $this->get_config('welcome', 1);
308✔
527
            }
528

529
            if (!isset($fields['customint3'])) {
392✔
530
                $fields['customint3'] = $this->get_config('keepgroup', 1);
357✔
531
            }
532

533
            $fields['customtext2'] = self::encode_groupfilter($fields['groupfilter'] ?? []);
392✔
534
            unset($fields['groupfilter']);
392✔
535
        }
536

537
        return parent::add_instance($course, $fields);
392✔
538
    }
539

540
    /**
541
     * Encode the selected group ids into the JSON stored in customtext2.
542
     *
543
     * @param mixed $value array of group ids from the form (or anything falsy)
544
     * @return string JSON array of ints ("[]" when none)
545
     */
546
    protected static function encode_groupfilter($value): string {
547
        $arr = is_array($value) ? array_values(array_filter(array_map('intval', $value))) : [];
392✔
548
        return json_encode($arr);
392✔
549
    }
550

551
    /**
552
     * Whether a user passes the group filter of an instance.
553
     *
554
     * When groups are selected (customtext2) the user must belong to at least one of them in the
555
     * completed course. No filter set = everyone passes. Only the user's own membership is checked,
556
     * so no hidden-group information about other users is revealed.
557
     *
558
     * @param stdClass $instance Enrol instance
559
     * @param int $userid User id
560
     * @return bool True when the user may be enrolled
561
     */
562
    public function passes_group_filter(stdClass $instance, int $userid): bool {
563
        global $CFG;
564
        if (empty($instance->customtext2)) {
245✔
NEW
565
            return true;
×
566
        }
567
        $groupids = json_decode($instance->customtext2);
245✔
568
        if (!is_array($groupids) || count($groupids) === 0) {
245✔
569
            return true;
238✔
570
        }
571
        require_once($CFG->dirroot . '/group/lib.php');
14✔
572
        $usergroups = groups_get_user_groups((int)$instance->customint1, $userid)[0] ?? [];
14✔
573
        return !empty(array_intersect(array_map('intval', $groupids), array_map('intval', $usergroups)));
14✔
574
    }
575

576
    /**
577
     * Update instance of enrol plugin.
578
     *
579
     * @param stdClass $instance Instance
580
     * @param stdClass $data modified instance fields
581
     * @return bool True if updated
582
     */
583
    public function update_instance($instance, $data) {
584
        $data->customtext2 = self::encode_groupfilter($data->groupfilter ?? []);
7✔
585
        $update = parent::update_instance($instance, $data);
7✔
586
        $hook = new \core_enrol\hook\after_enrol_instance_status_updated(enrolinstance: $instance, newstatus: (int)$data->status);
7✔
587
        \core\di::get(\core\hook\manager::class)->dispatch($hook);
7✔
588
        return $update;
7✔
589
    }
590

591
    /**
592
     * Perform custom validation of the data used to edit the instance.
593
     *
594
     * @param array $data array of ("fieldname"=>value) of submitted data
595
     * @param array $files array of uploaded files "element_name"=>tmp_file_path
596
     * @param object $instance The instance loaded from the DB
597
     * @param context $context The context of the instance we are editing
598
     * @return array of "element_name"=>"error_description" if there are errors,
599
     */
600
    public function edit_instance_validation($data, $files, $instance, $context): array {
601
        $errors = [];
7✔
602
        // Minimum duration of a course is one hour.
603
        if (!empty($data['enrolenddate']) && $data['enrolenddate'] <= $data['enrolstartdate'] + HOURSECS) {
7✔
604
            $errors['enrolenddate'] = get_string('enrolenddaterror', 'enrol_coursecompleted');
7✔
605
        }
606

607
        if (
608
            empty($data['customint1']) ||
7✔
609
            !context_course::instance($data['customint1'], IGNORE_MISSING)
7✔
610
        ) {
611
            $errors['customint1'] = get_string('error_nonexistingcourse', 'tool_generator');
7✔
612
        }
613

614
        return $errors;
7✔
615
    }
616

617
    /**
618
     * Build (possible) coursepath
619
     *
620
     * @param stdClass $instance Instance
621
     * @return array $items Children and Parents
622
     */
623
    private function build_course_path(stdClass $instance): array {
624
        $parents = $this->search_parents((int)$instance->customint1);
56✔
625
        $children = $this->search_children((int)$instance->courseid);
56✔
626
        return array_unique(array_merge($parents, $children));
56✔
627
    }
628

629
    /**
630
     * Search parents
631
     *
632
     * @param int $courseid Course id
633
     * @param int $level Level we are
634
     * @return array found items
635
     */
636
    private function search_parents(int $courseid, int $level = 1): array {
637
        global $DB;
638
        $arr = [$courseid];
56✔
639
        if ($level < 5) {
56✔
640
            $level++;
56✔
641
            $params = ['enrol' => 'coursecompleted', 'courseid' => $courseid];
56✔
642
            if ($parent = $DB->get_field('enrol', 'customint1', $params, IGNORE_MULTIPLE)) {
56✔
643
                $arr = array_merge($this->search_parents((int)$parent, $level), $arr);
21✔
644
            }
645
        }
646

647
        return $arr;
56✔
648
    }
649

650
    /**
651
     * Search children
652
     *
653
     * @param int $courseid Course id
654
     * @param int $level Level
655
     * @return array found items
656
     */
657
    private function search_children(int $courseid, int $level = 1): array {
658
        global $DB;
659
        $arr = [$courseid];
56✔
660
        if ($level < 5) {
56✔
661
            $level++;
56✔
662
            $params = ['enrol' => 'coursecompleted', 'customint1' => $courseid];
56✔
663
            if ($child = $DB->get_field('enrol', 'courseid', $params, IGNORE_MULTIPLE)) {
56✔
664
                $arr = array_merge($arr, $this->search_children((int)$child, $level));
35✔
665
            }
666
        }
667

668
        return $arr;
56✔
669
    }
670

671
    /**
672
     * Is this instance active?
673
     *
674
     * @param stdClass $instance Instance
675
     * @return bool true is active
676
     */
677
    private function is_active($instance): bool {
678
        $now = time();
266✔
679
        if ($instance->enrolstartdate && $instance->enrolstartdate > $now) {
266✔
680
            return false;
14✔
681
        }
682

683
        if ($instance->enrolenddate && $instance->enrolenddate < $now) {
259✔
684
            return false;
14✔
685
        }
686

687
        return true;
245✔
688
    }
689

690
    /**
691
     * Has bulk operations.
692
     *
693
     * @param \course_enrolment_manager $manager Manager
694
     * @return bool True if bulk operations
695
     */
696
    public function has_bulk_operations(\course_enrolment_manager $manager): bool {
697
        $instances = $manager->get_enrolment_instances();
14✔
698
        $return = false;
14✔
699
        foreach ($instances as $instance) {
14✔
700
            if ($instance->enrol === 'coursecompleted') {
14✔
701
                $return = true;
14✔
702
            }
703
        }
704

705
        return $return;
14✔
706
    }
707

708
    /**
709
     * The enrol plugin has bulk operations that can be performed.
710
     * @param \course_enrolment_manager $manager Manager
711
     * @return array of bulk operations
712
     */
713
    public function get_bulk_operations(\course_enrolment_manager $manager): array {
714
        $context = $manager->get_context();
14✔
715
        $bulkoperations = [];
14✔
716
        if ($this->has_bulk_operations($manager)) {
14✔
717
            if (has_capability("enrol/coursecompleted:manage", $context)) {
14✔
718
                $bulkoperations['editselectedusers'] = new \enrol_coursecompleted\bulkedit($manager, $this);
14✔
719
            }
720

721
            if (has_capability("enrol/coursecompleted:unenrol", $context)) {
14✔
722
                $bulkoperations['deleteselectedusers'] = new \enrol_coursecompleted\bulkdelete($manager, $this);
14✔
723
            }
724
        }
725

726
        return $bulkoperations;
14✔
727
    }
728

729
    /**
730
     * Get all candidates for an enrolment.
731
     * @param int $courseid Course id
732
     * @return array of candidates for enrolment
733
     */
734
    public static function get_candidates(int $courseid): array {
735
        global $DB;
736
        $condition = 'course = ? AND timecompleted > 0';
21✔
737
        $return = $DB->get_fieldset_select('course_completions', 'userid', $condition, [$courseid]);
21✔
738
        return $return ?? [];
21✔
739
    }
740
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc