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