Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core_courseformat;
18
 
19
use core\event\course_module_updated;
20
use cm_info;
21
use section_info;
22
use stdClass;
23
use course_modinfo;
24
use moodle_exception;
25
use context_module;
26
use context_course;
27
 
28
/**
29
 * Contains the core course state actions.
30
 *
31
 * The methods from this class should be executed via "core_courseformat_edit" web service.
32
 *
33
 * Each format plugin could extend this class to provide new actions to the editor.
34
 * Extended classes should be locate in "format_XXX\course" namespace and
35
 * extends core_courseformat\stateactions.
36
 *
37
 * @package    core_courseformat
38
 * @copyright  2021 Ferran Recio <ferran@moodle.com>
39
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40
 */
41
class stateactions {
42
 
43
    /**
44
     * Move course modules to another location in the same course.
45
     *
46
     * @param stateupdates $updates the affected course elements track
47
     * @param stdClass $course the course object
48
     * @param int[] $ids the list of affected course module ids
49
     * @param int $targetsectionid optional target section id
50
     * @param int $targetcmid optional target cm id
51
     */
52
    public function cm_move(
53
        stateupdates $updates,
54
        stdClass $course,
55
        array $ids,
56
        ?int $targetsectionid = null,
57
        ?int $targetcmid = null
58
    ): void {
59
        // Validate target elements.
60
        if (!$targetsectionid && !$targetcmid) {
61
            throw new moodle_exception("Action cm_move requires targetsectionid or targetcmid");
62
        }
63
 
64
        $this->validate_cms($course, $ids, __FUNCTION__, ['moodle/course:manageactivities']);
65
        // The moveto_module function move elements before a specific target.
66
        // To keep the order the movements must be done in descending order (last activity first).
67
        $ids = $this->sort_cm_ids_by_course_position($course, $ids, true);
68
 
69
        // Target cm has more priority than target section.
70
        if (!empty($targetcmid)) {
71
            $this->validate_cms($course, [$targetcmid], __FUNCTION__);
72
            $targetcm = get_fast_modinfo($course)->get_cm($targetcmid);
73
            $targetsectionid = $targetcm->section;
74
        } else {
75
            $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
76
        }
77
 
78
        // The origin sections must be updated as well.
79
        $originalsections = [];
80
 
81
        $beforecmdid = $targetcmid;
82
        foreach ($ids as $cmid) {
83
            // An updated $modinfo is needed on every loop as activities list change.
84
            $modinfo = get_fast_modinfo($course);
85
            $cm = $modinfo->get_cm($cmid);
86
            $currentsectionid = $cm->section;
87
            $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
88
            $beforecm = (!empty($beforecmdid)) ? $modinfo->get_cm($beforecmdid) : null;
89
            if ($beforecm === null || $beforecm->id != $cmid) {
90
                moveto_module($cm, $targetsection, $beforecm);
91
            }
92
            $beforecmdid = $cm->id;
93
            $updates->add_cm_put($cm->id);
94
            if ($currentsectionid != $targetsectionid) {
95
                $originalsections[$currentsectionid] = true;
96
            }
97
            // If some of the original sections are also target sections, we don't need to update them.
98
            if (array_key_exists($targetsectionid, $originalsections)) {
99
                unset($originalsections[$targetsectionid]);
100
            }
101
        }
102
 
103
        // Use section_state to return the full affected section and activities updated state.
104
        $this->cm_state($updates, $course, $ids, $targetsectionid, $targetcmid);
105
 
106
        foreach (array_keys($originalsections) as $sectionid) {
107
            $updates->add_section_put($sectionid);
108
        }
109
    }
110
 
111
    /**
112
     * Sort the cm ids list depending on the course position.
113
     *
114
     * Some actions like move should be done in an specific order.
115
     *
116
     * @param stdClass $course the course object
117
     * @param int[] $cmids the array of section $ids
118
     * @param bool $descending if the sort order must be descending instead of ascending
119
     * @return int[] the array of section ids sorted by section number
120
     */
121
    protected function sort_cm_ids_by_course_position(
122
        stdClass $course,
123
        array $cmids,
124
        bool $descending = false
125
    ): array {
126
        $modinfo = get_fast_modinfo($course);
127
        $cmlist = array_keys($modinfo->get_cms());
128
        $cmposition = [];
129
        foreach ($cmids as $cmid) {
130
            $cmposition[$cmid] = array_search($cmid, $cmlist);
131
        }
132
        $sorting = ($descending) ? -1 : 1;
133
        $sortfunction = function ($acmid, $bcmid) use ($sorting, $cmposition) {
134
            return ($cmposition[$acmid] <=> $cmposition[$bcmid]) * $sorting;
135
        };
136
        usort($cmids, $sortfunction);
137
        return $cmids;
138
    }
139
 
140
    /**
141
     * Move course sections to another location in the same course.
142
     *
143
     * @deprecated since Moodle 4.4 MDL-77038.
144
     * @todo MDL-80116 This will be deleted in Moodle 4.8.
145
     * @param stateupdates $updates the affected course elements track
146
     * @param stdClass $course the course object
147
     * @param int[] $ids the list of affected course module ids
148
     * @param int $targetsectionid optional target section id
149
     * @param int $targetcmid optional target cm id
150
     */
151
    public function section_move(
152
        stateupdates $updates,
153
        stdClass $course,
154
        array $ids,
155
        ?int $targetsectionid = null,
156
        ?int $targetcmid = null
157
    ): void {
158
        debugging(
159
            'The method stateactions::section_move() has been deprecated, please use stateactions::section_move_after() instead.',
160
            DEBUG_DEVELOPER
161
        );
162
        // Validate target elements.
163
        if (!$targetsectionid) {
164
            throw new moodle_exception("Action cm_move requires targetsectionid");
165
        }
166
 
167
        $this->validate_sections($course, $ids, __FUNCTION__);
168
 
169
        $coursecontext = context_course::instance($course->id);
170
        require_capability('moodle/course:movesections', $coursecontext);
171
 
172
        $modinfo = get_fast_modinfo($course);
173
 
174
        // Target section.
175
        $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
176
        $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
177
 
178
        $affectedsections = [$targetsection->section => true];
179
 
180
        $sections = $this->get_section_info($modinfo, $ids);
181
        foreach ($sections as $section) {
182
            $affectedsections[$section->section] = true;
183
            move_section_to($course, $section->section, $targetsection->section);
184
        }
185
 
186
        // Use section_state to return the section and activities updated state.
187
        $this->section_state($updates, $course, $ids, $targetsectionid);
188
 
189
        // All course sections can be renamed because of the resort.
190
        $allsections = $modinfo->get_section_info_all();
191
        foreach ($allsections as $section) {
192
            // Ignore the affected sections because they are already in the updates.
193
            if (isset($affectedsections[$section->section])) {
194
                continue;
195
            }
196
            $updates->add_section_put($section->id);
197
        }
198
        // The section order is at a course level.
199
        $updates->add_course_put();
200
    }
201
 
202
    /**
203
     * Move course sections after to another location in the same course.
204
     *
205
     * @param stateupdates $updates the affected course elements track
206
     * @param stdClass $course the course object
207
     * @param int[] $ids the list of affected course module ids
208
     * @param int $targetsectionid optional target section id
209
     * @param int $targetcmid optional target cm id
210
     */
211
    public function section_move_after(
212
        stateupdates $updates,
213
        stdClass $course,
214
        array $ids,
215
        ?int $targetsectionid = null,
216
        ?int $targetcmid = null
217
    ): void {
218
        // Validate target elements.
219
        if (!$targetsectionid) {
220
            throw new moodle_exception("Action section_move_after requires targetsectionid");
221
        }
222
 
223
        $this->validate_sections($course, $ids, __FUNCTION__);
224
 
225
        $coursecontext = context_course::instance($course->id);
226
        require_capability('moodle/course:movesections', $coursecontext);
227
 
228
        // Section will move after the target section. This means it should be processed in
229
        // descending order to keep the relative course order.
230
        $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
231
        $ids = $this->sort_section_ids_by_section_number($course, $ids, true);
232
 
233
        $format = course_get_format($course->id);
234
        $affectedsections = [$targetsectionid => true];
235
 
236
        foreach ($ids as $id) {
237
            // An update section_info is needed as section numbers can change on every section movement.
238
            $modinfo = get_fast_modinfo($course);
239
            $section = $modinfo->get_section_info_by_id($id, MUST_EXIST);
240
            $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
241
            $affectedsections[$section->id] = true;
242
            $format->move_section_after($section, $targetsection);
243
        }
244
 
245
        // Use section_state to return the section and activities updated state.
246
        $this->section_state($updates, $course, $ids, $targetsectionid);
247
 
248
        // All course sections can be renamed because of the resort.
249
        $modinfo = get_fast_modinfo($course);
250
        $allsections = $modinfo->get_section_info_all();
251
        foreach ($allsections as $section) {
252
            // Ignore the affected sections because they are already in the updates.
253
            if (isset($affectedsections[$section->id])) {
254
                continue;
255
            }
256
            $updates->add_section_put($section->id);
257
        }
258
        // The section order is at a course level.
259
        $updates->add_course_put();
260
    }
261
 
262
    /**
263
     * Sort the sections ids depending on the section number.
264
     *
265
     * Some actions like move should be done in an specific order.
266
     *
267
     * @param stdClass $course the course object
268
     * @param int[] $sectionids the array of section $ids
269
     * @param bool $descending if the sort order must be descending instead of ascending
270
     * @return int[] the array of section ids sorted by section number
271
     */
272
    protected function sort_section_ids_by_section_number(
273
        stdClass $course,
274
        array $sectionids,
275
        bool $descending = false
276
    ): array {
277
        $sorting = ($descending) ? -1 : 1;
278
        $sortfunction = function ($asection, $bsection) use ($sorting) {
279
            return ($asection->section <=> $bsection->section) * $sorting;
280
        };
281
        $modinfo = get_fast_modinfo($course);
282
        $sections = $this->get_section_info($modinfo, $sectionids);
283
        uasort($sections, $sortfunction);
284
        return array_keys($sections);
285
    }
286
 
287
    /**
288
     * Create a course section.
289
     *
290
     * This method follows the same logic as changenumsections.php.
291
     *
292
     * @param stateupdates $updates the affected course elements track
293
     * @param stdClass $course the course object
294
     * @param int[] $ids not used
295
     * @param int $targetsectionid optional target section id (if not passed section will be appended)
296
     * @param int $targetcmid not used
297
     */
298
    public function section_add(
299
        stateupdates $updates,
300
        stdClass $course,
301
        array $ids = [],
302
        ?int $targetsectionid = null,
303
        ?int $targetcmid = null
304
    ): void {
305
 
306
        $coursecontext = context_course::instance($course->id);
307
        require_capability('moodle/course:update', $coursecontext);
308
 
309
        // Get course format settings.
310
        $format = course_get_format($course->id);
311
        $lastsectionnumber = $format->get_last_section_number();
312
        $maxsections = $format->get_max_sections();
313
 
314
        if ($lastsectionnumber >= $maxsections) {
315
            throw new moodle_exception('maxsectionslimit', 'moodle', $maxsections);
316
        }
317
 
318
        $modinfo = get_fast_modinfo($course);
319
 
320
        // Get target section.
321
        if ($targetsectionid) {
322
            $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
323
            $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
324
            // Inserting sections at any position except in the very end requires capability to move sections.
325
            require_capability('moodle/course:movesections', $coursecontext);
326
            $insertposition = $targetsection->section + 1;
327
        } else {
328
            // Get last section.
329
            $insertposition = 0;
330
        }
331
 
332
        course_create_section($course, $insertposition);
333
 
334
        // Adding a section affects the full course structure.
335
        $this->course_state($updates, $course);
336
    }
337
 
338
    /**
339
     * Delete course sections.
340
     *
341
     * This method follows the same logic as editsection.php.
342
     *
343
     * @param stateupdates $updates the affected course elements track
344
     * @param stdClass $course the course object
345
     * @param int[] $ids section ids
346
     * @param int $targetsectionid not used
347
     * @param int $targetcmid not used
348
     */
349
    public function section_delete(
350
        stateupdates $updates,
351
        stdClass $course,
352
        array $ids = [],
353
        ?int $targetsectionid = null,
354
        ?int $targetcmid = null
355
    ): void {
356
 
357
        $coursecontext = context_course::instance($course->id);
358
        require_capability('moodle/course:update', $coursecontext);
359
        require_capability('moodle/course:movesections', $coursecontext);
360
 
361
        foreach ($ids as $sectionid) {
362
            // We need to get the latest modinfo on each iteration because the section numbers change.
363
            $modinfo = get_fast_modinfo($course);
364
            $section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST);
365
            // Send all activity deletions.
366
            if (!empty($modinfo->sections[$section->section])) {
367
                foreach ($modinfo->sections[$section->section] as $modnumber) {
368
                    $cm = $modinfo->cms[$modnumber];
369
                    $updates->add_cm_remove($cm->id);
370
                }
371
            }
372
            course_delete_section($course, $section, true, true);
373
            $updates->add_section_remove($sectionid);
374
        }
375
 
376
        // Removing a section affects the full course structure.
377
        $this->course_state($updates, $course);
378
    }
379
 
380
    /**
381
     * Hide course sections.
382
     *
383
     * @param stateupdates $updates the affected course elements track
384
     * @param stdClass $course the course object
385
     * @param int[] $ids section ids
386
     * @param int $targetsectionid not used
387
     * @param int $targetcmid not used
388
     */
389
    public function section_hide(
390
        stateupdates $updates,
391
        stdClass $course,
392
        array $ids = [],
393
        ?int $targetsectionid = null,
394
        ?int $targetcmid = null
395
    ): void {
396
        $this->set_section_visibility($updates, $course, $ids, 0);
397
    }
398
 
399
    /**
400
     * Show course sections.
401
     *
402
     * @param stateupdates $updates the affected course elements track
403
     * @param stdClass $course the course object
404
     * @param int[] $ids section ids
405
     * @param int $targetsectionid not used
406
     * @param int $targetcmid not used
407
     */
408
    public function section_show(
409
        stateupdates $updates,
410
        stdClass $course,
411
        array $ids = [],
412
        ?int $targetsectionid = null,
413
        ?int $targetcmid = null
414
    ): void {
415
        $this->set_section_visibility($updates, $course, $ids, 1);
416
    }
417
 
418
    /**
419
     * Show course sections.
420
     *
421
     * @param stateupdates $updates the affected course elements track
422
     * @param stdClass $course the course object
423
     * @param int[] $ids section ids
424
     * @param int $visible the new visible value
425
     */
426
    protected function set_section_visibility(
427
        stateupdates $updates,
428
        stdClass $course,
429
        array $ids,
430
        int $visible
431
    ) {
432
        $this->validate_sections($course, $ids, __FUNCTION__);
433
        $coursecontext = context_course::instance($course->id);
434
        require_all_capabilities(['moodle/course:update', 'moodle/course:sectionvisibility'], $coursecontext);
435
 
436
        $modinfo = get_fast_modinfo($course);
437
 
438
        foreach ($ids as $sectionid) {
439
            $section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST);
440
            course_update_section($course, $section, ['visible' => $visible]);
441
        }
442
        $this->section_state($updates, $course, $ids);
443
    }
444
 
445
    /**
446
     * Show course cms.
447
     *
448
     * @param stateupdates $updates the affected course elements track
449
     * @param stdClass $course the course object
450
     * @param int[] $ids cm ids
451
     * @param int $targetsectionid not used
452
     * @param int $targetcmid not used
453
     */
454
    public function cm_show(
455
        stateupdates $updates,
456
        stdClass $course,
457
        array $ids = [],
458
        ?int $targetsectionid = null,
459
        ?int $targetcmid = null
460
    ): void {
461
        $this->set_cm_visibility($updates, $course, $ids, 1, 1);
462
    }
463
 
464
    /**
465
     * Hide course cms.
466
     *
467
     * @param stateupdates $updates the affected course elements track
468
     * @param stdClass $course the course object
469
     * @param int[] $ids cm ids
470
     * @param int $targetsectionid not used
471
     * @param int $targetcmid not used
472
     */
473
    public function cm_hide(
474
        stateupdates $updates,
475
        stdClass $course,
476
        array $ids = [],
477
        ?int $targetsectionid = null,
478
        ?int $targetcmid = null
479
    ): void {
480
        $this->set_cm_visibility($updates, $course, $ids, 0, 1);
481
    }
482
 
483
    /**
484
     * Stealth course cms.
485
     *
486
     * @param stateupdates $updates the affected course elements track
487
     * @param stdClass $course the course object
488
     * @param int[] $ids cm ids
489
     * @param int $targetsectionid not used
490
     * @param int $targetcmid not used
491
     */
492
    public function cm_stealth(
493
        stateupdates $updates,
494
        stdClass $course,
495
        array $ids = [],
496
        ?int $targetsectionid = null,
497
        ?int $targetcmid = null
498
    ): void {
499
        $this->set_cm_visibility($updates, $course, $ids, 1, 0);
500
    }
501
 
502
    /**
503
     * Internal method to define the cm visibility.
504
     *
505
     * @param stateupdates $updates the affected course elements track
506
     * @param stdClass $course the course object
507
     * @param int[] $ids cm ids
508
     * @param int $visible the new visible value
509
     * @param int $coursevisible the new course visible value
510
     */
511
    protected function set_cm_visibility(
512
        stateupdates $updates,
513
        stdClass $course,
514
        array $ids,
515
        int $visible,
516
        int $coursevisible
517
    ): void {
518
        global $CFG;
519
 
520
        $this->validate_cms(
521
            $course,
522
            $ids,
523
            __FUNCTION__,
524
            ['moodle/course:manageactivities', 'moodle/course:activityvisibility']
525
        );
526
 
527
        $format = course_get_format($course->id);
528
        $modinfo = get_fast_modinfo($course);
529
 
530
        $cms = $this->get_cm_info($modinfo, $ids);
531
        foreach ($cms as $cm) {
532
            // Check stealth availability.
533
            if (!$coursevisible) {
534
                $section = $cm->get_section_info();
535
                $allowstealth = !empty($CFG->allowstealth) && $format->allow_stealth_module_visibility($cm, $section);
536
                $coursevisible = ($allowstealth) ? 0 : 1;
537
            }
538
            set_coursemodule_visible($cm->id, $visible, $coursevisible, false);
539
            $modcontext = context_module::instance($cm->id);
540
            course_module_updated::create_from_cm($cm, $modcontext)->trigger();
541
        }
542
        course_modinfo::purge_course_modules_cache($course->id, $ids);
543
        rebuild_course_cache($course->id, false, true);
544
 
545
        foreach ($cms as $cm) {
546
            $updates->add_cm_put($cm->id);
547
        }
548
    }
549
 
550
    /**
551
     * Duplicate a course modules instances into the same course.
552
     *
553
     * @param stateupdates $updates the affected course elements track
554
     * @param stdClass $course the course object
555
     * @param int[] $ids course modules ids to duplicate
556
     * @param int|null $targetsectionid optional target section id destination
557
     * @param int|null $targetcmid optional target before cm id destination
558
     */
559
    public function cm_duplicate(
560
        stateupdates $updates,
561
        stdClass $course,
562
        array $ids = [],
563
        ?int $targetsectionid = null,
564
        ?int $targetcmid = null
565
    ): void {
566
        $this->validate_cms(
567
            $course,
568
            $ids,
569
            __FUNCTION__,
570
            ['moodle/course:manageactivities', 'moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport']
571
        );
572
 
573
        $modinfo = get_fast_modinfo($course);
574
        $cms = $this->get_cm_info($modinfo, $ids);
575
 
576
        // Check capabilities on every activity context.
577
        foreach ($cms as $cm) {
578
            if (!course_allowed_module($course, $cm->modname)) {
579
                throw new moodle_exception('No permission to create that activity');
580
            }
581
        }
582
 
583
        $targetsection = null;
584
        if (!empty($targetsectionid)) {
585
            $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
586
            $targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
587
        }
588
 
589
        $beforecm = null;
590
        if (!empty($targetcmid)) {
591
            $this->validate_cms($course, [$targetcmid], __FUNCTION__);
592
            $beforecm = $modinfo->get_cm($targetcmid);
593
            $targetsection = $modinfo->get_section_info_by_id($beforecm->section, MUST_EXIST);
594
        }
595
 
596
        // Duplicate course modules.
597
        $affectedcmids = [];
598
        foreach ($cms as $cm) {
599
            if ($newcm = duplicate_module($course, $cm)) {
600
                if ($targetsection) {
601
                    moveto_module($newcm, $targetsection, $beforecm);
602
                } else {
603
                    $affectedcmids[] = $newcm->id;
604
                }
605
            }
606
        }
607
 
608
        if ($targetsection) {
609
            $this->section_state($updates, $course, [$targetsection->id]);
610
        } else {
611
            $this->cm_state($updates, $course, $affectedcmids);
612
        }
613
    }
614
 
615
    /**
616
     * Delete course cms.
617
     *
618
     * @param stateupdates $updates the affected course elements track
619
     * @param stdClass $course the course object
620
     * @param int[] $ids section ids
621
     * @param int $targetsectionid not used
622
     * @param int $targetcmid not used
623
     */
624
    public function cm_delete(
625
        stateupdates $updates,
626
        stdClass $course,
627
        array $ids = [],
628
        ?int $targetsectionid = null,
629
        ?int $targetcmid = null
630
    ): void {
631
 
632
        $this->validate_cms($course, $ids, __FUNCTION__, ['moodle/course:manageactivities']);
633
 
634
        $format = course_get_format($course->id);
635
        $modinfo = get_fast_modinfo($course);
636
        $affectedsections = [];
637
 
638
        $cms = $this->get_cm_info($modinfo, $ids);
639
        foreach ($cms as $cm) {
640
            $section = $cm->get_section_info();
641
            $affectedsections[$section->id] = $section;
642
            $format->delete_module($cm, true);
643
            $updates->add_cm_remove($cm->id);
644
        }
645
 
646
        foreach ($affectedsections as $sectionid => $section) {
647
            $updates->add_section_put($sectionid);
648
        }
649
    }
650
 
651
    /**
652
     * Move course cms to the right. Indent = 1.
653
     *
654
     * @param stateupdates $updates the affected course elements track
655
     * @param stdClass $course the course object
656
     * @param int[] $ids cm ids
657
     * @param int $targetsectionid not used
658
     * @param int $targetcmid not used
659
     */
660
    public function cm_moveright(
661
        stateupdates $updates,
662
        stdClass $course,
663
        array $ids = [],
664
        ?int $targetsectionid = null,
665
        ?int $targetcmid = null
666
    ): void {
667
        $this->set_cm_indentation($updates, $course, $ids, 1);
668
    }
669
 
670
    /**
671
     * Move course cms to the left. Indent = 0.
672
     *
673
     * @param stateupdates $updates the affected course elements track
674
     * @param stdClass $course the course object
675
     * @param int[] $ids cm ids
676
     * @param int $targetsectionid not used
677
     * @param int $targetcmid not used
678
     */
679
    public function cm_moveleft(
680
        stateupdates $updates,
681
        stdClass $course,
682
        array $ids = [],
683
        ?int $targetsectionid = null,
684
        ?int $targetcmid = null
685
    ): void {
686
        $this->set_cm_indentation($updates, $course, $ids, 0);
687
    }
688
 
689
    /**
690
     * Internal method to define the cm indentation level.
691
     *
692
     * @param stateupdates $updates the affected course elements track
693
     * @param stdClass $course the course object
694
     * @param int[] $ids cm ids
695
     * @param int $indent new value for indentation
696
     */
697
    protected function set_cm_indentation(
698
        stateupdates $updates,
699
        stdClass $course,
700
        array $ids,
701
        int $indent
702
    ): void {
703
        global $DB;
704
 
705
        $this->validate_cms($course, $ids, __FUNCTION__, ['moodle/course:manageactivities']);
706
        $modinfo = get_fast_modinfo($course);
707
        $cms = $this->get_cm_info($modinfo, $ids);
708
        list($insql, $inparams) = $DB->get_in_or_equal(array_keys($cms), SQL_PARAMS_NAMED);
709
        $DB->set_field_select('course_modules', 'indent', $indent, "id $insql", $inparams);
710
        rebuild_course_cache($course->id, false, true);
711
        foreach ($cms as $cm) {
712
            $modcontext = context_module::instance($cm->id);
713
            course_module_updated::create_from_cm($cm, $modcontext)->trigger();
714
            $updates->add_cm_put($cm->id);
715
        }
716
    }
717
 
718
    /**
719
     * Set NOGROUPS const value to cms groupmode.
720
     *
721
     * @param stateupdates $updates the affected course elements track
722
     * @param stdClass $course the course object
723
     * @param int[] $ids cm ids
724
     * @param int $targetsectionid not used
725
     * @param int $targetcmid not used
726
     */
727
    public function cm_nogroups(
728
        stateupdates $updates,
729
        stdClass $course,
730
        array $ids = [],
731
        ?int $targetsectionid = null,
732
        ?int $targetcmid = null
733
    ): void {
734
        $this->set_cm_groupmode($updates, $course, $ids, NOGROUPS);
735
    }
736
 
737
    /**
738
     * Set VISIBLEGROUPS const value to cms groupmode.
739
     *
740
     * @param stateupdates $updates the affected course elements track
741
     * @param stdClass $course the course object
742
     * @param int[] $ids cm ids
743
     * @param int $targetsectionid not used
744
     * @param int $targetcmid not used
745
     */
746
    public function cm_visiblegroups(
747
        stateupdates $updates,
748
        stdClass $course,
749
        array $ids = [],
750
        ?int $targetsectionid = null,
751
        ?int $targetcmid = null
752
    ): void {
753
        $this->set_cm_groupmode($updates, $course, $ids, VISIBLEGROUPS);
754
    }
755
 
756
    /**
757
     * Set SEPARATEGROUPS const value to cms groupmode.
758
     *
759
     * @param stateupdates $updates the affected course elements track
760
     * @param stdClass $course the course object
761
     * @param int[] $ids cm ids
762
     * @param int $targetsectionid not used
763
     * @param int $targetcmid not used
764
     */
765
    public function cm_separategroups(
766
        stateupdates $updates,
767
        stdClass $course,
768
        array $ids = [],
769
        ?int $targetsectionid = null,
770
        ?int $targetcmid = null
771
    ): void {
772
        $this->set_cm_groupmode($updates, $course, $ids, SEPARATEGROUPS);
773
    }
774
 
775
    /**
776
     * Internal method to define the cm groupmode value.
777
     *
778
     * @param stateupdates $updates the affected course elements track
779
     * @param stdClass $course the course object
780
     * @param int[] $ids cm ids
781
     * @param int $groupmode new value for groupmode: NOGROUPS, SEPARATEGROUPS, VISIBLEGROUPS
782
     */
783
    protected function set_cm_groupmode(
784
        stateupdates $updates,
785
        stdClass $course,
786
        array $ids,
787
        int $groupmode
788
    ): void {
789
        global $DB;
790
 
791
        $this->validate_cms($course, $ids, __FUNCTION__, ['moodle/course:manageactivities']);
792
        $modinfo = get_fast_modinfo($course);
793
        $cms = $this->get_cm_info($modinfo, $ids);
794
        list($insql, $inparams) = $DB->get_in_or_equal(array_keys($cms), SQL_PARAMS_NAMED);
795
        $DB->set_field_select('course_modules', 'groupmode', $groupmode, "id $insql", $inparams);
796
        rebuild_course_cache($course->id, false, true);
797
        foreach ($cms as $cm) {
798
            $modcontext = context_module::instance($cm->id);
799
            course_module_updated::create_from_cm($cm, $modcontext)->trigger();
800
            $updates->add_cm_put($cm->id);
801
        }
802
    }
803
 
804
    /**
805
     * Extract several cm_info from the course_modinfo.
806
     *
807
     * @param course_modinfo $modinfo the course modinfo.
808
     * @param int[] $ids the course modules $ids
809
     * @return cm_info[] the extracted cm_info objects
810
     */
811
    protected function get_cm_info(course_modinfo $modinfo, array $ids): array {
812
        $cms = [];
813
        foreach ($ids as $cmid) {
814
            $cms[$cmid] = $modinfo->get_cm($cmid);
815
        }
816
        return $cms;
817
    }
818
 
819
    /**
820
     * Extract several section_info from the course_modinfo.
821
     *
822
     * @param course_modinfo $modinfo the course modinfo.
823
     * @param int[] $ids the course modules $ids
824
     * @return section_info[] the extracted section_info objects
825
     */
826
    protected function get_section_info(course_modinfo $modinfo, array $ids): array {
827
        $sections = [];
828
        foreach ($ids as $sectionid) {
829
            $sections[$sectionid] = $modinfo->get_section_info_by_id($sectionid);
830
        }
831
        return $sections;
832
    }
833
 
834
    /**
835
     * Update the course content section state to collapse.
836
     *
837
     * @param stateupdates $updates the affected course elements track
838
     * @param stdClass $course the course object
839
     * @param int[] $ids the collapsed section ids
840
     * @param int $targetsectionid not used
841
     * @param int $targetcmid not used
842
     */
843
    public function section_content_collapsed(
844
        stateupdates $updates,
845
        stdClass $course,
846
        array $ids = [],
847
        ?int $targetsectionid = null,
848
        ?int $targetcmid = null,
849
    ): void {
850
        if (!empty($ids)) {
851
            $this->validate_sections($course, $ids, __FUNCTION__);
852
        }
853
        $format = course_get_format($course->id);
854
        $format->add_section_preference_ids('contentcollapsed', $ids);
855
    }
856
 
857
    /**
858
     * Update the course content section state to expand.
859
     *
860
     * @param stateupdates $updates the affected course elements track
861
     * @param stdClass $course the course object
862
     * @param int[] $ids the collapsed section ids
863
     * @param int|null $targetsectionid not used
864
     * @param int|null $targetcmid not used
865
     */
866
    public function section_content_expanded(
867
        stateupdates $updates,
868
        stdClass $course,
869
        array $ids = [],
870
        ?int $targetsectionid = null,
871
        ?int $targetcmid = null,
872
    ): void {
873
        if (!empty($ids)) {
874
            $this->validate_sections($course, $ids, __FUNCTION__);
875
        }
876
        $format = course_get_format($course->id);
877
        $format->remove_section_preference_ids('contentcollapsed', $ids);
878
    }
879
 
880
    /**
881
     * Update the course index section state to collapse.
882
     *
883
     * @param stateupdates $updates the affected course elements track
884
     * @param stdClass $course the course object
885
     * @param int[] $ids the collapsed section ids
886
     * @param int $targetsectionid not used
887
     * @param int $targetcmid not used
888
     */
889
    public function section_index_collapsed(
890
        stateupdates $updates,
891
        stdClass $course,
892
        array $ids = [],
893
        ?int $targetsectionid = null,
894
        ?int $targetcmid = null,
895
    ): void {
896
        if (!empty($ids)) {
897
            $this->validate_sections($course, $ids, __FUNCTION__);
898
        }
899
        $format = course_get_format($course->id);
900
        $format->add_section_preference_ids('indexcollapsed', $ids);
901
    }
902
 
903
    /**
904
     * Update the course index section state to expand.
905
     *
906
     * @param stateupdates $updates the affected course elements track
907
     * @param stdClass $course the course object
908
     * @param int[] $ids the collapsed section ids
909
     * @param int|null $targetsectionid not used
910
     * @param int|null $targetcmid not used
911
     */
912
    public function section_index_expanded(
913
        stateupdates $updates,
914
        stdClass $course,
915
        array $ids = [],
916
        ?int $targetsectionid = null,
917
        ?int $targetcmid = null,
918
    ): void {
919
        if (!empty($ids)) {
920
            $this->validate_sections($course, $ids, __FUNCTION__);
921
        }
922
        $format = course_get_format($course->id);
923
        $format->remove_section_preference_ids('indexcollapsed', $ids);
924
    }
925
 
926
    /**
927
     * Add the update messages of the updated version of any cm and section related to the cm ids.
928
     *
929
     * This action is mainly used by legacy actions to partially update the course state when the
930
     * result of core_course_edit_module is not enough to generate the correct state data.
931
     *
932
     * @param stateupdates $updates the affected course elements track
933
     * @param stdClass $course the course object
934
     * @param int[] $ids the list of affected course module ids
935
     * @param int $targetsectionid optional target section id
936
     * @param int $targetcmid optional target cm id
937
     */
938
    public function cm_state(
939
        stateupdates $updates,
940
        stdClass $course,
941
        array $ids,
942
        ?int $targetsectionid = null,
943
        ?int $targetcmid = null
944
    ): void {
945
 
946
        // Collect all section and cm to return.
947
        $cmids = [];
948
        foreach ($ids as $cmid) {
949
            $cmids[$cmid] = true;
950
        }
951
        if ($targetcmid) {
952
            $cmids[$targetcmid] = true;
953
        }
954
 
955
        $sectionids = [];
956
        if ($targetsectionid) {
957
            $this->validate_sections($course, [$targetsectionid], __FUNCTION__);
958
            $sectionids[$targetsectionid] = true;
959
        }
960
 
961
        $this->validate_cms($course, array_keys($cmids), __FUNCTION__);
962
 
963
        $modinfo = course_modinfo::instance($course);
964
 
965
        foreach (array_keys($cmids) as $cmid) {
966
 
967
            // Add this action to updates array.
968
            $updates->add_cm_put($cmid);
969
 
970
            $cm = $modinfo->get_cm($cmid);
971
            $sectionids[$cm->section] = true;
972
        }
973
 
974
        foreach (array_keys($sectionids) as $sectionid) {
975
            $updates->add_section_put($sectionid);
976
        }
977
    }
978
 
979
    /**
980
     * Add the update messages of the updated version of any cm and section related to the section ids.
981
     *
982
     * This action is mainly used by legacy actions to partially update the course state when the
983
     * result of core_course_edit_module is not enough to generate the correct state data.
984
     *
985
     * @param stateupdates $updates the affected course elements track
986
     * @param stdClass $course the course object
987
     * @param int[] $ids the list of affected course section ids
988
     * @param int $targetsectionid optional target section id
989
     * @param int $targetcmid optional target cm id
990
     */
991
    public function section_state(
992
        stateupdates $updates,
993
        stdClass $course,
994
        array $ids,
995
        ?int $targetsectionid = null,
996
        ?int $targetcmid = null
997
    ): void {
998
 
999
        $cmids = [];
1000
        if ($targetcmid) {
1001
            $this->validate_cms($course, [$targetcmid], __FUNCTION__);
1002
            $cmids[$targetcmid] = true;
1003
        }
1004
 
1005
        $sectionids = [];
1006
        foreach ($ids as $sectionid) {
1007
            $sectionids[$sectionid] = true;
1008
        }
1009
        if ($targetsectionid) {
1010
            $sectionids[$targetsectionid] = true;
1011
        }
1012
 
1013
        $this->validate_sections($course, array_keys($sectionids), __FUNCTION__);
1014
 
1015
        $modinfo = course_modinfo::instance($course);
1016
 
1017
        foreach (array_keys($sectionids) as $sectionid) {
1018
            $sectioninfo = $modinfo->get_section_info_by_id($sectionid);
1019
            $updates->add_section_put($sectionid);
1020
            // Add cms.
1021
            if (empty($modinfo->sections[$sectioninfo->section])) {
1022
                continue;
1023
            }
1024
 
1025
            foreach ($modinfo->sections[$sectioninfo->section] as $modnumber) {
1026
                $mod = $modinfo->cms[$modnumber];
1027
                if ($mod->is_visible_on_course_page()) {
1028
                    $cmids[$mod->id] = true;
1029
                }
1030
            }
1031
        }
1032
 
1033
        foreach (array_keys($cmids) as $cmid) {
1034
            // Add this action to updates array.
1035
            $updates->add_cm_put($cmid);
1036
        }
1037
    }
1038
 
1039
    /**
1040
     * Add all the update messages from the complete course state.
1041
     *
1042
     * This action is mainly used by legacy actions to partially update the course state when the
1043
     * result of core_course_edit_module is not enough to generate the correct state data.
1044
     *
1045
     * @param stateupdates $updates the affected course elements track
1046
     * @param stdClass $course the course object
1047
     * @param int[] $ids the list of affected course module ids (not used)
1048
     * @param int $targetsectionid optional target section id (not used)
1049
     * @param int $targetcmid optional target cm id (not used)
1050
     */
1051
    public function course_state(
1052
        stateupdates $updates,
1053
        stdClass $course,
1054
        array $ids = [],
1055
        ?int $targetsectionid = null,
1056
        ?int $targetcmid = null
1057
    ): void {
1058
 
1059
        $modinfo = course_modinfo::instance($course);
1060
 
1061
        $updates->add_course_put();
1062
 
1063
        // Add sections updates.
1064
        $sections = $modinfo->get_section_info_all();
1065
        $sectionids = [];
1066
        foreach ($sections as $sectioninfo) {
1067
            $sectionids[] = $sectioninfo->id;
1068
        }
1069
        if (!empty($sectionids)) {
1070
            $this->section_state($updates, $course, $sectionids);
1071
        }
1072
    }
1073
 
1074
    /**
1075
     * Checks related to sections: course format support them, all given sections exist and topic 0 is not included.
1076
     *
1077
     * @param stdClass $course The course where given $sectionids belong.
1078
     * @param array $sectionids List of sections to validate.
1079
     * @param string|null $info additional information in case of error (default null).
1080
     * @throws moodle_exception if any id is not valid
1081
     */
1082
    protected function validate_sections(stdClass $course, array $sectionids, ?string $info = null): void {
1083
        global $DB;
1084
 
1085
        if (empty($sectionids)) {
1086
            throw new moodle_exception('emptysectionids', 'core', null, $info);
1087
        }
1088
 
1089
        // No section actions are allowed if course format does not support sections.
1090
        $courseformat = course_get_format($course->id);
1091
        if (!$courseformat->uses_sections()) {
1092
            throw new moodle_exception('sectionactionnotsupported', 'core', null, $info);
1093
        }
1094
 
1095
        list($insql, $inparams) = $DB->get_in_or_equal($sectionids, SQL_PARAMS_NAMED);
1096
 
1097
        // Check if all the given sections exist.
1098
        $couintsections = $DB->count_records_select('course_sections', "id $insql", $inparams);
1099
        if ($couintsections != count($sectionids)) {
1100
            throw new moodle_exception('unexistingsectionid', 'core', null, $info);
1101
        }
1102
    }
1103
 
1104
    /**
1105
     * Checks related to course modules: all given cm exist and the user has the required capabilities.
1106
     *
1107
     * @param stdClass $course The course where given $cmids belong.
1108
     * @param array $cmids List of course module ids to validate.
1109
     * @param string $info additional information in case of error.
1110
     * @param array $capabilities optional capabilities checks per each cm context.
1111
     * @throws moodle_exception if any id is not valid
1112
     */
1113
    protected function validate_cms(stdClass $course, array $cmids, ?string $info = null, array $capabilities = []): void {
1114
 
1115
        if (empty($cmids)) {
1116
            throw new moodle_exception('emptycmids', 'core', null, $info);
1117
        }
1118
 
1119
        $moduleinfo = get_fast_modinfo($course->id);
1120
        $intersect = array_intersect($cmids, array_keys($moduleinfo->get_cms()));
1121
        if (count($cmids) != count($intersect)) {
1122
            throw new moodle_exception('unexistingcmid', 'core', null, $info);
1123
        }
1124
        if (!empty($capabilities)) {
1125
            foreach ($cmids as $cmid) {
1126
                $modcontext = context_module::instance($cmid);
1127
                require_all_capabilities($capabilities, $modcontext);
1128
            }
1129
        }
1130
    }
1131
}