MDL-62748 course: maxsections is a limit
[moodle.git] / course / format / lib.php
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/>.
17 /**
18  * Base class for course format plugins
19  *
20  * @package    core_course
21  * @copyright  2012 Marina Glancy
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die;
27 /**
28  * Returns an instance of format class (extending format_base) for given course
29  *
30  * @param int|stdClass $courseorid either course id or
31  *     an object that has the property 'format' and may contain property 'id'
32  * @return format_base
33  */
34 function course_get_format($courseorid) {
35     return format_base::instance($courseorid);
36 }
38 /**
39  * Base class for course formats
40  *
41  * Each course format must declare class
42  * class format_FORMATNAME extends format_base {}
43  * in file lib.php
44  *
45  * For each course just one instance of this class is created and it will always be returned by
46  * course_get_format($courseorid). Format may store it's specific course-dependent options in
47  * variables of this class.
48  *
49  * In rare cases instance of child class may be created just for format without course id
50  * i.e. to check if format supports AJAX.
51  *
52  * Also course formats may extend class section_info and overwrite
53  * format_base::build_section_cache() to return more information about sections.
54  *
55  * If you are upgrading from Moodle 2.3 start with copying the class format_legacy and renaming
56  * it to format_FORMATNAME, then move the code from your callback functions into
57  * appropriate functions of the class.
58  *
59  * @package    core_course
60  * @copyright  2012 Marina Glancy
61  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
62  */
63 abstract class format_base {
64     /** @var int Id of the course in this instance (maybe 0) */
65     protected $courseid;
66     /** @var string format used for this course. Please note that it can be different from
67      * course.format field if course referes to non-existing of disabled format */
68     protected $format;
69     /** @var stdClass data for course object, please use {@link format_base::get_course()} */
70     protected $course = false;
71     /** @var array caches format options, please use {@link format_base::get_format_options()} */
72     protected $formatoptions = array();
73     /** @var array cached instances */
74     private static $instances = array();
75     /** @var array plugin name => class name. */
76     private static $classesforformat = array('site' => 'site');
78     /**
79      * Creates a new instance of class
80      *
81      * Please use {@link course_get_format($courseorid)} to get an instance of the format class
82      *
83      * @param string $format
84      * @param int $courseid
85      * @return format_base
86      */
87     protected function __construct($format, $courseid) {
88         $this->format = $format;
89         $this->courseid = $courseid;
90     }
92     /**
93      * Validates that course format exists and enabled and returns either itself or default format
94      *
95      * @param string $format
96      * @return string
97      */
98     protected static final function get_format_or_default($format) {
99         global $CFG;
100         require_once($CFG->dirroot . '/course/lib.php');
102         if (array_key_exists($format, self::$classesforformat)) {
103             return self::$classesforformat[$format];
104         }
106         $plugins = get_sorted_course_formats();
107         foreach ($plugins as $plugin) {
108             self::$classesforformat[$plugin] = $plugin;
109         }
111         if (array_key_exists($format, self::$classesforformat)) {
112             return self::$classesforformat[$format];
113         }
115         if (PHPUNIT_TEST && class_exists('format_' . $format)) {
116             // Allow unittests to use non-existing course formats.
117             return $format;
118         }
120         // Else return default format
121         $defaultformat = get_config('moodlecourse', 'format');
122         if (!in_array($defaultformat, $plugins)) {
123             // when default format is not set correctly, use the first available format
124             $defaultformat = reset($plugins);
125         }
126         debugging('Format plugin format_'.$format.' is not found. Using default format_'.$defaultformat, DEBUG_DEVELOPER);
128         self::$classesforformat[$format] = $defaultformat;
129         return $defaultformat;
130     }
132     /**
133      * Get class name for the format
134      *
135      * If course format xxx does not declare class format_xxx, format_legacy will be returned.
136      * This function also includes lib.php file from corresponding format plugin
137      *
138      * @param string $format
139      * @return string
140      */
141     protected static final function get_class_name($format) {
142         global $CFG;
143         static $classnames = array('site' => 'format_site');
144         if (!isset($classnames[$format])) {
145             $plugins = core_component::get_plugin_list('format');
146             $usedformat = self::get_format_or_default($format);
147             if (isset($plugins[$usedformat]) && file_exists($plugins[$usedformat].'/lib.php')) {
148                 require_once($plugins[$usedformat].'/lib.php');
149             }
150             $classnames[$format] = 'format_'. $usedformat;
151             if (!class_exists($classnames[$format])) {
152                 require_once($CFG->dirroot.'/course/format/formatlegacy.php');
153                 $classnames[$format] = 'format_legacy';
154             }
155         }
156         return $classnames[$format];
157     }
159     /**
160      * Returns an instance of the class
161      *
162      * @todo MDL-35727 use MUC for caching of instances, limit the number of cached instances
163      *
164      * @param int|stdClass $courseorid either course id or
165      *     an object that has the property 'format' and may contain property 'id'
166      * @return format_base
167      */
168     public static final function instance($courseorid) {
169         global $DB;
170         if (!is_object($courseorid)) {
171             $courseid = (int)$courseorid;
172             if ($courseid && isset(self::$instances[$courseid]) && count(self::$instances[$courseid]) == 1) {
173                 $formats = array_keys(self::$instances[$courseid]);
174                 $format = reset($formats);
175             } else {
176                 $format = $DB->get_field('course', 'format', array('id' => $courseid), MUST_EXIST);
177             }
178         } else {
179             $format = $courseorid->format;
180             if (isset($courseorid->id)) {
181                 $courseid = clean_param($courseorid->id, PARAM_INT);
182             } else {
183                 $courseid = 0;
184             }
185         }
186         // validate that format exists and enabled, use default otherwise
187         $format = self::get_format_or_default($format);
188         if (!isset(self::$instances[$courseid][$format])) {
189             $classname = self::get_class_name($format);
190             self::$instances[$courseid][$format] = new $classname($format, $courseid);
191         }
192         return self::$instances[$courseid][$format];
193     }
195     /**
196      * Resets cache for the course (or all caches)
197      * To be called from {@link rebuild_course_cache()}
198      *
199      * @param int $courseid
200      */
201     public static final function reset_course_cache($courseid = 0) {
202         if ($courseid) {
203             if (isset(self::$instances[$courseid])) {
204                 foreach (self::$instances[$courseid] as $format => $object) {
205                     // in case somebody keeps the reference to course format object
206                     self::$instances[$courseid][$format]->course = false;
207                     self::$instances[$courseid][$format]->formatoptions = array();
208                 }
209                 unset(self::$instances[$courseid]);
210             }
211         } else {
212             self::$instances = array();
213         }
214     }
216     /**
217      * Returns the format name used by this course
218      *
219      * @return string
220      */
221     public final function get_format() {
222         return $this->format;
223     }
225     /**
226      * Returns id of the course (0 if course is not specified)
227      *
228      * @return int
229      */
230     public final function get_courseid() {
231         return $this->courseid;
232     }
234     /**
235      * Returns a record from course database table plus additional fields
236      * that course format defines
237      *
238      * @return stdClass
239      */
240     public function get_course() {
241         global $DB;
242         if (!$this->courseid) {
243             return null;
244         }
245         if ($this->course === false) {
246             $this->course = get_course($this->courseid);
247             $options = $this->get_format_options();
248             $dbcoursecolumns = null;
249             foreach ($options as $optionname => $optionvalue) {
250                 if (isset($this->course->$optionname)) {
251                     // Course format options must not have the same names as existing columns in db table "course".
252                     if (!isset($dbcoursecolumns)) {
253                         $dbcoursecolumns = $DB->get_columns('course');
254                     }
255                     if (isset($dbcoursecolumns[$optionname])) {
256                         debugging('The option name '.$optionname.' in course format '.$this->format.
257                             ' is invalid because the field with the same name exists in {course} table',
258                             DEBUG_DEVELOPER);
259                         continue;
260                     }
261                 }
262                 $this->course->$optionname = $optionvalue;
263             }
264         }
265         return $this->course;
266     }
268     /**
269      * Method used in the rendered and during backup instead of legacy 'numsections'
270      *
271      * Default renderer will treat sections with sectionnumber greater that the value returned by this
272      * method as "orphaned" and not display them on the course page unless in editing mode.
273      * Backup will store this value as 'numsections'.
274      *
275      * This method ensures that 3rd party course format plugins that still use 'numsections' continue to
276      * work but at the same time we no longer expect formats to have 'numsections' property.
277      *
278      * @return int
279      */
280     public function get_last_section_number() {
281         $course = $this->get_course();
282         if (isset($course->numsections)) {
283             return $course->numsections;
284         }
285         $modinfo = get_fast_modinfo($course);
286         $sections = $modinfo->get_section_info_all();
287         return (int)max(array_keys($sections));
288     }
290     /**
291      * Method used to get the maximum number of sections for this course format.
292      * @return int
293      */
294     public function get_max_sections() {
295         $maxsections = get_config('moodlecourse', 'maxsections');
296         if (!isset($maxsections) || !is_numeric($maxsections)) {
297             $maxsections = 52;
298         }
299         return $maxsections;
300     }
302     /**
303      * Returns true if the course has a front page.
304      *
305      * This function is called to determine if the course has a view page, whether or not
306      * it contains a listing of activities. It can be useful to set this to false when the course
307      * format has only one activity and ignores the course page. Or if there are multiple
308      * activities but no page to see the centralised information.
309      *
310      * Initially this was created to know if forms should add a button to return to the course page.
311      * So if 'Return to course' does not make sense in your format your should probably return false.
312      *
313      * @return boolean
314      * @since Moodle 2.6
315      */
316     public function has_view_page() {
317         return true;
318     }
320     /**
321      * Returns true if this course format uses sections
322      *
323      * This function may be called without specifying the course id
324      * i.e. in {@link course_format_uses_sections()}
325      *
326      * Developers, note that if course format does use sections there should be defined a language
327      * string with the name 'sectionname' defining what the section relates to in the format, i.e.
328      * $string['sectionname'] = 'Topic';
329      * or
330      * $string['sectionname'] = 'Week';
331      *
332      * @return bool
333      */
334     public function uses_sections() {
335         return false;
336     }
338     /**
339      * Returns a list of sections used in the course
340      *
341      * This is a shortcut to get_fast_modinfo()->get_section_info_all()
342      * @see get_fast_modinfo()
343      * @see course_modinfo::get_section_info_all()
344      *
345      * @return array of section_info objects
346      */
347     public final function get_sections() {
348         if ($course = $this->get_course()) {
349             $modinfo = get_fast_modinfo($course);
350             return $modinfo->get_section_info_all();
351         }
352         return array();
353     }
355     /**
356      * Returns information about section used in course
357      *
358      * @param int|stdClass $section either section number (field course_section.section) or row from course_section table
359      * @param int $strictness
360      * @return section_info
361      */
362     public final function get_section($section, $strictness = IGNORE_MISSING) {
363         if (is_object($section)) {
364             $sectionnum = $section->section;
365         } else {
366             $sectionnum = $section;
367         }
368         $sections = $this->get_sections();
369         if (array_key_exists($sectionnum, $sections)) {
370             return $sections[$sectionnum];
371         }
372         if ($strictness == MUST_EXIST) {
373             throw new moodle_exception('sectionnotexist');
374         }
375         return null;
376     }
378     /**
379      * Returns the display name of the given section that the course prefers.
380      *
381      * @param int|stdClass $section Section object from database or just field course_sections.section
382      * @return Display name that the course format prefers, e.g. "Topic 2"
383      */
384     public function get_section_name($section) {
385         if (is_object($section)) {
386             $sectionnum = $section->section;
387         } else {
388             $sectionnum = $section;
389         }
391         if (get_string_manager()->string_exists('sectionname', 'format_' . $this->format)) {
392             return get_string('sectionname', 'format_' . $this->format) . ' ' . $sectionnum;
393         }
395         // Return an empty string if there's no available section name string for the given format.
396         return '';
397     }
399     /**
400      * Returns the default section using format_base's implementation of get_section_name.
401      *
402      * @param int|stdClass $section Section object from database or just field course_sections section
403      * @return string The default value for the section name based on the given course format.
404      */
405     public function get_default_section_name($section) {
406         return self::get_section_name($section);
407     }
409     /**
410      * Returns the information about the ajax support in the given source format
411      *
412      * The returned object's property (boolean)capable indicates that
413      * the course format supports Moodle course ajax features.
414      *
415      * @return stdClass
416      */
417     public function supports_ajax() {
418         // no support by default
419         $ajaxsupport = new stdClass();
420         $ajaxsupport->capable = false;
421         return $ajaxsupport;
422     }
424     /**
425      * Custom action after section has been moved in AJAX mode
426      *
427      * Used in course/rest.php
428      *
429      * @return array This will be passed in ajax respose
430      */
431     public function ajax_section_move() {
432         return null;
433     }
435     /**
436      * The URL to use for the specified course (with section)
437      *
438      * Please note that course view page /course/view.php?id=COURSEID is hardcoded in many
439      * places in core and contributed modules. If course format wants to change the location
440      * of the view script, it is not enough to change just this function. Do not forget
441      * to add proper redirection.
442      *
443      * @param int|stdClass $section Section object from database or just field course_sections.section
444      *     if null the course view page is returned
445      * @param array $options options for view URL. At the moment core uses:
446      *     'navigation' (bool) if true and section has no separate page, the function returns null
447      *     'sr' (int) used by multipage formats to specify to which section to return
448      * @return null|moodle_url
449      */
450     public function get_view_url($section, $options = array()) {
451         global $CFG;
452         $course = $this->get_course();
453         $url = new moodle_url('/course/view.php', array('id' => $course->id));
455         if (array_key_exists('sr', $options)) {
456             $sectionno = $options['sr'];
457         } else if (is_object($section)) {
458             $sectionno = $section->section;
459         } else {
460             $sectionno = $section;
461         }
462         if (empty($CFG->linkcoursesections) && !empty($options['navigation']) && $sectionno !== null) {
463             // by default assume that sections are never displayed on separate pages
464             return null;
465         }
466         if ($this->uses_sections() && $sectionno !== null) {
467             $url->set_anchor('section-'.$sectionno);
468         }
469         return $url;
470     }
472     /**
473      * Loads all of the course sections into the navigation
474      *
475      * This method is called from {@link global_navigation::load_course_sections()}
476      *
477      * By default the method {@link global_navigation::load_generic_course_sections()} is called
478      *
479      * When overwriting please note that navigationlib relies on using the correct values for
480      * arguments $type and $key in {@link navigation_node::add()}
481      *
482      * Example of code creating a section node:
483      * $sectionnode = $node->add($sectionname, $url, navigation_node::TYPE_SECTION, null, $section->id);
484      * $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH;
485      *
486      * Example of code creating an activity node:
487      * $activitynode = $sectionnode->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $activity->id, $icon);
488      * if (global_navigation::module_extends_navigation($activity->modname)) {
489      *     $activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
490      * } else {
491      *     $activitynode->nodetype = navigation_node::NODETYPE_LEAF;
492      * }
493      *
494      * Also note that if $navigation->includesectionnum is not null, the section with this relative
495      * number needs is expected to be loaded
496      *
497      * @param global_navigation $navigation
498      * @param navigation_node $node The course node within the navigation
499      */
500     public function extend_course_navigation($navigation, navigation_node $node) {
501         if ($course = $this->get_course()) {
502             $navigation->load_generic_course_sections($course, $node);
503         }
504         return array();
505     }
507     /**
508      * Returns the list of blocks to be automatically added for the newly created course
509      *
510      * @see blocks_add_default_course_blocks()
511      *
512      * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
513      *     each of values is an array of block names (for left and right side columns)
514      */
515     public function get_default_blocks() {
516         global $CFG;
517         if (isset($CFG->defaultblocks)) {
518             return blocks_parse_default_blocks_list($CFG->defaultblocks);
519         }
520         $blocknames = array(
521             BLOCK_POS_LEFT => array(),
522             BLOCK_POS_RIGHT => array()
523         );
524         return $blocknames;
525     }
527     /**
528      * Returns the localised name of this course format plugin
529      *
530      * @return lang_string
531      */
532     public final function get_format_name() {
533         return new lang_string('pluginname', 'format_'.$this->get_format());
534     }
536     /**
537      * Definitions of the additional options that this course format uses for course
538      *
539      * This function may be called often, it should be as fast as possible.
540      * Avoid using get_string() method, use "new lang_string()" instead
541      * It is not recommended to use dynamic or course-dependant expressions here
542      * This function may be also called when course does not exist yet.
543      *
544      * Option names must be different from fields in the {course} talbe or any form elements on
545      * course edit form, it may even make sence to use special prefix for them.
546      *
547      * Each option must have the option name as a key and the array of properties as a value:
548      * 'default' - default value for this option (assumed null if not specified)
549      * 'type' - type of the option value (PARAM_INT, PARAM_RAW, etc.)
550      *
551      * Additional properties used by default implementation of
552      * {@link format_base::create_edit_form_elements()} (calls this method with $foreditform = true)
553      * 'label' - localised human-readable label for the edit form
554      * 'element_type' - type of the form element, default 'text'
555      * 'element_attributes' - additional attributes for the form element, these are 4th and further
556      *    arguments in the moodleform::addElement() method
557      * 'help' - string for help button. Note that if 'help' value is 'myoption' then the string with
558      *    the name 'myoption_help' must exist in the language file
559      * 'help_component' - language component to look for help string, by default this the component
560      *    for this course format
561      *
562      * This is an interface for creating simple form elements. If format plugin wants to use other
563      * methods such as disableIf, it can be done by overriding create_edit_form_elements().
564      *
565      * Course format options can be accessed as:
566      * $this->get_course()->OPTIONNAME (inside the format class)
567      * course_get_format($course)->get_course()->OPTIONNAME (outside of format class)
568      *
569      * All course options are returned by calling:
570      * $this->get_format_options();
571      *
572      * @param bool $foreditform
573      * @return array of options
574      */
575     public function course_format_options($foreditform = false) {
576         return array();
577     }
579     /**
580      * Definitions of the additional options that this course format uses for section
581      *
582      * See {@link format_base::course_format_options()} for return array definition.
583      *
584      * Additionally section format options may have property 'cache' set to true
585      * if this option needs to be cached in {@link get_fast_modinfo()}. The 'cache' property
586      * is recommended to be set only for fields used in {@link format_base::get_section_name()},
587      * {@link format_base::extend_course_navigation()} and {@link format_base::get_view_url()}
588      *
589      * For better performance cached options are recommended to have 'cachedefault' property
590      * Unlike 'default', 'cachedefault' should be static and not access get_config().
591      *
592      * Regardless of value of 'cache' all options are accessed in the code as
593      * $sectioninfo->OPTIONNAME
594      * where $sectioninfo is instance of section_info, returned by
595      * get_fast_modinfo($course)->get_section_info($sectionnum)
596      * or get_fast_modinfo($course)->get_section_info_all()
597      *
598      * All format options for particular section are returned by calling:
599      * $this->get_format_options($section);
600      *
601      * @param bool $foreditform
602      * @return array
603      */
604     public function section_format_options($foreditform = false) {
605         return array();
606     }
608     /**
609      * Returns the format options stored for this course or course section
610      *
611      * When overriding please note that this function is called from rebuild_course_cache()
612      * and section_info object, therefore using of get_fast_modinfo() and/or any function that
613      * accesses it may lead to recursion.
614      *
615      * @param null|int|stdClass|section_info $section if null the course format options will be returned
616      *     otherwise options for specified section will be returned. This can be either
617      *     section object or relative section number (field course_sections.section)
618      * @return array
619      */
620     public function get_format_options($section = null) {
621         global $DB;
622         if ($section === null) {
623             $options = $this->course_format_options();
624         } else {
625             $options = $this->section_format_options();
626         }
627         if (empty($options)) {
628             // there are no option for course/sections anyway, no need to go further
629             return array();
630         }
631         if ($section === null) {
632             // course format options will be returned
633             $sectionid = 0;
634         } else if ($this->courseid && isset($section->id)) {
635             // course section format options will be returned
636             $sectionid = $section->id;
637         } else if ($this->courseid && is_int($section) &&
638                 ($sectionobj = $DB->get_record('course_sections',
639                         array('section' => $section, 'course' => $this->courseid), 'id'))) {
640             // course section format options will be returned
641             $sectionid = $sectionobj->id;
642         } else {
643             // non-existing (yet) section was passed as an argument
644             // default format options for course section will be returned
645             $sectionid = -1;
646         }
647         if (!array_key_exists($sectionid, $this->formatoptions)) {
648             $this->formatoptions[$sectionid] = array();
649             // first fill with default values
650             foreach ($options as $optionname => $optionparams) {
651                 $this->formatoptions[$sectionid][$optionname] = null;
652                 if (array_key_exists('default', $optionparams)) {
653                     $this->formatoptions[$sectionid][$optionname] = $optionparams['default'];
654                 }
655             }
656             if ($this->courseid && $sectionid !== -1) {
657                 // overwrite the default options values with those stored in course_format_options table
658                 // nothing can be stored if we are interested in generic course ($this->courseid == 0)
659                 // or generic section ($sectionid === 0)
660                 $records = $DB->get_records('course_format_options',
661                         array('courseid' => $this->courseid,
662                               'format' => $this->format,
663                               'sectionid' => $sectionid
664                             ), '', 'id,name,value');
665                 foreach ($records as $record) {
666                     if (array_key_exists($record->name, $this->formatoptions[$sectionid])) {
667                         $value = $record->value;
668                         if ($value !== null && isset($options[$record->name]['type'])) {
669                             // this will convert string value to number if needed
670                             $value = clean_param($value, $options[$record->name]['type']);
671                         }
672                         $this->formatoptions[$sectionid][$record->name] = $value;
673                     }
674                 }
675             }
676         }
677         return $this->formatoptions[$sectionid];
678     }
680     /**
681      * Adds format options elements to the course/section edit form
682      *
683      * This function is called from {@link course_edit_form::definition_after_data()}
684      *
685      * @param MoodleQuickForm $mform form the elements are added to
686      * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form
687      * @return array array of references to the added form elements
688      */
689     public function create_edit_form_elements(&$mform, $forsection = false) {
690         $elements = array();
691         if ($forsection) {
692             $options = $this->section_format_options(true);
693         } else {
694             $options = $this->course_format_options(true);
695         }
696         foreach ($options as $optionname => $option) {
697             if (!isset($option['element_type'])) {
698                 $option['element_type'] = 'text';
699             }
700             $args = array($option['element_type'], $optionname, $option['label']);
701             if (!empty($option['element_attributes'])) {
702                 $args = array_merge($args, $option['element_attributes']);
703             }
704             $elements[] = call_user_func_array(array($mform, 'addElement'), $args);
705             if (isset($option['help'])) {
706                 $helpcomponent = 'format_'. $this->get_format();
707                 if (isset($option['help_component'])) {
708                     $helpcomponent = $option['help_component'];
709                 }
710                 $mform->addHelpButton($optionname, $option['help'], $helpcomponent);
711             }
712             if (isset($option['type'])) {
713                 $mform->setType($optionname, $option['type']);
714             }
715             if (isset($option['default']) && !array_key_exists($optionname, $mform->_defaultValues)) {
716                 // Set defaults for the elements in the form.
717                 // Since we call this method after set_data() make sure that we don't override what was already set.
718                 $mform->setDefault($optionname, $option['default']);
719             }
720         }
722         if (!$forsection && empty($this->courseid)) {
723             // Check if course end date form field should be enabled by default.
724             // If a default date is provided to the form element, it is magically enabled by default in the
725             // MoodleQuickForm_date_time_selector class, otherwise it's disabled by default.
726             if (get_config('moodlecourse', 'courseenddateenabled')) {
727                 // At this stage (this is called from definition_after_data) course data is already set as default.
728                 // We can not overwrite what is in the database.
729                 $mform->setDefault('enddate', $this->get_default_course_enddate($mform));
730             }
731         }
733         return $elements;
734     }
736     /**
737      * Override if you need to perform some extra validation of the format options
738      *
739      * @param array $data array of ("fieldname"=>value) of submitted data
740      * @param array $files array of uploaded files "element_name"=>tmp_file_path
741      * @param array $errors errors already discovered in edit form validation
742      * @return array of "element_name"=>"error_description" if there are errors,
743      *         or an empty array if everything is OK.
744      *         Do not repeat errors from $errors param here
745      */
746     public function edit_form_validation($data, $files, $errors) {
747         return array();
748     }
750     /**
751      * Prepares values of course or section format options before storing them in DB
752      *
753      * If an option has invalid value it is not returned
754      *
755      * @param array $rawdata associative array of the proposed course/section format options
756      * @param int|null $sectionid null if it is course format option
757      * @return array array of options that have valid values
758      */
759     protected function validate_format_options(array $rawdata, int $sectionid = null) : array {
760         if (!$sectionid) {
761             $allformatoptions = $this->course_format_options(true);
762         } else {
763             $allformatoptions = $this->section_format_options(true);
764         }
765         $data = array_intersect_key($rawdata, $allformatoptions);
766         foreach ($data as $key => $value) {
767             $option = $allformatoptions[$key] + ['type' => PARAM_RAW, 'element_type' => null, 'element_attributes' => [[]]];
768             $data[$key] = clean_param($value, $option['type']);
769             if ($option['element_type'] === 'select' && !array_key_exists($data[$key], $option['element_attributes'][0])) {
770                 // Value invalid for select element, skip.
771                 unset($data[$key]);
772             }
773         }
774         return $data;
775     }
777     /**
778      * Validates format options for the course
779      *
780      * @param array $data data to insert/update
781      * @return array array of options that have valid values
782      */
783     public function validate_course_format_options(array $data) : array {
784         return $this->validate_format_options($data);
785     }
787     /**
788      * Updates format options for a course or section
789      *
790      * If $data does not contain property with the option name, the option will not be updated
791      *
792      * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data
793      * @param null|int null if these are options for course or section id (course_sections.id)
794      *     if these are options for section
795      * @return bool whether there were any changes to the options values
796      */
797     protected function update_format_options($data, $sectionid = null) {
798         global $DB;
799         $data = $this->validate_format_options((array)$data, $sectionid);
800         if (!$sectionid) {
801             $allformatoptions = $this->course_format_options();
802             $sectionid = 0;
803         } else {
804             $allformatoptions = $this->section_format_options();
805         }
806         if (empty($allformatoptions)) {
807             // nothing to update anyway
808             return false;
809         }
810         $defaultoptions = array();
811         $cached = array();
812         foreach ($allformatoptions as $key => $option) {
813             $defaultoptions[$key] = null;
814             if (array_key_exists('default', $option)) {
815                 $defaultoptions[$key] = $option['default'];
816             }
817             $cached[$key] = ($sectionid === 0 || !empty($option['cache']));
818         }
819         $records = $DB->get_records('course_format_options',
820                 array('courseid' => $this->courseid,
821                       'format' => $this->format,
822                       'sectionid' => $sectionid
823                     ), '', 'name,id,value');
824         $changed = $needrebuild = false;
825         foreach ($defaultoptions as $key => $value) {
826             if (isset($records[$key])) {
827                 if (array_key_exists($key, $data) && $records[$key]->value !== $data[$key]) {
828                     $DB->set_field('course_format_options', 'value',
829                             $data[$key], array('id' => $records[$key]->id));
830                     $changed = true;
831                     $needrebuild = $needrebuild || $cached[$key];
832                 }
833             } else {
834                 if (array_key_exists($key, $data) && $data[$key] !== $value) {
835                     $newvalue = $data[$key];
836                     $changed = true;
837                     $needrebuild = $needrebuild || $cached[$key];
838                 } else {
839                     $newvalue = $value;
840                     // we still insert entry in DB but there are no changes from user point of
841                     // view and no need to call rebuild_course_cache()
842                 }
843                 $DB->insert_record('course_format_options', array(
844                     'courseid' => $this->courseid,
845                     'format' => $this->format,
846                     'sectionid' => $sectionid,
847                     'name' => $key,
848                     'value' => $newvalue
849                 ));
850             }
851         }
852         if ($needrebuild) {
853             rebuild_course_cache($this->courseid, true);
854         }
855         if ($changed) {
856             // reset internal caches
857             if (!$sectionid) {
858                 $this->course = false;
859             }
860             unset($this->formatoptions[$sectionid]);
861         }
862         return $changed;
863     }
865     /**
866      * Updates format options for a course
867      *
868      * If $data does not contain property with the option name, the option will not be updated
869      *
870      * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data
871      * @param stdClass $oldcourse if this function is called from {@link update_course()}
872      *     this object contains information about the course before update
873      * @return bool whether there were any changes to the options values
874      */
875     public function update_course_format_options($data, $oldcourse = null) {
876         return $this->update_format_options($data);
877     }
879     /**
880      * Updates format options for a section
881      *
882      * Section id is expected in $data->id (or $data['id'])
883      * If $data does not contain property with the option name, the option will not be updated
884      *
885      * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data
886      * @return bool whether there were any changes to the options values
887      */
888     public function update_section_format_options($data) {
889         $data = (array)$data;
890         return $this->update_format_options($data, $data['id']);
891     }
893     /**
894      * Return an instance of moodleform to edit a specified section
895      *
896      * Default implementation returns instance of editsection_form that automatically adds
897      * additional fields defined in {@link format_base::section_format_options()}
898      *
899      * Format plugins may extend editsection_form if they want to have custom edit section form.
900      *
901      * @param mixed $action the action attribute for the form. If empty defaults to auto detect the
902      *              current url. If a moodle_url object then outputs params as hidden variables.
903      * @param array $customdata the array with custom data to be passed to the form
904      *     /course/editsection.php passes section_info object in 'cs' field
905      *     for filling availability fields
906      * @return moodleform
907      */
908     public function editsection_form($action, $customdata = array()) {
909         global $CFG;
910         require_once($CFG->dirroot. '/course/editsection_form.php');
911         $context = context_course::instance($this->courseid);
912         if (!array_key_exists('course', $customdata)) {
913             $customdata['course'] = $this->get_course();
914         }
915         return new editsection_form($action, $customdata);
916     }
918     /**
919      * Allows course format to execute code on moodle_page::set_course()
920      *
921      * @param moodle_page $page instance of page calling set_course
922      */
923     public function page_set_course(moodle_page $page) {
924     }
926     /**
927      * Allows course format to execute code on moodle_page::set_cm()
928      *
929      * Current module can be accessed as $page->cm (returns instance of cm_info)
930      *
931      * @param moodle_page $page instance of page calling set_cm
932      */
933     public function page_set_cm(moodle_page $page) {
934     }
936     /**
937      * Course-specific information to be output on any course page (usually above navigation bar)
938      *
939      * Example of usage:
940      * define
941      * class format_FORMATNAME_XXX implements renderable {}
942      *
943      * create format renderer in course/format/FORMATNAME/renderer.php, define rendering function:
944      * class format_FORMATNAME_renderer extends plugin_renderer_base {
945      *     protected function render_format_FORMATNAME_XXX(format_FORMATNAME_XXX $xxx) {
946      *         return html_writer::tag('div', 'This is my header/footer');
947      *     }
948      * }
949      *
950      * Return instance of format_FORMATNAME_XXX in this function, the appropriate method from
951      * plugin renderer will be called
952      *
953      * @return null|renderable null for no output or object with data for plugin renderer
954      */
955     public function course_header() {
956         return null;
957     }
959     /**
960      * Course-specific information to be output on any course page (usually in the beginning of
961      * standard footer)
962      *
963      * See {@link format_base::course_header()} for usage
964      *
965      * @return null|renderable null for no output or object with data for plugin renderer
966      */
967     public function course_footer() {
968         return null;
969     }
971     /**
972      * Course-specific information to be output immediately above content on any course page
973      *
974      * See {@link format_base::course_header()} for usage
975      *
976      * @return null|renderable null for no output or object with data for plugin renderer
977      */
978     public function course_content_header() {
979         return null;
980     }
982     /**
983      * Course-specific information to be output immediately below content on any course page
984      *
985      * See {@link format_base::course_header()} for usage
986      *
987      * @return null|renderable null for no output or object with data for plugin renderer
988      */
989     public function course_content_footer() {
990         return null;
991     }
993     /**
994      * Returns instance of page renderer used by this plugin
995      *
996      * @param moodle_page $page
997      * @return renderer_base
998      */
999     public function get_renderer(moodle_page $page) {
1000         return $page->get_renderer('format_'. $this->get_format());
1001     }
1003     /**
1004      * Returns true if the specified section is current
1005      *
1006      * By default we analyze $course->marker
1007      *
1008      * @param int|stdClass|section_info $section
1009      * @return bool
1010      */
1011     public function is_section_current($section) {
1012         if (is_object($section)) {
1013             $sectionnum = $section->section;
1014         } else {
1015             $sectionnum = $section;
1016         }
1017         return ($sectionnum && ($course = $this->get_course()) && $course->marker == $sectionnum);
1018     }
1020     /**
1021      * Allows to specify for modinfo that section is not available even when it is visible and conditionally available.
1022      *
1023      * Note: affected user can be retrieved as: $section->modinfo->userid
1024      *
1025      * Course format plugins can override the method to change the properties $available and $availableinfo that were
1026      * calculated by conditional availability.
1027      * To make section unavailable set:
1028      *     $available = false;
1029      * To make unavailable section completely hidden set:
1030      *     $availableinfo = '';
1031      * To make unavailable section visible with availability message set:
1032      *     $availableinfo = get_string('sectionhidden', 'format_xxx');
1033      *
1034      * @param section_info $section
1035      * @param bool $available the 'available' propery of the section_info as it was evaluated by conditional availability.
1036      *     Can be changed by the method but 'false' can not be overridden by 'true'.
1037      * @param string $availableinfo the 'availableinfo' propery of the section_info as it was evaluated by conditional availability.
1038      *     Can be changed by the method
1039      */
1040     public function section_get_available_hook(section_info $section, &$available, &$availableinfo) {
1041     }
1043     /**
1044      * Whether this format allows to delete sections
1045      *
1046      * If format supports deleting sections it is also recommended to define language string
1047      * 'deletesection' inside the format.
1048      *
1049      * Do not call this function directly, instead use {@link course_can_delete_section()}
1050      *
1051      * @param int|stdClass|section_info $section
1052      * @return bool
1053      */
1054     public function can_delete_section($section) {
1055         return false;
1056     }
1058     /**
1059      * Deletes a section
1060      *
1061      * Do not call this function directly, instead call {@link course_delete_section()}
1062      *
1063      * @param int|stdClass|section_info $section
1064      * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it.
1065      * @return bool whether section was deleted
1066      */
1067     public function delete_section($section, $forcedeleteifnotempty = false) {
1068         global $DB;
1069         if (!$this->uses_sections()) {
1070             // Not possible to delete section if sections are not used.
1071             return false;
1072         }
1073         if (!is_object($section)) {
1074             $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section),
1075                 'id,section,sequence,summary');
1076         }
1077         if (!$section || !$section->section) {
1078             // Not possible to delete 0-section.
1079             return false;
1080         }
1082         if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
1083             return false;
1084         }
1086         $course = $this->get_course();
1088         // Remove the marker if it points to this section.
1089         if ($section->section == $course->marker) {
1090             course_set_marker($course->id, 0);
1091         }
1093         $lastsection = $DB->get_field_sql('SELECT max(section) from {course_sections}
1094                             WHERE course = ?', array($course->id));
1096         // Find out if we need to descrease the 'numsections' property later.
1097         $courseformathasnumsections = array_key_exists('numsections',
1098             $this->get_format_options());
1099         $decreasenumsections = $courseformathasnumsections && ($section->section <= $course->numsections);
1101         // Move the section to the end.
1102         move_section_to($course, $section->section, $lastsection, true);
1104         // Delete all modules from the section.
1105         foreach (preg_split('/,/', $section->sequence, -1, PREG_SPLIT_NO_EMPTY) as $cmid) {
1106             course_delete_module($cmid);
1107         }
1109         // Delete section and it's format options.
1110         $DB->delete_records('course_format_options', array('sectionid' => $section->id));
1111         $DB->delete_records('course_sections', array('id' => $section->id));
1112         rebuild_course_cache($course->id, true);
1114         // Descrease 'numsections' if needed.
1115         if ($decreasenumsections) {
1116             $this->update_course_format_options(array('numsections' => $course->numsections - 1));
1117         }
1119         return true;
1120     }
1122     /**
1123      * Prepares the templateable object to display section name
1124      *
1125      * @param \section_info|\stdClass $section
1126      * @param bool $linkifneeded
1127      * @param bool $editable
1128      * @param null|lang_string|string $edithint
1129      * @param null|lang_string|string $editlabel
1130      * @return \core\output\inplace_editable
1131      */
1132     public function inplace_editable_render_section_name($section, $linkifneeded = true,
1133                                                          $editable = null, $edithint = null, $editlabel = null) {
1134         global $USER, $CFG;
1135         require_once($CFG->dirroot.'/course/lib.php');
1137         if ($editable === null) {
1138             $editable = !empty($USER->editing) && has_capability('moodle/course:update',
1139                     context_course::instance($section->course));
1140         }
1142         $displayvalue = $title = get_section_name($section->course, $section);
1143         if ($linkifneeded) {
1144             // Display link under the section name if the course format setting is to display one section per page.
1145             $url = course_get_url($section->course, $section->section, array('navigation' => true));
1146             if ($url) {
1147                 $displayvalue = html_writer::link($url, $title);
1148             }
1149             $itemtype = 'sectionname';
1150         } else {
1151             // If $linkifneeded==false, we never display the link (this is used when rendering the section header).
1152             // Itemtype 'sectionnamenl' (nl=no link) will tell the callback that link should not be rendered -
1153             // there is no other way callback can know where we display the section name.
1154             $itemtype = 'sectionnamenl';
1155         }
1156         if (empty($edithint)) {
1157             $edithint = new lang_string('editsectionname');
1158         }
1159         if (empty($editlabel)) {
1160             $editlabel = new lang_string('newsectionname', '', $title);
1161         }
1163         return new \core\output\inplace_editable('format_' . $this->format, $itemtype, $section->id, $editable,
1164             $displayvalue, $section->name, $edithint, $editlabel);
1165     }
1167     /**
1168      * Updates the value in the database and modifies this object respectively.
1169      *
1170      * ALWAYS check user permissions before performing an update! Throw exceptions if permissions are not sufficient
1171      * or value is not legit.
1172      *
1173      * @param stdClass $section
1174      * @param string $itemtype
1175      * @param mixed $newvalue
1176      * @return \core\output\inplace_editable
1177      */
1178     public function inplace_editable_update_section_name($section, $itemtype, $newvalue) {
1179         if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') {
1180             $context = context_course::instance($section->course);
1181             external_api::validate_context($context);
1182             require_capability('moodle/course:update', $context);
1184             $newtitle = clean_param($newvalue, PARAM_TEXT);
1185             if (strval($section->name) !== strval($newtitle)) {
1186                 course_update_section($section->course, $section, array('name' => $newtitle));
1187             }
1188             return $this->inplace_editable_render_section_name($section, ($itemtype === 'sectionname'), true);
1189         }
1190     }
1193     /**
1194      * Returns the default end date value based on the start date.
1195      *
1196      * This is the default implementation for course formats, it is based on
1197      * moodlecourse/courseduration setting. Course formats like format_weeks for
1198      * example can overwrite this method and return a value based on their internal options.
1199      *
1200      * @param moodleform $mform
1201      * @param array $fieldnames The form - field names mapping.
1202      * @return int
1203      */
1204     public function get_default_course_enddate($mform, $fieldnames = array()) {
1206         if (empty($fieldnames)) {
1207             $fieldnames = array('startdate' => 'startdate');
1208         }
1210         $startdate = $this->get_form_start_date($mform, $fieldnames);
1211         $courseduration = intval(get_config('moodlecourse', 'courseduration'));
1212         if (!$courseduration) {
1213             // Default, it should be already set during upgrade though.
1214             $courseduration = YEARSECS;
1215         }
1217         return $startdate + $courseduration;
1218     }
1220     /**
1221      * Indicates whether the course format supports the creation of the Announcements forum.
1222      *
1223      * For course format plugin developers, please override this to return true if you want the Announcements forum
1224      * to be created upon course creation.
1225      *
1226      * @return bool
1227      */
1228     public function supports_news() {
1229         // For backwards compatibility, check if default blocks include the news_items block.
1230         $defaultblocks = $this->get_default_blocks();
1231         foreach ($defaultblocks as $blocks) {
1232             if (in_array('news_items', $blocks)) {
1233                 return true;
1234             }
1235         }
1236         // Return false by default.
1237         return false;
1238     }
1240     /**
1241      * Get the start date value from the course settings page form.
1242      *
1243      * @param moodleform $mform
1244      * @param array $fieldnames The form - field names mapping.
1245      * @return int
1246      */
1247     protected function get_form_start_date($mform, $fieldnames) {
1248         $startdate = $mform->getElementValue($fieldnames['startdate']);
1249         return $mform->getElement($fieldnames['startdate'])->exportValue($startdate);
1250     }
1252     /**
1253      * Returns whether this course format allows the activity to
1254      * have "triple visibility state" - visible always, hidden on course page but available, hidden.
1255      *
1256      * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module)
1257      * @param stdClass|section_info $section section where this module is located or will be added to
1258      * @return bool
1259      */
1260     public function allow_stealth_module_visibility($cm, $section) {
1261         return false;
1262     }
1264     /**
1265      * Callback used in WS core_course_edit_section when teacher performs an AJAX action on a section (show/hide)
1266      *
1267      * Access to the course is already validated in the WS but the callback has to make sure
1268      * that particular action is allowed by checking capabilities
1269      *
1270      * Course formats should register
1271      *
1272      * @param stdClass|section_info $section
1273      * @param string $action
1274      * @param int $sr
1275      * @return null|array|stdClass any data for the Javascript post-processor (must be json-encodeable)
1276      */
1277     public function section_action($section, $action, $sr) {
1278         global $PAGE;
1279         if (!$this->uses_sections() || !$section->section) {
1280             // No section actions are allowed if course format does not support sections.
1281             // No actions are allowed on the 0-section by default (overwrite in course format if needed).
1282             throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action));
1283         }
1285         $course = $this->get_course();
1286         $coursecontext = context_course::instance($course->id);
1287         switch($action) {
1288             case 'hide':
1289             case 'show':
1290                 require_capability('moodle/course:sectionvisibility', $coursecontext);
1291                 $visible = ($action === 'hide') ? 0 : 1;
1292                 course_update_section($course, $section, array('visible' => $visible));
1293                 break;
1294             default:
1295                 throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action));
1296         }
1298         $modules = [];
1300         $modinfo = get_fast_modinfo($course);
1301         $coursesections = $modinfo->sections;
1302         if (array_key_exists($section->section, $coursesections)) {
1303             $courserenderer = $PAGE->get_renderer('core', 'course');
1304             $completioninfo = new completion_info($course);
1305             foreach ($coursesections[$section->section] as $cmid) {
1306                 $cm = $modinfo->get_cm($cmid);
1307                 $modules[] = $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sr);
1308             }
1309         }
1311         return ['modules' => $modules];
1312     }
1314     /**
1315      * Return the plugin config settings for external functions,
1316      * in some cases the configs will need formatting or be returned only if the current user has some capabilities enabled.
1317      *
1318      * @return array the list of configs
1319      * @since Moodle 3.5
1320      */
1321     public function get_config_for_external() {
1322         return array();
1323     }
1326 /**
1327  * Pseudo course format used for the site main page
1328  *
1329  * @package    core_course
1330  * @copyright  2012 Marina Glancy
1331  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1332  */
1333 class format_site extends format_base {
1335     /**
1336      * Returns the display name of the given section that the course prefers.
1337      *
1338      * @param int|stdClass $section Section object from database or just field section.section
1339      * @return Display name that the course format prefers, e.g. "Topic 2"
1340      */
1341     function get_section_name($section) {
1342         return get_string('site');
1343     }
1345     /**
1346      * For this fake course referring to the whole site, the site homepage is always returned
1347      * regardless of arguments
1348      *
1349      * @param int|stdClass $section
1350      * @param array $options
1351      * @return null|moodle_url
1352      */
1353     public function get_view_url($section, $options = array()) {
1354         return new moodle_url('/', array('redirect' => 0));
1355     }
1357     /**
1358      * Returns the list of blocks to be automatically added on the site frontpage when moodle is installed
1359      *
1360      * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
1361      *     each of values is an array of block names (for left and right side columns)
1362      */
1363     public function get_default_blocks() {
1364         return blocks_get_default_site_course_blocks();
1365     }
1367     /**
1368      * Definitions of the additional options that site uses
1369      *
1370      * @param bool $foreditform
1371      * @return array of options
1372      */
1373     public function course_format_options($foreditform = false) {
1374         static $courseformatoptions = false;
1375         if ($courseformatoptions === false) {
1376             $courseformatoptions = array(
1377                 'numsections' => array(
1378                     'default' => 1,
1379                     'type' => PARAM_INT,
1380                 ),
1381             );
1382         }
1383         return $courseformatoptions;
1384     }
1386     /**
1387      * Returns whether this course format allows the activity to
1388      * have "triple visibility state" - visible always, hidden on course page but available, hidden.
1389      *
1390      * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module)
1391      * @param stdClass|section_info $section section where this module is located or will be added to
1392      * @return bool
1393      */
1394     public function allow_stealth_module_visibility($cm, $section) {
1395         return true;
1396     }