MDL-76362 various: Avoid passing nulls to functions that don't allow nulls
[moodle.git] / course / format / classes / base.php
blobbc8a6d69ef7b1813ee6d8043e1466c2e065dab0c
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.
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/>.
17 /**
18 * Contains the base definition class for any course format plugin.
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
25 namespace core_courseformat;
27 use navigation_node;
28 use moodle_page;
29 use core_component;
30 use course_modinfo;
31 use html_writer;
32 use section_info;
33 use context_course;
34 use editsection_form;
35 use moodle_exception;
36 use coding_exception;
37 use moodle_url;
38 use lang_string;
39 use completion_info;
40 use external_api;
41 use stdClass;
42 use cache;
43 use core_courseformat\output\legacy_renderer;
45 /**
46 * Base class for course formats
48 * Each course format must declare class
49 * class format_FORMATNAME extends core_courseformat\base {}
50 * in file lib.php
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.
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.
59 * Also course formats may extend class section_info and overwrite
60 * course_format::build_section_cache() to return more information about sections.
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.
66 * @package core_courseformat
67 * @copyright 2012 Marina Glancy
68 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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 the section number in single section format, zero for multiple section formats. */
81 protected $singlesection = 0;
82 /** @var course_modinfo the current course modinfo, please use course_format::get_modinfo() */
83 private $modinfo = null;
84 /** @var array cached instances */
85 private static $instances = array();
86 /** @var array plugin name => class name. */
87 private static $classesforformat = array('site' => 'site');
89 /**
90 * Creates a new instance of class
92 * Please use course_get_format($courseorid) to get an instance of the format class
94 * @param string $format
95 * @param int $courseid
96 * @return course_format
98 protected function __construct($format, $courseid) {
99 $this->format = $format;
100 $this->courseid = $courseid;
104 * Validates that course format exists and enabled and returns either itself or default format
106 * @param string $format
107 * @return string
109 protected static final function get_format_or_default($format) {
110 global $CFG;
111 require_once($CFG->dirroot . '/course/lib.php');
113 if (array_key_exists($format, self::$classesforformat)) {
114 return self::$classesforformat[$format];
117 $plugins = get_sorted_course_formats();
118 foreach ($plugins as $plugin) {
119 self::$classesforformat[$plugin] = $plugin;
122 if (array_key_exists($format, self::$classesforformat)) {
123 return self::$classesforformat[$format];
126 if (PHPUNIT_TEST && class_exists('format_' . $format)) {
127 // Allow unittests to use non-existing course formats.
128 return $format;
131 // Else return default format.
132 $defaultformat = get_config('moodlecourse', 'format');
133 if (!in_array($defaultformat, $plugins)) {
134 // When default format is not set correctly, use the first available format.
135 $defaultformat = reset($plugins);
137 debugging('Format plugin format_'.$format.' is not found. Using default format_'.$defaultformat, DEBUG_DEVELOPER);
139 self::$classesforformat[$format] = $defaultformat;
140 return $defaultformat;
144 * Get class name for the format.
146 * If course format xxx does not declare class format_xxx, format_legacy will be returned.
147 * This function also includes lib.php file from corresponding format plugin
149 * @param string $format
150 * @return string
152 protected static final function get_class_name($format) {
153 global $CFG;
154 static $classnames = array('site' => 'format_site');
155 if (!isset($classnames[$format])) {
156 $plugins = core_component::get_plugin_list('format');
157 $usedformat = self::get_format_or_default($format);
158 if (isset($plugins[$usedformat]) && file_exists($plugins[$usedformat].'/lib.php')) {
159 require_once($plugins[$usedformat].'/lib.php');
161 $classnames[$format] = 'format_'. $usedformat;
162 if (!class_exists($classnames[$format])) {
163 require_once($CFG->dirroot.'/course/format/formatlegacy.php');
164 $classnames[$format] = 'format_legacy';
167 return $classnames[$format];
171 * Returns an instance of the class
173 * @todo MDL-35727 use MUC for caching of instances, limit the number of cached instances
175 * @param int|stdClass $courseorid either course id or
176 * an object that has the property 'format' and may contain property 'id'
177 * @return course_format
179 public static final function instance($courseorid) {
180 global $DB;
181 if (!is_object($courseorid)) {
182 $courseid = (int)$courseorid;
183 if ($courseid && isset(self::$instances[$courseid]) && count(self::$instances[$courseid]) == 1) {
184 $formats = array_keys(self::$instances[$courseid]);
185 $format = reset($formats);
186 } else {
187 $format = $DB->get_field('course', 'format', array('id' => $courseid), MUST_EXIST);
189 } else {
190 $format = $courseorid->format;
191 if (isset($courseorid->id)) {
192 $courseid = clean_param($courseorid->id, PARAM_INT);
193 } else {
194 $courseid = 0;
197 // Validate that format exists and enabled, use default otherwise.
198 $format = self::get_format_or_default($format);
199 if (!isset(self::$instances[$courseid][$format])) {
200 $classname = self::get_class_name($format);
201 self::$instances[$courseid][$format] = new $classname($format, $courseid);
203 return self::$instances[$courseid][$format];
207 * Resets cache for the course (or all caches)
209 * To be called from rebuild_course_cache()
211 * @param int $courseid
213 public static final function reset_course_cache($courseid = 0) {
214 if ($courseid) {
215 if (isset(self::$instances[$courseid])) {
216 foreach (self::$instances[$courseid] as $format => $object) {
217 // In case somebody keeps the reference to course format object.
218 self::$instances[$courseid][$format]->course = false;
219 self::$instances[$courseid][$format]->formatoptions = array();
220 self::$instances[$courseid][$format]->modinfo = null;
222 unset(self::$instances[$courseid]);
224 } else {
225 self::$instances = array();
230 * Reset the current user course format cache.
232 * The course format cache resets every time the course cache resets but
233 * also when the user changes their course format preference, complete
234 * an activity...
236 * @param stdClass $course the course object
237 * @return string the new statekey
239 public static function session_cache_reset(stdClass $course): string {
240 $statecache = cache::make('core', 'courseeditorstate');
241 $newkey = $course->cacherev . '_' . time();
242 $statecache->set($course->id, $newkey);
243 return $newkey;
247 * Return the current user course format cache key.
249 * The course format session cache can be used to cache the
250 * user course representation. The statekey will be reset when the
251 * the course state changes. For example when the course is edited,
252 * the user completes an activity or simply some course preference
253 * like collapsing a section happens.
255 * @param stdClass $course the course object
256 * @return string the current statekey
258 public static function session_cache(stdClass $course): string {
259 $statecache = cache::make('core', 'courseeditorstate');
260 $statekey = $statecache->get($course->id);
261 // Validate the statekey code.
262 if (preg_match('/^[0-9]+_[0-9]+$/', $statekey)) {
263 list($cacherev) = explode('_', $statekey);
264 if ($cacherev == $course->cacherev) {
265 return $statekey;
268 return self::session_cache_reset($course);
272 * Returns the format name used by this course
274 * @return string
276 public final function get_format() {
277 return $this->format;
281 * Returns id of the course (0 if course is not specified)
283 * @return int
285 public final function get_courseid() {
286 return $this->courseid;
290 * Returns a record from course database table plus additional fields
291 * that course format defines
293 * @return stdClass
295 public function get_course() {
296 global $DB;
297 if (!$this->courseid) {
298 return null;
300 if ($this->course === false) {
301 $this->course = get_course($this->courseid);
302 $options = $this->get_format_options();
303 $dbcoursecolumns = null;
304 foreach ($options as $optionname => $optionvalue) {
305 if (isset($this->course->$optionname)) {
306 // Course format options must not have the same names as existing columns in db table "course".
307 if (!isset($dbcoursecolumns)) {
308 $dbcoursecolumns = $DB->get_columns('course');
310 if (isset($dbcoursecolumns[$optionname])) {
311 debugging('The option name '.$optionname.' in course format '.$this->format.
312 ' is invalid because the field with the same name exists in {course} table',
313 DEBUG_DEVELOPER);
314 continue;
317 $this->course->$optionname = $optionvalue;
320 return $this->course;
324 * Get the course display value for the current course.
326 * Formats extending topics or weeks will use coursedisplay as this setting name
327 * so they don't need to override the method. However, if the format uses a different
328 * display logic it must override this method to ensure the core renderers know
329 * if a COURSE_DISPLAY_MULTIPAGE or COURSE_DISPLAY_SINGLEPAGE is being used.
331 * @return int The current value (COURSE_DISPLAY_MULTIPAGE or COURSE_DISPLAY_SINGLEPAGE)
333 public function get_course_display(): int {
334 return $this->get_course()->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE;
338 * Return the current course modinfo.
340 * This method is used mainly by the output components to avoid unnecesary get_fast_modinfo calls.
342 * @return course_modinfo
344 public function get_modinfo(): course_modinfo {
345 global $USER;
346 if ($this->modinfo === null) {
347 $this->modinfo = course_modinfo::instance($this->courseid, $USER->id);
349 return $this->modinfo;
353 * Method used in the rendered and during backup instead of legacy 'numsections'
355 * Default renderer will treat sections with sectionnumber greater that the value returned by this
356 * method as "orphaned" and not display them on the course page unless in editing mode.
357 * Backup will store this value as 'numsections'.
359 * This method ensures that 3rd party course format plugins that still use 'numsections' continue to
360 * work but at the same time we no longer expect formats to have 'numsections' property.
362 * @return int
364 public function get_last_section_number() {
365 $course = $this->get_course();
366 if (isset($course->numsections)) {
367 return $course->numsections;
369 $modinfo = get_fast_modinfo($course);
370 $sections = $modinfo->get_section_info_all();
371 return (int)max(array_keys($sections));
375 * Method used to get the maximum number of sections for this course format.
376 * @return int
378 public function get_max_sections() {
379 $maxsections = get_config('moodlecourse', 'maxsections');
380 if (!isset($maxsections) || !is_numeric($maxsections)) {
381 $maxsections = 52;
383 return $maxsections;
387 * Returns true if the course has a front page.
389 * This function is called to determine if the course has a view page, whether or not
390 * it contains a listing of activities. It can be useful to set this to false when the course
391 * format has only one activity and ignores the course page. Or if there are multiple
392 * activities but no page to see the centralised information.
394 * Initially this was created to know if forms should add a button to return to the course page.
395 * So if 'Return to course' does not make sense in your format your should probably return false.
397 * @return boolean
398 * @since Moodle 2.6
400 public function has_view_page() {
401 return true;
405 * Generate the title for this section page.
407 * @return string the page title
409 public function page_title(): string {
410 global $PAGE;
411 return $PAGE->title;
415 * Returns true if this course format uses sections
417 * This function may be called without specifying the course id
418 * i.e. in course_format_uses_sections()
420 * Developers, note that if course format does use sections there should be defined a language
421 * string with the name 'sectionname' defining what the section relates to in the format, i.e.
422 * $string['sectionname'] = 'Topic';
423 * or
424 * $string['sectionname'] = 'Week';
426 * @return bool
428 public function uses_sections() {
429 return false;
433 * Returns true if this course format uses course index
435 * This function may be called without specifying the course id
436 * i.e. in course_index_drawer()
438 * @return bool
440 public function uses_course_index() {
441 return false;
445 * Returns true if this course format uses activity indentation.
447 * Indentation is not supported by core formats anymore and may be deprecated in the future.
448 * This method will keep a default return "true" for legacy reasons but new formats should override
449 * it with a return false to prevent future deprecations.
451 * A message in a bottle: if indentation is finally deprecated, both behat steps i_indent_right_activity
452 * and i_indent_left_activity should be removed as well. Right now no core behat uses them but indentation
453 * is not officially deprecated so they are still available for the contrib formats.
455 * @return bool if the course format uses indentation.
457 public function uses_indentation(): bool {
458 return true;
462 * Returns a list of sections used in the course
464 * This is a shortcut to get_fast_modinfo()->get_section_info_all()
465 * @see get_fast_modinfo()
466 * @see course_modinfo::get_section_info_all()
468 * @return array of section_info objects
470 public final function get_sections() {
471 if ($course = $this->get_course()) {
472 $modinfo = get_fast_modinfo($course);
473 return $modinfo->get_section_info_all();
475 return array();
479 * Returns information about section used in course
481 * @param int|stdClass $section either section number (field course_section.section) or row from course_section table
482 * @param int $strictness
483 * @return section_info
485 public final function get_section($section, $strictness = IGNORE_MISSING) {
486 if (is_object($section)) {
487 $sectionnum = $section->section;
488 } else {
489 $sectionnum = $section;
491 $sections = $this->get_sections();
492 if (array_key_exists($sectionnum, $sections)) {
493 return $sections[$sectionnum];
495 if ($strictness == MUST_EXIST) {
496 throw new moodle_exception('sectionnotexist');
498 return null;
502 * Returns the display name of the given section that the course prefers.
504 * @param int|stdClass $section Section object from database or just field course_sections.section
505 * @return Display name that the course format prefers, e.g. "Topic 2"
507 public function get_section_name($section) {
508 if (is_object($section)) {
509 $sectionnum = $section->section;
510 } else {
511 $sectionnum = $section;
514 if (get_string_manager()->string_exists('sectionname', 'format_' . $this->format)) {
515 return get_string('sectionname', 'format_' . $this->format) . ' ' . $sectionnum;
518 // Return an empty string if there's no available section name string for the given format.
519 return '';
523 * Returns the default section using course_format's implementation of get_section_name.
525 * @param int|stdClass $section Section object from database or just field course_sections section
526 * @return string The default value for the section name based on the given course format.
528 public function get_default_section_name($section) {
529 return self::get_section_name($section);
533 * Returns the name for the highlighted section.
535 * @return string The name for the highlighted section based on the given course format.
537 public function get_section_highlighted_name(): string {
538 return get_string('highlighted');
542 * Set if the current format instance will show multiple sections or an individual one.
544 * Some formats has the hability to swith from one section to multiple sections per page,
545 * this method replaces the old print_multiple_section_page and print_single_section_page.
547 * @param int $singlesection zero for all sections or a section number
549 public function set_section_number(int $singlesection): void {
550 $this->singlesection = $singlesection;
554 * Set if the current format instance will show multiple sections or an individual one.
556 * Some formats has the hability to swith from one section to multiple sections per page,
557 * output components will use this method to know if the current display is a single or
558 * multiple sections.
560 * @return int zero for all sections or the sectin number
562 public function get_section_number(): int {
563 return $this->singlesection;
567 * Return the format section preferences.
569 * @return array of preferences indexed by sectionid
571 public function get_sections_preferences(): array {
572 global $USER;
574 $result = [];
576 $course = $this->get_course();
577 $coursesectionscache = cache::make('core', 'coursesectionspreferences');
579 $coursesections = $coursesectionscache->get($course->id);
580 if ($coursesections) {
581 return $coursesections;
584 $sectionpreferences = $this->get_sections_preferences_by_preference();
586 foreach ($sectionpreferences as $preference => $sectionids) {
587 if (!empty($sectionids) && is_array($sectionids)) {
588 foreach ($sectionids as $sectionid) {
589 if (!isset($result[$sectionid])) {
590 $result[$sectionid] = new stdClass();
592 $result[$sectionid]->$preference = true;
597 $coursesectionscache->set($course->id, $result);
598 return $result;
602 * Return the format section preferences.
604 * @return array of preferences indexed by preference name
606 public function get_sections_preferences_by_preference(): array {
607 global $USER;
608 $course = $this->get_course();
609 try {
610 $sectionpreferences = (array) json_decode(
611 get_user_preferences('coursesectionspreferences_' . $course->id, null, $USER->id) ?? ''
613 if (empty($sectionpreferences)) {
614 $sectionpreferences = [];
616 } catch (\Throwable $e) {
617 $sectionpreferences = [];
619 return $sectionpreferences;
623 * Return the format section preferences.
625 * @param string $preferencename preference name
626 * @param int[] $sectionids affected section ids
629 public function set_sections_preference(string $preferencename, array $sectionids) {
630 global $USER;
631 $course = $this->get_course();
632 $sectionpreferences = $this->get_sections_preferences_by_preference();
633 $sectionpreferences[$preferencename] = $sectionids;
634 set_user_preference('coursesectionspreferences_' . $course->id, json_encode($sectionpreferences), $USER->id);
635 // Invalidate section preferences cache.
636 $coursesectionscache = cache::make('core', 'coursesectionspreferences');
637 $coursesectionscache->delete($course->id);
641 * Returns the information about the ajax support in the given source format
643 * The returned object's property (boolean)capable indicates that
644 * the course format supports Moodle course ajax features.
646 * @return stdClass
648 public function supports_ajax() {
649 // No support by default.
650 $ajaxsupport = new stdClass();
651 $ajaxsupport->capable = false;
652 return $ajaxsupport;
656 * Returns true if this course format is compatible with content components.
658 * Using components means the content elements can watch the frontend course state and
659 * react to the changes. Formats with component compatibility can have more interactions
660 * without refreshing the page, like having drag and drop from the course index to reorder
661 * sections and activities.
663 * @return bool if the format is compatible with components.
665 public function supports_components() {
666 return false;
671 * Custom action after section has been moved in AJAX mode
673 * Used in course/rest.php
675 * @return array This will be passed in ajax respose
677 public function ajax_section_move() {
678 return null;
682 * The URL to use for the specified course (with section)
684 * Please note that course view page /course/view.php?id=COURSEID is hardcoded in many
685 * places in core and contributed modules. If course format wants to change the location
686 * of the view script, it is not enough to change just this function. Do not forget
687 * to add proper redirection.
689 * @param int|stdClass $section Section object from database or just field course_sections.section
690 * if null the course view page is returned
691 * @param array $options options for view URL. At the moment core uses:
692 * 'navigation' (bool) if true and section has no separate page, the function returns null
693 * 'sr' (int) used by multipage formats to specify to which section to return
694 * @return null|moodle_url
696 public function get_view_url($section, $options = array()) {
697 global $CFG;
698 $course = $this->get_course();
699 $url = new moodle_url('/course/view.php', array('id' => $course->id));
701 if (array_key_exists('sr', $options)) {
702 $sectionno = $options['sr'];
703 } else if (is_object($section)) {
704 $sectionno = $section->section;
705 } else {
706 $sectionno = $section;
708 if (empty($CFG->linkcoursesections) && !empty($options['navigation']) && $sectionno !== null) {
709 // By default assume that sections are never displayed on separate pages.
710 return null;
712 if ($this->uses_sections() && $sectionno !== null) {
713 $url->set_anchor('section-'.$sectionno);
715 return $url;
719 * Loads all of the course sections into the navigation
721 * This method is called from global_navigation::load_course_sections()
723 * By default the method global_navigation::load_generic_course_sections() is called
725 * When overwriting please note that navigationlib relies on using the correct values for
726 * arguments $type and $key in navigation_node::add()
728 * Example of code creating a section node:
729 * $sectionnode = $node->add($sectionname, $url, navigation_node::TYPE_SECTION, null, $section->id);
730 * $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH;
732 * Example of code creating an activity node:
733 * $activitynode = $sectionnode->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $activity->id, $icon);
734 * if (global_navigation::module_extends_navigation($activity->modname)) {
735 * $activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
736 * } else {
737 * $activitynode->nodetype = navigation_node::NODETYPE_LEAF;
740 * Also note that if $navigation->includesectionnum is not null, the section with this relative
741 * number needs is expected to be loaded
743 * @param global_navigation $navigation
744 * @param navigation_node $node The course node within the navigation
746 public function extend_course_navigation($navigation, navigation_node $node) {
747 if ($course = $this->get_course()) {
748 $navigation->load_generic_course_sections($course, $node);
750 return array();
754 * Returns the list of blocks to be automatically added for the newly created course
756 * @see blocks_add_default_course_blocks()
758 * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
759 * each of values is an array of block names (for left and right side columns)
761 public function get_default_blocks() {
762 global $CFG;
763 if (isset($CFG->defaultblocks)) {
764 return blocks_parse_default_blocks_list($CFG->defaultblocks);
766 $blocknames = array(
767 BLOCK_POS_LEFT => array(),
768 BLOCK_POS_RIGHT => array()
770 return $blocknames;
774 * Returns the localised name of this course format plugin
776 * @return lang_string
778 public final function get_format_name() {
779 return new lang_string('pluginname', 'format_'.$this->get_format());
783 * Definitions of the additional options that this course format uses for course
785 * This function may be called often, it should be as fast as possible.
786 * Avoid using get_string() method, use "new lang_string()" instead
787 * It is not recommended to use dynamic or course-dependant expressions here
788 * This function may be also called when course does not exist yet.
790 * Option names must be different from fields in the {course} talbe or any form elements on
791 * course edit form, it may even make sence to use special prefix for them.
793 * Each option must have the option name as a key and the array of properties as a value:
794 * 'default' - default value for this option (assumed null if not specified)
795 * 'type' - type of the option value (PARAM_INT, PARAM_RAW, etc.)
797 * Additional properties used by default implementation of
798 * course_format::create_edit_form_elements() (calls this method with $foreditform = true)
799 * 'label' - localised human-readable label for the edit form
800 * 'element_type' - type of the form element, default 'text'
801 * 'element_attributes' - additional attributes for the form element, these are 4th and further
802 * arguments in the moodleform::addElement() method
803 * 'help' - string for help button. Note that if 'help' value is 'myoption' then the string with
804 * the name 'myoption_help' must exist in the language file
805 * 'help_component' - language component to look for help string, by default this the component
806 * for this course format
808 * This is an interface for creating simple form elements. If format plugin wants to use other
809 * methods such as disableIf, it can be done by overriding create_edit_form_elements().
811 * Course format options can be accessed as:
812 * $this->get_course()->OPTIONNAME (inside the format class)
813 * course_get_format($course)->get_course()->OPTIONNAME (outside of format class)
815 * All course options are returned by calling:
816 * $this->get_format_options();
818 * @param bool $foreditform
819 * @return array of options
821 public function course_format_options($foreditform = false) {
822 return array();
826 * Definitions of the additional options that this course format uses for section
828 * See course_format::course_format_options() for return array definition.
830 * Additionally section format options may have property 'cache' set to true
831 * if this option needs to be cached in get_fast_modinfo(). The 'cache' property
832 * is recommended to be set only for fields used in course_format::get_section_name(),
833 * course_format::extend_course_navigation() and course_format::get_view_url()
835 * For better performance cached options are recommended to have 'cachedefault' property
836 * Unlike 'default', 'cachedefault' should be static and not access get_config().
838 * Regardless of value of 'cache' all options are accessed in the code as
839 * $sectioninfo->OPTIONNAME
840 * where $sectioninfo is instance of section_info, returned by
841 * get_fast_modinfo($course)->get_section_info($sectionnum)
842 * or get_fast_modinfo($course)->get_section_info_all()
844 * All format options for particular section are returned by calling:
845 * $this->get_format_options($section);
847 * @param bool $foreditform
848 * @return array
850 public function section_format_options($foreditform = false) {
851 return array();
855 * Returns the format options stored for this course or course section
857 * When overriding please note that this function is called from rebuild_course_cache()
858 * and section_info object, therefore using of get_fast_modinfo() and/or any function that
859 * accesses it may lead to recursion.
861 * @param null|int|stdClass|section_info $section if null the course format options will be returned
862 * otherwise options for specified section will be returned. This can be either
863 * section object or relative section number (field course_sections.section)
864 * @return array
866 public function get_format_options($section = null) {
867 global $DB;
868 if ($section === null) {
869 $options = $this->course_format_options();
870 } else {
871 $options = $this->section_format_options();
873 if (empty($options)) {
874 // There are no option for course/sections anyway, no need to go further.
875 return array();
877 if ($section === null) {
878 // Course format options will be returned.
879 $sectionid = 0;
880 } else if ($this->courseid && isset($section->id)) {
881 // Course section format options will be returned.
882 $sectionid = $section->id;
883 } else if ($this->courseid && is_int($section) &&
884 ($sectionobj = $DB->get_record('course_sections',
885 array('section' => $section, 'course' => $this->courseid), 'id'))) {
886 // Course section format options will be returned.
887 $sectionid = $sectionobj->id;
888 } else {
889 // Non-existing (yet) section was passed as an argument
890 // default format options for course section will be returned.
891 $sectionid = -1;
893 if (!array_key_exists($sectionid, $this->formatoptions)) {
894 $this->formatoptions[$sectionid] = array();
895 // First fill with default values.
896 foreach ($options as $optionname => $optionparams) {
897 $this->formatoptions[$sectionid][$optionname] = null;
898 if (array_key_exists('default', $optionparams)) {
899 $this->formatoptions[$sectionid][$optionname] = $optionparams['default'];
902 if ($this->courseid && $sectionid !== -1) {
903 // Overwrite the default options values with those stored in course_format_options table
904 // nothing can be stored if we are interested in generic course ($this->courseid == 0)
905 // or generic section ($sectionid === 0).
906 $records = $DB->get_records('course_format_options',
907 array('courseid' => $this->courseid,
908 'format' => $this->format,
909 'sectionid' => $sectionid
910 ), '', 'id,name,value');
911 $indexedrecords = [];
912 foreach ($records as $record) {
913 $indexedrecords[$record->name] = $record->value;
915 foreach ($options as $optionname => $option) {
916 contract_value($this->formatoptions[$sectionid], $indexedrecords, $option, $optionname);
920 return $this->formatoptions[$sectionid];
924 * Adds format options elements to the course/section edit form
926 * This function is called from course_edit_form::definition_after_data()
928 * @param MoodleQuickForm $mform form the elements are added to
929 * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form
930 * @return array array of references to the added form elements
932 public function create_edit_form_elements(&$mform, $forsection = false) {
933 $elements = array();
934 if ($forsection) {
935 $options = $this->section_format_options(true);
936 } else {
937 $options = $this->course_format_options(true);
939 foreach ($options as $optionname => $option) {
940 if (!isset($option['element_type'])) {
941 $option['element_type'] = 'text';
943 $args = array($option['element_type'], $optionname, $option['label']);
944 if (!empty($option['element_attributes'])) {
945 $args = array_merge($args, $option['element_attributes']);
947 $elements[] = call_user_func_array(array($mform, 'addElement'), $args);
948 if (isset($option['help'])) {
949 $helpcomponent = 'format_'. $this->get_format();
950 if (isset($option['help_component'])) {
951 $helpcomponent = $option['help_component'];
953 $mform->addHelpButton($optionname, $option['help'], $helpcomponent);
955 if (isset($option['type'])) {
956 $mform->setType($optionname, $option['type']);
958 if (isset($option['default']) && !array_key_exists($optionname, $mform->_defaultValues)) {
959 // Set defaults for the elements in the form.
960 // Since we call this method after set_data() make sure that we don't override what was already set.
961 $mform->setDefault($optionname, $option['default']);
965 if (!$forsection && empty($this->courseid)) {
966 // Check if course end date form field should be enabled by default.
967 // If a default date is provided to the form element, it is magically enabled by default in the
968 // MoodleQuickForm_date_time_selector class, otherwise it's disabled by default.
969 if (get_config('moodlecourse', 'courseenddateenabled')) {
970 // At this stage (this is called from definition_after_data) course data is already set as default.
971 // We can not overwrite what is in the database.
972 $mform->setDefault('enddate', $this->get_default_course_enddate($mform));
976 return $elements;
980 * Override if you need to perform some extra validation of the format options
982 * @param array $data array of ("fieldname"=>value) of submitted data
983 * @param array $files array of uploaded files "element_name"=>tmp_file_path
984 * @param array $errors errors already discovered in edit form validation
985 * @return array of "element_name"=>"error_description" if there are errors,
986 * or an empty array if everything is OK.
987 * Do not repeat errors from $errors param here
989 public function edit_form_validation($data, $files, $errors) {
990 return array();
994 * Prepares values of course or section format options before storing them in DB
996 * If an option has invalid value it is not returned
998 * @param array $rawdata associative array of the proposed course/section format options
999 * @param int|null $sectionid null if it is course format option
1000 * @return array array of options that have valid values
1002 protected function validate_format_options(array $rawdata, int $sectionid = null) : array {
1003 if (!$sectionid) {
1004 $allformatoptions = $this->course_format_options(true);
1005 } else {
1006 $allformatoptions = $this->section_format_options(true);
1008 $data = array_intersect_key($rawdata, $allformatoptions);
1009 foreach ($data as $key => $value) {
1010 $option = $allformatoptions[$key] + ['type' => PARAM_RAW, 'element_type' => null, 'element_attributes' => [[]]];
1011 expand_value($data, $data, $option, $key);
1012 if ($option['element_type'] === 'select' && !array_key_exists($data[$key], $option['element_attributes'][0])) {
1013 // Value invalid for select element, skip.
1014 unset($data[$key]);
1017 return $data;
1021 * Validates format options for the course
1023 * @param array $data data to insert/update
1024 * @return array array of options that have valid values
1026 public function validate_course_format_options(array $data) : array {
1027 return $this->validate_format_options($data);
1031 * Updates format options for a course or section
1033 * If $data does not contain property with the option name, the option will not be updated
1035 * @param stdClass|array $data return value from moodleform::get_data() or array with data
1036 * @param null|int $sectionid null if these are options for course or section id (course_sections.id)
1037 * if these are options for section
1038 * @return bool whether there were any changes to the options values
1040 protected function update_format_options($data, $sectionid = null) {
1041 global $DB;
1042 $data = $this->validate_format_options((array)$data, $sectionid);
1043 if (!$sectionid) {
1044 $allformatoptions = $this->course_format_options();
1045 $sectionid = 0;
1046 } else {
1047 $allformatoptions = $this->section_format_options();
1049 if (empty($allformatoptions)) {
1050 // Nothing to update anyway.
1051 return false;
1053 $defaultoptions = array();
1054 $cached = array();
1055 foreach ($allformatoptions as $key => $option) {
1056 $defaultoptions[$key] = null;
1057 if (array_key_exists('default', $option)) {
1058 $defaultoptions[$key] = $option['default'];
1060 expand_value($defaultoptions, $defaultoptions, $option, $key);
1061 $cached[$key] = ($sectionid === 0 || !empty($option['cache']));
1063 $records = $DB->get_records('course_format_options',
1064 array('courseid' => $this->courseid,
1065 'format' => $this->format,
1066 'sectionid' => $sectionid
1067 ), '', 'name,id,value');
1068 $changed = $needrebuild = false;
1069 foreach ($defaultoptions as $key => $value) {
1070 if (isset($records[$key])) {
1071 if (array_key_exists($key, $data) && $records[$key]->value != $data[$key]) {
1072 $DB->set_field('course_format_options', 'value',
1073 $data[$key], array('id' => $records[$key]->id));
1074 $changed = true;
1075 $needrebuild = $needrebuild || $cached[$key];
1077 } else {
1078 if (array_key_exists($key, $data) && $data[$key] !== $value) {
1079 $newvalue = $data[$key];
1080 $changed = true;
1081 $needrebuild = $needrebuild || $cached[$key];
1082 } else {
1083 $newvalue = $value;
1084 // We still insert entry in DB but there are no changes from user point of
1085 // view and no need to call rebuild_course_cache().
1087 $DB->insert_record('course_format_options', array(
1088 'courseid' => $this->courseid,
1089 'format' => $this->format,
1090 'sectionid' => $sectionid,
1091 'name' => $key,
1092 'value' => $newvalue
1096 if ($needrebuild) {
1097 if ($sectionid) {
1098 // Invalidate the section cache by given section id.
1099 course_modinfo::purge_course_section_cache_by_id($this->courseid, $sectionid);
1100 // Partial rebuild sections that have been invalidated.
1101 rebuild_course_cache($this->courseid, true, true);
1102 } else {
1103 // Full rebuild if sectionid is null.
1104 rebuild_course_cache($this->courseid);
1107 if ($changed) {
1108 // Reset internal caches.
1109 if (!$sectionid) {
1110 $this->course = false;
1112 unset($this->formatoptions[$sectionid]);
1114 return $changed;
1118 * Updates format options for a course
1120 * If $data does not contain property with the option name, the option will not be updated
1122 * @param stdClass|array $data return value from moodleform::get_data() or array with data
1123 * @param stdClass $oldcourse if this function is called from update_course()
1124 * this object contains information about the course before update
1125 * @return bool whether there were any changes to the options values
1127 public function update_course_format_options($data, $oldcourse = null) {
1128 return $this->update_format_options($data);
1132 * Updates format options for a section
1134 * Section id is expected in $data->id (or $data['id'])
1135 * If $data does not contain property with the option name, the option will not be updated
1137 * @param stdClass|array $data return value from moodleform::get_data() or array with data
1138 * @return bool whether there were any changes to the options values
1140 public function update_section_format_options($data) {
1141 $data = (array)$data;
1142 return $this->update_format_options($data, $data['id']);
1146 * Return an instance of moodleform to edit a specified section
1148 * Default implementation returns instance of editsection_form that automatically adds
1149 * additional fields defined in course_format::section_format_options()
1151 * Format plugins may extend editsection_form if they want to have custom edit section form.
1153 * @param mixed $action the action attribute for the form. If empty defaults to auto detect the
1154 * current url. If a moodle_url object then outputs params as hidden variables.
1155 * @param array $customdata the array with custom data to be passed to the form
1156 * /course/editsection.php passes section_info object in 'cs' field
1157 * for filling availability fields
1158 * @return moodleform
1160 public function editsection_form($action, $customdata = array()) {
1161 global $CFG;
1162 require_once($CFG->dirroot. '/course/editsection_form.php');
1163 if (!array_key_exists('course', $customdata)) {
1164 $customdata['course'] = $this->get_course();
1166 return new editsection_form($action, $customdata);
1170 * Allows course format to execute code on moodle_page::set_course()
1172 * @param moodle_page $page instance of page calling set_course
1174 public function page_set_course(moodle_page $page) {
1178 * Allows course format to execute code on moodle_page::set_cm()
1180 * Current module can be accessed as $page->cm (returns instance of cm_info)
1182 * @param moodle_page $page instance of page calling set_cm
1184 public function page_set_cm(moodle_page $page) {
1188 * Course-specific information to be output on any course page (usually above navigation bar)
1190 * Example of usage:
1191 * define
1192 * class format_FORMATNAME_XXX implements renderable {}
1194 * create format renderer in course/format/FORMATNAME/renderer.php, define rendering function:
1195 * class format_FORMATNAME_renderer extends plugin_renderer_base {
1196 * protected function render_format_FORMATNAME_XXX(format_FORMATNAME_XXX $xxx) {
1197 * return html_writer::tag('div', 'This is my header/footer');
1201 * Return instance of format_FORMATNAME_XXX in this function, the appropriate method from
1202 * plugin renderer will be called
1204 * @return null|renderable null for no output or object with data for plugin renderer
1206 public function course_header() {
1207 return null;
1211 * Course-specific information to be output on any course page (usually in the beginning of
1212 * standard footer)
1214 * See course_format::course_header() for usage
1216 * @return null|renderable null for no output or object with data for plugin renderer
1218 public function course_footer() {
1219 return null;
1223 * Course-specific information to be output immediately above content on any course page
1225 * See course_format::course_header() for usage
1227 * @return null|renderable null for no output or object with data for plugin renderer
1229 public function course_content_header() {
1230 return null;
1234 * Course-specific information to be output immediately below content on any course page
1236 * See course_format::course_header() for usage
1238 * @return null|renderable null for no output or object with data for plugin renderer
1240 public function course_content_footer() {
1241 return null;
1245 * Returns instance of page renderer used by this plugin
1247 * @param moodle_page $page
1248 * @return renderer_base
1250 public function get_renderer(moodle_page $page) {
1251 try {
1252 $renderer = $page->get_renderer('format_'. $this->get_format());
1253 } catch (moodle_exception $e) {
1254 $formatname = $this->get_format();
1255 $expectedrenderername = 'format_'. $this->get_format() . '\output\renderer';
1256 debugging(
1257 "The '{$formatname}' course format does not define the {$expectedrenderername} renderer class. This is required since Moodle 4.0.",
1258 DEBUG_DEVELOPER
1260 $renderer = new legacy_renderer($page, null);
1263 return $renderer;
1267 * Returns instance of output component used by this plugin
1269 * @throws coding_exception if the format class does not extends the original core one.
1270 * @param string $outputname the element to render (section, activity...)
1271 * @return string the output component classname
1273 public function get_output_classname(string $outputname): string {
1274 // The core output class.
1275 $baseclass = "core_courseformat\\output\\local\\$outputname";
1277 // Look in this format and any parent formats before we get to the base one.
1278 $classes = array_merge([get_class($this)], class_parents($this));
1279 foreach ($classes as $component) {
1280 if ($component === self::class) {
1281 break;
1284 // Because course formats are in the root namespace, there is no need to process the
1285 // class name - it is already a Frankenstyle component name beginning 'format_'.
1287 // Check if there is a specific class in this format.
1288 $outputclass = "$component\\output\\courseformat\\$outputname";
1289 if (class_exists($outputclass)) {
1290 // Check that the outputclass is a subclass of the base class.
1291 if (!is_subclass_of($outputclass, $baseclass)) {
1292 throw new coding_exception("The \"$outputclass\" must extend \"$baseclass\"");
1294 return $outputclass;
1298 return $baseclass;
1302 * Returns true if the specified section is current
1304 * By default we analyze $course->marker
1306 * @param int|stdClass|section_info $section
1307 * @return bool
1309 public function is_section_current($section) {
1310 if (is_object($section)) {
1311 $sectionnum = $section->section;
1312 } else {
1313 $sectionnum = $section;
1315 return ($sectionnum && ($course = $this->get_course()) && $course->marker == $sectionnum);
1319 * Returns if an specific section is visible to the current user.
1321 * Formats can overrride this method to implement any special section logic.
1323 * @param section_info $section the section modinfo
1324 * @return bool;
1326 public function is_section_visible(section_info $section): bool {
1327 // Previous to Moodle 4.0 thas logic was hardcoded. To prevent errors in the contrib plugins
1328 // the default logic is the same required for topics and weeks format and still uses
1329 // a "hiddensections" format setting.
1330 $course = $this->get_course();
1331 $hidesections = $course->hiddensections ?? true;
1332 // Show the section if the user is permitted to access it, OR if it's not available
1333 // but there is some available info text which explains the reason & should display,
1334 // OR it is hidden but the course has a setting to display hidden sections as unavailable.
1335 return $section->uservisible ||
1336 ($section->visible && !$section->available && !empty($section->availableinfo)) ||
1337 (!$section->visible && !$hidesections);
1341 * return true if the course editor must be displayed.
1343 * @param array|null $capabilities array of capabilities a user needs to have to see edit controls in general.
1344 * If null or not specified, the user needs to have 'moodle/course:manageactivities'.
1345 * @return bool true if edit controls must be displayed
1347 public function show_editor(?array $capabilities = ['moodle/course:manageactivities']): bool {
1348 global $PAGE;
1349 $course = $this->get_course();
1350 $coursecontext = context_course::instance($course->id);
1351 if ($capabilities === null) {
1352 $capabilities = ['moodle/course:manageactivities'];
1354 return $PAGE->user_is_editing() && has_all_capabilities($capabilities, $coursecontext);
1358 * Allows to specify for modinfo that section is not available even when it is visible and conditionally available.
1360 * Note: affected user can be retrieved as: $section->modinfo->userid
1362 * Course format plugins can override the method to change the properties $available and $availableinfo that were
1363 * calculated by conditional availability.
1364 * To make section unavailable set:
1365 * $available = false;
1366 * To make unavailable section completely hidden set:
1367 * $availableinfo = '';
1368 * To make unavailable section visible with availability message set:
1369 * $availableinfo = get_string('sectionhidden', 'format_xxx');
1371 * @param section_info $section
1372 * @param bool $available the 'available' propery of the section_info as it was evaluated by conditional availability.
1373 * Can be changed by the method but 'false' can not be overridden by 'true'.
1374 * @param string $availableinfo the 'availableinfo' propery of the section_info as it was evaluated by conditional availability.
1375 * Can be changed by the method
1377 public function section_get_available_hook(section_info $section, &$available, &$availableinfo) {
1381 * Whether this format allows to delete sections
1383 * If format supports deleting sections it is also recommended to define language string
1384 * 'deletesection' inside the format.
1386 * Do not call this function directly, instead use course_can_delete_section()
1388 * @param int|stdClass|section_info $section
1389 * @return bool
1391 public function can_delete_section($section) {
1392 return false;
1396 * Deletes a section
1398 * Do not call this function directly, instead call course_delete_section()
1400 * @param int|stdClass|section_info $section
1401 * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it.
1402 * @return bool whether section was deleted
1404 public function delete_section($section, $forcedeleteifnotempty = false) {
1405 global $DB;
1406 if (!$this->uses_sections()) {
1407 // Not possible to delete section if sections are not used.
1408 return false;
1410 if (!is_object($section)) {
1411 $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section),
1412 'id,section,sequence,summary');
1414 if (!$section || !$section->section) {
1415 // Not possible to delete 0-section.
1416 return false;
1419 if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
1420 return false;
1423 $course = $this->get_course();
1425 // Remove the marker if it points to this section.
1426 if ($section->section == $course->marker) {
1427 course_set_marker($course->id, 0);
1430 $lastsection = $DB->get_field_sql('SELECT max(section) from {course_sections}
1431 WHERE course = ?', array($course->id));
1433 // Find out if we need to descrease the 'numsections' property later.
1434 $courseformathasnumsections = array_key_exists('numsections',
1435 $this->get_format_options());
1436 $decreasenumsections = $courseformathasnumsections && ($section->section <= $course->numsections);
1438 // Move the section to the end.
1439 move_section_to($course, $section->section, $lastsection, true);
1441 // Delete all modules from the section.
1442 foreach (preg_split('/,/', $section->sequence, -1, PREG_SPLIT_NO_EMPTY) as $cmid) {
1443 course_delete_module($cmid);
1446 // Delete section and it's format options.
1447 $DB->delete_records('course_format_options', array('sectionid' => $section->id));
1448 $DB->delete_records('course_sections', array('id' => $section->id));
1449 // Invalidate the section cache by given section id.
1450 course_modinfo::purge_course_section_cache_by_id($course->id, $section->id);
1451 // Partial rebuild section cache that has been purged.
1452 rebuild_course_cache($course->id, true, true);
1454 // Delete section summary files.
1455 $context = \context_course::instance($course->id);
1456 $fs = get_file_storage();
1457 $fs->delete_area_files($context->id, 'course', 'section', $section->id);
1459 // Descrease 'numsections' if needed.
1460 if ($decreasenumsections) {
1461 $this->update_course_format_options(array('numsections' => $course->numsections - 1));
1464 return true;
1468 * Prepares the templateable object to display section name
1470 * @param \section_info|\stdClass $section
1471 * @param bool $linkifneeded
1472 * @param bool $editable
1473 * @param null|lang_string|string $edithint
1474 * @param null|lang_string|string $editlabel
1475 * @return \core\output\inplace_editable
1477 public function inplace_editable_render_section_name($section, $linkifneeded = true,
1478 $editable = null, $edithint = null, $editlabel = null) {
1479 global $USER, $CFG;
1480 require_once($CFG->dirroot.'/course/lib.php');
1482 if ($editable === null) {
1483 $editable = !empty($USER->editing) && has_capability('moodle/course:update',
1484 context_course::instance($section->course));
1487 $displayvalue = $title = get_section_name($section->course, $section);
1488 if ($linkifneeded) {
1489 // Display link under the section name if the course format setting is to display one section per page.
1490 $url = course_get_url($section->course, $section->section, array('navigation' => true));
1491 if ($url) {
1492 $displayvalue = html_writer::link($url, $title);
1494 $itemtype = 'sectionname';
1495 } else {
1496 // If $linkifneeded==false, we never display the link (this is used when rendering the section header).
1497 // Itemtype 'sectionnamenl' (nl=no link) will tell the callback that link should not be rendered -
1498 // there is no other way callback can know where we display the section name.
1499 $itemtype = 'sectionnamenl';
1501 if (empty($edithint)) {
1502 $edithint = new lang_string('editsectionname');
1504 if (empty($editlabel)) {
1505 $editlabel = new lang_string('newsectionname', '', $title);
1508 return new \core\output\inplace_editable('format_' . $this->format, $itemtype, $section->id, $editable,
1509 $displayvalue, $section->name, $edithint, $editlabel);
1513 * Updates the value in the database and modifies this object respectively.
1515 * ALWAYS check user permissions before performing an update! Throw exceptions if permissions are not sufficient
1516 * or value is not legit.
1518 * @param stdClass $section
1519 * @param string $itemtype
1520 * @param mixed $newvalue
1521 * @return \core\output\inplace_editable
1523 public function inplace_editable_update_section_name($section, $itemtype, $newvalue) {
1524 if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') {
1525 $context = context_course::instance($section->course);
1526 external_api::validate_context($context);
1527 require_capability('moodle/course:update', $context);
1529 $newtitle = clean_param($newvalue, PARAM_TEXT);
1530 if (strval($section->name) !== strval($newtitle)) {
1531 course_update_section($section->course, $section, array('name' => $newtitle));
1533 return $this->inplace_editable_render_section_name($section, ($itemtype === 'sectionname'), true);
1539 * Returns the default end date value based on the start date.
1541 * This is the default implementation for course formats, it is based on
1542 * moodlecourse/courseduration setting. Course formats like format_weeks for
1543 * example can overwrite this method and return a value based on their internal options.
1545 * @param moodleform $mform
1546 * @param array $fieldnames The form - field names mapping.
1547 * @return int
1549 public function get_default_course_enddate($mform, $fieldnames = array()) {
1551 if (empty($fieldnames)) {
1552 $fieldnames = array('startdate' => 'startdate');
1555 $startdate = $this->get_form_start_date($mform, $fieldnames);
1556 $courseduration = intval(get_config('moodlecourse', 'courseduration'));
1557 if (!$courseduration) {
1558 // Default, it should be already set during upgrade though.
1559 $courseduration = YEARSECS;
1562 return $startdate + $courseduration;
1566 * Indicates whether the course format supports the creation of the Announcements forum.
1568 * For course format plugin developers, please override this to return true if you want the Announcements forum
1569 * to be created upon course creation.
1571 * @return bool
1573 public function supports_news() {
1574 // For backwards compatibility, check if default blocks include the news_items block.
1575 $defaultblocks = $this->get_default_blocks();
1576 foreach ($defaultblocks as $blocks) {
1577 if (in_array('news_items', $blocks)) {
1578 return true;
1581 // Return false by default.
1582 return false;
1586 * Get the start date value from the course settings page form.
1588 * @param moodleform $mform
1589 * @param array $fieldnames The form - field names mapping.
1590 * @return int
1592 protected function get_form_start_date($mform, $fieldnames) {
1593 $startdate = $mform->getElementValue($fieldnames['startdate']);
1594 return $mform->getElement($fieldnames['startdate'])->exportValue($startdate);
1598 * Returns whether this course format allows the activity to
1599 * have "triple visibility state" - visible always, hidden on course page but available, hidden.
1601 * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module)
1602 * @param stdClass|section_info $section section where this module is located or will be added to
1603 * @return bool
1605 public function allow_stealth_module_visibility($cm, $section) {
1606 return false;
1610 * Callback used in WS core_course_edit_section when teacher performs an AJAX action on a section (show/hide)
1612 * Access to the course is already validated in the WS but the callback has to make sure
1613 * that particular action is allowed by checking capabilities
1615 * Course formats should register
1617 * @param stdClass|section_info $section
1618 * @param string $action
1619 * @param int $sr the section return
1620 * @return null|array|stdClass any data for the Javascript post-processor (must be json-encodeable)
1622 public function section_action($section, $action, $sr) {
1623 global $PAGE;
1624 if (!$this->uses_sections() || !$section->section) {
1625 // No section actions are allowed if course format does not support sections.
1626 // No actions are allowed on the 0-section by default (overwrite in course format if needed).
1627 throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action));
1630 $course = $this->get_course();
1631 $coursecontext = context_course::instance($course->id);
1632 $modinfo = $this->get_modinfo();
1633 $renderer = $this->get_renderer($PAGE);
1635 if (!($section instanceof section_info)) {
1636 $section = $modinfo->get_section_info($section->section);
1639 if ($sr) {
1640 $this->set_section_number($sr);
1643 switch($action) {
1644 case 'hide':
1645 case 'show':
1646 require_capability('moodle/course:sectionvisibility', $coursecontext);
1647 $visible = ($action === 'hide') ? 0 : 1;
1648 course_update_section($course, $section, array('visible' => $visible));
1649 break;
1650 case 'refresh':
1651 return [
1652 'content' => $renderer->course_section_updated($this, $section),
1654 default:
1655 throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action));
1658 return ['modules' => $this->get_section_modules_updated($section)];
1662 * Return an array with all section modules content.
1664 * This method is used in section_action method to generate the updated modules content
1665 * after a modinfo change.
1667 * @param section_info $section the section
1668 * @return string[] the full modules content.
1670 protected function get_section_modules_updated(section_info $section): array {
1671 global $PAGE;
1673 $modules = [];
1675 if (!$this->uses_sections() || !$section->section) {
1676 return $modules;
1679 // Load the cmlist output from the updated modinfo.
1680 $renderer = $this->get_renderer($PAGE);
1681 $modinfo = $this->get_modinfo();
1682 $coursesections = $modinfo->sections;
1683 if (array_key_exists($section->section, $coursesections)) {
1684 foreach ($coursesections[$section->section] as $cmid) {
1685 $cm = $modinfo->get_cm($cmid);
1686 $modules[] = $renderer->course_section_updated_cm_item($this, $section, $cm);
1689 return $modules;
1693 * Return the plugin config settings for external functions,
1694 * in some cases the configs will need formatting or be returned only if the current user has some capabilities enabled.
1696 * @return array the list of configs
1697 * @since Moodle 3.5
1699 public function get_config_for_external() {
1700 return array();
1704 * Course deletion hook.
1706 * Format plugins can override this method to clean any format specific data and dependencies.
1709 public function delete_format_data() {
1710 global $DB;
1711 $course = $this->get_course();
1712 // By default, formats store some most display specifics in a user preference.
1713 $DB->delete_records('user_preferences', ['name' => 'coursesectionspreferences_' . $course->id]);