Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Contains the base definition class for any course format plugin.
19
 *
20
 * @package   core_courseformat
21
 * @copyright 2020 Ferran Recio <ferran@moodle.com>
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace core_courseformat;
26
 
27
use navigation_node;
28
use moodle_page;
29
use cm_info;
30
use core_component;
31
use course_modinfo;
32
use html_writer;
33
use section_info;
34
use context_course;
35
use editsection_form;
1441 ariadna 36
use core\exception\moodle_exception;
37
use core\exception\coding_exception;
1 efrain 38
use moodle_url;
39
use lang_string;
40
use core_external\external_api;
41
use stdClass;
42
use cache;
43
use core_courseformat\output\legacy_renderer;
44
 
45
/**
46
 * Base class for course formats
47
 *
48
 * Each course format must declare class
49
 * class format_FORMATNAME extends core_courseformat\base {}
50
 * in file lib.php
51
 *
52
 * For each course just one instance of this class is created and it will always be returned by
53
 * course_get_format($courseorid). Format may store it's specific course-dependent options in
54
 * variables of this class.
55
 *
56
 * In rare cases instance of child class may be created just for format without course id
57
 * i.e. to check if format supports AJAX.
58
 *
59
 * Also course formats may extend class section_info and overwrite
60
 * course_format::build_section_cache() to return more information about sections.
61
 *
62
 * If you are upgrading from Moodle 2.3 start with copying the class format_legacy and renaming
63
 * it to format_FORMATNAME, then move the code from your callback functions into
64
 * appropriate functions of the class.
65
 *
66
 * @package    core_courseformat
67
 * @copyright  2012 Marina Glancy
68
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
69
 */
70
abstract class base {
71
    /** @var int Id of the course in this instance (maybe 0) */
72
    protected $courseid;
73
    /** @var string format used for this course. Please note that it can be different from
74
     * course.format field if course referes to non-existing of disabled format */
75
    protected $format;
76
    /** @var stdClass course data for course object, please use course_format::get_course() */
77
    protected $course = false;
78
    /** @var array caches format options, please use course_format::get_format_options() */
79
    protected $formatoptions = array();
80
    /** @var int|null the section number in single section format, null for multiple section formats. */
81
    protected $singlesection = null;
82
    /** @var int|null the sectionid when a single section is selected, null when multiple sections are displayed. */
83
    protected $singlesectionid = null;
84
    /** @var course_modinfo the current course modinfo, please use course_format::get_modinfo() */
85
    private $modinfo = null;
86
    /** @var array cached instances */
87
    private static $instances = array();
88
    /** @var array plugin name => class name. */
89
    private static $classesforformat = array('site' => 'site');
90
    /** @var sectionmanager the format section manager. */
91
    protected $sectionmanager = null;
92
 
93
    /**
94
     * Creates a new instance of class
95
     *
96
     * Please use course_get_format($courseorid) to get an instance of the format class
97
     *
98
     * @param string $format
99
     * @param int $courseid
100
     */
101
    protected function __construct($format, $courseid) {
102
        $this->format = $format;
103
        $this->courseid = $courseid;
104
    }
105
 
106
    /**
107
     * Validates that course format exists and enabled and returns either itself or default format
108
     *
109
     * @param string $format
110
     * @return string
111
     */
112
    final protected static function get_format_or_default($format) {
113
        global $CFG;
114
        require_once($CFG->dirroot . '/course/lib.php');
115
 
116
        if (array_key_exists($format, self::$classesforformat)) {
117
            return self::$classesforformat[$format];
118
        }
119
 
120
        $plugins = get_sorted_course_formats();
121
        foreach ($plugins as $plugin) {
122
            self::$classesforformat[$plugin] = $plugin;
123
        }
124
 
125
        if (array_key_exists($format, self::$classesforformat)) {
126
            return self::$classesforformat[$format];
127
        }
128
 
129
        if (PHPUNIT_TEST && class_exists('format_' . $format)) {
130
            // Allow unittests to use non-existing course formats.
131
            return $format;
132
        }
133
 
134
        // Else return default format.
135
        $defaultformat = get_config('moodlecourse', 'format');
136
        if (!in_array($defaultformat, $plugins)) {
137
            // When default format is not set correctly, use the first available format.
138
            $defaultformat = reset($plugins);
139
        }
140
        debugging('Format plugin format_'.$format.' is not found. Using default format_'.$defaultformat, DEBUG_DEVELOPER);
141
 
142
        self::$classesforformat[$format] = $defaultformat;
143
        return $defaultformat;
144
    }
145
 
146
    /**
147
     * Get class name for the format.
148
     *
149
     * If course format xxx does not declare class format_xxx, format_legacy will be returned.
150
     * This function also includes lib.php file from corresponding format plugin
151
     *
152
     * @param string $format
153
     * @return string
154
     */
155
    final protected static function get_class_name($format) {
156
        global $CFG;
157
        static $classnames = array('site' => 'format_site');
158
        if (!isset($classnames[$format])) {
159
            $plugins = core_component::get_plugin_list('format');
160
            $usedformat = self::get_format_or_default($format);
161
            if (isset($plugins[$usedformat]) && file_exists($plugins[$usedformat].'/lib.php')) {
162
                require_once($plugins[$usedformat].'/lib.php');
163
            }
164
            $classnames[$format] = 'format_'. $usedformat;
165
            if (!class_exists($classnames[$format])) {
166
                require_once($CFG->dirroot.'/course/format/formatlegacy.php');
167
                $classnames[$format] = 'format_legacy';
168
            }
169
        }
170
        return $classnames[$format];
171
    }
172
 
173
    /**
174
     * Returns an instance of the class
175
     *
176
     * @todo MDL-35727 use MUC for caching of instances, limit the number of cached instances
177
     *
178
     * @param int|stdClass $courseorid either course id or
179
     *     an object that has the property 'format' and may contain property 'id'
180
     * @return base
181
     */
182
    final public static function instance($courseorid) {
183
        global $DB;
184
        if (!is_object($courseorid)) {
185
            $courseid = (int)$courseorid;
186
            if ($courseid && isset(self::$instances[$courseid]) && count(self::$instances[$courseid]) == 1) {
187
                $formats = array_keys(self::$instances[$courseid]);
188
                $format = reset($formats);
189
            } else {
190
                $format = $DB->get_field('course', 'format', array('id' => $courseid), MUST_EXIST);
191
            }
192
        } else {
193
            $format = $courseorid->format;
194
            if (isset($courseorid->id)) {
195
                $courseid = clean_param($courseorid->id, PARAM_INT);
196
            } else {
197
                $courseid = 0;
198
            }
199
        }
200
        // Validate that format exists and enabled, use default otherwise.
201
        $format = self::get_format_or_default($format);
202
        if (!isset(self::$instances[$courseid][$format])) {
203
            $classname = self::get_class_name($format);
204
            self::$instances[$courseid][$format] = new $classname($format, $courseid);
205
        }
206
        return self::$instances[$courseid][$format];
207
    }
208
 
209
    /**
210
     * Resets cache for the course (or all caches)
211
     *
212
     * To be called from rebuild_course_cache()
213
     *
214
     * @param int $courseid
215
     */
216
    final public static function reset_course_cache($courseid = 0) {
217
        if ($courseid) {
218
            if (isset(self::$instances[$courseid])) {
219
                foreach (self::$instances[$courseid] as $format => $object) {
220
                    // In case somebody keeps the reference to course format object.
221
                    self::$instances[$courseid][$format]->course = false;
222
                    self::$instances[$courseid][$format]->formatoptions = array();
223
                    self::$instances[$courseid][$format]->modinfo = null;
224
                }
225
                unset(self::$instances[$courseid]);
226
            }
227
        } else {
228
            self::$instances = array();
229
        }
230
    }
231
    /**
232
     * Reset the current user for all courses.
233
     *
234
     * The course format cache resets every time the course cache resets but
235
     * also when the user changes their language, all course editors
236
     *
237
     * @return void
238
     */
239
    public static function session_cache_reset_all(): void {
240
        $statecache = cache::make('core', 'courseeditorstate');
241
        $statecache->purge();
242
    }
243
 
244
    /**
245
     * Reset the current user course format cache.
246
     *
247
     * The course format cache resets every time the course cache resets but
248
     * also when the user changes their course format preference, complete
249
     * an activity...
250
     *
251
     * @param stdClass $course the course object
252
     * @return string the new statekey
253
     */
254
    public static function session_cache_reset(stdClass $course): string {
255
        $statecache = cache::make('core', 'courseeditorstate');
256
        $newkey = $course->cacherev . '_' . time();
257
        $statecache->set($course->id, $newkey);
258
        return $newkey;
259
    }
260
 
261
    /**
262
     * Return the current user course format cache key.
263
     *
264
     * The course format session cache can be used to cache the
265
     * user course representation. The statekey will be reset when the
266
     * the course state changes. For example when the course is edited,
267
     * the user completes an activity or simply some course preference
268
     * like collapsing a section happens.
269
     *
270
     * @param stdClass $course the course object
271
     * @return string the current statekey
272
     */
273
    public static function session_cache(stdClass $course): string {
274
        $statecache = cache::make('core', 'courseeditorstate');
275
        $statekey = $statecache->get($course->id);
276
        // Validate the statekey code.
277
        if (preg_match('/^[0-9]+_[0-9]+$/', $statekey)) {
278
            list($cacherev) = explode('_', $statekey);
279
            if ($cacherev == $course->cacherev) {
280
                return $statekey;
281
            }
282
        }
283
        return self::session_cache_reset($course);
284
    }
285
 
286
    /**
1441 ariadna 287
     * Prune a course state cache for all open sessions.
288
     *
289
     * Most course edits does not require to invalidate the cache for all users
290
     * because the cache relies on the course cacherev value. However, there are
291
     * actions like editing the groups that do not change the course cacherev.
292
     *
293
     * @param \stdClass $course
294
     * @return void
295
     */
296
    public static function invalidate_all_session_caches_for_course(stdClass $course): void {
297
        \cache_helper::invalidate_by_event('changesincoursestate', [$course->id]);
298
    }
299
 
300
    /**
1 efrain 301
     * Returns the format name used by this course
302
     *
303
     * @return string
304
     */
305
    final public function get_format() {
306
        return $this->format;
307
    }
308
 
309
    /**
310
     * Returns id of the course (0 if course is not specified)
311
     *
312
     * @return int
313
     */
314
    final public function get_courseid() {
315
        return $this->courseid;
316
    }
317
 
318
    /**
319
     * Returns the course context.
320
     *
321
     * @return context_course the course context
322
     */
323
    final public function get_context(): context_course {
324
        return context_course::instance($this->courseid);
325
    }
326
 
327
    /**
328
     * Returns a record from course database table plus additional fields
329
     * that course format defines
330
     *
331
     * @return ?stdClass
332
     */
333
    public function get_course() {
334
        global $DB;
335
        if (!$this->courseid) {
336
            return null;
337
        }
338
        if ($this->course === false) {
339
            $this->course = get_course($this->courseid);
340
            $options = $this->get_format_options();
341
            $dbcoursecolumns = null;
342
            foreach ($options as $optionname => $optionvalue) {
343
                if (isset($this->course->$optionname)) {
344
                    // Course format options must not have the same names as existing columns in db table "course".
345
                    if (!isset($dbcoursecolumns)) {
346
                        $dbcoursecolumns = $DB->get_columns('course');
347
                    }
348
                    if (isset($dbcoursecolumns[$optionname])) {
349
                        debugging('The option name '.$optionname.' in course format '.$this->format.
350
                            ' is invalid because the field with the same name exists in {course} table',
351
                            DEBUG_DEVELOPER);
352
                        continue;
353
                    }
354
                }
355
                $this->course->$optionname = $optionvalue;
356
            }
357
        }
358
        return $this->course;
359
    }
360
 
361
    /**
362
     * Get the course display value for the current course.
363
     *
364
     * Formats extending topics or weeks will use coursedisplay as this setting name
365
     * so they don't need to override the method. However, if the format uses a different
366
     * display logic it must override this method to ensure the core renderers know
367
     * if a COURSE_DISPLAY_MULTIPAGE or COURSE_DISPLAY_SINGLEPAGE is being used.
368
     *
369
     * @return int The current value (COURSE_DISPLAY_MULTIPAGE or COURSE_DISPLAY_SINGLEPAGE)
370
     */
371
    public function get_course_display(): int {
372
        return $this->get_course()->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE;
373
    }
374
 
375
    /**
376
     * Return the current course modinfo.
377
     *
378
     * This method is used mainly by the output components to avoid unnecesary get_fast_modinfo calls.
379
     *
380
     * @return course_modinfo
381
     */
382
    public function get_modinfo(): course_modinfo {
383
        global $USER;
384
        if ($this->modinfo === null) {
385
            $this->modinfo = course_modinfo::instance($this->courseid, $USER->id);
386
        }
387
        return $this->modinfo;
388
    }
389
 
390
    /**
1441 ariadna 391
     * Return a format state updates instance.
392
     */
393
    public function get_stateupdates_instance(): \core_courseformat\stateupdates {
394
        $defaultupdatesclass = 'core_courseformat\\stateupdates';
395
        $updatesclass = 'format_' . $this->format . '\\courseformat\\stateupdates';
396
        if (!class_exists($updatesclass)) {
397
            $updatesclass = $defaultupdatesclass;
398
        }
399
 
400
        $updates = new $updatesclass($this);
401
        if (!is_a($updates, $defaultupdatesclass)) {
402
            throw new coding_exception("The \"$updatesclass\" class must extend \"$defaultupdatesclass\"");
403
        }
404
 
405
        return $updates;
406
    }
407
 
408
    /**
409
     * Return a format state actions instance.
410
     * @return \core_courseformat\stateactions
411
     */
412
    public function get_stateactions_instance(): \core_courseformat\stateactions {
413
        // Get the actions class from the course format.
414
        $actionsclass = 'format_'. $this->format.'\\courseformat\\stateactions';
415
        if (!class_exists($actionsclass)) {
416
            $actionsclass = 'core_courseformat\\stateactions';
417
        }
418
        return new $actionsclass();
419
    }
420
 
421
    /**
1 efrain 422
     * Method used in the rendered and during backup instead of legacy 'numsections'
423
     *
424
     * Default renderer will treat sections with sectionnumber greater that the value returned by this
425
     * method as "orphaned" and not display them on the course page unless in editing mode.
426
     * Backup will store this value as 'numsections'.
427
     *
428
     * This method ensures that 3rd party course format plugins that still use 'numsections' continue to
429
     * work but at the same time we no longer expect formats to have 'numsections' property.
430
     *
431
     * @return int The last section number, or -1 if sections are entirely missing
432
     */
433
    public function get_last_section_number() {
434
        $course = $this->get_course();
435
        if (isset($course->numsections)) {
436
            return $course->numsections;
437
        }
438
        $modinfo = get_fast_modinfo($course);
439
        $sections = $modinfo->get_section_info_all();
440
 
441
        // Sections seem to be missing entirely. Avoid subsequent errors and return early.
442
        if (count($sections) === 0) {
443
            return -1;
444
        }
445
 
446
        return (int)max(array_keys($sections));
447
    }
448
 
449
    /**
450
     * Method used to get the maximum number of sections for this course format.
451
     * @return int
452
     */
453
    public function get_max_sections() {
454
        $maxsections = get_config('moodlecourse', 'maxsections');
455
        if (!isset($maxsections) || !is_numeric($maxsections)) {
456
            $maxsections = 52;
457
        }
458
        return $maxsections;
459
    }
460
 
461
    /**
462
     * Returns true if the course has a front page.
463
     *
464
     * This function is called to determine if the course has a view page, whether or not
465
     * it contains a listing of activities. It can be useful to set this to false when the course
466
     * format has only one activity and ignores the course page. Or if there are multiple
467
     * activities but no page to see the centralised information.
468
     *
469
     * Initially this was created to know if forms should add a button to return to the course page.
470
     * So if 'Return to course' does not make sense in your format your should probably return false.
471
     *
472
     * @return boolean
473
     * @since Moodle 2.6
474
     */
475
    public function has_view_page() {
476
        return true;
477
    }
478
 
479
    /**
480
     * Generate the title for this section page.
481
     *
482
     * @return string the page title
483
     */
484
    public function page_title(): string {
485
        global $PAGE;
486
        return $PAGE->title;
487
    }
488
 
489
    /**
490
     * Returns true if this course format uses sections
491
     *
492
     * This function may be called without specifying the course id
493
     * i.e. in course_format_uses_sections()
494
     *
495
     * Developers, note that if course format does use sections there should be defined a language
496
     * string with the name 'sectionname' defining what the section relates to in the format, i.e.
497
     * $string['sectionname'] = 'Topic';
498
     * or
499
     * $string['sectionname'] = 'Week';
500
     *
501
     * @return bool
502
     */
503
    public function uses_sections() {
504
        return false;
505
    }
506
 
507
    /**
508
     * Returns true if this course format uses course index
509
     *
510
     * This function may be called without specifying the course id
511
     * i.e. in course_index_drawer()
512
     *
513
     * @return bool
514
     */
515
    public function uses_course_index() {
516
        return false;
517
    }
518
 
519
    /**
520
     * Returns true if this course format uses activity indentation.
521
     *
522
     * @return bool if the course format uses indentation.
523
     */
524
    public function uses_indentation(): bool {
525
        return true;
526
    }
527
 
528
    /**
529
     * Returns a list of sections used in the course
530
     *
531
     * This is a shortcut to get_fast_modinfo()->get_section_info_all()
532
     * @see get_fast_modinfo()
533
     * @see course_modinfo::get_section_info_all()
534
     *
535
     * @return array of section_info objects
536
     */
537
    final public function get_sections() {
538
        if ($course = $this->get_course()) {
539
            $modinfo = get_fast_modinfo($course);
540
            return $modinfo->get_section_info_all();
541
        }
542
        return array();
543
    }
544
 
545
    /**
546
     * Returns information about section used in course
547
     *
548
     * @param int|stdClass $section either section number (field course_section.section) or row from course_section table
549
     * @param int $strictness
550
     * @return ?section_info
551
     */
552
    final public function get_section($section, $strictness = IGNORE_MISSING) {
553
        if (is_object($section)) {
554
            $sectionnum = $section->section;
555
        } else {
556
            $sectionnum = $section;
557
        }
558
        $sections = $this->get_sections();
559
        if (array_key_exists($sectionnum, $sections)) {
560
            return $sections[$sectionnum];
561
        }
562
        if ($strictness == MUST_EXIST) {
563
            throw new moodle_exception('sectionnotexist');
564
        }
565
        return null;
566
    }
567
 
568
    /**
569
     * Returns the display name of the given section that the course prefers.
570
     *
571
     * @param int|stdClass|section_info $section Section object from database or just field course_sections.section
572
     * @return string Display name that the course format prefers, e.g. "Topic 2"
573
     */
574
    public function get_section_name($section) {
575
        if (is_object($section)) {
576
            $sectionnum = $section->section;
577
        } else {
578
            $sectionnum = $section;
579
        }
580
 
581
        if (get_string_manager()->string_exists('sectionname', 'format_' . $this->format)) {
582
            return get_string('sectionname', 'format_' . $this->format) . ' ' . $sectionnum;
583
        }
584
 
585
        // Return an empty string if there's no available section name string for the given format.
586
        return '';
587
    }
588
 
589
    /**
590
     * Returns the default section using course_format's implementation of get_section_name.
591
     *
592
     * @param int|stdClass $section Section object from database or just field course_sections section
593
     * @return string The default value for the section name based on the given course format.
594
     */
595
    public function get_default_section_name($section) {
596
        return self::get_section_name($section);
597
    }
598
 
599
    /**
1441 ariadna 600
     * Returns the generic name for sections in this course format.
601
     *
602
     * @return string
603
     */
604
    public function get_generic_section_name() {
605
        if (get_string_manager()->string_exists('sectionname', 'format_' . $this->format)) {
606
            return get_string('sectionname', 'format_' . $this->format);
607
        }
608
        return get_string('section');
609
    }
610
 
611
    /**
1 efrain 612
     * Returns the name for the highlighted section.
613
     *
614
     * @return string The name for the highlighted section based on the given course format.
615
     */
616
    public function get_section_highlighted_name(): string {
617
        return get_string('highlighted');
618
    }
619
 
620
    /**
621
     * Set if the current format instance will show multiple sections or an individual one.
622
     *
623
     * Some formats has the hability to swith from one section to multiple sections per page.
624
     *
625
     * @param int|null $sectionid null for all sections or a sectionid.
626
     */
627
    public function set_sectionid(?int $sectionid): void {
628
        if ($sectionid === null) {
629
            $this->singlesection = null;
630
            $this->singlesectionid = null;
631
            return;
632
        }
633
 
634
        $modinfo = get_fast_modinfo($this->courseid);
635
        $sectioninfo = $modinfo->get_section_info_by_id($sectionid);
636
        if ($sectioninfo === null) {
637
            throw new coding_exception('Invalid sectionid: '. $sectionid);
638
        }
639
 
640
        $this->singlesection = $sectioninfo->section;
641
        $this->singlesectionid = $sectionid;
642
    }
643
 
644
    /**
645
     * Get if the current format instance will show multiple sections or an individual one.
646
     *
647
     * Some formats has the hability to swith from one section to multiple sections per page,
648
     * output components will use this method to know if the current display is a single or
649
     * multiple sections.
650
     *
651
     * @return int|null null for all sections or the sectionid.
652
     */
653
    public function get_sectionid(): ?int {
654
        return $this->singlesectionid;
655
    }
656
 
657
    /**
658
     * @deprecated Since 4.4. Use set_sectionnum instead.
659
     */
1441 ariadna 660
    #[\core\attribute\deprecated(
661
        replacement: 'base::set_sectionnum',
662
        since: '4.4',
663
        mdl: 'MDL-80248',
664
        final: true,
665
    )]
1 efrain 666
    public function set_section_number(int $singlesection): void {
1441 ariadna 667
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 668
    }
669
 
670
    /**
671
     * Set the current section number to display.
672
     * Some formats has the hability to swith from one section to multiple sections per page.
673
     *
674
     * @since Moodle 4.4
675
     * @param int|null $sectionnum null for all sections or a sectionid.
676
     */
677
    public function set_sectionnum(?int $sectionnum): void {
678
        if ($sectionnum === null) {
679
            $this->singlesection = null;
680
            $this->singlesectionid = null;
681
            return;
682
        }
683
 
684
        $modinfo = get_fast_modinfo($this->courseid);
685
        $sectioninfo = $modinfo->get_section_info($sectionnum);
686
        if ($sectioninfo === null) {
687
            throw new coding_exception('Invalid sectionnum: '. $sectionnum);
688
        }
689
 
690
        $this->singlesection = $sectionnum;
691
        $this->singlesectionid = $sectioninfo->id;
692
    }
693
 
694
    /**
695
     * Get the current section number to display.
696
     * Some formats has the hability to swith from one section to multiple sections per page.
697
     *
698
     * @since Moodle 4.4
699
     * @return int|null the current section number or null when there is no single section.
700
     */
701
    public function get_sectionnum(): ?int {
702
        return $this->singlesection;
703
    }
704
 
705
    /**
706
     * @deprecated Since 4.4. Use get_sectionnum instead.
707
     */
1441 ariadna 708
    #[\core\attribute\deprecated(
709
        replacement: 'base::get_sectionnum',
710
        since: '4.4',
711
        mdl: 'MDL-80248',
712
        final: true,
713
    )]
1 efrain 714
    public function get_section_number(): int {
1441 ariadna 715
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
716
        return 0;
1 efrain 717
    }
718
 
719
    /**
720
     * Return the format section preferences.
721
     *
722
     * @return array of preferences indexed by sectionid
723
     */
724
    public function get_sections_preferences(): array {
725
        global $USER;
726
 
727
        $result = [];
728
 
729
        $course = $this->get_course();
730
        $coursesectionscache = cache::make('core', 'coursesectionspreferences');
731
 
732
        $coursesections = $coursesectionscache->get($course->id);
733
        if ($coursesections) {
734
            return $coursesections;
735
        }
736
 
737
        $sectionpreferences = $this->get_sections_preferences_by_preference();
738
 
739
        foreach ($sectionpreferences as $preference => $sectionids) {
740
            if (!empty($sectionids) && is_array($sectionids)) {
741
                foreach ($sectionids as $sectionid) {
742
                    if (!isset($result[$sectionid])) {
743
                        $result[$sectionid] = new stdClass();
744
                    }
745
                    $result[$sectionid]->$preference = true;
746
                }
747
            }
748
        }
749
 
750
        $coursesectionscache->set($course->id, $result);
751
        return $result;
752
    }
753
 
754
    /**
755
     * Return the format section preferences.
756
     *
757
     * @return array of preferences indexed by preference name
758
     */
759
    public function get_sections_preferences_by_preference(): array {
760
        global $USER;
761
        $course = $this->get_course();
762
        try {
763
            $sectionpreferences = json_decode(
764
                get_user_preferences("coursesectionspreferences_{$course->id}", '', $USER->id),
765
                true,
766
            );
767
            if (empty($sectionpreferences)) {
768
                $sectionpreferences = [];
769
            }
770
        } catch (\Throwable $e) {
771
            $sectionpreferences = [];
772
        }
773
        return $sectionpreferences;
774
    }
775
 
776
    /**
777
     * Return the format section preferences.
778
     *
779
     * @param string $preferencename preference name
780
     * @param int[] $sectionids affected section ids
781
     *
782
     */
783
    public function set_sections_preference(string $preferencename, array $sectionids) {
784
        $sectionpreferences = $this->get_sections_preferences_by_preference();
785
        $sectionpreferences[$preferencename] = $sectionids;
786
        $this->persist_to_user_preference($sectionpreferences);
787
    }
788
 
789
    /**
790
     * Add section preference ids.
791
     *
792
     * @param string $preferencename preference name
793
     * @param array $sectionids affected section ids
794
     */
795
    public function add_section_preference_ids(
796
        string $preferencename,
797
        array $sectionids,
798
    ): void {
799
        $sectionpreferences = $this->get_sections_preferences_by_preference();
800
        if (!isset($sectionpreferences[$preferencename])) {
801
            $sectionpreferences[$preferencename] = [];
802
        }
803
        foreach ($sectionids as $sectionid) {
804
            if (!in_array($sectionid, $sectionpreferences[$preferencename])) {
805
                $sectionpreferences[$preferencename][] = $sectionid;
806
            }
807
        }
808
        $this->persist_to_user_preference($sectionpreferences);
809
    }
810
 
811
    /**
812
     * Remove section preference ids.
813
     *
814
     * @param string $preferencename preference name
815
     * @param array $sectionids affected section ids
816
     */
817
    public function remove_section_preference_ids(
818
        string $preferencename,
819
        array $sectionids,
820
    ): void {
821
        $sectionpreferences = $this->get_sections_preferences_by_preference();
822
        if (!isset($sectionpreferences[$preferencename])) {
823
            $sectionpreferences[$preferencename] = [];
824
        }
825
        foreach ($sectionids as $sectionid) {
826
            if (($key = array_search($sectionid, $sectionpreferences[$preferencename])) !== false) {
827
                unset($sectionpreferences[$preferencename][$key]);
828
            }
829
        }
830
        $this->persist_to_user_preference($sectionpreferences);
831
    }
832
 
833
    /**
834
     * Persist the section preferences to the user preferences.
835
     *
836
     * @param array $sectionpreferences the section preferences
837
     */
838
    private function persist_to_user_preference(
839
        array $sectionpreferences,
840
    ): void {
841
        global $USER;
842
        $course = $this->get_course();
843
        set_user_preference('coursesectionspreferences_' . $course->id, json_encode($sectionpreferences), $USER->id);
844
        // Invalidate section preferences cache.
845
        $coursesectionscache = cache::make('core', 'coursesectionspreferences');
846
        $coursesectionscache->delete($course->id);
847
    }
848
 
849
    /**
850
     * Returns the information about the ajax support in the given source format
851
     *
852
     * The returned object's property (boolean)capable indicates that
853
     * the course format supports Moodle course ajax features.
854
     *
855
     * @return stdClass
856
     */
857
    public function supports_ajax() {
858
        // No support by default.
859
        $ajaxsupport = new stdClass();
860
        $ajaxsupport->capable = false;
861
        return $ajaxsupport;
862
    }
863
 
864
    /**
865
     * Returns true if this course format is compatible with content components.
866
     *
867
     * Using components means the content elements can watch the frontend course state and
868
     * react to the changes. Formats with component compatibility can have more interactions
869
     * without refreshing the page, like having drag and drop from the course index to reorder
870
     * sections and activities.
871
     *
872
     * @return bool if the format is compatible with components.
873
     */
874
    public function supports_components() {
875
        return false;
876
    }
877
 
878
 
879
    /**
880
     * Custom action after section has been moved in AJAX mode
881
     *
882
     * Used in course/rest.php
883
     *
884
     * @return ?array This will be passed in ajax respose
885
     */
886
    public function ajax_section_move() {
887
        return null;
888
    }
889
 
890
    /**
891
     * The URL to use for the specified course (with section)
892
     *
893
     * Please note that course view page /course/view.php?id=COURSEID is hardcoded in many
894
     * places in core and contributed modules. If course format wants to change the location
895
     * of the view script, it is not enough to change just this function. Do not forget
896
     * to add proper redirection.
897
     *
1441 ariadna 898
     * @param int|stdClass|section_info|null $section Section object from database or just field course_sections.section
1 efrain 899
     *     if null the course view page is returned
900
     * @param array $options options for view URL. At the moment core uses:
901
     *     'navigation' (bool) if true and section not empty, the function returns section page; otherwise, it returns course page.
902
     *     'sr' (int) used by course formats to specify to which section to return
903
     *     'expanded' (bool) if true the section will be shown expanded, true by default
904
     * @return null|moodle_url
905
     */
906
    public function get_view_url($section, $options = array()) {
907
        $course = $this->get_course();
908
        $url = new moodle_url('/course/view.php', ['id' => $course->id]);
909
 
910
        if (array_key_exists('sr', $options)) {
911
            $sectionno = $options['sr'];
912
        } else if (is_object($section)) {
913
            $sectionno = $section->section;
914
        } else {
915
            $sectionno = $section;
916
        }
917
        if ((!empty($options['navigation']) || array_key_exists('sr', $options)) && $sectionno !== null) {
918
            // Display section on separate page.
919
            $sectioninfo = $this->get_section($sectionno);
920
            return new moodle_url('/course/section.php', ['id' => $sectioninfo->id]);
921
        }
922
        if ($this->uses_sections() && $sectionno !== null) {
923
            // The url includes the parameter to expand the section by default.
924
            if (!array_key_exists('expanded', $options)) {
925
                $options['expanded'] = true;
926
            }
927
            if ($options['expanded']) {
928
                // This parameter is being set by default.
929
                $url->param('expandsection', $sectionno);
930
            }
931
            $url->set_anchor('section-'.$sectionno);
932
        }
933
 
934
        return $url;
935
    }
936
 
937
    /**
1441 ariadna 938
     * The URL to update the course format.
939
     *
940
     * If no section is specified, the update will redirect to the general course page.
941
     *
942
     * @param string $action action name the reactive action
943
     * @param array $ids list of ids to update
944
     * @param int|null $targetsectionid optional target section id
945
     * @param int|null $targetcmid optional target cm id
946
     * @param moodle_url|null $returnurl optional custom return url
947
     * @return moodle_url
948
     */
949
    public function get_update_url(
950
        string $action,
951
        array $ids = [],
952
        ?int $targetsectionid = null,
953
        ?int $targetcmid = null,
954
        ?moodle_url $returnurl = null
955
    ): moodle_url {
956
        $params = [
957
            'courseid' => $this->get_courseid(),
958
            'sesskey' => sesskey(),
959
            'action' => $action,
960
        ];
961
 
962
        if (count($ids) === 1) {
963
            $params['id'] = reset($ids);
964
        } else {
965
            foreach ($ids as $key => $id) {
966
                $params["ids[]"] = $id;
967
            }
968
        }
969
 
970
        if (isset($targetsectionid)) {
971
            $params['targetsectionid'] = $targetsectionid;
972
        }
973
        if (isset($targetcmid)) {
974
            $params['targetcmid'] = $targetcmid;
975
        }
976
        if ($returnurl) {
977
            $params['returnurl'] = $returnurl->out_as_local_url();
978
        }
979
        return new moodle_url('/course/format/update.php', $params);
980
    }
981
 
982
    /**
1 efrain 983
     * Return the old non-ajax activity action url.
984
     *
985
     * BrowserKit behats tests cannot trigger javascript events,
986
     * so we must translate to an old non-ajax url while non-ajax
987
     * course editing is still supported.
988
     *
1441 ariadna 989
     * @deprecated since Moodle 5.0
990
     * @todo Remove this method in Moodle 6.0 (MDL-83530).
991
     *
1 efrain 992
     * @param string $action action name the reactive action
993
     * @param cm_info $cm course module
994
     * @return moodle_url
995
     */
1441 ariadna 996
    #[\core\attribute\deprecated(
997
        replacement: 'core_courseformat\base::get_update_url',
998
        since: '5.0',
999
        mdl: 'MDL-82767',
1000
    )]
1 efrain 1001
    public function get_non_ajax_cm_action_url(string $action, cm_info $cm): moodle_url {
1002
        $nonajaxactions = [
1441 ariadna 1003
            'cmDelete' => 'cm_delete',
1004
            'cmDuplicate' => 'cm_duplicate',
1005
            'cmHide' => 'cm_hide',
1006
            'cmShow' => 'cm_show',
1007
            'cmStealth' => 'cm_stealth',
1 efrain 1008
        ];
1009
        if (!isset($nonajaxactions[$action])) {
1010
            throw new coding_exception('Unknown activity action: ' . $action);
1011
        }
1441 ariadna 1012
        \core\deprecation::emit_deprecation([$this, __FUNCTION__]);
1 efrain 1013
        $nonajaxaction = $nonajaxactions[$action];
1441 ariadna 1014
        return $this->get_update_url(
1015
            action: $nonajaxaction,
1016
            ids: [$cm->id],
1017
            returnurl: $this->get_view_url($this->get_sectionnum(), ['navigation' => true]),
1 efrain 1018
        );
1019
    }
1020
 
1021
    /**
1022
     * Loads all of the course sections into the navigation
1023
     *
1024
     * This method is called from global_navigation::load_course_sections()
1025
     *
1026
     * By default the method global_navigation::load_generic_course_sections() is called
1027
     *
1028
     * When overwriting please note that navigationlib relies on using the correct values for
1029
     * arguments $type and $key in navigation_node::add()
1030
     *
1031
     * Example of code creating a section node:
1032
     * $sectionnode = $node->add($sectionname, $url, navigation_node::TYPE_SECTION, null, $section->id);
1033
     * $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH;
1034
     *
1035
     * Example of code creating an activity node:
1036
     * $activitynode = $sectionnode->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $activity->id, $icon);
1037
     * if (global_navigation::module_extends_navigation($activity->modname)) {
1038
     *     $activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
1039
     * } else {
1040
     *     $activitynode->nodetype = navigation_node::NODETYPE_LEAF;
1041
     * }
1042
     *
1043
     * Also note that if $navigation->includesectionnum is not null, the section with this relative
1044
     * number needs is expected to be loaded
1045
     *
1046
     * @param \global_navigation $navigation
1047
     * @param navigation_node $node The course node within the navigation
1048
     */
1049
    public function extend_course_navigation($navigation, navigation_node $node) {
1050
        if ($course = $this->get_course()) {
1051
            $navigation->load_generic_course_sections($course, $node);
1052
        }
1053
        return array();
1054
    }
1055
 
1056
    /**
1057
     * Returns the list of blocks to be automatically added for the newly created course
1058
     *
1059
     * @see blocks_add_default_course_blocks()
1060
     *
1061
     * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
1062
     *     each of values is an array of block names (for left and right side columns)
1063
     */
1064
    public function get_default_blocks() {
1065
        global $CFG;
1066
        if (isset($CFG->defaultblocks)) {
1067
            return blocks_parse_default_blocks_list($CFG->defaultblocks);
1068
        }
1069
        $blocknames = array(
1070
            BLOCK_POS_LEFT => array(),
1071
            BLOCK_POS_RIGHT => array()
1072
        );
1073
        return $blocknames;
1074
    }
1075
 
1076
    /**
1077
     * Return custom strings for the course editor.
1078
     *
1079
     * This method is mainly used to translate the "section" related strings into
1080
     * the specific format plugins name such as "Topics" or "Weeks".
1081
     *
1082
     * @return stdClass[] an array of objects with string "component" and "key"
1083
     */
1084
    public function get_editor_custom_strings(): array {
1085
        $result = [];
1086
        $stringmanager = get_string_manager();
1087
        $component = 'format_' . $this->format;
1088
        $formatoverridbles = [
1089
            'sectionavailability_title',
1090
            'sectiondelete_title',
1091
            'sectiondelete_info',
1092
            'sectionsdelete_title',
1093
            'sectionsdelete_info',
1094
            'sectionmove_title',
1095
            'sectionmove_info',
1096
            'sectionsavailability_title',
1097
            'sectionsmove_title',
1098
            'sectionsmove_info',
1099
            'selectsection'
1100
        ];
1101
        foreach ($formatoverridbles as $key) {
1102
            if ($stringmanager->string_exists($key, $component)) {
1103
                $result[] = (object)['component' => $component, 'key' => $key];
1104
            }
1105
        }
1106
        return $result;
1107
    }
1108
 
1109
    /**
1110
     * Get the proper format plugin string if it exists.
1111
     *
1112
     * If the format_PLUGINNAME does not provide a valid string,
1113
     * core_courseformat will be user as the component.
1114
     *
1115
     * @param string $key the string key
1116
     * @param string|object|array|int $data extra data that can be used within translation strings
1117
     * @return string the get_string result
1118
     */
1119
    public function get_format_string(string $key, $data = null): string {
1120
        $component = 'format_' . $this->get_format();
1121
        if (!get_string_manager()->string_exists($key, $component)) {
1122
            $component = 'core_courseformat';
1123
        }
1124
        return get_string($key, $component, $data);
1125
    }
1126
 
1127
    /**
1128
     * Returns the localised name of this course format plugin
1129
     *
1130
     * @return lang_string
1131
     */
1132
    final public function get_format_name() {
1133
        return new lang_string('pluginname', 'format_'.$this->get_format());
1134
    }
1135
 
1136
    /**
1137
     * Definitions of the additional options that this course format uses for course
1138
     *
1139
     * This function may be called often, it should be as fast as possible.
1140
     * Avoid using get_string() method, use "new lang_string()" instead
1141
     * It is not recommended to use dynamic or course-dependant expressions here
1142
     * This function may be also called when course does not exist yet.
1143
     *
1144
     * Option names must be different from fields in the {course} talbe or any form elements on
1145
     * course edit form, it may even make sence to use special prefix for them.
1146
     *
1147
     * Each option must have the option name as a key and the array of properties as a value:
1148
     * 'default' - default value for this option (assumed null if not specified)
1149
     * 'type' - type of the option value (PARAM_INT, PARAM_RAW, etc.)
1150
     *
1151
     * Additional properties used by default implementation of
1152
     * course_format::create_edit_form_elements() (calls this method with $foreditform = true)
1153
     * 'label' - localised human-readable label for the edit form
1154
     * 'element_type' - type of the form element, default 'text'
1155
     * 'element_attributes' - additional attributes for the form element, these are 4th and further
1156
     *    arguments in the moodleform::addElement() method
1157
     * 'help' - string for help button. Note that if 'help' value is 'myoption' then the string with
1158
     *    the name 'myoption_help' must exist in the language file
1159
     * 'help_component' - language component to look for help string, by default this the component
1160
     *    for this course format
1161
     *
1162
     * This is an interface for creating simple form elements. If format plugin wants to use other
1163
     * methods such as disableIf, it can be done by overriding create_edit_form_elements().
1164
     *
1165
     * Course format options can be accessed as:
1166
     * $this->get_course()->OPTIONNAME (inside the format class)
1167
     * course_get_format($course)->get_course()->OPTIONNAME (outside of format class)
1168
     *
1169
     * All course options are returned by calling:
1170
     * $this->get_format_options();
1171
     *
1172
     * @param bool $foreditform
1173
     * @return array of options
1174
     */
1175
    public function course_format_options($foreditform = false) {
1176
        return array();
1177
    }
1178
 
1179
    /**
1180
     * Definitions of the additional options that this course format uses for section
1181
     *
1182
     * See course_format::course_format_options() for return array definition.
1183
     *
1184
     * Additionally section format options may have property 'cache' set to true
1185
     * if this option needs to be cached in get_fast_modinfo(). The 'cache' property
1186
     * is recommended to be set only for fields used in course_format::get_section_name(),
1187
     * course_format::extend_course_navigation() and course_format::get_view_url()
1188
     *
1189
     * For better performance cached options are recommended to have 'cachedefault' property
1190
     * Unlike 'default', 'cachedefault' should be static and not access get_config().
1191
     *
1192
     * Regardless of value of 'cache' all options are accessed in the code as
1193
     * $sectioninfo->OPTIONNAME
1194
     * where $sectioninfo is instance of section_info, returned by
1195
     * get_fast_modinfo($course)->get_section_info($sectionnum)
1196
     * or get_fast_modinfo($course)->get_section_info_all()
1197
     *
1198
     * All format options for particular section are returned by calling:
1199
     * $this->get_format_options($section);
1200
     *
1201
     * @param bool $foreditform
1202
     * @return array
1203
     */
1204
    public function section_format_options($foreditform = false) {
1205
        return array();
1206
    }
1207
 
1208
    /**
1209
     * Returns the format options stored for this course or course section
1210
     *
1211
     * When overriding please note that this function is called from rebuild_course_cache()
1212
     * and section_info object, therefore using of get_fast_modinfo() and/or any function that
1213
     * accesses it may lead to recursion.
1214
     *
1215
     * @param null|int|stdClass|section_info $section if null the course format options will be returned
1216
     *     otherwise options for specified section will be returned. This can be either
1217
     *     section object or relative section number (field course_sections.section)
1218
     * @return array
1219
     */
1220
    public function get_format_options($section = null) {
1221
        global $DB;
1222
        if ($section === null) {
1223
            $options = $this->course_format_options();
1224
        } else {
1225
            $options = $this->section_format_options();
1226
        }
1227
        if (empty($options)) {
1228
            // There are no option for course/sections anyway, no need to go further.
1229
            return array();
1230
        }
1231
        if ($section === null) {
1232
            // Course format options will be returned.
1233
            $sectionid = 0;
1234
        } else if ($this->courseid && isset($section->id)) {
1235
            // Course section format options will be returned.
1236
            $sectionid = $section->id;
1237
        } else if ($this->courseid && is_int($section) &&
1238
                ($sectionobj = $DB->get_record('course_sections',
1239
                        array('section' => $section, 'course' => $this->courseid), 'id'))) {
1240
            // Course section format options will be returned.
1241
            $sectionid = $sectionobj->id;
1242
        } else {
1243
            // Non-existing (yet) section was passed as an argument
1244
            // default format options for course section will be returned.
1245
            $sectionid = -1;
1246
        }
1247
        if (!array_key_exists($sectionid, $this->formatoptions)) {
1248
            $this->formatoptions[$sectionid] = array();
1249
            // First fill with default values.
1250
            foreach ($options as $optionname => $optionparams) {
1251
                $this->formatoptions[$sectionid][$optionname] = null;
1252
                if (array_key_exists('default', $optionparams)) {
1253
                    $this->formatoptions[$sectionid][$optionname] = $optionparams['default'];
1254
                }
1255
            }
1256
            if ($this->courseid && $sectionid !== -1) {
1257
                // Overwrite the default options values with those stored in course_format_options table
1258
                // nothing can be stored if we are interested in generic course ($this->courseid == 0)
1259
                // or generic section ($sectionid === 0).
1260
                $records = $DB->get_records('course_format_options',
1261
                        array('courseid' => $this->courseid,
1262
                              'format' => $this->format,
1263
                              'sectionid' => $sectionid
1264
                            ), '', 'id,name,value');
1265
                $indexedrecords = [];
1266
                foreach ($records as $record) {
1267
                    $indexedrecords[$record->name] = $record->value;
1268
                }
1269
                foreach ($options as $optionname => $option) {
1270
                    contract_value($this->formatoptions[$sectionid], $indexedrecords, $option, $optionname);
1271
                }
1272
            }
1273
        }
1274
        return $this->formatoptions[$sectionid];
1275
    }
1276
 
1277
    /**
1278
     * Adds format options elements to the course/section edit form
1279
     *
1280
     * This function is called from course_edit_form::definition_after_data()
1281
     *
1282
     * @param \MoodleQuickForm $mform form the elements are added to
1283
     * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form
1284
     * @return array array of references to the added form elements
1285
     */
1286
    public function create_edit_form_elements(&$mform, $forsection = false) {
1287
        $elements = array();
1288
        if ($forsection) {
1289
            $options = $this->section_format_options(true);
1290
        } else {
1291
            $options = $this->course_format_options(true);
1292
        }
1293
        foreach ($options as $optionname => $option) {
1294
            if (!isset($option['element_type'])) {
1295
                $option['element_type'] = 'text';
1296
            }
1297
            $args = array($option['element_type'], $optionname, $option['label']);
1298
            if (!empty($option['element_attributes'])) {
1299
                $args = array_merge($args, $option['element_attributes']);
1300
            }
1301
            $elements[] = call_user_func_array(array($mform, 'addElement'), $args);
1302
            if (isset($option['help'])) {
1303
                $helpcomponent = 'format_'. $this->get_format();
1304
                if (isset($option['help_component'])) {
1305
                    $helpcomponent = $option['help_component'];
1306
                }
1307
                $mform->addHelpButton($optionname, $option['help'], $helpcomponent);
1308
            }
1309
            if (isset($option['type'])) {
1310
                $mform->setType($optionname, $option['type']);
1311
            }
1312
            if (isset($option['default']) && !array_key_exists($optionname, $mform->_defaultValues)) {
1313
                // Set defaults for the elements in the form.
1314
                // Since we call this method after set_data() make sure that we don't override what was already set.
1315
                $mform->setDefault($optionname, $option['default']);
1316
            }
1317
        }
1318
 
1319
        if (!$forsection && empty($this->courseid)) {
1320
            // Check if course end date form field should be enabled by default.
1321
            // If a default date is provided to the form element, it is magically enabled by default in the
1322
            // MoodleQuickForm_date_time_selector class, otherwise it's disabled by default.
1323
            if (get_config('moodlecourse', 'courseenddateenabled')) {
1324
                // At this stage (this is called from definition_after_data) course data is already set as default.
1325
                // We can not overwrite what is in the database.
1326
                $mform->setDefault('enddate', $this->get_default_course_enddate($mform));
1327
            }
1328
        }
1329
 
1330
        return $elements;
1331
    }
1332
 
1333
    /**
1334
     * Override if you need to perform some extra validation of the format options
1335
     *
1336
     * @param array $data array of ("fieldname"=>value) of submitted data
1337
     * @param array $files array of uploaded files "element_name"=>tmp_file_path
1338
     * @param array $errors errors already discovered in edit form validation
1339
     * @return array of "element_name"=>"error_description" if there are errors,
1340
     *         or an empty array if everything is OK.
1341
     *         Do not repeat errors from $errors param here
1342
     */
1343
    public function edit_form_validation($data, $files, $errors) {
1344
        return array();
1345
    }
1346
 
1347
    /**
1348
     * Prepares values of course or section format options before storing them in DB
1349
     *
1350
     * If an option has invalid value it is not returned
1351
     *
1352
     * @param array $rawdata associative array of the proposed course/section format options
1353
     * @param int|null $sectionid null if it is course format option
1354
     * @return array array of options that have valid values
1355
     */
1441 ariadna 1356
    protected function validate_format_options(array $rawdata, ?int $sectionid = null): array {
1 efrain 1357
        if (!$sectionid) {
1358
            $allformatoptions = $this->course_format_options(true);
1359
        } else {
1360
            $allformatoptions = $this->section_format_options(true);
1361
        }
1362
        $data = array_intersect_key($rawdata, $allformatoptions);
1363
        foreach ($data as $key => $value) {
1364
            $option = $allformatoptions[$key] + ['type' => PARAM_RAW, 'element_type' => null, 'element_attributes' => [[]]];
1365
            expand_value($data, $data, $option, $key);
1366
            if ($option['element_type'] === 'select' && !array_key_exists($data[$key], $option['element_attributes'][0])) {
1367
                // Value invalid for select element, skip.
1368
                unset($data[$key]);
1369
            }
1370
        }
1371
        return $data;
1372
    }
1373
 
1374
    /**
1375
     * Validates format options for the course
1376
     *
1377
     * @param array $data data to insert/update
1378
     * @return array array of options that have valid values
1379
     */
1380
    public function validate_course_format_options(array $data): array {
1381
        return $this->validate_format_options($data);
1382
    }
1383
 
1384
    /**
1385
     * Updates format options for a course or section
1386
     *
1387
     * If $data does not contain property with the option name, the option will not be updated
1388
     *
1389
     * @param stdClass|array $data return value from moodleform::get_data() or array with data
1390
     * @param null|int $sectionid null if these are options for course or section id (course_sections.id)
1391
     *     if these are options for section
1392
     * @return bool whether there were any changes to the options values
1393
     */
1394
    protected function update_format_options($data, $sectionid = null) {
1395
        global $DB;
1396
        $data = $this->validate_format_options((array)$data, $sectionid);
1397
        if (!$sectionid) {
1398
            $allformatoptions = $this->course_format_options();
1399
            $sectionid = 0;
1400
        } else {
1401
            $allformatoptions = $this->section_format_options();
1402
        }
1403
        if (empty($allformatoptions)) {
1404
            // Nothing to update anyway.
1405
            return false;
1406
        }
1407
        $defaultoptions = array();
1408
        $cached = array();
1409
        foreach ($allformatoptions as $key => $option) {
1410
            $defaultoptions[$key] = null;
1411
            if (array_key_exists('default', $option)) {
1412
                $defaultoptions[$key] = $option['default'];
1413
            }
1414
            expand_value($defaultoptions, $defaultoptions, $option, $key);
1415
            $cached[$key] = ($sectionid === 0 || !empty($option['cache']));
1416
        }
1417
        $records = $DB->get_records('course_format_options',
1418
                array('courseid' => $this->courseid,
1419
                      'format' => $this->format,
1420
                      'sectionid' => $sectionid
1421
                    ), '', 'name,id,value');
1422
        $changed = $needrebuild = false;
1423
        foreach ($defaultoptions as $key => $value) {
1424
            if (isset($records[$key])) {
1425
                if (array_key_exists($key, $data) && $records[$key]->value != $data[$key]) {
1426
                    $DB->set_field('course_format_options', 'value',
1427
                            $data[$key], array('id' => $records[$key]->id));
1428
                    $changed = true;
1429
                    $needrebuild = $needrebuild || $cached[$key];
1430
                }
1431
            } else {
1432
                if (array_key_exists($key, $data) && $data[$key] !== $value) {
1433
                    $newvalue = $data[$key];
1434
                    $changed = true;
1435
                    $needrebuild = $needrebuild || $cached[$key];
1436
                } else {
1437
                    $newvalue = $value;
1438
                    // We still insert entry in DB but there are no changes from user point of
1439
                    // view and no need to call rebuild_course_cache().
1440
                }
1441
                $DB->insert_record('course_format_options', array(
1442
                    'courseid' => $this->courseid,
1443
                    'format' => $this->format,
1444
                    'sectionid' => $sectionid,
1445
                    'name' => $key,
1446
                    'value' => $newvalue
1447
                ));
1448
            }
1449
        }
1450
        if ($needrebuild) {
1451
            if ($sectionid) {
1452
                // Invalidate the section cache by given section id.
1453
                course_modinfo::purge_course_section_cache_by_id($this->courseid, $sectionid);
1454
                // Partial rebuild sections that have been invalidated.
1455
                rebuild_course_cache($this->courseid, true, true);
1456
            } else {
1457
                // Full rebuild if sectionid is null.
1458
                rebuild_course_cache($this->courseid);
1459
            }
1460
        }
1461
        if ($changed) {
1462
            // Reset internal caches.
1463
            if (!$sectionid) {
1464
                $this->course = false;
1465
            }
1466
            unset($this->formatoptions[$sectionid]);
1467
        }
1468
        return $changed;
1469
    }
1470
 
1471
    /**
1472
     * Updates format options for a course
1473
     *
1474
     * If $data does not contain property with the option name, the option will not be updated
1475
     *
1476
     * @param stdClass|array $data return value from moodleform::get_data() or array with data
1477
     * @param stdClass $oldcourse if this function is called from update_course()
1478
     *     this object contains information about the course before update
1479
     * @return bool whether there were any changes to the options values
1480
     */
1481
    public function update_course_format_options($data, $oldcourse = null) {
1482
        return $this->update_format_options($data);
1483
    }
1484
 
1485
    /**
1486
     * Updates format options for a section
1487
     *
1488
     * Section id is expected in $data->id (or $data['id'])
1489
     * If $data does not contain property with the option name, the option will not be updated
1490
     *
1491
     * @param stdClass|array $data return value from moodleform::get_data() or array with data
1492
     * @return bool whether there were any changes to the options values
1493
     */
1494
    public function update_section_format_options($data) {
1495
        $data = (array)$data;
1496
        return $this->update_format_options($data, $data['id']);
1497
    }
1498
 
1499
    /**
1500
     * Return an instance of moodleform to edit a specified section
1501
     *
1502
     * Default implementation returns instance of editsection_form that automatically adds
1503
     * additional fields defined in course_format::section_format_options()
1504
     *
1505
     * Format plugins may extend editsection_form if they want to have custom edit section form.
1506
     *
1507
     * @param mixed $action the action attribute for the form. If empty defaults to auto detect the
1508
     *              current url. If a moodle_url object then outputs params as hidden variables.
1509
     * @param array $customdata the array with custom data to be passed to the form
1510
     *     /course/editsection.php passes section_info object in 'cs' field
1511
     *     for filling availability fields
1512
     * @return \moodleform
1513
     */
1514
    public function editsection_form($action, $customdata = array()) {
1515
        global $CFG;
1516
        require_once($CFG->dirroot. '/course/editsection_form.php');
1517
        if (!array_key_exists('course', $customdata)) {
1518
            $customdata['course'] = $this->get_course();
1519
        }
1520
        return new editsection_form($action, $customdata);
1521
    }
1522
 
1523
    /**
1524
     * Allows course format to execute code on moodle_page::set_course()
1525
     *
1526
     * @param moodle_page $page instance of page calling set_course
1527
     */
1528
    public function page_set_course(moodle_page $page) {
1529
    }
1530
 
1531
    /**
1532
     * Allows course format to execute code on moodle_page::set_cm()
1533
     *
1534
     * Current module can be accessed as $page->cm (returns instance of cm_info)
1535
     *
1536
     * @param moodle_page $page instance of page calling set_cm
1537
     */
1538
    public function page_set_cm(moodle_page $page) {
1539
    }
1540
 
1541
    /**
1542
     * Course-specific information to be output on any course page (usually above navigation bar)
1543
     *
1544
     * Example of usage:
1545
     * define
1546
     * class format_FORMATNAME_XXX implements renderable {}
1547
     *
1548
     * create format renderer in course/format/FORMATNAME/renderer.php, define rendering function:
1549
     * class format_FORMATNAME_renderer extends plugin_renderer_base {
1550
     *     protected function render_format_FORMATNAME_XXX(format_FORMATNAME_XXX $xxx) {
1551
     *         return html_writer::tag('div', 'This is my header/footer');
1552
     *     }
1553
     * }
1554
     *
1555
     * Return instance of format_FORMATNAME_XXX in this function, the appropriate method from
1556
     * plugin renderer will be called
1557
     *
1558
     * @return null|\renderable null for no output or object with data for plugin renderer
1559
     */
1560
    public function course_header() {
1561
        return null;
1562
    }
1563
 
1564
    /**
1565
     * Course-specific information to be output on any course page (usually in the beginning of
1566
     * standard footer)
1567
     *
1568
     * See course_format::course_header() for usage
1569
     *
1570
     * @return null|\renderable null for no output or object with data for plugin renderer
1571
     */
1572
    public function course_footer() {
1573
        return null;
1574
    }
1575
 
1576
    /**
1577
     * Course-specific information to be output immediately above content on any course page
1578
     *
1579
     * See course_format::course_header() for usage
1580
     *
1581
     * @return null|\renderable null for no output or object with data for plugin renderer
1582
     */
1583
    public function course_content_header() {
1584
        return null;
1585
    }
1586
 
1587
    /**
1588
     * Course-specific information to be output immediately below content on any course page
1589
     *
1590
     * See course_format::course_header() for usage
1591
     *
1592
     * @return null|\renderable null for no output or object with data for plugin renderer
1593
     */
1594
    public function course_content_footer() {
1595
        return null;
1596
    }
1597
 
1598
    /**
1599
     * Returns instance of page renderer used by this plugin
1600
     *
1601
     * @param moodle_page $page
1602
     * @return \renderer_base
1603
     */
1604
    public function get_renderer(moodle_page $page) {
1605
        try {
1606
            $renderer = $page->get_renderer('format_'. $this->get_format());
1607
        } catch (moodle_exception $e) {
1608
            $formatname = $this->get_format();
1609
            $expectedrenderername = 'format_'. $this->get_format() . '\output\renderer';
1610
            debugging(
1611
                "The '{$formatname}' course format does not define the {$expectedrenderername} renderer class. This is required since Moodle 4.0.",
1612
                 DEBUG_DEVELOPER
1613
            );
1614
            $renderer = new legacy_renderer($page, null);
1615
        }
1616
 
1617
        return $renderer;
1618
    }
1619
 
1620
    /**
1621
     * Returns instance of output component used by this plugin
1622
     *
1623
     * @throws coding_exception if the format class does not extends the original core one.
1624
     * @param string $outputname the element to render (section, activity...)
1625
     * @return string the output component classname
1626
     */
1627
    public function get_output_classname(string $outputname): string {
1628
        // The core output class.
1629
        $baseclass = "core_courseformat\\output\\local\\$outputname";
1630
 
1631
        // Look in this format and any parent formats before we get to the base one.
1632
        $classes = array_merge([get_class($this)], class_parents($this));
1633
        foreach ($classes as $component) {
1634
            if ($component === self::class) {
1635
                break;
1636
            }
1637
 
1638
            // Because course formats are in the root namespace, there is no need to process the
1639
            // class name - it is already a Frankenstyle component name beginning 'format_'.
1640
 
1641
            // Check if there is a specific class in this format.
1642
            $outputclass = "$component\\output\\courseformat\\$outputname";
1643
            if (class_exists($outputclass)) {
1644
                // Check that the outputclass is a subclass of the base class.
1645
                if (!is_subclass_of($outputclass, $baseclass)) {
1646
                    throw new coding_exception("The \"$outputclass\" must extend \"$baseclass\"");
1647
                }
1648
                return $outputclass;
1649
            }
1650
        }
1651
 
1652
        return $baseclass;
1653
    }
1654
 
1655
    /**
1656
     * Returns true if the specified section is current
1657
     *
1658
     * By default we analyze $course->marker
1659
     *
1660
     * @param int|stdClass|section_info $section
1661
     * @return bool
1662
     */
1663
    public function is_section_current($section) {
1664
        if (is_object($section)) {
1665
            $sectionnum = $section->section;
1666
        } else {
1667
            $sectionnum = $section;
1668
        }
1669
        return ($sectionnum && ($course = $this->get_course()) && $course->marker == $sectionnum);
1670
    }
1671
 
1672
    /**
1673
     * Returns if an specific section is visible to the current user.
1674
     *
1675
     * Formats can overrride this method to implement any special section logic.
1676
     *
1677
     * @param section_info $section the section modinfo
1678
     * @return bool;
1679
     */
1680
    public function is_section_visible(section_info $section): bool {
1441 ariadna 1681
        // It is unlikely that a section is orphan, but it needs to be checked.
1682
        if ($section->is_orphan() && !has_capability('moodle/course:viewhiddensections', $this->get_context())) {
1683
            return false;
1684
        }
1 efrain 1685
        // Previous to Moodle 4.0 thas logic was hardcoded. To prevent errors in the contrib plugins
1686
        // the default logic is the same required for topics and weeks format and still uses
1687
        // a "hiddensections" format setting.
1688
        $course = $this->get_course();
1689
        $hidesections = $course->hiddensections ?? true;
1690
        // Show the section if the user is permitted to access it, OR if it's not available
1691
        // but there is some available info text which explains the reason & should display,
1692
        // OR it is hidden but the course has a setting to display hidden sections as unavailable.
1693
        return $section->uservisible ||
1694
            ($section->visible && !$section->available && !empty($section->availableinfo)) ||
1695
            (!$section->visible && !$hidesections);
1696
    }
1697
 
1698
    /**
1699
     * return true if the course editor must be displayed.
1700
     *
1701
     * @param array|null $capabilities array of capabilities a user needs to have to see edit controls in general.
1702
     *  If null or not specified, the user needs to have 'moodle/course:manageactivities'.
1703
     * @return bool true if edit controls must be displayed
1704
     */
1705
    public function show_editor(?array $capabilities = ['moodle/course:manageactivities']): bool {
1706
        global $PAGE;
1707
        $course = $this->get_course();
1708
        $coursecontext = context_course::instance($course->id);
1709
        if ($capabilities === null) {
1710
            $capabilities = ['moodle/course:manageactivities'];
1711
        }
1712
        return $PAGE->user_is_editing() && has_all_capabilities($capabilities, $coursecontext);
1713
    }
1714
 
1715
    /**
1716
     * Check if the group mode can be displayed.
1717
     * @param cm_info $cm the activity module
1718
     * @return bool
1719
     */
1720
    public function show_groupmode(cm_info $cm): bool {
1721
        if (!plugin_supports('mod', $cm->modname, FEATURE_GROUPS, false)) {
1722
            return false;
1723
        }
1724
        if (!has_capability('moodle/course:manageactivities', $cm->context)) {
1725
            return false;
1726
        }
1727
        return true;
1728
    }
1729
 
1730
    /**
1731
     * Allows to specify for modinfo that section is not available even when it is visible and conditionally available.
1732
     *
1733
     * Note: affected user can be retrieved as: $section->modinfo->userid
1734
     *
1735
     * Course format plugins can override the method to change the properties $available and $availableinfo that were
1736
     * calculated by conditional availability.
1737
     * To make section unavailable set:
1738
     *     $available = false;
1739
     * To make unavailable section completely hidden set:
1740
     *     $availableinfo = '';
1741
     * To make unavailable section visible with availability message set:
1742
     *     $availableinfo = get_string('sectionhidden', 'format_xxx');
1743
     *
1744
     * @param section_info $section
1745
     * @param bool $available the 'available' propery of the section_info as it was evaluated by conditional availability.
1746
     *     Can be changed by the method but 'false' can not be overridden by 'true'.
1747
     * @param string $availableinfo the 'availableinfo' propery of the section_info as it was evaluated by conditional availability.
1748
     *     Can be changed by the method
1749
     */
1750
    public function section_get_available_hook(section_info $section, &$available, &$availableinfo) {
1751
    }
1752
 
1753
    /**
1754
     * Whether this format allows to delete sections
1755
     *
1756
     * If format supports deleting sections it is also recommended to define language string
1757
     * 'deletesection' inside the format.
1758
     *
1759
     * Do not call this function directly, instead use course_can_delete_section()
1760
     *
1761
     * @param int|stdClass|section_info $section
1762
     * @return bool
1763
     */
1764
    public function can_delete_section($section) {
1765
        return false;
1766
    }
1767
 
1768
    /**
1769
     * Deletes a section
1770
     *
1771
     * Do not call this function directly, instead call course_delete_section()
1772
     *
1773
     * @param int|stdClass|section_info $sectionornum
1774
     * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it.
1775
     * @return bool whether section was deleted
1776
     */
1777
    public function delete_section($sectionornum, $forcedeleteifnotempty = false) {
1778
        global $DB;
1779
        if (!$this->uses_sections()) {
1780
            // Not possible to delete section if sections are not used.
1781
            return false;
1782
        }
1783
        if (is_object($sectionornum)) {
1784
            $section = $sectionornum;
1785
        } else {
1786
            $section = $DB->get_record(
1787
                'course_sections',
1788
                ['course' => $this->get_courseid(), 'section' => $sectionornum],
1789
                'id,section,sequence,summary');
1790
        }
1791
        if (!$section || !$section->section) {
1792
            // Not possible to delete 0-section.
1793
            return false;
1794
        }
1795
 
1796
        if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
1797
            return false;
1798
        }
1799
 
1800
        $course = $this->get_course();
1801
 
1802
        // Remove the marker if it points to this section.
1803
        if ($section->section == $course->marker) {
1804
            course_set_marker($course->id, 0);
1805
        }
1806
 
1807
        $lastsection = $DB->get_field_sql('SELECT max(section) from {course_sections}
1808
                            WHERE course = ?', array($course->id));
1809
 
1810
        // Find out if we need to descrease the 'numsections' property later.
1811
        $courseformathasnumsections = array_key_exists('numsections',
1812
            $this->get_format_options());
1813
        $decreasenumsections = $courseformathasnumsections && ($section->section <= $course->numsections);
1814
 
1815
        // Move the section to the end.
1816
        move_section_to($course, $section->section, $lastsection, true);
1817
 
1818
        // Delete all modules from the section.
1819
        foreach (preg_split('/,/', $section->sequence, -1, PREG_SPLIT_NO_EMPTY) as $cmid) {
1820
            course_delete_module($cmid);
1821
        }
1822
 
1823
        // Delete section and it's format options.
1824
        $DB->delete_records('course_format_options', array('sectionid' => $section->id));
1825
        $DB->delete_records('course_sections', array('id' => $section->id));
1826
        // Invalidate the section cache by given section id.
1827
        course_modinfo::purge_course_section_cache_by_id($course->id, $section->id);
1828
        // Partial rebuild section cache that has been purged.
1829
        rebuild_course_cache($course->id, true, true);
1830
 
1831
        // Delete section summary files.
1832
        $context = \context_course::instance($course->id);
1833
        $fs = get_file_storage();
1834
        $fs->delete_area_files($context->id, 'course', 'section', $section->id);
1835
 
1836
        // Descrease 'numsections' if needed.
1837
        if ($decreasenumsections) {
1838
            $this->update_course_format_options(array('numsections' => $course->numsections - 1));
1839
        }
1840
 
1841
        return true;
1842
    }
1843
 
1844
    /**
1845
     * Wrapper for course_delete_module method.
1846
     *
1847
     * Format plugins can override this method to provide their own implementation of course_delete_module.
1848
     *
1849
     * @param cm_info $cm the course module information
1850
     * @param bool $async whether or not to try to delete the module using an adhoc task. Async also depends on a plugin hook.
1851
     * @throws moodle_exception
1852
     */
1853
    public function delete_module(cm_info $cm, bool $async = false) {
1854
        course_delete_module($cm->id, $async);
1855
    }
1856
 
1857
    /**
1858
     * Moves a section just after the target section.
1859
     *
1860
     * @param section_info $section the section to move
1861
     * @param section_info $destination the section that should be below the moved section
1862
     * @return boolean if the section can be moved or not
1863
     */
1864
    public function move_section_after(section_info $section, section_info $destination): bool {
1865
        if ($section->section == $destination->section || $section->section == $destination->section + 1) {
1866
            return true;
1867
        }
1868
        // The move_section_to moves relative to the section to move. However, this
1869
        // method will move the target section always after the destination.
1870
        if ($section->section > $destination->section) {
1871
            $newsectionnumber = $destination->section + 1;
1872
        } else {
1873
            $newsectionnumber = $destination->section;
1874
        }
1875
        return move_section_to(
1876
            $this->get_course(),
1877
            $section->section,
1878
            $newsectionnumber
1879
        );
1880
    }
1881
 
1882
    /**
1883
     * Prepares the templateable object to display section name
1884
     *
1885
     * @param \section_info|\stdClass $section
1886
     * @param bool $linkifneeded
1887
     * @param bool $editable
1888
     * @param null|lang_string|string $edithint
1889
     * @param null|lang_string|string $editlabel
1890
     * @return \core\output\inplace_editable
1891
     */
1892
    public function inplace_editable_render_section_name($section, $linkifneeded = true,
1893
                                                         $editable = null, $edithint = null, $editlabel = null) {
1894
        global $USER, $CFG;
1895
        require_once($CFG->dirroot.'/course/lib.php');
1896
 
1897
        if ($editable === null) {
1898
            $editable = !empty($USER->editing) && has_capability('moodle/course:update',
1899
                    context_course::instance($section->course));
1900
        }
1901
 
1902
        $displayvalue = $title = get_section_name($section->course, $section);
1903
        if ($linkifneeded) {
1904
            // Display link under the section name if the course format setting is to display one section per page.
1905
            $url = course_get_url($section->course, $section->section, array('navigation' => true));
1906
            if ($url) {
1907
                $displayvalue = html_writer::link($url, $title);
1908
            }
1909
            $itemtype = 'sectionname';
1910
        } else {
1911
            // If $linkifneeded==false, we never display the link (this is used when rendering the section header).
1912
            // Itemtype 'sectionnamenl' (nl=no link) will tell the callback that link should not be rendered -
1913
            // there is no other way callback can know where we display the section name.
1914
            $itemtype = 'sectionnamenl';
1915
        }
1916
        if (empty($edithint)) {
1917
            $edithint = new lang_string('editsectionname');
1918
        }
1919
        if (empty($editlabel)) {
1920
            $editlabel = new lang_string('newsectionname', '', $title);
1921
        }
1922
 
1923
        return new \core\output\inplace_editable('format_' . $this->format, $itemtype, $section->id, $editable,
1924
            $displayvalue, $section->name, $edithint, $editlabel);
1925
    }
1926
 
1927
    /**
1928
     * Updates the value in the database and modifies this object respectively.
1929
     *
1930
     * ALWAYS check user permissions before performing an update! Throw exceptions if permissions are not sufficient
1931
     * or value is not legit.
1932
     *
1933
     * @param stdClass $section
1934
     * @param string $itemtype
1935
     * @param mixed $newvalue
1936
     * @return ?\core\output\inplace_editable
1937
     */
1938
    public function inplace_editable_update_section_name($section, $itemtype, $newvalue) {
1939
        if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') {
1940
            $context = context_course::instance($section->course);
1941
            external_api::validate_context($context);
1942
            require_capability('moodle/course:update', $context);
1943
 
1944
            $newtitle = clean_param($newvalue, PARAM_TEXT);
1945
            if (strval($section->name) !== strval($newtitle)) {
1946
                course_update_section($section->course, $section, array('name' => $newtitle));
1947
            }
1948
            return $this->inplace_editable_render_section_name($section, ($itemtype === 'sectionname'), true);
1949
        }
1950
    }
1951
 
1952
 
1953
    /**
1954
     * Returns the default end date value based on the start date.
1955
     *
1956
     * This is the default implementation for course formats, it is based on
1957
     * moodlecourse/courseduration setting. Course formats like format_weeks for
1958
     * example can overwrite this method and return a value based on their internal options.
1959
     *
1960
     * @param \MoodleQuickForm $mform
1961
     * @param array $fieldnames The form - field names mapping.
1962
     * @return int
1963
     */
1964
    public function get_default_course_enddate($mform, $fieldnames = array()) {
1965
 
1966
        if (empty($fieldnames)) {
1967
            $fieldnames = array('startdate' => 'startdate');
1968
        }
1969
 
1970
        $startdate = $this->get_form_start_date($mform, $fieldnames);
1971
        $courseduration = intval(get_config('moodlecourse', 'courseduration'));
1972
        if (!$courseduration) {
1973
            // Default, it should be already set during upgrade though.
1974
            $courseduration = YEARSECS;
1975
        }
1976
 
1977
        return $startdate + $courseduration;
1978
    }
1979
 
1980
    /**
1981
     * Indicates whether the course format supports the creation of the Announcements forum.
1982
     *
1983
     * For course format plugin developers, please override this to return true if you want the Announcements forum
1984
     * to be created upon course creation.
1985
     *
1986
     * @return bool
1987
     */
1988
    public function supports_news() {
1989
        // For backwards compatibility, check if default blocks include the news_items block.
1990
        $defaultblocks = $this->get_default_blocks();
1991
        foreach ($defaultblocks as $blocks) {
1992
            if (in_array('news_items', $blocks)) {
1993
                return true;
1994
            }
1995
        }
1996
        // Return false by default.
1997
        return false;
1998
    }
1999
 
2000
    /**
2001
     * Get the start date value from the course settings page form.
2002
     *
2003
     * @param \MoodleQuickForm $mform
2004
     * @param array $fieldnames The form - field names mapping.
2005
     * @return int
2006
     */
2007
    protected function get_form_start_date($mform, $fieldnames) {
2008
        $startdate = $mform->getElementValue($fieldnames['startdate']);
2009
        return $mform->getElement($fieldnames['startdate'])->exportValue($startdate);
2010
    }
2011
 
2012
    /**
2013
     * Returns whether this course format allows the activity to
2014
     * have "triple visibility state" - visible always, hidden on course page but available, hidden.
2015
     *
2016
     * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module)
2017
     * @param stdClass|section_info $section section where this module is located or will be added to
2018
     * @return bool
2019
     */
2020
    public function allow_stealth_module_visibility($cm, $section) {
2021
        return false;
2022
    }
2023
 
2024
    /**
2025
     * Callback used in WS core_course_edit_section when teacher performs an AJAX action on a section (show/hide)
2026
     *
2027
     * Access to the course is already validated in the WS but the callback has to make sure
2028
     * that particular action is allowed by checking capabilities
2029
     *
2030
     * Course formats should register
2031
     *
2032
     * @param stdClass|section_info $section
2033
     * @param string $action
2034
     * @param int $sr the section return
2035
     * @return null|array|stdClass any data for the Javascript post-processor (must be json-encodeable)
2036
     */
2037
    public function section_action($section, $action, $sr) {
2038
        global $PAGE;
2039
        if (!$this->uses_sections() || !$section->section) {
2040
            // No section actions are allowed if course format does not support sections.
2041
            // No actions are allowed on the 0-section by default (overwrite in course format if needed).
2042
            throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action));
2043
        }
2044
 
2045
        $course = $this->get_course();
2046
        $coursecontext = context_course::instance($course->id);
2047
        $modinfo = $this->get_modinfo();
2048
        $renderer = $this->get_renderer($PAGE);
2049
 
2050
        if (!($section instanceof section_info)) {
2051
            $section = $modinfo->get_section_info($section->section);
2052
        }
2053
 
2054
        if (!is_null($sr)) {
2055
            $this->set_sectionnum($sr);
2056
        }
2057
 
2058
        switch($action) {
2059
            case 'hide':
2060
            case 'show':
2061
                require_capability('moodle/course:sectionvisibility', $coursecontext);
2062
                $visible = ($action === 'hide') ? 0 : 1;
2063
                course_update_section($course, $section, array('visible' => $visible));
2064
                break;
2065
            case 'refresh':
2066
                return [
2067
                    'content' => $renderer->course_section_updated($this, $section),
2068
                ];
2069
            default:
2070
                throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action));
2071
        }
2072
 
2073
        return ['modules' => $this->get_section_modules_updated($section)];
2074
    }
2075
 
2076
    /**
2077
     * Return an array with all section modules content.
2078
     *
2079
     * This method is used in section_action method to generate the updated modules content
2080
     * after a modinfo change.
2081
     *
2082
     * @param section_info $section the section
2083
     * @return string[] the full modules content.
2084
     */
2085
    protected function get_section_modules_updated(section_info $section): array {
2086
        global $PAGE;
2087
 
2088
        $modules = [];
2089
 
2090
        if (!$this->uses_sections() || !$section->section) {
2091
            return $modules;
2092
        }
2093
 
2094
        // Load the cmlist output from the updated modinfo.
2095
        $renderer = $this->get_renderer($PAGE);
2096
        $modinfo = $this->get_modinfo();
2097
        $coursesections = $modinfo->sections;
2098
        if (array_key_exists($section->section, $coursesections)) {
2099
            foreach ($coursesections[$section->section] as $cmid) {
2100
                $cm = $modinfo->get_cm($cmid);
2101
                $modules[] = $renderer->course_section_updated_cm_item($this, $section, $cm);
2102
            }
2103
        }
2104
        return $modules;
2105
    }
2106
 
2107
    /**
2108
     * Return the plugin config settings for external functions,
2109
     * in some cases the configs will need formatting or be returned only if the current user has some capabilities enabled.
2110
     *
2111
     * @return array the list of configs
2112
     * @since Moodle 3.5
2113
     */
2114
    public function get_config_for_external() {
2115
        return array();
2116
    }
2117
 
2118
    /**
2119
     * Course deletion hook.
2120
     *
2121
     * Format plugins can override this method to clean any format specific data and dependencies.
2122
     *
2123
     */
2124
    public function delete_format_data() {
2125
        global $DB;
2126
        $course = $this->get_course();
2127
        // By default, formats store some most display specifics in a user preference.
2128
        $DB->delete_records('user_preferences', ['name' => 'coursesectionspreferences_' . $course->id]);
2129
    }
2130
 
2131
    /**
2132
     * Duplicate a section
2133
     *
2134
     * @param section_info $originalsection The section to be duplicated
2135
     * @return section_info The new duplicated section
2136
     * @since Moodle 4.2
2137
     */
2138
    public function duplicate_section(section_info $originalsection): section_info {
2139
        if (!$this->uses_sections()) {
2140
            throw new moodle_exception('sectionsnotsupported', 'core_courseformat');
2141
        }
2142
 
2143
        $course = $this->get_course();
11 efrain 2144
        $context = context_course::instance($course->id);
2145
        $newsection = course_create_section($course, $originalsection->section + 1); // Place new section after existing one.
1 efrain 2146
 
11 efrain 2147
        $newsectiondata = new stdClass();
1 efrain 2148
        if (!empty($originalsection->name)) {
11 efrain 2149
            $newsectiondata->name = get_string('duplicatedsection', 'moodle', $originalsection->name);
1 efrain 2150
        } else {
11 efrain 2151
            $newsectiondata->name = $originalsection->name;
1 efrain 2152
        }
11 efrain 2153
        $newsectiondata->summary = $originalsection->summary;
2154
        $newsectiondata->summaryformat = $originalsection->summaryformat;
2155
        $newsectiondata->visible = $originalsection->visible;
2156
        $newsectiondata->availability = $originalsection->availability;
2157
        foreach ($this->section_format_options() as $key => $value) {
2158
            $newsectiondata->$key = $originalsection->$key;
2159
        }
2160
        course_update_section($course, $newsection, $newsectiondata);
1 efrain 2161
 
11 efrain 2162
        try {
2163
            $fs = get_file_storage();
2164
            $files = $fs->get_area_files($context->id, 'course', 'section', $originalsection->id);
2165
 
2166
            foreach ($files as $f) {
2167
 
2168
                $fileinfo = [
2169
                    'contextid' => $context->id,
2170
                    'component' => 'course',
2171
                    'filearea' => 'section',
2172
                    'itemid' => $newsection->id,
2173
                ];
2174
 
2175
                $fs->create_file_from_storedfile($fileinfo, $f);
2176
            }
2177
        } catch (\Exception $e) {
2178
            debugging('Error copying section files.' . $e->getMessage(), DEBUG_DEVELOPER);
2179
        }
2180
 
1 efrain 2181
        $modinfo = $this->get_modinfo();
2182
 
2183
        // Duplicate the section modules, should they exist.
2184
        if (array_key_exists($originalsection->section, $modinfo->sections)) {
2185
            foreach ($modinfo->sections[$originalsection->section] as $modnumber) {
2186
                $originalcm = $modinfo->cms[$modnumber];
1441 ariadna 2187
                // We don't want to duplicate any modules with the FEATURE_CAN_DISPLAY set to false.
2188
                // These mod types are always in section 0 so safe to say we are currently duplicating that section,
2189
                // and we don't want to inadvertantly duplicate mods we can't see.
2190
                if (!$originalcm->is_of_type_that_can_display()) {
2191
                    continue;
2192
                }
11 efrain 2193
                if (!$originalcm->deletioninprogress) {
2194
                    duplicate_module($course, $originalcm, $newsection->id, false);
2195
                }
1 efrain 2196
            }
2197
        }
2198
 
2199
        return get_fast_modinfo($course)->get_section_info_by_id($newsection->id);
2200
    }
2201
 
2202
    /**
2203
     * Get the required javascript files for the course format.
2204
     *
2205
     * @return array The list of javascript files required by the course format.
2206
     */
2207
    public function get_required_jsfiles(): array {
2208
        return [];
2209
    }
2210
 
2211
    /**
2212
     * Determines whether section items can be removed from the navigation, just like the breadcrumb feature seen on activity pages.
2213
     * By default, it returns false but can be overridden by the course format to change the behaviour.
2214
     *
2215
     * @return bool True if sections can be removed, false otherwise.
2216
     */
2217
    public function can_sections_be_removed_from_navigation(): bool {
2218
        return false;
2219
    }
1441 ariadna 2220
 
2221
    /**
2222
     * Determines whether the course module should display the activity editor options.
2223
     *
2224
     * @param cm_info $cm The activity module.
2225
     * @return bool True if the activity editor options are displayed, false otherwise.
2226
     */
2227
    public function show_activity_editor_options(cm_info $cm): bool {
2228
        if ($cm->get_delegated_section_info() && component_callback_exists('mod_' . $cm->modname, 'cm_info_view')) {
2229
            return false;
2230
        }
2231
        return true;
2232
    }
1 efrain 2233
}