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\local;
18
 
19
use section_info;
20
use stdClass;
21
use core\event\course_module_updated;
22
use core\event\course_section_deleted;
23
 
24
/**
25
 * Section course format actions.
26
 *
27
 * @package    core_courseformat
28
 * @copyright  2023 Ferran Recio <ferran@moodle.com>
29
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30
 */
31
class sectionactions extends baseactions {
32
    /**
33
     * Create a course section using a record object.
34
     *
35
     * If $fields->section is not set, the section is added to the end of the course.
36
     *
37
     * @param stdClass $fields the fields to set on the section
38
     * @param bool $skipcheck the position check has already been made and we know it can be used
39
     * @return stdClass the created section record
40
     */
41
    protected function create_from_object(stdClass $fields, bool $skipcheck = false): stdClass {
42
        global $DB;
43
        [
44
            'position' => $position,
45
            'lastsection' => $lastsection,
46
        ] = $this->calculate_positions($fields, $skipcheck);
47
 
48
        // First add section to the end.
49
        $sectionrecord = (object) [
50
            'course' => $this->course->id,
51
            'section' => $lastsection + 1,
52
            'summary' => $fields->summary ?? '',
53
            'summaryformat' => $fields->summaryformat ?? FORMAT_HTML,
54
            'sequence' => '',
55
            'name' => $fields->name ?? null,
56
            'visible' => $fields->visible ?? 1,
57
            'availability' => null,
58
            'component' => $fields->component ?? null,
59
            'itemid' => $fields->itemid ?? null,
60
            'timemodified' => time(),
61
        ];
62
        $sectionrecord->id = $DB->insert_record("course_sections", $sectionrecord);
63
 
64
        // Now move it to the specified position.
65
        if ($position > 0 && $position <= $lastsection) {
66
            move_section_to($this->course, $sectionrecord->section, $position, true);
67
            $sectionrecord->section = $position;
68
        }
69
 
70
        \core\event\course_section_created::create_from_section($sectionrecord)->trigger();
71
 
72
        rebuild_course_cache($this->course->id, true);
73
        return $sectionrecord;
74
    }
75
 
76
    /**
77
     * Calculate the position and lastsection values.
78
     *
79
     * Each section number must be unique inside a course. However, the section creation is not always
80
     * explicit about the final position. By default, regular sections are created at the last position.
81
     * However, delegated section can alter that order, because all delegated sections should have higher
82
     * numbers. Apart, restore operations can also create sections with a forced specific number.
83
     *
84
     * This method returns what is the best position for a new section data and, also, what is the current
85
     * last section number. The last section is needed to decide if the new section must be moved or not after
86
     * insertion.
87
     *
88
     * @param stdClass $fields the fields to set on the section
89
     * @param bool $skipcheck the position check has already been made and we know it can be used
90
     * @return array with the new section position (position key) and the course last section value (lastsection key)
91
     */
92
    private function calculate_positions($fields, $skipcheck): array {
93
        if (!isset($fields->section)) {
94
            $skipcheck = false;
95
        }
96
        if ($skipcheck) {
97
            return [
98
                'position' => $fields->section,
99
                'lastsection' => $fields->section - 1,
100
            ];
101
        }
102
 
103
        $lastsection = $this->get_last_section_number();
104
        if (!empty($fields->component)) {
105
            return [
106
                'position' => $fields->section ?? $lastsection + 1,
107
                'lastsection' => $lastsection,
108
            ];
109
        }
110
        return [
111
            'position' => $fields->section ?? $this->get_last_section_number(false) + 1,
112
            'lastsection' => $lastsection,
113
        ];
114
    }
115
 
116
    /**
117
     * Get the last section number in the course.
118
     * @param bool $includedelegated whether to include delegated sections
119
     * @return int
120
     */
121
    protected function get_last_section_number(bool $includedelegated = true): int {
122
        global $DB;
123
 
124
        $delegtadefilter = $includedelegated ? '' : ' AND component IS NULL';
125
 
126
        return (int) $DB->get_field_sql(
127
            'SELECT max(section) from {course_sections} WHERE course = ?' . $delegtadefilter,
128
            [$this->course->id]
129
        );
130
    }
131
 
132
    /**
133
     * Create a delegated section.
134
     *
135
     * @param string $component the name of the plugin
136
     * @param int|null $itemid the id of the delegated section
137
     * @param stdClass|null $fields the fields to set on the section
138
     * @return section_info the created section
139
     */
140
    public function create_delegated(
141
        string $component,
142
        ?int $itemid = null,
143
        ?stdClass $fields = null
144
    ): section_info {
145
        $record = ($fields) ? clone $fields : new stdClass();
146
        $record->component = $component;
147
        $record->itemid = $itemid;
148
 
149
        $record = $this->create_from_object($record);
150
        return $this->get_section_info($record->id);
151
    }
152
 
153
    /**
154
     * Creates a course section and adds it to the specified position
155
     *
156
     * This method returns a section record, not a section_info object. This prevents the regeneration
157
     * of the modinfo object each time we create a section.
158
     *
159
     * If position is greater than number of existing sections, the section is added to the end.
160
     * This will become sectionnum of the new section. All existing sections at this or bigger
161
     * position will be shifted down.
162
     *
163
     * @param int $position The position to add to, 0 means to the end.
164
     * @param bool $skipcheck the check has already been made and we know that the section with this position does not exist
165
     * @return stdClass created section object)
166
     */
167
    public function create(int $position = 0, bool $skipcheck = false): stdClass {
168
        $record = (object) [
169
            'section' => $position,
170
        ];
171
        return $this->create_from_object($record, $skipcheck);
172
    }
173
 
174
    /**
175
     * Create course sections if they are not created yet.
176
     *
177
     * The calculations will ignore sections delegated to components.
178
     * If the section is created, all delegated sections will be pushed down.
179
     *
180
     * @param int[] $sectionnums the section numbers to create
181
     * @return bool whether any section was created
182
     */
183
    public function create_if_missing(array $sectionnums): bool {
184
        $result = false;
185
        $modinfo = get_fast_modinfo($this->course);
186
        // Ensure we add the sections in order.
187
        sort($sectionnums);
188
        // Delegated sections must be displaced when creating a regular section.
189
        $skipcheck = !$modinfo->has_delegated_sections();
190
 
191
        $sections = $modinfo->get_section_info_all();
192
        foreach ($sectionnums as $sectionnum) {
193
            if (isset($sections[$sectionnum]) && empty($sections[$sectionnum]->component)) {
194
                continue;
195
            }
196
            $this->create($sectionnum, $skipcheck);
197
            $result = true;
198
        }
199
        return $result;
200
    }
201
 
202
    /**
203
     * Delete a course section.
204
     * @param section_info $sectioninfo the section to delete.
205
     * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules.
206
     * @param bool $async whether or not to try to delete the section using an adhoc task. Async also depends on a plugin hook.
207
     * @return bool whether section was deleted
208
     */
209
    public function delete(section_info $sectioninfo, bool $forcedeleteifnotempty = true, bool $async = false): bool {
210
        // Check the 'course_module_background_deletion_recommended' hook first.
211
        // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested.
212
        // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it.
213
        // It's up to plugins to handle things like whether or not they are enabled.
214
        if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
215
            foreach ($pluginsfunction as $plugintype => $plugins) {
216
                foreach ($plugins as $pluginfunction) {
217
                    if ($pluginfunction()) {
218
                        return $this->delete_async($sectioninfo, $forcedeleteifnotempty);
219
                    }
220
                }
221
            }
222
        }
223
 
224
        return $this->delete_format_data(
225
            $sectioninfo,
226
            $forcedeleteifnotempty,
227
            $this->get_delete_event($sectioninfo)
228
        );
229
    }
230
 
231
    /**
232
     * Get the event to trigger when deleting a section.
233
     * @param section_info $sectioninfo the section to delete.
234
     * @return course_section_deleted the event to trigger
235
     */
236
    protected function get_delete_event(section_info $sectioninfo): course_section_deleted {
237
        global $DB;
238
        // Section record is needed for the event snapshot.
239
        $sectionrecord = $DB->get_record('course_sections', ['id' => $sectioninfo->id]);
240
 
241
        $format = course_get_format($this->course);
242
        $sectionname = $format->get_section_name($sectioninfo);
243
        $context = \context_course::instance($this->course->id);
244
        $event = course_section_deleted::create(
245
            [
246
                'objectid' => $sectioninfo->id,
247
                'courseid' => $this->course->id,
248
                'context' => $context,
249
                'other' => [
250
                    'sectionnum' => $sectioninfo->section,
251
                    'sectionname' => $sectionname,
252
                ],
253
            ]
254
        );
255
        $event->add_record_snapshot('course_sections', $sectionrecord);
256
        return $event;
257
    }
258
 
259
    /**
260
     * Delete a course section.
261
     * @param section_info $sectioninfo the section to delete.
262
     * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules.
263
     * @param course_section_deleted $event the event to trigger
264
     * @return bool whether section was deleted
265
     */
266
    protected function delete_format_data(
267
        section_info $sectioninfo,
268
        bool $forcedeleteifnotempty,
269
        course_section_deleted $event
270
    ): bool {
271
        $format = course_get_format($this->course);
272
        $result = $format->delete_section($sectioninfo, $forcedeleteifnotempty);
273
        if ($result) {
274
            $event->trigger();
275
        }
276
        rebuild_course_cache($this->course->id, true);
277
        return $result;
278
    }
279
 
280
 
281
 
282
    /**
283
     * Course section deletion, using an adhoc task for deletion of the modules it contains.
284
     * 1. Schedule all modules within the section for adhoc removal.
285
     * 2. Move all modules to course section 0.
286
     * 3. Delete the resulting empty section.
287
     *
288
     * @param section_info $sectioninfo the section to schedule for deletion.
289
     * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules.
290
     * @return bool true if the section was scheduled for deletion, false otherwise.
291
     */
292
    protected function delete_async(section_info $sectioninfo, bool $forcedeleteifnotempty = true): bool {
293
        global $DB, $USER;
294
 
295
        if (!$forcedeleteifnotempty && (!empty($sectioninfo->sequence) || !empty($sectioninfo->summary))) {
296
            return false;
297
        }
298
 
299
        // Event needs to be created before the section activities are moved to section 0.
300
        $event = $this->get_delete_event($sectioninfo);
301
 
302
        $affectedmods = $DB->get_records_select(
303
            'course_modules',
304
            'course = ? AND section = ? AND deletioninprogress <> ?',
305
            [$this->course->id, $sectioninfo->id, 1],
306
            '',
307
            'id'
308
        );
309
 
310
        // Flag those modules having no existing deletion flag. Some modules may have been
311
        // scheduled for deletion manually, and we don't want to create additional adhoc deletion
312
        // tasks for these. Moving them to section 0 will suffice.
313
        $DB->set_field(
314
            'course_modules',
315
            'deletioninprogress',
316
            '1',
317
            ['course' => $this->course->id, 'section' => $sectioninfo->id]
318
        );
319
 
320
        // Move all modules to section 0.
321
        $sectionzero = $DB->get_record('course_sections', ['course' => $this->course->id, 'section' => '0']);
322
        $modules = $DB->get_records('course_modules', ['section' => $sectioninfo->id], '');
323
        foreach ($modules as $mod) {
324
            moveto_module($mod, $sectionzero);
325
        }
326
 
327
        $removaltask = new \core_course\task\course_delete_modules();
328
        $data = [
329
            'cms' => $affectedmods,
330
            'userid' => $USER->id,
331
            'realuserid' => \core\session\manager::get_realuser()->id,
332
        ];
333
        $removaltask->set_custom_data($data);
334
        \core\task\manager::queue_adhoc_task($removaltask);
335
 
336
        // Ensure we have the latest section info.
337
        $sectioninfo = $this->get_section_info($sectioninfo->id);
338
        return $this->delete_format_data($sectioninfo, $forcedeleteifnotempty, $event);
339
    }
340
 
341
    /**
342
     * Update a course section.
343
     *
344
     * @param section_info $sectioninfo the section info or database record to update.
345
     * @param array|stdClass $fields the fields to update.
346
     * @return bool whether section was updated
347
     */
348
    public function update(section_info $sectioninfo, array|stdClass $fields): bool {
349
        global $DB;
350
 
351
        $courseid = $this->course->id;
352
 
353
        // Some fields can not be updated using this method.
354
        $fields = array_diff_key((array) $fields, array_flip(['id', 'course', 'section', 'sequence']));
355
        if (array_key_exists('name', $fields) && \core_text::strlen($fields['name']) > 255) {
356
            throw new \moodle_exception('maximumchars', 'moodle', '', 255);
357
        }
358
 
359
        // If the section is delegated to a component, it may control some section values.
360
        $fields = $this->preprocess_delegated_section_fields($sectioninfo, $fields);
361
 
362
        if (empty($fields)) {
363
            return false;
364
        }
365
 
366
        $fields['id'] = $sectioninfo->id;
367
        $fields['timemodified'] = time();
368
        $DB->update_record('course_sections', $fields);
369
 
370
        // We need to update the section cache before the format options are updated.
371
        \course_modinfo::purge_course_section_cache_by_id($courseid, $sectioninfo->id);
372
        rebuild_course_cache($courseid, false, true);
373
 
374
        course_get_format($courseid)->update_section_format_options($fields);
375
 
376
        $event = \core\event\course_section_updated::create(
377
            [
378
                'objectid' => $sectioninfo->id,
379
                'courseid' => $courseid,
380
                'context' => \context_course::instance($courseid),
381
                'other' => ['sectionnum' => $sectioninfo->section],
382
            ]
383
        );
384
        $event->trigger();
385
 
386
        if (isset($fields['visible'])) {
387
            $this->transfer_visibility_to_cms($sectioninfo, (bool) $fields['visible']);
388
        }
389
        return true;
390
    }
391
 
392
    /**
393
     * Transfer the visibility of the section to the course modules.
394
     *
395
     * @param section_info $sectioninfo the section info or database record to update.
396
     * @param bool $visibility the new visibility of the section.
397
     */
398
    protected function transfer_visibility_to_cms(section_info $sectioninfo, bool $visibility): void {
399
        global $DB;
400
 
401
        if (empty($sectioninfo->sequence) || $visibility == (bool) $sectioninfo->visible) {
402
            return;
403
        }
404
 
405
        $modules = explode(',', $sectioninfo->sequence);
406
        $cmids = [];
407
        foreach ($modules as $moduleid) {
408
            $cm = get_coursemodule_from_id(null, $moduleid, $this->course->id);
409
            if (!$cm) {
410
                continue;
411
            }
412
 
413
            $modupdated = false;
414
            if ($visibility) {
415
                // As we unhide the section, we use the previously saved visibility stored in visibleold.
416
                $modupdated = set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage, false);
417
            } else {
418
                // We hide the section, so we hide the module but we store the original state in visibleold.
419
                $modupdated = set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage, false);
420
                if ($modupdated) {
421
                    $DB->set_field('course_modules', 'visibleold', $cm->visible, ['id' => $moduleid]);
422
                }
423
            }
424
 
425
            if ($modupdated) {
426
                $cmids[] = $cm->id;
427
                course_module_updated::create_from_cm($cm)->trigger();
428
            }
429
        }
430
 
431
        \course_modinfo::purge_course_modules_cache($this->course->id, $cmids);
432
        rebuild_course_cache($this->course->id, false, true);
433
    }
434
 
435
    /**
436
     * Preprocess the section fields before updating a delegated section.
437
     *
438
     * @param section_info $sectioninfo the section info or database record to update.
439
     * @param array $fields the fields to update.
440
     * @return array the updated fields
441
     */
442
    protected function preprocess_delegated_section_fields(section_info $sectioninfo, array $fields): array {
443
        $delegated = $sectioninfo->get_component_instance();
444
        if (!$delegated) {
445
            return $fields;
446
        }
447
        if (array_key_exists('name', $fields)) {
448
            $fields['name'] = $delegated->preprocess_section_name($sectioninfo, $fields['name']);
449
        }
450
        return $fields;
451
    }
452
}