From 1c839f9072d7bc2ff7b7bbf4a8b3c91249ce364e Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Thu, 22 Oct 2020 09:18:59 +0200 Subject: [PATCH 1/2] MDL-65974 course: move format_base to core_course\course_format --- .../tests/moodle2_course_format_test.php | 8 +- course/classes/course_format.php | 1416 +++++++++++++++++ course/format/formatlegacy.php | 2 +- course/format/singleactivity/lib.php | 2 +- course/format/social/lib.php | 2 +- course/format/weeks/lib.php | 2 +- course/lib.php | 6 +- course/tests/courseformat_test.php | 4 +- lib/adminlib.php | 8 +- lib/behat/classes/util.php | 5 +- lib/db/renamedclasses.php | 1 + lib/deprecatedlib.php | 3 +- lib/modinfolib.php | 5 +- lib/moodlelib.php | 4 +- lib/navigationlib.php | 2 +- lib/phpunit/classes/util.php | 5 +- 16 files changed, 1440 insertions(+), 35 deletions(-) create mode 100644 course/classes/course_format.php diff --git a/backup/moodle2/tests/moodle2_course_format_test.php b/backup/moodle2/tests/moodle2_course_format_test.php index bac585934bf..9ca25197dff 100644 --- a/backup/moodle2/tests/moodle2_course_format_test.php +++ b/backup/moodle2/tests/moodle2_course_format_test.php @@ -58,7 +58,7 @@ class core_backup_moodle2_course_format_testcase extends advanced_testcase { 'enablecompletion' => COMPLETION_ENABLED), array('createsections' => true)); - $courseobject = format_base::instance($course->id); + $courseobject = core_course\course_format::instance($course->id); $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1), '*', MUST_EXIST); $data = array('id' => $section->id, @@ -91,7 +91,7 @@ class core_backup_moodle2_course_format_testcase extends advanced_testcase { 'enablecompletion' => COMPLETION_ENABLED), array('createsections' => true)); - $courseobject = format_base::instance($course->id); + $courseobject = core_course\course_format::instance($course->id); $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1), '*', MUST_EXIST); $data = array('id' => $section->id, @@ -134,7 +134,7 @@ class core_backup_moodle2_course_format_testcase extends advanced_testcase { array('createsections' => true)); // Set section 2 to have both options, and a name. - $courseobject = format_base::instance($course->id); + $courseobject = core_course\course_format::instance($course->id); $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 2), '*', MUST_EXIST); $data = array('id' => $section->id, @@ -151,7 +151,7 @@ class core_backup_moodle2_course_format_testcase extends advanced_testcase { // Check that the section contains the options suitable for the new // format and that even the one with the same name as from the old format // has NOT been set. - $newcourseobject = format_base::instance($newcourse->id); + $newcourseobject = core_course\course_format::instance($newcourse->id); $sectionoptions = $newcourseobject->get_format_options(2); $this->assertArrayHasKey('numdaystocomplete', $sectionoptions); $this->assertArrayNotHasKey('secondparameter', $sectionoptions); diff --git a/course/classes/course_format.php b/course/classes/course_format.php new file mode 100644 index 00000000000..debd894f51b --- /dev/null +++ b/course/classes/course_format.php @@ -0,0 +1,1416 @@ +. + +/** + * Contains the default activity list from a section. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course; + +use navigation_node; +use moodle_page; +use core_component; +use course_modinfo; +use html_writer; +use section_info; +use context_course; +use editsection_form; +use moodle_exception; +use moodle_url; +use lang_string; +use completion_info; +use external_api; +use stdClass; + +/** + * Base class for course formats + * + * Each course format must declare class + * class format_FORMATNAME extends core_course\course_format {} + * in file lib.php + * + * For each course just one instance of this class is created and it will always be returned by + * course_get_format($courseorid). Format may store it's specific course-dependent options in + * variables of this class. + * + * In rare cases instance of child class may be created just for format without course id + * i.e. to check if format supports AJAX. + * + * Also course formats may extend class section_info and overwrite + * course_format::build_section_cache() to return more information about sections. + * + * If you are upgrading from Moodle 2.3 start with copying the class format_legacy and renaming + * it to format_FORMATNAME, then move the code from your callback functions into + * appropriate functions of the class. + * + * @package core_course + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class course_format { + /** @var int Id of the course in this instance (maybe 0) */ + protected $courseid; + /** @var string format used for this course. Please note that it can be different from + * course.format field if course referes to non-existing of disabled format */ + protected $format; + /** @var stdClass course data for course object, please use course_format::get_course() */ + protected $course = false; + /** @var array caches format options, please use course_format::get_format_options() */ + protected $formatoptions = array(); + /** @var int the section number in single section format, zero for multiple section formats. */ + protected $singlesection = 0; + /** @var course_modinfo the current course modinfo, please use course_format::get_modinfo() */ + private $modinfo = null; + /** @var array cached instances */ + private static $instances = array(); + /** @var array plugin name => class name. */ + private static $classesforformat = array('site' => 'site'); + + /** + * Creates a new instance of class + * + * Please use course_get_format($courseorid) to get an instance of the format class + * + * @param string $format + * @param int $courseid + * @return course_format + */ + protected function __construct($format, $courseid) { + $this->format = $format; + $this->courseid = $courseid; + } + + /** + * Validates that course format exists and enabled and returns either itself or default format + * + * @param string $format + * @return string + */ + protected static final function get_format_or_default($format) { + global $CFG; + require_once($CFG->dirroot . '/course/lib.php'); + + if (array_key_exists($format, self::$classesforformat)) { + return self::$classesforformat[$format]; + } + + $plugins = get_sorted_course_formats(); + foreach ($plugins as $plugin) { + self::$classesforformat[$plugin] = $plugin; + } + + if (array_key_exists($format, self::$classesforformat)) { + return self::$classesforformat[$format]; + } + + if (PHPUNIT_TEST && class_exists('format_' . $format)) { + // Allow unittests to use non-existing course formats. + return $format; + } + + // Else return default format. + $defaultformat = get_config('moodlecourse', 'format'); + if (!in_array($defaultformat, $plugins)) { + // When default format is not set correctly, use the first available format. + $defaultformat = reset($plugins); + } + debugging('Format plugin format_'.$format.' is not found. Using default format_'.$defaultformat, DEBUG_DEVELOPER); + + self::$classesforformat[$format] = $defaultformat; + return $defaultformat; + } + + /** + * Get class name for the format. + * + * If course format xxx does not declare class format_xxx, format_legacy will be returned. + * This function also includes lib.php file from corresponding format plugin + * + * @param string $format + * @return string + */ + protected static final function get_class_name($format) { + global $CFG; + static $classnames = array('site' => 'format_site'); + if (!isset($classnames[$format])) { + $plugins = core_component::get_plugin_list('format'); + $usedformat = self::get_format_or_default($format); + if (isset($plugins[$usedformat]) && file_exists($plugins[$usedformat].'/lib.php')) { + require_once($plugins[$usedformat].'/lib.php'); + } + $classnames[$format] = 'format_'. $usedformat; + if (!class_exists($classnames[$format])) { + require_once($CFG->dirroot.'/course/format/formatlegacy.php'); + $classnames[$format] = 'format_legacy'; + } + } + return $classnames[$format]; + } + + /** + * Returns an instance of the class + * + * @todo MDL-35727 use MUC for caching of instances, limit the number of cached instances + * + * @param int|stdClass $courseorid either course id or + * an object that has the property 'format' and may contain property 'id' + * @return course_format + */ + public static final function instance($courseorid) { + global $DB; + if (!is_object($courseorid)) { + $courseid = (int)$courseorid; + if ($courseid && isset(self::$instances[$courseid]) && count(self::$instances[$courseid]) == 1) { + $formats = array_keys(self::$instances[$courseid]); + $format = reset($formats); + } else { + $format = $DB->get_field('course', 'format', array('id' => $courseid), MUST_EXIST); + } + } else { + $format = $courseorid->format; + if (isset($courseorid->id)) { + $courseid = clean_param($courseorid->id, PARAM_INT); + } else { + $courseid = 0; + } + } + // Validate that format exists and enabled, use default otherwise. + $format = self::get_format_or_default($format); + if (!isset(self::$instances[$courseid][$format])) { + $classname = self::get_class_name($format); + self::$instances[$courseid][$format] = new $classname($format, $courseid); + } + return self::$instances[$courseid][$format]; + } + + /** + * Resets cache for the course (or all caches) + * + * To be called from rebuild_course_cache() + * + * @param int $courseid + */ + public static final function reset_course_cache($courseid = 0) { + if ($courseid) { + if (isset(self::$instances[$courseid])) { + foreach (self::$instances[$courseid] as $format => $object) { + // In case somebody keeps the reference to course format object. + self::$instances[$courseid][$format]->course = false; + self::$instances[$courseid][$format]->formatoptions = array(); + } + unset(self::$instances[$courseid]); + } + } else { + self::$instances = array(); + } + } + + /** + * Returns the format name used by this course + * + * @return string + */ + public final function get_format() { + return $this->format; + } + + /** + * Returns id of the course (0 if course is not specified) + * + * @return int + */ + public final function get_courseid() { + return $this->courseid; + } + + /** + * Returns a record from course database table plus additional fields + * that course format defines + * + * @return stdClass + */ + public function get_course() { + global $DB; + if (!$this->courseid) { + return null; + } + if ($this->course === false) { + $this->course = get_course($this->courseid); + $options = $this->get_format_options(); + $dbcoursecolumns = null; + foreach ($options as $optionname => $optionvalue) { + if (isset($this->course->$optionname)) { + // Course format options must not have the same names as existing columns in db table "course". + if (!isset($dbcoursecolumns)) { + $dbcoursecolumns = $DB->get_columns('course'); + } + if (isset($dbcoursecolumns[$optionname])) { + debugging('The option name '.$optionname.' in course format '.$this->format. + ' is invalid because the field with the same name exists in {course} table', + DEBUG_DEVELOPER); + continue; + } + } + $this->course->$optionname = $optionvalue; + } + } + return $this->course; + } + + /** + * Return the current course modinfo. + * + * This method is used mainly by the output components to avoid unnecesary get_fast_modinfo calls. + * + * @return course_modinfo + */ + public function get_modinfo(): course_modinfo { + global $USER; + if ($this->modinfo === null) { + $this->modinfo = course_modinfo::instance($this->courseid, $USER->id); + } + return $this->modinfo; + } + + /** + * Method used in the rendered and during backup instead of legacy 'numsections' + * + * Default renderer will treat sections with sectionnumber greater that the value returned by this + * method as "orphaned" and not display them on the course page unless in editing mode. + * Backup will store this value as 'numsections'. + * + * This method ensures that 3rd party course format plugins that still use 'numsections' continue to + * work but at the same time we no longer expect formats to have 'numsections' property. + * + * @return int + */ + public function get_last_section_number() { + $course = $this->get_course(); + if (isset($course->numsections)) { + return $course->numsections; + } + $modinfo = get_fast_modinfo($course); + $sections = $modinfo->get_section_info_all(); + return (int)max(array_keys($sections)); + } + + /** + * Method used to get the maximum number of sections for this course format. + * @return int + */ + public function get_max_sections() { + $maxsections = get_config('moodlecourse', 'maxsections'); + if (!isset($maxsections) || !is_numeric($maxsections)) { + $maxsections = 52; + } + return $maxsections; + } + + /** + * Returns true if the course has a front page. + * + * This function is called to determine if the course has a view page, whether or not + * it contains a listing of activities. It can be useful to set this to false when the course + * format has only one activity and ignores the course page. Or if there are multiple + * activities but no page to see the centralised information. + * + * Initially this was created to know if forms should add a button to return to the course page. + * So if 'Return to course' does not make sense in your format your should probably return false. + * + * @return boolean + * @since Moodle 2.6 + */ + public function has_view_page() { + return true; + } + + /** + * Generate the title for this section page. + * + * @return string the page title + */ + public function page_title(): string { + global $PAGE; + return $PAGE->title; + } + + /** + * Returns true if this course format uses sections + * + * This function may be called without specifying the course id + * i.e. in course_format_uses_sections() + * + * Developers, note that if course format does use sections there should be defined a language + * string with the name 'sectionname' defining what the section relates to in the format, i.e. + * $string['sectionname'] = 'Topic'; + * or + * $string['sectionname'] = 'Week'; + * + * @return bool + */ + public function uses_sections() { + return false; + } + + /** + * Returns a list of sections used in the course + * + * This is a shortcut to get_fast_modinfo()->get_section_info_all() + * @see get_fast_modinfo() + * @see course_modinfo::get_section_info_all() + * + * @return array of section_info objects + */ + public final function get_sections() { + if ($course = $this->get_course()) { + $modinfo = get_fast_modinfo($course); + return $modinfo->get_section_info_all(); + } + return array(); + } + + /** + * Returns information about section used in course + * + * @param int|stdClass $section either section number (field course_section.section) or row from course_section table + * @param int $strictness + * @return section_info + */ + public final function get_section($section, $strictness = IGNORE_MISSING) { + if (is_object($section)) { + $sectionnum = $section->section; + } else { + $sectionnum = $section; + } + $sections = $this->get_sections(); + if (array_key_exists($sectionnum, $sections)) { + return $sections[$sectionnum]; + } + if ($strictness == MUST_EXIST) { + throw new moodle_exception('sectionnotexist'); + } + return null; + } + + /** + * Returns the display name of the given section that the course prefers. + * + * @param int|stdClass $section Section object from database or just field course_sections.section + * @return Display name that the course format prefers, e.g. "Topic 2" + */ + public function get_section_name($section) { + if (is_object($section)) { + $sectionnum = $section->section; + } else { + $sectionnum = $section; + } + + if (get_string_manager()->string_exists('sectionname', 'format_' . $this->format)) { + return get_string('sectionname', 'format_' . $this->format) . ' ' . $sectionnum; + } + + // Return an empty string if there's no available section name string for the given format. + return ''; + } + + /** + * Returns the default section using course_format's implementation of get_section_name. + * + * @param int|stdClass $section Section object from database or just field course_sections section + * @return string The default value for the section name based on the given course format. + */ + public function get_default_section_name($section) { + return self::get_section_name($section); + } + + /** + * Set if the current format instance will show multiple sections or an individual one. + * + * Some formats has the hability to swith from one section to multiple sections per page, + * this method replaces the old print_multiple_section_page and print_single_section_page. + * + * @param int $singlesection zero for all sections or a section number + */ + public function set_section_number(int $singlesection): void { + $this->singlesection = $singlesection; + } + + /** + * Set if the current format instance will show multiple sections or an individual one. + * + * Some formats has the hability to swith from one section to multiple sections per page, + * output components will use this method to know if the current display is a single or + * multiple sections. + * + * @return int zero for all sections or the sectin number + */ + public function get_section_number(): int { + return $this->singlesection; + } + + /** + * Returns the information about the ajax support in the given source format + * + * The returned object's property (boolean)capable indicates that + * the course format supports Moodle course ajax features. + * + * @return stdClass + */ + public function supports_ajax() { + // No support by default. + $ajaxsupport = new stdClass(); + $ajaxsupport->capable = false; + return $ajaxsupport; + } + + /** + * Custom action after section has been moved in AJAX mode + * + * Used in course/rest.php + * + * @return array This will be passed in ajax respose + */ + public function ajax_section_move() { + return null; + } + + /** + * The URL to use for the specified course (with section) + * + * Please note that course view page /course/view.php?id=COURSEID is hardcoded in many + * places in core and contributed modules. If course format wants to change the location + * of the view script, it is not enough to change just this function. Do not forget + * to add proper redirection. + * + * @param int|stdClass $section Section object from database or just field course_sections.section + * if null the course view page is returned + * @param array $options options for view URL. At the moment core uses: + * 'navigation' (bool) if true and section has no separate page, the function returns null + * 'sr' (int) used by multipage formats to specify to which section to return + * @return null|moodle_url + */ + public function get_view_url($section, $options = array()) { + global $CFG; + $course = $this->get_course(); + $url = new moodle_url('/course/view.php', array('id' => $course->id)); + + if (array_key_exists('sr', $options)) { + $sectionno = $options['sr']; + } else if (is_object($section)) { + $sectionno = $section->section; + } else { + $sectionno = $section; + } + if (empty($CFG->linkcoursesections) && !empty($options['navigation']) && $sectionno !== null) { + // By default assume that sections are never displayed on separate pages. + return null; + } + if ($this->uses_sections() && $sectionno !== null) { + $url->set_anchor('section-'.$sectionno); + } + return $url; + } + + /** + * Loads all of the course sections into the navigation + * + * This method is called from global_navigation::load_course_sections() + * + * By default the method global_navigation::load_generic_course_sections() is called + * + * When overwriting please note that navigationlib relies on using the correct values for + * arguments $type and $key in navigation_node::add() + * + * Example of code creating a section node: + * $sectionnode = $node->add($sectionname, $url, navigation_node::TYPE_SECTION, null, $section->id); + * $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH; + * + * Example of code creating an activity node: + * $activitynode = $sectionnode->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $activity->id, $icon); + * if (global_navigation::module_extends_navigation($activity->modname)) { + * $activitynode->nodetype = navigation_node::NODETYPE_BRANCH; + * } else { + * $activitynode->nodetype = navigation_node::NODETYPE_LEAF; + * } + * + * Also note that if $navigation->includesectionnum is not null, the section with this relative + * number needs is expected to be loaded + * + * @param global_navigation $navigation + * @param navigation_node $node The course node within the navigation + */ + public function extend_course_navigation($navigation, navigation_node $node) { + if ($course = $this->get_course()) { + $navigation->load_generic_course_sections($course, $node); + } + return array(); + } + + /** + * Returns the list of blocks to be automatically added for the newly created course + * + * @see blocks_add_default_course_blocks() + * + * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT + * each of values is an array of block names (for left and right side columns) + */ + public function get_default_blocks() { + global $CFG; + if (isset($CFG->defaultblocks)) { + return blocks_parse_default_blocks_list($CFG->defaultblocks); + } + $blocknames = array( + BLOCK_POS_LEFT => array(), + BLOCK_POS_RIGHT => array() + ); + return $blocknames; + } + + /** + * Returns the localised name of this course format plugin + * + * @return lang_string + */ + public final function get_format_name() { + return new lang_string('pluginname', 'format_'.$this->get_format()); + } + + /** + * Definitions of the additional options that this course format uses for course + * + * This function may be called often, it should be as fast as possible. + * Avoid using get_string() method, use "new lang_string()" instead + * It is not recommended to use dynamic or course-dependant expressions here + * This function may be also called when course does not exist yet. + * + * Option names must be different from fields in the {course} talbe or any form elements on + * course edit form, it may even make sence to use special prefix for them. + * + * Each option must have the option name as a key and the array of properties as a value: + * 'default' - default value for this option (assumed null if not specified) + * 'type' - type of the option value (PARAM_INT, PARAM_RAW, etc.) + * + * Additional properties used by default implementation of + * course_format::create_edit_form_elements() (calls this method with $foreditform = true) + * 'label' - localised human-readable label for the edit form + * 'element_type' - type of the form element, default 'text' + * 'element_attributes' - additional attributes for the form element, these are 4th and further + * arguments in the moodleform::addElement() method + * 'help' - string for help button. Note that if 'help' value is 'myoption' then the string with + * the name 'myoption_help' must exist in the language file + * 'help_component' - language component to look for help string, by default this the component + * for this course format + * + * This is an interface for creating simple form elements. If format plugin wants to use other + * methods such as disableIf, it can be done by overriding create_edit_form_elements(). + * + * Course format options can be accessed as: + * $this->get_course()->OPTIONNAME (inside the format class) + * course_get_format($course)->get_course()->OPTIONNAME (outside of format class) + * + * All course options are returned by calling: + * $this->get_format_options(); + * + * @param bool $foreditform + * @return array of options + */ + public function course_format_options($foreditform = false) { + return array(); + } + + /** + * Definitions of the additional options that this course format uses for section + * + * See course_format::course_format_options() for return array definition. + * + * Additionally section format options may have property 'cache' set to true + * if this option needs to be cached in get_fast_modinfo(). The 'cache' property + * is recommended to be set only for fields used in course_format::get_section_name(), + * course_format::extend_course_navigation() and course_format::get_view_url() + * + * For better performance cached options are recommended to have 'cachedefault' property + * Unlike 'default', 'cachedefault' should be static and not access get_config(). + * + * Regardless of value of 'cache' all options are accessed in the code as + * $sectioninfo->OPTIONNAME + * where $sectioninfo is instance of section_info, returned by + * get_fast_modinfo($course)->get_section_info($sectionnum) + * or get_fast_modinfo($course)->get_section_info_all() + * + * All format options for particular section are returned by calling: + * $this->get_format_options($section); + * + * @param bool $foreditform + * @return array + */ + public function section_format_options($foreditform = false) { + return array(); + } + + /** + * Returns the format options stored for this course or course section + * + * When overriding please note that this function is called from rebuild_course_cache() + * and section_info object, therefore using of get_fast_modinfo() and/or any function that + * accesses it may lead to recursion. + * + * @param null|int|stdClass|section_info $section if null the course format options will be returned + * otherwise options for specified section will be returned. This can be either + * section object or relative section number (field course_sections.section) + * @return array + */ + public function get_format_options($section = null) { + global $DB; + if ($section === null) { + $options = $this->course_format_options(); + } else { + $options = $this->section_format_options(); + } + if (empty($options)) { + // There are no option for course/sections anyway, no need to go further. + return array(); + } + if ($section === null) { + // Course format options will be returned. + $sectionid = 0; + } else if ($this->courseid && isset($section->id)) { + // Course section format options will be returned. + $sectionid = $section->id; + } else if ($this->courseid && is_int($section) && + ($sectionobj = $DB->get_record('course_sections', + array('section' => $section, 'course' => $this->courseid), 'id'))) { + // Course section format options will be returned. + $sectionid = $sectionobj->id; + } else { + // Non-existing (yet) section was passed as an argument + // default format options for course section will be returned. + $sectionid = -1; + } + if (!array_key_exists($sectionid, $this->formatoptions)) { + $this->formatoptions[$sectionid] = array(); + // First fill with default values. + foreach ($options as $optionname => $optionparams) { + $this->formatoptions[$sectionid][$optionname] = null; + if (array_key_exists('default', $optionparams)) { + $this->formatoptions[$sectionid][$optionname] = $optionparams['default']; + } + } + if ($this->courseid && $sectionid !== -1) { + // Overwrite the default options values with those stored in course_format_options table + // nothing can be stored if we are interested in generic course ($this->courseid == 0) + // or generic section ($sectionid === 0). + $records = $DB->get_records('course_format_options', + array('courseid' => $this->courseid, + 'format' => $this->format, + 'sectionid' => $sectionid + ), '', 'id,name,value'); + foreach ($records as $record) { + if (array_key_exists($record->name, $this->formatoptions[$sectionid])) { + $value = $record->value; + if ($value !== null && isset($options[$record->name]['type'])) { + // This will convert string value to number if needed. + $value = clean_param($value, $options[$record->name]['type']); + } + $this->formatoptions[$sectionid][$record->name] = $value; + } + } + } + } + return $this->formatoptions[$sectionid]; + } + + /** + * Adds format options elements to the course/section edit form + * + * This function is called from course_edit_form::definition_after_data() + * + * @param MoodleQuickForm $mform form the elements are added to + * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form + * @return array array of references to the added form elements + */ + public function create_edit_form_elements(&$mform, $forsection = false) { + $elements = array(); + if ($forsection) { + $options = $this->section_format_options(true); + } else { + $options = $this->course_format_options(true); + } + foreach ($options as $optionname => $option) { + if (!isset($option['element_type'])) { + $option['element_type'] = 'text'; + } + $args = array($option['element_type'], $optionname, $option['label']); + if (!empty($option['element_attributes'])) { + $args = array_merge($args, $option['element_attributes']); + } + $elements[] = call_user_func_array(array($mform, 'addElement'), $args); + if (isset($option['help'])) { + $helpcomponent = 'format_'. $this->get_format(); + if (isset($option['help_component'])) { + $helpcomponent = $option['help_component']; + } + $mform->addHelpButton($optionname, $option['help'], $helpcomponent); + } + if (isset($option['type'])) { + $mform->setType($optionname, $option['type']); + } + if (isset($option['default']) && !array_key_exists($optionname, $mform->_defaultValues)) { + // Set defaults for the elements in the form. + // Since we call this method after set_data() make sure that we don't override what was already set. + $mform->setDefault($optionname, $option['default']); + } + } + + if (!$forsection && empty($this->courseid)) { + // Check if course end date form field should be enabled by default. + // If a default date is provided to the form element, it is magically enabled by default in the + // MoodleQuickForm_date_time_selector class, otherwise it's disabled by default. + if (get_config('moodlecourse', 'courseenddateenabled')) { + // At this stage (this is called from definition_after_data) course data is already set as default. + // We can not overwrite what is in the database. + $mform->setDefault('enddate', $this->get_default_course_enddate($mform)); + } + } + + return $elements; + } + + /** + * Override if you need to perform some extra validation of the format options + * + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @param array $errors errors already discovered in edit form validation + * @return array of "element_name"=>"error_description" if there are errors, + * or an empty array if everything is OK. + * Do not repeat errors from $errors param here + */ + public function edit_form_validation($data, $files, $errors) { + return array(); + } + + /** + * Prepares values of course or section format options before storing them in DB + * + * If an option has invalid value it is not returned + * + * @param array $rawdata associative array of the proposed course/section format options + * @param int|null $sectionid null if it is course format option + * @return array array of options that have valid values + */ + protected function validate_format_options(array $rawdata, int $sectionid = null) : array { + if (!$sectionid) { + $allformatoptions = $this->course_format_options(true); + } else { + $allformatoptions = $this->section_format_options(true); + } + $data = array_intersect_key($rawdata, $allformatoptions); + foreach ($data as $key => $value) { + $option = $allformatoptions[$key] + ['type' => PARAM_RAW, 'element_type' => null, 'element_attributes' => [[]]]; + $data[$key] = clean_param($value, $option['type']); + if ($option['element_type'] === 'select' && !array_key_exists($data[$key], $option['element_attributes'][0])) { + // Value invalid for select element, skip. + unset($data[$key]); + } + } + return $data; + } + + /** + * Validates format options for the course + * + * @param array $data data to insert/update + * @return array array of options that have valid values + */ + public function validate_course_format_options(array $data) : array { + return $this->validate_format_options($data); + } + + /** + * Updates format options for a course or section + * + * If $data does not contain property with the option name, the option will not be updated + * + * @param stdClass|array $data return value from moodleform::get_data() or array with data + * @param null|int $sectionid null if these are options for course or section id (course_sections.id) + * if these are options for section + * @return bool whether there were any changes to the options values + */ + protected function update_format_options($data, $sectionid = null) { + global $DB; + $data = $this->validate_format_options((array)$data, $sectionid); + if (!$sectionid) { + $allformatoptions = $this->course_format_options(); + $sectionid = 0; + } else { + $allformatoptions = $this->section_format_options(); + } + if (empty($allformatoptions)) { + // Nothing to update anyway. + return false; + } + $defaultoptions = array(); + $cached = array(); + foreach ($allformatoptions as $key => $option) { + $defaultoptions[$key] = null; + if (array_key_exists('default', $option)) { + $defaultoptions[$key] = $option['default']; + } + $cached[$key] = ($sectionid === 0 || !empty($option['cache'])); + } + $records = $DB->get_records('course_format_options', + array('courseid' => $this->courseid, + 'format' => $this->format, + 'sectionid' => $sectionid + ), '', 'name,id,value'); + $changed = $needrebuild = false; + foreach ($defaultoptions as $key => $value) { + if (isset($records[$key])) { + if (array_key_exists($key, $data) && $records[$key]->value !== $data[$key]) { + $DB->set_field('course_format_options', 'value', + $data[$key], array('id' => $records[$key]->id)); + $changed = true; + $needrebuild = $needrebuild || $cached[$key]; + } + } else { + if (array_key_exists($key, $data) && $data[$key] !== $value) { + $newvalue = $data[$key]; + $changed = true; + $needrebuild = $needrebuild || $cached[$key]; + } else { + $newvalue = $value; + // We still insert entry in DB but there are no changes from user point of + // view and no need to call rebuild_course_cache(). + } + $DB->insert_record('course_format_options', array( + 'courseid' => $this->courseid, + 'format' => $this->format, + 'sectionid' => $sectionid, + 'name' => $key, + 'value' => $newvalue + )); + } + } + if ($needrebuild) { + rebuild_course_cache($this->courseid, true); + } + if ($changed) { + // Reset internal caches. + if (!$sectionid) { + $this->course = false; + } + unset($this->formatoptions[$sectionid]); + } + return $changed; + } + + /** + * Updates format options for a course + * + * If $data does not contain property with the option name, the option will not be updated + * + * @param stdClass|array $data return value from moodleform::get_data() or array with data + * @param stdClass $oldcourse if this function is called from update_course() + * this object contains information about the course before update + * @return bool whether there were any changes to the options values + */ + public function update_course_format_options($data, $oldcourse = null) { + return $this->update_format_options($data); + } + + /** + * Updates format options for a section + * + * Section id is expected in $data->id (or $data['id']) + * If $data does not contain property with the option name, the option will not be updated + * + * @param stdClass|array $data return value from moodleform::get_data() or array with data + * @return bool whether there were any changes to the options values + */ + public function update_section_format_options($data) { + $data = (array)$data; + return $this->update_format_options($data, $data['id']); + } + + /** + * Return an instance of moodleform to edit a specified section + * + * Default implementation returns instance of editsection_form that automatically adds + * additional fields defined in course_format::section_format_options() + * + * Format plugins may extend editsection_form if they want to have custom edit section form. + * + * @param mixed $action the action attribute for the form. If empty defaults to auto detect the + * current url. If a moodle_url object then outputs params as hidden variables. + * @param array $customdata the array with custom data to be passed to the form + * /course/editsection.php passes section_info object in 'cs' field + * for filling availability fields + * @return moodleform + */ + public function editsection_form($action, $customdata = array()) { + global $CFG; + require_once($CFG->dirroot. '/course/editsection_form.php'); + $context = context_course::instance($this->courseid); + if (!array_key_exists('course', $customdata)) { + $customdata['course'] = $this->get_course(); + } + return new editsection_form($action, $customdata); + } + + /** + * Allows course format to execute code on moodle_page::set_course() + * + * @param moodle_page $page instance of page calling set_course + */ + public function page_set_course(moodle_page $page) { + } + + /** + * Allows course format to execute code on moodle_page::set_cm() + * + * Current module can be accessed as $page->cm (returns instance of cm_info) + * + * @param moodle_page $page instance of page calling set_cm + */ + public function page_set_cm(moodle_page $page) { + } + + /** + * Course-specific information to be output on any course page (usually above navigation bar) + * + * Example of usage: + * define + * class format_FORMATNAME_XXX implements renderable {} + * + * create format renderer in course/format/FORMATNAME/renderer.php, define rendering function: + * class format_FORMATNAME_renderer extends plugin_renderer_base { + * protected function render_format_FORMATNAME_XXX(format_FORMATNAME_XXX $xxx) { + * return html_writer::tag('div', 'This is my header/footer'); + * } + * } + * + * Return instance of format_FORMATNAME_XXX in this function, the appropriate method from + * plugin renderer will be called + * + * @return null|renderable null for no output or object with data for plugin renderer + */ + public function course_header() { + return null; + } + + /** + * Course-specific information to be output on any course page (usually in the beginning of + * standard footer) + * + * See course_format::course_header() for usage + * + * @return null|renderable null for no output or object with data for plugin renderer + */ + public function course_footer() { + return null; + } + + /** + * Course-specific information to be output immediately above content on any course page + * + * See course_format::course_header() for usage + * + * @return null|renderable null for no output or object with data for plugin renderer + */ + public function course_content_header() { + return null; + } + + /** + * Course-specific information to be output immediately below content on any course page + * + * See course_format::course_header() for usage + * + * @return null|renderable null for no output or object with data for plugin renderer + */ + public function course_content_footer() { + return null; + } + + /** + * Returns instance of page renderer used by this plugin + * + * @param moodle_page $page + * @return renderer_base + */ + public function get_renderer(moodle_page $page) { + return $page->get_renderer('format_'. $this->get_format()); + } + + /** + * Returns instance of output compornent used by this plugin + * + * @param string $outputname the element to render (section, activity...) + * @return string the output component classname + */ + public function get_output_classname(?string $outputname): string { + $component = 'format_'. $this->get_format(); + $outputclass = "$component\\output\\$outputname"; + if (class_exists($outputclass)) { + return $outputclass; + } + return "core_course\\output\\$outputname"; + } + + /** + * Returns true if the specified section is current + * + * By default we analyze $course->marker + * + * @param int|stdClass|section_info $section + * @return bool + */ + public function is_section_current($section) { + if (is_object($section)) { + $sectionnum = $section->section; + } else { + $sectionnum = $section; + } + return ($sectionnum && ($course = $this->get_course()) && $course->marker == $sectionnum); + } + + /** + * Allows to specify for modinfo that section is not available even when it is visible and conditionally available. + * + * Note: affected user can be retrieved as: $section->modinfo->userid + * + * Course format plugins can override the method to change the properties $available and $availableinfo that were + * calculated by conditional availability. + * To make section unavailable set: + * $available = false; + * To make unavailable section completely hidden set: + * $availableinfo = ''; + * To make unavailable section visible with availability message set: + * $availableinfo = get_string('sectionhidden', 'format_xxx'); + * + * @param section_info $section + * @param bool $available the 'available' propery of the section_info as it was evaluated by conditional availability. + * Can be changed by the method but 'false' can not be overridden by 'true'. + * @param string $availableinfo the 'availableinfo' propery of the section_info as it was evaluated by conditional availability. + * Can be changed by the method + */ + public function section_get_available_hook(section_info $section, &$available, &$availableinfo) { + } + + /** + * Whether this format allows to delete sections + * + * If format supports deleting sections it is also recommended to define language string + * 'deletesection' inside the format. + * + * Do not call this function directly, instead use course_can_delete_section() + * + * @param int|stdClass|section_info $section + * @return bool + */ + public function can_delete_section($section) { + return false; + } + + /** + * Deletes a section + * + * Do not call this function directly, instead call course_delete_section() + * + * @param int|stdClass|section_info $section + * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it. + * @return bool whether section was deleted + */ + public function delete_section($section, $forcedeleteifnotempty = false) { + global $DB; + if (!$this->uses_sections()) { + // Not possible to delete section if sections are not used. + return false; + } + if (!is_object($section)) { + $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section), + 'id,section,sequence,summary'); + } + if (!$section || !$section->section) { + // Not possible to delete 0-section. + return false; + } + + if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) { + return false; + } + + $course = $this->get_course(); + + // Remove the marker if it points to this section. + if ($section->section == $course->marker) { + course_set_marker($course->id, 0); + } + + $lastsection = $DB->get_field_sql('SELECT max(section) from {course_sections} + WHERE course = ?', array($course->id)); + + // Find out if we need to descrease the 'numsections' property later. + $courseformathasnumsections = array_key_exists('numsections', + $this->get_format_options()); + $decreasenumsections = $courseformathasnumsections && ($section->section <= $course->numsections); + + // Move the section to the end. + move_section_to($course, $section->section, $lastsection, true); + + // Delete all modules from the section. + foreach (preg_split('/,/', $section->sequence, -1, PREG_SPLIT_NO_EMPTY) as $cmid) { + course_delete_module($cmid); + } + + // Delete section and it's format options. + $DB->delete_records('course_format_options', array('sectionid' => $section->id)); + $DB->delete_records('course_sections', array('id' => $section->id)); + rebuild_course_cache($course->id, true); + + // Delete section summary files. + $context = \context_course::instance($course->id); + $fs = get_file_storage(); + $fs->delete_area_files($context->id, 'course', 'section', $section->id); + + // Descrease 'numsections' if needed. + if ($decreasenumsections) { + $this->update_course_format_options(array('numsections' => $course->numsections - 1)); + } + + return true; + } + + /** + * Prepares the templateable object to display section name + * + * @param \section_info|\stdClass $section + * @param bool $linkifneeded + * @param bool $editable + * @param null|lang_string|string $edithint + * @param null|lang_string|string $editlabel + * @return \core\output\inplace_editable + */ + public function inplace_editable_render_section_name($section, $linkifneeded = true, + $editable = null, $edithint = null, $editlabel = null) { + global $USER, $CFG; + require_once($CFG->dirroot.'/course/lib.php'); + + if ($editable === null) { + $editable = !empty($USER->editing) && has_capability('moodle/course:update', + context_course::instance($section->course)); + } + + $displayvalue = $title = get_section_name($section->course, $section); + if ($linkifneeded) { + // Display link under the section name if the course format setting is to display one section per page. + $url = course_get_url($section->course, $section->section, array('navigation' => true)); + if ($url) { + $displayvalue = html_writer::link($url, $title); + } + $itemtype = 'sectionname'; + } else { + // If $linkifneeded==false, we never display the link (this is used when rendering the section header). + // Itemtype 'sectionnamenl' (nl=no link) will tell the callback that link should not be rendered - + // there is no other way callback can know where we display the section name. + $itemtype = 'sectionnamenl'; + } + if (empty($edithint)) { + $edithint = new lang_string('editsectionname'); + } + if (empty($editlabel)) { + $editlabel = new lang_string('newsectionname', '', $title); + } + + return new \core\output\inplace_editable('format_' . $this->format, $itemtype, $section->id, $editable, + $displayvalue, $section->name, $edithint, $editlabel); + } + + /** + * Updates the value in the database and modifies this object respectively. + * + * ALWAYS check user permissions before performing an update! Throw exceptions if permissions are not sufficient + * or value is not legit. + * + * @param stdClass $section + * @param string $itemtype + * @param mixed $newvalue + * @return \core\output\inplace_editable + */ + public function inplace_editable_update_section_name($section, $itemtype, $newvalue) { + if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') { + $context = context_course::instance($section->course); + external_api::validate_context($context); + require_capability('moodle/course:update', $context); + + $newtitle = clean_param($newvalue, PARAM_TEXT); + if (strval($section->name) !== strval($newtitle)) { + course_update_section($section->course, $section, array('name' => $newtitle)); + } + return $this->inplace_editable_render_section_name($section, ($itemtype === 'sectionname'), true); + } + } + + + /** + * Returns the default end date value based on the start date. + * + * This is the default implementation for course formats, it is based on + * moodlecourse/courseduration setting. Course formats like format_weeks for + * example can overwrite this method and return a value based on their internal options. + * + * @param moodleform $mform + * @param array $fieldnames The form - field names mapping. + * @return int + */ + public function get_default_course_enddate($mform, $fieldnames = array()) { + + if (empty($fieldnames)) { + $fieldnames = array('startdate' => 'startdate'); + } + + $startdate = $this->get_form_start_date($mform, $fieldnames); + $courseduration = intval(get_config('moodlecourse', 'courseduration')); + if (!$courseduration) { + // Default, it should be already set during upgrade though. + $courseduration = YEARSECS; + } + + return $startdate + $courseduration; + } + + /** + * Indicates whether the course format supports the creation of the Announcements forum. + * + * For course format plugin developers, please override this to return true if you want the Announcements forum + * to be created upon course creation. + * + * @return bool + */ + public function supports_news() { + // For backwards compatibility, check if default blocks include the news_items block. + $defaultblocks = $this->get_default_blocks(); + foreach ($defaultblocks as $blocks) { + if (in_array('news_items', $blocks)) { + return true; + } + } + // Return false by default. + return false; + } + + /** + * Get the start date value from the course settings page form. + * + * @param moodleform $mform + * @param array $fieldnames The form - field names mapping. + * @return int + */ + protected function get_form_start_date($mform, $fieldnames) { + $startdate = $mform->getElementValue($fieldnames['startdate']); + return $mform->getElement($fieldnames['startdate'])->exportValue($startdate); + } + + /** + * Returns whether this course format allows the activity to + * have "triple visibility state" - visible always, hidden on course page but available, hidden. + * + * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module) + * @param stdClass|section_info $section section where this module is located or will be added to + * @return bool + */ + public function allow_stealth_module_visibility($cm, $section) { + return false; + } + + /** + * Callback used in WS core_course_edit_section when teacher performs an AJAX action on a section (show/hide) + * + * Access to the course is already validated in the WS but the callback has to make sure + * that particular action is allowed by checking capabilities + * + * Course formats should register + * + * @param stdClass|section_info $section + * @param string $action + * @param int $sr the section return + * @return null|array|stdClass any data for the Javascript post-processor (must be json-encodeable) + */ + public function section_action($section, $action, $sr) { + global $PAGE; + if (!$this->uses_sections() || !$section->section) { + // No section actions are allowed if course format does not support sections. + // No actions are allowed on the 0-section by default (overwrite in course format if needed). + throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action)); + } + + $course = $this->get_course(); + $coursecontext = context_course::instance($course->id); + switch($action) { + case 'hide': + case 'show': + require_capability('moodle/course:sectionvisibility', $coursecontext); + $visible = ($action === 'hide') ? 0 : 1; + course_update_section($course, $section, array('visible' => $visible)); + break; + default: + throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action)); + } + + $modules = []; + + $modinfo = $this->get_modinfo(); + + if (!($section instanceof section_info)) { + $section = $modinfo->get_section_info($section->section); + } + + if ($sr) { + $this->set_section_number($sr); + } + + // Load the cmlist output. + $cmitemclass = $this->get_output_classname('section_format\\cmitem'); + $renderer = $this->get_renderer($PAGE); + + $coursesections = $modinfo->sections; + if (array_key_exists($section->section, $coursesections)) { + $completioninfo = new completion_info($course); + foreach ($coursesections[$section->section] as $cmid) { + $cm = $modinfo->get_cm($cmid); + $cmitem = new $cmitemclass($this, $section, $cm); + $modules[] = $renderer->render($cmitem); + } + } + + return ['modules' => $modules]; + } + + /** + * Return the plugin config settings for external functions, + * in some cases the configs will need formatting or be returned only if the current user has some capabilities enabled. + * + * @return array the list of configs + * @since Moodle 3.5 + */ + public function get_config_for_external() { + return array(); + } +} diff --git a/course/format/formatlegacy.php b/course/format/formatlegacy.php index 665f5833821..063e47ab348 100644 --- a/course/format/formatlegacy.php +++ b/course/format/formatlegacy.php @@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die; * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class format_legacy extends format_base { +class format_legacy extends core_course\course_format { /** * Returns true if this course format uses sections diff --git a/course/format/singleactivity/lib.php b/course/format/singleactivity/lib.php index 88aba735101..b468c2bc047 100644 --- a/course/format/singleactivity/lib.php +++ b/course/format/singleactivity/lib.php @@ -32,7 +32,7 @@ require_once($CFG->dirroot. '/course/format/lib.php'); * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class format_singleactivity extends format_base { +class format_singleactivity extends core_course\course_format { /** @var cm_info the current activity. Use get_activity() to retrieve it. */ private $activity = false; diff --git a/course/format/social/lib.php b/course/format/social/lib.php index 088719a0a72..5ca13cba3ae 100644 --- a/course/format/social/lib.php +++ b/course/format/social/lib.php @@ -33,7 +33,7 @@ require_once($CFG->dirroot. '/course/format/lib.php'); * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class format_social extends format_base { +class format_social extends core_course\course_format { /** * The URL to use for the specified course diff --git a/course/format/weeks/lib.php b/course/format/weeks/lib.php index bc9326ed2f0..30fab80f671 100644 --- a/course/format/weeks/lib.php +++ b/course/format/weeks/lib.php @@ -34,7 +34,7 @@ require_once($CFG->dirroot. '/course/lib.php'); * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class format_weeks extends format_base { +class format_weeks extends core_course\course_format { /** * Returns true if this course format uses sections diff --git a/course/lib.php b/course/lib.php index d1b676ef53c..4fe1c9a67c9 100644 --- a/course/lib.php +++ b/course/lib.php @@ -592,9 +592,7 @@ function course_set_marker($courseid, $marker) { if ($COURSE && $COURSE->id == $courseid) { $COURSE->marker = $marker; } - if (class_exists('format_base')) { - format_base::reset_course_cache($courseid); - } + core_course\course_format::reset_course_cache($courseid); course_modinfo::clear_instance_cache($courseid); } @@ -2149,7 +2147,7 @@ function move_courses($courseids, $categoryid) { * Returns the display name of the given section that the course prefers * * Implementation of this function is provided by course format - * @see format_base::get_section_name() + * @see core_course\course_format::get_section_name() * * @param int|stdClass $courseorid The course to get the section name for (object or just course id) * @param int|stdClass $section Section object from database or just field course_sections.section diff --git a/course/tests/courseformat_test.php b/course/tests/courseformat_test.php index dd7b1a94dfe..15cb2d723c2 100644 --- a/course/tests/courseformat_test.php +++ b/course/tests/courseformat_test.php @@ -210,7 +210,7 @@ class core_course_courseformat_testcase extends advanced_testcase { * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class format_testformat extends format_base { +class format_testformat extends core_course\course_format { /** * Returns the list of blocks to be automatically added for the newly created course. * @@ -232,7 +232,7 @@ class format_testformat extends format_base { * @copyright 2016 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class format_testlegacy extends format_base { +class format_testlegacy extends core_course\course_format { /** * Returns the list of blocks to be automatically added for the newly created course. * diff --git a/lib/adminlib.php b/lib/adminlib.php index d23d1b15885..536413a786e 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -4479,7 +4479,7 @@ class admin_setting_sitesetselect extends admin_setting_configselect { if ($SITE->id == $COURSE->id) { $COURSE = $SITE; } - format_base::reset_course_cache($SITE->id); + core_course\course_format::reset_course_cache($SITE->id); return ''; @@ -4690,7 +4690,7 @@ class admin_setting_sitesetcheckbox extends admin_setting_configcheckbox { if ($SITE->id == $COURSE->id) { $COURSE = $SITE; } - format_base::reset_course_cache($SITE->id); + core_course\course_format::reset_course_cache($SITE->id); return ''; } @@ -4772,7 +4772,7 @@ class admin_setting_sitesettext extends admin_setting_configtext { if ($SITE->id == $COURSE->id) { $COURSE = $SITE; } - format_base::reset_course_cache($SITE->id); + core_course\course_format::reset_course_cache($SITE->id); return ''; } @@ -4824,7 +4824,7 @@ class admin_setting_special_frontpagedesc extends admin_setting_confightmleditor if ($SITE->id == $COURSE->id) { $COURSE = $SITE; } - format_base::reset_course_cache($SITE->id); + core_course\course_format::reset_course_cache($SITE->id); return ''; } diff --git a/lib/behat/classes/util.php b/lib/behat/classes/util.php index feaf4e67280..a0a13768895 100644 --- a/lib/behat/classes/util.php +++ b/lib/behat/classes/util.php @@ -411,10 +411,7 @@ class behat_util extends testing_util { filter_manager::reset_caches(); // Reset course and module caches. - if (class_exists('format_base')) { - // If file containing class is not loaded, there is no cache there anyway. - format_base::reset_course_cache(0); - } + core_course\course_format::reset_course_cache(0); get_fast_modinfo(0, 0, true); // Inform data generator. diff --git a/lib/db/renamedclasses.php b/lib/db/renamedclasses.php index d54813c06d7..390cd2993ed 100644 --- a/lib/db/renamedclasses.php +++ b/lib/db/renamedclasses.php @@ -42,4 +42,5 @@ $renamedclasses = array( 'core\\analytics\\target\\course_completion' => 'core_course\\analytics\\target\\course_completion', 'core\\analytics\\target\\course_gradetopass' => 'core_course\\analytics\\target\\course_gradetopass', 'core\\analytics\\target\\no_teaching' => 'core_course\\analytics\\target\\no_teaching', + 'format_base' => 'core_course\\course_format', ); diff --git a/lib/deprecatedlib.php b/lib/deprecatedlib.php index b02ddc0acf6..1a2f3d94c5a 100644 --- a/lib/deprecatedlib.php +++ b/lib/deprecatedlib.php @@ -918,7 +918,8 @@ function textlib_get_instance() { * @deprecated since 2.4 */ function get_generic_section_name() { - throw new coding_exception('get_generic_section_name() is deprecated. Please use appropriate functionality from class format_base'); + throw new coding_exception('get_generic_section_name() is deprecated. Please use appropriate functionality ' + .'from class core_course\\course_format'); } /** diff --git a/lib/modinfolib.php b/lib/modinfolib.php index 69bfbcb1009..933a14c03ca 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -2357,10 +2357,7 @@ function rebuild_course_cache($courseid=0, $clearonly=false) { // Destroy navigation caches navigation_cache::destroy_volatile_caches(); - if (class_exists('format_base')) { - // if file containing class is not loaded, there is no cache there anyway - format_base::reset_course_cache($courseid); - } + core_course\course_format::reset_course_cache($courseid); $cachecoursemodinfo = cache::make('core', 'coursemodinfo'); if (empty($courseid)) { diff --git a/lib/moodlelib.php b/lib/moodlelib.php index bbadbf47617..496bf20cbfc 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -5179,9 +5179,7 @@ function delete_course($courseorid, $showfeedback = true) { $DB->delete_records("course_format_options", array("courseid" => $courseid)); // Reset all course related caches here. - if (class_exists('format_base', false)) { - format_base::reset_course_cache($courseid); - } + core_course\course_format::reset_course_cache($courseid); // Tell search that we have deleted the course so it can delete course data from the index. \core_search\manager::course_deleting_finish($courseid); diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 531056ef798..494f745b26a 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -2005,7 +2005,7 @@ class global_navigation extends navigation_node { * Loads all of the courses section into the navigation. * * This function calls method from current course format, see - * {@link format_base::extend_course_navigation()} + * {@link core_course\course_format::extend_course_navigation()} * If course module ($cm) is specified but course format failed to create the node, * the activity node is created anyway. * diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index 94ec4414dbe..a9be6517ebd 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -240,10 +240,7 @@ class phpunit_util extends testing_util { //TODO MDL-25290: add more resets here and probably refactor them to new core function // Reset course and module caches. - if (class_exists('format_base')) { - // If file containing class is not loaded, there is no cache there anyway. - format_base::reset_course_cache(0); - } + core_course\course_format::reset_course_cache(0); get_fast_modinfo(0, 0, true); // Reset other singletons. From 8e7dc4244026bd1dbeebcd4ea12c5c17ade3565f Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Thu, 22 Oct 2020 09:22:54 +0200 Subject: [PATCH 2/2] MDL-65974 course: move format renderers to outputs --- course/classes/course_format.php | 18 +- course/classes/output/cm_format.php | 134 ++ course/classes/output/course_format.php | 185 +++ .../output/course_format/addsection.php | 131 ++ .../output/course_format/frontpagesection.php | 101 ++ .../course_format/sectionnavigation.php | 123 ++ .../output/course_format/sectionselector.php | 100 ++ course/classes/output/section_format.php | 211 +++ .../output/section_format/availability.php | 123 ++ .../classes/output/section_format/cmitem.php | 101 ++ .../classes/output/section_format/cmlist.php | 132 ++ .../output/section_format/cmsummary.php | 128 ++ .../output/section_format/controlmenu.php | 232 +++ .../classes/output/section_format/header.php | 99 ++ .../classes/output/section_format/summary.php | 94 ++ course/dnduploadlib.php | 14 +- course/externallib.php | 38 +- course/format/README.txt | 32 + course/format/lib.php | 1326 +---------------- course/format/renderer.php | 715 +++++---- course/format/singleactivity/renderer.php | 21 +- .../output/section_format/controlmenu.php | 118 ++ course/format/topics/format.php | 12 +- course/format/topics/lib.php | 25 +- course/format/topics/renderer.php | 100 -- course/format/weeks/format.php | 13 +- course/format/weeks/lib.php | 19 +- course/format/weeks/renderer.php | 24 +- course/lib.php | 13 +- course/renderer.php | 192 +-- course/templates/local/cm_format.mustache | 69 + course/templates/local/course_format.mustache | 181 +++ .../local/course_format/addsection.mustache | 56 + .../course_format/frontpagesection.mustache | 46 + .../course_format/sectionnavigation.mustache | 49 + .../course_format/sectionselector.mustache | 53 + .../templates/local/section_format.mustache | 107 ++ .../section_format/availability.mustache | 34 + .../local/section_format/cmitem.mustache | 43 + .../local/section_format/cmlist.mustache | 77 + .../local/section_format/cmsummary.mustache | 44 + .../local/section_format/controlmenu.mustache | 32 + .../local/section_format/header.mustache | 37 + .../local/section_format/summary.mustache | 31 + .../behat/paged_course_navigation.feature | 20 +- course/tests/fixtures/format_theunittest.php | 6 +- course/upgrade.txt | 44 + lib/deprecatedlib.php | 4 +- lib/navigationlib.php | 4 +- 49 files changed, 3553 insertions(+), 1958 deletions(-) create mode 100644 course/classes/output/cm_format.php create mode 100644 course/classes/output/course_format.php create mode 100644 course/classes/output/course_format/addsection.php create mode 100644 course/classes/output/course_format/frontpagesection.php create mode 100644 course/classes/output/course_format/sectionnavigation.php create mode 100644 course/classes/output/course_format/sectionselector.php create mode 100644 course/classes/output/section_format.php create mode 100644 course/classes/output/section_format/availability.php create mode 100644 course/classes/output/section_format/cmitem.php create mode 100644 course/classes/output/section_format/cmlist.php create mode 100644 course/classes/output/section_format/cmsummary.php create mode 100644 course/classes/output/section_format/controlmenu.php create mode 100644 course/classes/output/section_format/header.php create mode 100644 course/classes/output/section_format/summary.php create mode 100644 course/format/topics/classes/output/section_format/controlmenu.php create mode 100644 course/templates/local/cm_format.mustache create mode 100644 course/templates/local/course_format.mustache create mode 100644 course/templates/local/course_format/addsection.mustache create mode 100644 course/templates/local/course_format/frontpagesection.mustache create mode 100644 course/templates/local/course_format/sectionnavigation.mustache create mode 100644 course/templates/local/course_format/sectionselector.mustache create mode 100644 course/templates/local/section_format.mustache create mode 100644 course/templates/local/section_format/availability.mustache create mode 100644 course/templates/local/section_format/cmitem.mustache create mode 100644 course/templates/local/section_format/cmlist.mustache create mode 100644 course/templates/local/section_format/cmsummary.mustache create mode 100644 course/templates/local/section_format/controlmenu.mustache create mode 100644 course/templates/local/section_format/header.mustache create mode 100644 course/templates/local/section_format/summary.mustache diff --git a/course/classes/course_format.php b/course/classes/course_format.php index debd894f51b..f55b3c97163 100644 --- a/course/classes/course_format.php +++ b/course/classes/course_format.php @@ -967,7 +967,6 @@ abstract class course_format { public function editsection_form($action, $customdata = array()) { global $CFG; require_once($CFG->dirroot. '/course/editsection_form.php'); - $context = context_course::instance($this->courseid); if (!array_key_exists('course', $customdata)) { $customdata['course'] = $this->get_course(); } @@ -1091,6 +1090,18 @@ abstract class course_format { return ($sectionnum && ($course = $this->get_course()) && $course->marker == $sectionnum); } + /** + * return true if the course editor must be displayed. + * + * @return bool true if edit controls must be displayed + */ + public function show_editor(): bool { + global $PAGE; + $course = $this->get_course(); + $coursecontext = context_course::instance($course->id); + return $PAGE->user_is_editing() && has_capability('moodle/course:update', $coursecontext); + } + /** * Allows to specify for modinfo that section is not available even when it is visible and conditionally available. * @@ -1387,16 +1398,13 @@ abstract class course_format { } // Load the cmlist output. - $cmitemclass = $this->get_output_classname('section_format\\cmitem'); $renderer = $this->get_renderer($PAGE); $coursesections = $modinfo->sections; if (array_key_exists($section->section, $coursesections)) { - $completioninfo = new completion_info($course); foreach ($coursesections[$section->section] as $cmid) { $cm = $modinfo->get_cm($cmid); - $cmitem = new $cmitemclass($this, $section, $cm); - $modules[] = $renderer->render($cmitem); + $modules[] = $renderer->course_section_updated_cm_item($this, $section, $cm); } } diff --git a/course/classes/output/cm_format.php b/course/classes/output/cm_format.php new file mode 100644 index 00000000000..1ae369554b4 --- /dev/null +++ b/course/classes/output/cm_format.php @@ -0,0 +1,134 @@ +. + +/** + * Contains the default activity list from a section. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output; + +use core_course\course_format; +use section_info; +use completion_info; +use renderable; +use templatable; +use cm_info; +use stdClass; + +/** + * Base class to render a course module inside a course format. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cm_format implements renderable, templatable { + + /** @var course_format the course format */ + protected $format; + + /** @var section_info the section object */ + private $section; + + /** @var cm_info the course module instance */ + protected $mod; + + /** @var array optional display options */ + protected $displayoptions; + + /** @var completion_info the course completion */ + protected $completioninfo; + + /** + * Constructor. + * + * @param course_format $format the course format + * @param section_info $section the section info + * @param completion_info $completioninfo the course completion info + * @param cm_info $mod the course module ionfo + * @param array $displayoptions optional extra display options + */ + public function __construct(course_format $format, section_info $section, completion_info $completioninfo, + cm_info $mod, array $displayoptions = []) { + + $this->format = $format; + $this->section = $section; + $this->completioninfo = $completioninfo; + $this->mod = $mod; + $this->displayoptions = $displayoptions; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return stdClass data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + $format = $this->format; + $course = $format->get_course(); + $mod = $this->mod; + $displayoptions = $this->displayoptions; + + $data = (object)[ + 'cmname' => $output->course_section_cm_name($mod, $displayoptions), + 'afterlink' => $mod->afterlink, + 'altcontent' => $output->course_section_cm_text($mod, $displayoptions), + 'availability' => $output->course_section_cm_availability($mod, $displayoptions), + 'url' => $mod->url, + 'completion' => $output->course_section_cm_completion( + $course, $this->completioninfo, $mod, $displayoptions + ), + ]; + + if (!empty($mod->indent)) { + $data->indent = $mod->indent; + if ($mod->indent > 15) { + $data->hugeindent = true; + } + } + + if (!empty($data->cmname)) { + $data->hasname = true; + } + if (!empty($data->url)) { + $data->hasurl = true; + } + + $returnsection = $format->get_section_number(); + $data->extras = []; + if ($format->show_editor()) { + // Edit actions. + $editactions = course_get_cm_edit_actions($mod, $mod->indent, $returnsection); + $data->extras[] = $output->course_section_cm_edit_actions($editactions, $mod, $displayoptions); + if (!empty($mod->afterediticons)) { + $data->extras[] = $mod->afterediticons; + } + // Move and select options. + $data->moveicon = course_get_cm_move($mod, $returnsection); + } + + if (!empty($data->completion) || !empty($data->extras)) { + $data->hasextras = true; + } + + return $data; + } +} diff --git a/course/classes/output/course_format.php b/course/classes/output/course_format.php new file mode 100644 index 00000000000..dc55b011ed9 --- /dev/null +++ b/course/classes/output/course_format.php @@ -0,0 +1,185 @@ +. + +/** + * Contains the main course format out class. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output; + +use core_course\course_format as course_format_base; +use course_modinfo; +use renderable; +use templatable; +use stdClass; + +/** + * Base class to render a course format. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_format implements renderable, templatable { + + /** @var core_course\course_format the course format class */ + protected $format; + + /** @var string the section format class */ + protected $sectionclass; + + /** @var string the add section output class name */ + protected $addsectionclass; + + /** @var string section navigation class name */ + protected $sectionnavigationclass; + + /** @var string section selector class name */ + protected $sectionselectorclass; + + /** + * Constructor. + * + * @param course_format_base $format the coruse format + */ + public function __construct(course_format_base $format) { + $this->format = $format; + + // Load output classes names from format. + $this->sectionclass = $format->get_output_classname('section_format'); + $this->addsectionclass = $format->get_output_classname('course_format\\addsection'); + $this->sectionnavigationclass = $format->get_output_classname('course_format\\sectionnavigation'); + $this->sectionselectorclass = $format->get_output_classname('course_format\\sectionselector'); + } + + /** + * Export this data so it can be used as the context for a mustache template (core/inplace_editable). + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return stdClass data context for a mustache template + */ + public function export_for_template(\renderer_base $output) { + $format = $this->format; + + $addsection = new $this->addsectionclass($format); + + // Most formats uses section 0 as a separate section so we remove from the list. + $sections = $this->export_sections($output); + $initialsection = ''; + if (!empty($sections)) { + $initialsection = array_shift($sections); + } + + $data = (object)[ + 'title' => $format->page_title(), // This method should be in the course_format class. + 'initialsection' => $initialsection, + 'sections' => $sections, + 'numsections' => $addsection->export_for_template($output), + 'format' => $format->get_format(), + ]; + + // The single section format has extra navigation. + $singlesection = $this->format->get_section_number(); + if ($singlesection) { + $sectionnavigation = new $this->sectionnavigationclass($format, $singlesection); + $data->sectionnavigation = $sectionnavigation->export_for_template($output); + + $sectionselector = new $this->sectionselectorclass($format, $sectionnavigation); + $data->sectionselector = $sectionselector->export_for_template($output); + + $data->hasnavigation = true; + $data->singlesection = array_shift($data->sections); + } + + return $data; + } + + /** + * Export sections array data. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return array data context for a mustache template + */ + protected function export_sections(\renderer_base $output): array { + + $format = $this->format; + $course = $format->get_course(); + $modinfo = $this->format->get_modinfo(); + + // Generate section list. + $sections = []; + $stealthsections = []; + $numsections = $format->get_last_section_number(); + foreach ($this->get_sections_to_display($modinfo) as $sectionnum => $thissection) { + // The course/view.php check the section existence but the output can be called + // from other parts so we need to check it. + if (!$thissection) { + print_error('unknowncoursesection', 'error', course_get_url($course), format_string($course->fullname)); + } + + $section = new $this->sectionclass($format, $thissection); + + if ($sectionnum > $numsections) { + // Activities inside this section are 'orphaned', this section will be printed as 'stealth' below. + if (!empty($modinfo->sections[$sectionnum])) { + $stealthsections[] = $section->export_for_template($output); + } + continue; + } + + // Show the section if the user is permitted to access it, OR if it's not available + // but there is some available info text which explains the reason & should display, + // OR it is hidden but the course has a setting to display hidden sections as unavilable. + $showsection = $thissection->uservisible || + ($thissection->visible && !$thissection->available && !empty($thissection->availableinfo)) || + (!$thissection->visible && !$course->hiddensections); + if (!$showsection) { + continue; + } + + $sections[] = $section->export_for_template($output); + } + if (!empty($stealthsections)) { + $sections = array_merge($sections, $stealthsections); + } + return $sections; + } + + /** + * Return an array of sections to display. + * + * This method is used to differentiate between display a specific section + * or a list of them. + * + * @param course_modinfo $modinfo the current course modinfo object + * @return section_info[] an array of section_info to display + */ + private function get_sections_to_display(course_modinfo $modinfo): array { + $singlesection = $this->format->get_section_number(); + if ($singlesection) { + return [ + $modinfo->get_section_info(0), + $modinfo->get_section_info($singlesection), + ]; + } + + return $modinfo->get_section_info_all(); + } +} diff --git a/course/classes/output/course_format/addsection.php b/course/classes/output/course_format/addsection.php new file mode 100644 index 00000000000..2f1d9a3de75 --- /dev/null +++ b/course/classes/output/course_format/addsection.php @@ -0,0 +1,131 @@ +. + +/** + * Contains the default section course format output class. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\course_format; + +use core_course\course_format; +use renderable; +use templatable; +use moodle_url; +use stdClass; + +/** + * Base class to render a course add section buttons. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class addsection implements renderable, templatable { + + /** @var course_format the course format class */ + protected $format; + + /** + * Constructor. + * + * @param course_format $format the course format + */ + public function __construct(course_format $format) { + $this->format = $format; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return stdClass data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + + $format = $this->format; + $course = $format->get_course(); + $options = $format->get_format_options(); + + $lastsection = $format->get_last_section_number(); + $maxsections = $format->get_max_sections(); + + $data = new stdClass(); + + // If no editor must be displayed, just retun an empty structure. + if (!$format->show_editor()) { + return $data; + } + + $supportsnumsections = array_key_exists('numsections', $options); + if ($supportsnumsections) { + // Current course format has 'numsections' option, which is very confusing and we suggest course format + // developers to get rid of it (see MDL-57769 on how to do it). + + if ($lastsection < $maxsections) { + $data->increase = (object) [ + 'url' => new moodle_url( + '/course/changenumsections.php', + ['courseid' => $course->id, 'increase' => true, 'sesskey' => sesskey()] + ), + ]; + } + + if ($course->numsections > 0) { + $data->decrease = (object) [ + 'url' => new moodle_url( + '/course/changenumsections.php', + ['courseid' => $course->id, 'increase' => false, 'sesskey' => sesskey()] + ), + ]; + } + + } else if (course_get_format($course)->uses_sections() && $lastsection < $maxsections) { + // Current course format does not have 'numsections' option but it has multiple sections suppport. + // Display the "Add section" link that will insert a section in the end. + // Note to course format developers: inserting sections in the other positions should check both + // capabilities 'moodle/course:update' and 'moodle/course:movesections'. + + if (get_string_manager()->string_exists('addsections', 'format_'.$course->format)) { + $addstring = get_string('addsections', 'format_'.$course->format); + } else { + $addstring = get_string('addsections'); + } + + $params = ['courseid' => $course->id, 'insertsection' => 0, 'sesskey' => sesskey()]; + + $singlesection = $this->format->get_section_number(); + if ($singlesection) { + $params['sectionreturn'] = $singlesection; + } + + $data->addsections = (object) [ + 'url' => new moodle_url('/course/changenumsections.php', $params), + 'title' => $addstring, + 'newsection' => $maxsections - $lastsection, + ]; + } + + if (count((array)$data)) { + $data->showaddsection = true; + } + + return $data; + } +} diff --git a/course/classes/output/course_format/frontpagesection.php b/course/classes/output/course_format/frontpagesection.php new file mode 100644 index 00000000000..3e91b9e5229 --- /dev/null +++ b/course/classes/output/course_format/frontpagesection.php @@ -0,0 +1,101 @@ +. + +/** + * Contains the default frontpage section displayer. + * + * The frontpage has a different wat of rendering the main topic. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\course_format; + +use core_course\course_format; +use renderable; +use templatable; +use section_info; +use context_course; +use moodle_url; +use stdClass; + +/** + * Represents the frontpage section 1. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class frontpagesection implements renderable, templatable { + + /** @var course_format the course format class */ + protected $format; + + /** @var section_info the course section class */ + protected $section; + + /** @var string the section output class name */ + protected $sectionclass; + + /** + * Constructor. + * + * @param course_format $format the course format + * @param section_info $section the section info + */ + public function __construct(course_format $format, section_info $section) { + $this->format = $format; + $this->section = $section; + + // Get the necessary classes. + $this->sectionclass = $format->get_output_classname('section_format'); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return stdClass data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + global $USER; + + $format = $this->format; + $course = $format->get_course(); + $context = context_course::instance($course->id); + $section = $this->section; + + $sectionoutput = new $this->sectionclass($format, $section); + $sectionoutput->hide_controls(); + + if (trim($section->name) == '') { + $sectionoutput->hide_title(); + } + + $data = (object)[ + 'sections' => [$sectionoutput->export_for_template($output)], + ]; + + if ($format->show_editor() && has_capability('moodle/course:update', $context)) { + $data->showsettings = true; + $data->settingsurl = new moodle_url('/course/editsection.php', ['id' => $section->id]); + } + + return $data; + } +} diff --git a/course/classes/output/course_format/sectionnavigation.php b/course/classes/output/course_format/sectionnavigation.php new file mode 100644 index 00000000000..9ded0563fd4 --- /dev/null +++ b/course/classes/output/course_format/sectionnavigation.php @@ -0,0 +1,123 @@ +. + +/** + * Contains the default section navigation output class. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\course_format; + +use core_course\course_format; +use renderable; +use templatable; +use context_course; +use stdClass; + +/** + * Base class to render a course add section navigation. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sectionnavigation implements renderable, templatable { + + /** @var course_format the course format class */ + protected $format; + + /** @var int the course displayed section */ + protected $sectionno; + + /** @var stdClass the calculated data to prevent calculations when rendered several times */ + private $data = null; + + /** + * Constructor. + * + * @param course_format $format the course format + * @param int $sectionno the section number + */ + public function __construct(course_format $format, int $sectionno) { + $this->format = $format; + $this->sectionno = $sectionno; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return stdClass data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + global $USER; + + if ($this->data !== null) { + return $this->data; + } + + $format = $this->format; + $course = $format->get_course(); + $context = context_course::instance($course->id); + + $modinfo = $this->format->get_modinfo(); + $sections = $modinfo->get_section_info_all(); + + // FIXME: This is really evil and should by using the navigation API. + $canviewhidden = has_capability('moodle/course:viewhiddensections', $context, $USER) || !$course->hiddensections; + + $data = (object)[ + 'previousurl' => '', + 'nexturl' => '', + 'larrow' => $output->larrow(), + 'rarrow' => $output->rarrow(), + 'currentsection' => $this->sectionno, + ]; + + $back = $this->sectionno - 1; + while ($back > 0 and empty($data->previousurl)) { + if ($canviewhidden || $sections[$back]->uservisible) { + if (!$sections[$back]->visible) { + $data->previoushidden = true; + } + $data->previousname = get_section_name($course, $sections[$back]); + $data->previousurl = course_get_url($course, $back); + $data->hasprevious = true; + } + $back--; + } + + $forward = $this->sectionno + 1; + $numsections = course_get_format($course)->get_last_section_number(); + while ($forward <= $numsections and empty($data->nexturl)) { + if ($canviewhidden || $sections[$forward]->uservisible) { + if (!$sections[$forward]->visible) { + $data->nexthidden = true; + } + $data->nextname = get_section_name($course, $sections[$forward]); + $data->nexturl = course_get_url($course, $forward); + $data->hasnext = true; + } + $forward++; + } + + $this->data = $data; + return $data; + } +} diff --git a/course/classes/output/course_format/sectionselector.php b/course/classes/output/course_format/sectionselector.php new file mode 100644 index 00000000000..0eb5b548e09 --- /dev/null +++ b/course/classes/output/course_format/sectionselector.php @@ -0,0 +1,100 @@ +. + +/** + * Contains the default section selector. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\course_format; + +use core_course\course_format; +use renderable; +use templatable; +use url_select; +use stdClass; + +/** + * Represents the section selector. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class sectionselector implements renderable, templatable { + + /** @var course_format the course format class */ + protected $format; + + /** @var sectionnavigation the main section navigation class */ + protected $navigation; + + /** + * Constructor. + * + * In the current imeplementaiton the seciton selector is almost a variation of the section navigator + * but in the 4.0 this selector will be a kind of dropdown menu. When this happens the construct params + * will change. + * + * @param course_format $format the course format + * @param sectionnavigation $navigation the current section navigation + */ + public function __construct(course_format $format, sectionnavigation $navigation) { + $this->format = $format; + $this->navigation = $navigation; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return stdClass data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + + $format = $this->format; + $course = $format->get_course(); + + $modinfo = $this->format->get_modinfo(); + + $data = $this->navigation->export_for_template($output); + + // Add the section selector. + $sectionmenu = []; + $sectionmenu[course_get_url($course)->out(false)] = get_string('maincoursepage'); + $section = 1; + $numsections = $format->get_last_section_number(); + while ($section <= $numsections) { + $thissection = $modinfo->get_section_info($section); + $showsection = $thissection->uservisible || !$course->hiddensections; + $url = course_get_url($course, $section); + if ($showsection && $url && $section != $data->currentsection) { + $sectionmenu[$url->out(false)] = get_section_name($course, $section); + } + $section++; + } + + $select = new url_select($sectionmenu, '', ['' => get_string('jumpto')]); + $select->class = 'jumpmenu'; + $select->formid = 'sectionmenu'; + + $data->selector = $output->render($select); + return $data; + } +} diff --git a/course/classes/output/section_format.php b/course/classes/output/section_format.php new file mode 100644 index 00000000000..ffd9d102ac2 --- /dev/null +++ b/course/classes/output/section_format.php @@ -0,0 +1,211 @@ +. + +/** + * Contains the default section course format output class. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output; + +use core_course\course_format; +use completion_info; +use renderable; +use templatable; +use section_info; +use stdClass; + +/** + * Base class to render a course section. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class section_format implements renderable, templatable { + + /** @var course_format the course format */ + protected $format; + + /** @var section_info the section info */ + protected $thissection; + + /** @var section header output class */ + protected $headerclass; + + /** @var cm list output class */ + protected $cmlistclass; + + /** @var section summary output class */ + protected $summaryclass; + + /** @var activities summary output class */ + protected $cmsummaryclass; + + /** @var section control menu output class */ + protected $controlclass; + + /** @var section availability output class */ + protected $availabilityclass; + + /** @var optional move here output class */ + protected $movehereclass; + + /** @var bool if the title is hidden for some reason */ + protected $hidetitle = false; + + /** @var bool if the title is hidden for some reason */ + protected $hidecontrols = false; + + + /** + * Constructor. + * + * @param course_format $format the course format + * @param section_info $thissection the section info + */ + public function __construct(course_format $format, section_info $thissection) { + $this->format = $format; + $this->thissection = $thissection; + + // Load output classes names from format. + $this->headerclass = $format->get_output_classname('section_format\\header'); + $this->cmlistclass = $format->get_output_classname('section_format\\cmlist'); + $this->summaryclass = $format->get_output_classname('section_format\\summary'); + $this->cmsummaryclass = $format->get_output_classname('section_format\\cmsummary'); + $this->controlmenuclass = $format->get_output_classname('section_format\\controlmenu'); + $this->availabilityclass = $format->get_output_classname('section_format\\availability'); + $this->movehereclass = $format->get_output_classname('section_format\\movehere'); + } + + /** + * Hide the section title. + * + * This is used on blocks or in the home page where an isolated section is displayed. + */ + public function hide_title(): void { + $this->hidetitle = true; + } + + /** + * Hide the section controls. + * + * This is used on blocks or in the home page where an isolated section is displayed. + */ + public function hide_controls(): void { + $this->hidecontrols = true; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return stdClass data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + + $format = $this->format; + $course = $format->get_course(); + $thissection = $this->thissection; + $singlesection = $format->get_section_number(); + + $summary = new $this->summaryclass($format, $thissection); + $availability = new $this->availabilityclass($format, $thissection); + + $data = (object)[ + 'num' => $thissection->section ?? '0', + 'id' => $thissection->id, + 'sectionreturnid' => $singlesection, + 'summary' => $summary->export_for_template($output), + 'availability' => $availability->export_for_template($output), + ]; + + // Check if it is a stealth sections (orphaned). + if ($thissection->section > $format->get_last_section_number()) { + $data->isstealth = true; + $data->ishidden = true; + } + + if ($format->show_editor()) { + if (empty($this->hidecontrols)) { + $controlmenu = new $this->controlmenuclass($format, $thissection); + $data->controlmenu = $controlmenu->export_for_template($output); + } + if (empty($data->isstealth)) { + $data->cmcontrols = $output->course_section_add_cm_control($course, $thissection->section, 0); + } + } + + if ($thissection->section == 0) { + // Section zero is always visible only as a cmlist. + $cmlist = new $this->cmlistclass($format, $thissection); + $data->cmlist = $cmlist->export_for_template($output); + + // Section 0 could have a completion help icon. + $completioninfo = new completion_info($course); + $data->completioninfo = $completioninfo->display_help_icon(); + + return $data; + } + + // When a section is displayed alone the title goes over the section, not inside it. + $header = new $this->headerclass($format, $thissection); + + if ($thissection->section == $singlesection) { + if (empty($this->hidetitle)) { + $data->singleheader = $header->export_for_template($output); + } + } else { + if (empty($this->hidetitle)) { + $data->header = $header->export_for_template($output); + } + + // Add activities summary if necessary. + if (!$format->show_editor() && $course->coursedisplay == COURSE_DISPLAY_MULTIPAGE) { + $cmsummary = new $this->cmsummaryclass($format, $thissection); + $data->cmsummary = $cmsummary->export_for_template($output); + + $data->onlysummary = true; + if (!$format->is_section_current($thissection)) { + // In multipage, only the current section (and the section zero) has elements. + return $data; + } + } + } + + // Add the cm list. + if ($thissection->uservisible) { + $cmlist = new $this->cmlistclass($format, $thissection); + $data->cmlist = $cmlist->export_for_template($output); + } + + if (!$thissection->visible) { + $data->ishidden = true; + } + if ($format->is_section_current($thissection)) { + $data->iscurrent = true; + $data->currentlink = get_accesshide( + get_string('currentsection', 'format_'.$format->get_format()) + ); + } + + return $data; + } +} + diff --git a/course/classes/output/section_format/availability.php b/course/classes/output/section_format/availability.php new file mode 100644 index 00000000000..bd10d886079 --- /dev/null +++ b/course/classes/output/section_format/availability.php @@ -0,0 +1,123 @@ +. + +/** + * Contains the default section availability output class. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\section_format; + +use core_course\course_format; +use section_info; +use renderable; +use templatable; +use core_availability\info_section; +use core_availability\info; +use context_course; +use stdClass; + +/** + * Base class to render section availability. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class availability implements renderable, templatable { + + /** @var course_format the course format class */ + protected $format; + + /** @var section_info the section object */ + protected $section; + + /** + * Constructor. + * + * @param course_format $format the course format + * @param section_info $section the section info + */ + public function __construct(course_format $format, section_info $section) { + $this->format = $format; + $this->section = $section; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * If section is not visible, display the message about that ('Not available + * until...', that sort of thing). Otherwise, returns blank. + * + * For users with the ability to view hidden sections, it shows the + * information even though you can view the section and also may include + * slightly fuller information (so that teachers can tell when sections + * are going to be unavailable etc). This logic is the same as for + * activities. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return stdclass data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + global $CFG, $USER; + + $format = $this->format; + $section = $this->section; + $course = $format->get_course(); + $context = context_course::instance($section->course); + + $canviewhidden = has_capability('moodle/course:viewhiddensections', $context, $USER); + + $info = ''; + if (!$section->visible) { + if ($canviewhidden) { + $info = $output->availability_info(get_string('hiddenfromstudents'), 'ishidden'); + } else { + // We are here because of the setting "Hidden sections are shown in collapsed form". + // Student can not see the section contents but can see its name. + $info = $output->availability_info(get_string('notavailable'), 'ishidden'); + } + } else if (!$section->uservisible) { + if ($section->availableinfo) { + // Note: We only get to this function if availableinfo is non-empty, + // so there is definitely something to print. + $formattedinfo = info::format_info($section->availableinfo, $section->course); + $info = $output->availability_info($formattedinfo, 'isrestricted'); + } + } else if ($canviewhidden && !empty($CFG->enableavailability)) { + // Check if there is an availability restriction. + $ci = new info_section($section); + $fullinfo = $ci->get_full_information(); + if ($fullinfo) { + $formattedinfo = info::format_info($fullinfo, $section->course); + $info = $output->availability_info($formattedinfo, 'isrestricted isfullinfo'); + } + } + + $data = (object)[ + 'info' => $info, + ]; + + if (!empty($info)) { + $data->hasavailability = true; + } + + return $data; + } +} diff --git a/course/classes/output/section_format/cmitem.php b/course/classes/output/section_format/cmitem.php new file mode 100644 index 00000000000..6a6bc5eb3a7 --- /dev/null +++ b/course/classes/output/section_format/cmitem.php @@ -0,0 +1,101 @@ +. + +/** + * Contains the default activity item from a section. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\section_format; + +use core_course\course_format; +use section_info; +use completion_info; +use renderable; +use templatable; +use cm_info; +use stdClass; + +/** + * Base class to render a section activity in the activities list. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cmitem implements renderable, templatable { + + /** @var course_format the course format class */ + protected $format; + + /** @var section_format the course section class */ + protected $section; + + /** @var cm_info the course module to display */ + protected $mod; + + /** @var array optional display options */ + protected $displayoptions; + + /** @var string the cm output class name */ + protected $cmclass; + + /** + * Constructor. + * + * @param course_format $format the course format + * @param section_info $section the section info + * @param cm_info $mod the course module ionfo + * @param array $displayoptions optional extra display options + */ + public function __construct(course_format $format, section_info $section, cm_info $mod, array $displayoptions = []) { + $this->format = $format; + $this->section = $section; + $this->mod = $mod; + $this->displayoptions = $displayoptions; + + // Get the necessary classes. + $this->cmclass = $format->get_output_classname('cm_format'); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return array data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + $format = $this->format; + $course = $format->get_course(); + $completioninfo = new completion_info($course); + $mod = $this->mod; + + $data = new stdClass(); + $data->cms = []; + + $item = new $this->cmclass($format, $this->section, $completioninfo, $mod, $this->displayoptions); + return (object)[ + 'id' => $mod->id, + 'module' => $mod->modname, + 'extraclasses' => $mod->extraclasses, + 'cmformat' => $item->export_for_template($output), + ]; + } +} + diff --git a/course/classes/output/section_format/cmlist.php b/course/classes/output/section_format/cmlist.php new file mode 100644 index 00000000000..97e47a01be1 --- /dev/null +++ b/course/classes/output/section_format/cmlist.php @@ -0,0 +1,132 @@ +. + +/** + * Contains the default activity list from a section. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\section_format; + +use core_course\course_format; +use section_info; +use renderable; +use templatable; +use moodle_url; +use stdClass; + +/** + * Base class to render a section activity list. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cmlist implements renderable, templatable { + + /** @var course_format the course format class */ + protected $format; + + /** @var section_format the course section class */ + protected $section; + + /** @var array optional display options */ + protected $displayoptions; + + /** @var string the item output class name */ + protected $itemclass; + + /** @var optional move here output class */ + protected $movehereclass; + + /** + * Constructor. + * + * @param course_format $format the course format + * @param section_info $section the section info + * @param array $displayoptions optional extra display options + */ + public function __construct(course_format $format, section_info $section, array $displayoptions = []) { + $this->format = $format; + $this->section = $section; + $this->displayoptions = $displayoptions; + + // Get the necessary classes. + $this->itemclass = $format->get_output_classname('section_format\cmitem'); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return array data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + global $USER; + + $format = $this->format; + $section = $this->section; + $course = $format->get_course(); + $modinfo = $format->get_modinfo(); + $user = $USER; + + $data = new stdClass(); + $data->cms = []; + + // By default, non-ajax controls are disabled but in some places like the frontpage + // it is necessary to display them. This is a temporal solution while JS is still + // optional for course editing. + $showmovehere = ismoving($course->id); + + if ($showmovehere) { + $data->hascms = true; + $data->showmovehere = true; + $data->strmovefull = strip_tags(get_string("movefull", "", "'$user->activitycopyname'")); + $data->movetosectionurl = new moodle_url('/course/mod.php', ['movetosection' => $section->id, 'sesskey' => sesskey()]); + $data->movingstr = strip_tags(get_string('activityclipboard', '', $user->activitycopyname)); + $data->cancelcopyurl = new moodle_url('/course/mod.php', ['cancelcopy' => 'true', 'sesskey' => sesskey()]); + } + + if (empty($modinfo->sections[$section->section])) { + return $data; + } + + foreach ($modinfo->sections[$section->section] as $modnumber) { + $mod = $modinfo->cms[$modnumber]; + // If the old non-ajax move is necessary, we do not print the selected cm. + if ($showmovehere && $USER->activitycopy == $mod->id) { + continue; + } + if ($mod->is_visible_on_course_page()) { + $item = new $this->itemclass($format, $section, $mod, $this->displayoptions); + $data->cms[] = (object)[ + 'cmitem' => $item->export_for_template($output), + 'moveurl' => new moodle_url('/course/mod.php', array('moveto' => $modnumber, 'sesskey' => sesskey())), + ]; + } + } + + if (!empty($data->cms)) { + $data->hascms = true; + } + + return $data; + } +} + diff --git a/course/classes/output/section_format/cmsummary.php b/course/classes/output/section_format/cmsummary.php new file mode 100644 index 00000000000..f4207264d78 --- /dev/null +++ b/course/classes/output/section_format/cmsummary.php @@ -0,0 +1,128 @@ +. + +/** + * Contains the default activities summary (used for singlesection format). + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\section_format; + +use core_course\course_format; +use section_info; +use completion_info; +use renderable; +use templatable; +use stdClass; + +/** + * Base class to render a course section summary. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cmsummary implements renderable, templatable { + + /** @var course_format the course format class */ + protected $format; + + /** @var section_info the course section class */ + protected $section; + + /** + * Constructor. + * + * @param course_format $format the course format + * @param section_info $section the section info + */ + public function __construct(course_format $format, section_info $section) { + $this->format = $format; + $this->section = $section; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return array data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + + list($mods, $complete, $total) = $this->calculate_section_stats(); + + if (empty($mods)) { + return new stdClass(); + } + + $data = (object)[ + 'total' => $total, + 'complete' => $complete, + 'mods' => array_values($mods), + ]; + + $data->modprogress = get_string('progresstotal', 'completion', $data); + + return $data; + } + + /** + * Calculate the activities count of the current section. + * + * @return array with [[count by activity type], completed activities, total of activitites] + */ + private function calculate_section_stats(): array { + $format = $this->format; + $course = $format->get_course(); + $section = $this->section; + $modinfo = $format->get_modinfo(); + $completioninfo = new completion_info($course); + + $mods = []; + $total = 0; + $complete = 0; + + $cmids = $modinfo->sections[$section->section] ?? []; + + $cancomplete = isloggedin() && !isguestuser(); + foreach ($cmids as $cmid) { + $thismod = $modinfo->cms[$cmid]; + + if ($thismod->uservisible) { + if (isset($mods[$thismod->modname])) { + $mods[$thismod->modname]['name'] = $thismod->modplural; + $mods[$thismod->modname]['count']++; + } else { + $mods[$thismod->modname]['name'] = $thismod->modfullname; + $mods[$thismod->modname]['count'] = 1; + } + if ($cancomplete && $completioninfo->is_enabled($thismod) != COMPLETION_TRACKING_NONE) { + $total++; + $completiondata = $completioninfo->get_data($thismod, true); + if ($completiondata->completionstate == COMPLETION_COMPLETE || + $completiondata->completionstate == COMPLETION_COMPLETE_PASS) { + $complete++; + } + } + } + } + + return [$mods, $complete, $total]; + } +} diff --git a/course/classes/output/section_format/controlmenu.php b/course/classes/output/section_format/controlmenu.php new file mode 100644 index 00000000000..4c1d97bbc08 --- /dev/null +++ b/course/classes/output/section_format/controlmenu.php @@ -0,0 +1,232 @@ +. + +/** + * Contains the default section controls output class. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\section_format; + +use core_course\course_format; +use section_info; +use renderable; +use templatable; +use context_course; +use moodle_url; +use pix_icon; +use action_menu_link_secondary; +use action_menu; +use stdClass; + +/** + * Base class to render section controls. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class controlmenu implements renderable, templatable { + + /** @var course_format the course format class */ + protected $format; + + /** @var section_info the course section class */ + protected $section; + + /** + * Constructor. + * + * @param course_format $format the course format + * @param section_info $section the section info + */ + public function __construct(course_format $format, section_info $section) { + $this->format = $format; + $this->section = $section; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return array data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + + $section = $this->section; + + $controls = $this->section_control_items(); + + if (empty($controls)) { + return new stdClass(); + } + + // Convert control array into an action_menu. + $menu = new action_menu(); + $menu->set_menu_trigger(get_string('edit')); + $menu->attributes['class'] .= ' section-actions'; + foreach ($controls as $value) { + $url = empty($value['url']) ? '' : $value['url']; + $icon = empty($value['icon']) ? '' : $value['icon']; + $name = empty($value['name']) ? '' : $value['name']; + $attr = empty($value['attr']) ? array() : $value['attr']; + $class = empty($value['pixattr']['class']) ? '' : $value['pixattr']['class']; + $al = new action_menu_link_secondary( + new moodle_url($url), + new pix_icon($icon, '', null, array('class' => "smallicon " . $class)), + $name, + $attr + ); + $menu->add($al); + } + + $data = (object)[ + 'menu' => $output->render($menu), + 'hasmenu' => true, + 'id' => $section->id, + ]; + + return $data; + } + + /** + * Generate the edit control items of a section. + * + * It is not clear this kind of controls are still available in 4.0 so, for now, this + * method is almost a clone of the previous section_control_items from the course/renderer.php. + * + * This method must remain public until the final deprecation of section_edit_control_items. + * + * @return array of edit control items + */ + public function section_control_items() { + global $USER; + + $format = $this->format; + $section = $this->section; + $course = $format->get_course(); + $sectionreturn = $format->get_section_number(); + $user = $USER; + + $coursecontext = context_course::instance($course->id); + $numsections = $format->get_last_section_number(); + $isstealth = $section->section > $numsections; + + $baseurl = course_get_url($course, $sectionreturn); + $baseurl->param('sesskey', sesskey()); + + $controls = []; + + if (!$isstealth && has_capability('moodle/course:update', $coursecontext, $user)) { + if ($section->section > 0 + && get_string_manager()->string_exists('editsection', 'format_'.$format->get_format())) { + $streditsection = get_string('editsection', 'format_'.$format->get_format()); + } else { + $streditsection = get_string('editsection'); + } + + $controls['edit'] = array( + 'url' => new moodle_url('/course/editsection.php', array('id' => $section->id, 'sr' => $sectionreturn)), + 'icon' => 'i/settings', + 'name' => $streditsection, + 'pixattr' => array('class' => ''), + 'attr' => array('class' => 'icon edit')); + } + + if ($section->section) { + $url = clone($baseurl); + if (!$isstealth) { + if (has_capability('moodle/course:sectionvisibility', $coursecontext, $user)) { + if ($section->visible) { // Show the hide/show eye. + $strhidefromothers = get_string('hidefromothers', 'format_'.$course->format); + $url->param('hide', $section->section); + $controls['visiblity'] = array( + 'url' => $url, + 'icon' => 'i/hide', + 'name' => $strhidefromothers, + 'pixattr' => array('class' => ''), + 'attr' => array('class' => 'icon editing_showhide', + 'data-sectionreturn' => $sectionreturn, 'data-action' => 'hide')); + } else { + $strshowfromothers = get_string('showfromothers', 'format_'.$course->format); + $url->param('show', $section->section); + $controls['visiblity'] = array( + 'url' => $url, + 'icon' => 'i/show', + 'name' => $strshowfromothers, + 'pixattr' => array('class' => ''), + 'attr' => array('class' => 'icon editing_showhide', + 'data-sectionreturn' => $sectionreturn, 'data-action' => 'show')); + } + } + + if (!$sectionreturn) { + if (has_capability('moodle/course:movesections', $coursecontext, $user)) { + $url = clone($baseurl); + if ($section->section > 1) { // Add a arrow to move section up. + $url->param('section', $section->section); + $url->param('move', -1); + $strmoveup = get_string('moveup'); + $controls['moveup'] = array( + 'url' => $url, + 'icon' => 'i/up', + 'name' => $strmoveup, + 'pixattr' => array('class' => ''), + 'attr' => array('class' => 'icon moveup')); + } + + $url = clone($baseurl); + if ($section->section < $numsections) { // Add a arrow to move section down. + $url->param('section', $section->section); + $url->param('move', 1); + $strmovedown = get_string('movedown'); + $controls['movedown'] = array( + 'url' => $url, + 'icon' => 'i/down', + 'name' => $strmovedown, + 'pixattr' => array('class' => ''), + 'attr' => array('class' => 'icon movedown')); + } + } + } + } + + if (course_can_delete_section($course, $section)) { + if (get_string_manager()->string_exists('deletesection', 'format_'.$course->format)) { + $strdelete = get_string('deletesection', 'format_'.$course->format); + } else { + $strdelete = get_string('deletesection'); + } + $url = new moodle_url('/course/editsection.php', array( + 'id' => $section->id, + 'sr' => $sectionreturn, + 'delete' => 1, + 'sesskey' => sesskey())); + $controls['delete'] = array( + 'url' => $url, + 'icon' => 'i/delete', + 'name' => $strdelete, + 'pixattr' => array('class' => ''), + 'attr' => array('class' => 'icon editing_delete')); + } + } + + return $controls; + } +} diff --git a/course/classes/output/section_format/header.php b/course/classes/output/section_format/header.php new file mode 100644 index 00000000000..c49a4d563d0 --- /dev/null +++ b/course/classes/output/section_format/header.php @@ -0,0 +1,99 @@ +. + +/** + * Contains the default section header format output class. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\section_format; + +use core_course\course_format; +use section_info; +use renderable; +use templatable; +use stdClass; + +/** + * Base class to render a section header. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class header implements renderable, templatable { + + /** @var course_format the course format class */ + protected $format; + + /** @var section_info the course section class */ + protected $section; + + /** + * Constructor. + * + * @param course_format $format the course format + * @param section_info $section the section info + */ + public function __construct(course_format $format, section_info $section) { + $this->format = $format; + $this->section = $section; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return array data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + + $format = $this->format; + $section = $this->section; + $course = $format->get_course(); + + $data = (object)[ + 'num' => $section->section, + 'id' => $section->id, + ]; + + if ($section->section > $format->get_last_section_number()) { + // Stealth sections (orphaned) has special title. + $data->title = get_string('orphanedactivitiesinsectionno', '', $section->section); + } else if ($section->section == $format->get_section_number()) { + // Regular section title. + $data->title = $output->section_title_without_link($section, $course); + $data->issinglesection = true; + } else { + // Regular section title. + $data->title = $output->section_title($section, $course); + } + + if (!$section->visible) { + $data->ishidden = true; + } + + if (!$format->show_editor() && $course->coursedisplay == COURSE_DISPLAY_MULTIPAGE && empty($data->issinglesection)) { + $data->url = course_get_url($course, $section->section); + $data->name = get_section_name($course, $section); + } + + return $data; + } +} diff --git a/course/classes/output/section_format/summary.php b/course/classes/output/section_format/summary.php new file mode 100644 index 00000000000..60bf6d3c73d --- /dev/null +++ b/course/classes/output/section_format/summary.php @@ -0,0 +1,94 @@ +. + +/** + * Contains the default section summary (used for multipage format). + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\output\section_format; + +use core_course\course_format; +use section_info; +use renderable; +use templatable; +use context_course; +use stdClass; + +/** + * Base class to render a course section summary. + * + * @package core_course + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class summary implements renderable, templatable { + + /** @var course_format the course format class */ + protected $format; + + /** @var section_info the course section class */ + private $section; + + /** + * Constructor. + * + * @param course_format $format the course format + * @param section_info $section the section info + */ + public function __construct(course_format $format, section_info $section) { + $this->format = $format; + $this->section = $section; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output typically, the renderer that's calling this function + * @return array data context for a mustache template + */ + public function export_for_template(\renderer_base $output): stdClass { + + $section = $this->section; + + $data = new stdClass(); + + if ($section->uservisible || $section->visible) { + $data->summarytext = $this->format_summary_text(); + } + return $data; + } + + /** + * Generate html for a section summary text + * + * @return string HTML to output. + */ + public function format_summary_text(): string { + $section = $this->section; + $context = context_course::instance($section->course); + $summarytext = file_rewrite_pluginfile_urls($section->summary, 'pluginfile.php', + $context->id, 'course', 'section', $section->id); + + $options = new stdClass(); + $options->noclean = true; + $options->overflowdiv = true; + return format_text($summarytext, $section->summaryformat, $options); + } +} diff --git a/course/dnduploadlib.php b/course/dnduploadlib.php index 11289b1b3f5..3feb01ff262 100644 --- a/course/dnduploadlib.php +++ b/course/dnduploadlib.php @@ -652,13 +652,13 @@ class dndupload_ajax_processor { $resp->error = self::ERROR_OK; $resp->elementid = 'module-' . $mod->id; - $courserenderer = $PAGE->get_renderer('core', 'course'); - $completioninfo = new completion_info($this->course); - $info = get_fast_modinfo($this->course); - $sr = null; - $modulehtml = $courserenderer->course_section_cm($this->course, $completioninfo, - $mod, null, array()); - $resp->fullcontent = $courserenderer->course_section_cm_list_item($this->course, $completioninfo, $mod, $sr); + $format = course_get_format($this->course); + $renderer = $format->get_renderer($PAGE); + $modinfo = $format->get_modinfo(); + $section = $modinfo->get_section_info($mod->sectionnum); + + // Get the new element html content. + $resp->fullcontent = $renderer->course_section_updated_cm_item($format, $section, $mod); echo $OUTPUT->header(); echo json_encode($resp); diff --git a/course/externallib.php b/course/externallib.php index 0e61c579ebb..a93d7734cfa 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -3521,8 +3521,11 @@ class core_course_external extends external_api { $modcontext = context_module::instance($cm->id); $coursecontext = context_course::instance($course->id); self::validate_context($modcontext); - $courserenderer = $PAGE->get_renderer('core', 'course'); - $completioninfo = new completion_info($course); + $format = course_get_format($course); + if ($sectionreturn) { + $format->set_section_number($sectionreturn); + } + $renderer = $format->get_renderer($PAGE); switch($action) { case 'hide': @@ -3542,10 +3545,15 @@ class core_course_external extends external_api { throw new moodle_exception('No permission to create that activity'); } if ($newcm = duplicate_module($course, $cm)) { - $cm = get_fast_modinfo($course)->get_cm($id); - $newcm = get_fast_modinfo($course)->get_cm($newcm->id); - return $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sectionreturn) . - $courserenderer->course_section_cm_list_item($course, $completioninfo, $newcm, $sectionreturn); + + $modinfo = $format->get_modinfo(); + $section = $modinfo->get_section_info($newcm->sectionnum); + $cm = $modinfo->get_cm($id); + + // Get both original and new element html. + $result = $renderer->course_section_updated_cm_item($format, $section, $cm); + $result .= $renderer->course_section_updated_cm_item($format, $section, $newcm); + return $result; } break; case 'groupsseparate': @@ -3580,8 +3588,10 @@ class core_course_external extends external_api { throw new coding_exception('Unrecognised action'); } - $cm = get_fast_modinfo($course)->get_cm($id); - return $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sectionreturn); + $modinfo = $format->get_modinfo(); + $section = $modinfo->get_section_info($cm->sectionnum); + $cm = $modinfo->get_cm($id); + return $renderer->course_section_updated_cm_item($format, $section, $cm); } /** @@ -3639,9 +3649,15 @@ class core_course_external extends external_api { list($course, $cm) = get_course_and_cm_from_cmid($id); self::validate_context(context_course::instance($course->id)); - $courserenderer = $PAGE->get_renderer('core', 'course'); - $completioninfo = new completion_info($course); - return $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sectionreturn); + $format = course_get_format($course); + if ($sectionreturn) { + $format->set_section_number($sectionreturn); + } + $renderer = $format->get_renderer($PAGE); + + $modinfo = $format->get_modinfo(); + $section = $modinfo->get_section_info($cm->sectionnum); + return $renderer->course_section_updated_cm_item($format, $section, $cm); } /** diff --git a/course/format/README.txt b/course/format/README.txt index 6705b5b6ca8..4e319331c67 100644 --- a/course/format/README.txt +++ b/course/format/README.txt @@ -9,6 +9,10 @@ below. If you want to store information in the database for your format, or control access to features of your format, you need some of the optional files too. +If you want to override some standard course output component (located in +coure/classes/output/{course|section|cm}_format/*) you need to create an +extend class inside your course/format/yourformat/classes/output. + All names below assume that your format is called 'yourformat'. @@ -139,3 +143,31 @@ Optional file (styles) If this file exists it will be included in the CSS Moodle generates. + +Optional files (outputs) +---------------------- + +By default, the format renderer will use those output classes: + +* core_course\output\course_format: for the general course structure +* core_course\output\course_format\*: to render specific course structure parts + +* core_course\output\section_format: for the complete section output +* core_course\output\section_format\*: to render specific section parts + +* core_course\output\cm_format: for output an activity inside a section +* core_course\output\cm_format\*: for speficis parts of the cm output + + Your format can override any of this output classes just by creating class + inside your format_yourformat\output\ classes. We recommend to extend the + original class to ensure all element will work as expected. + + For example: if you want to change the section header, you should create + format_yourformat\output\section_format\header, which will extend the original + core_course\output\section_format\header class. + + By default, only two format renderer methods are needed to render a course: + - render_course_format to render a full course + - course_section_updated_cm_item used by the course editor to refresh a specific cm item + + Formats can override those two methods to use different templates to render a course. diff --git a/course/format/lib.php b/course/format/lib.php index 40c880d5eb1..72bd6fb5015 100644 --- a/course/format/lib.php +++ b/course/format/lib.php @@ -24,1308 +24,17 @@ defined('MOODLE_INTERNAL') || die; +use core_course\course_format; + /** - * Returns an instance of format class (extending format_base) for given course + * Returns an instance of format class (extending course_format) for given course * * @param int|stdClass $courseorid either course id or * an object that has the property 'format' and may contain property 'id' - * @return format_base + * @return course_format */ function course_get_format($courseorid) { - return format_base::instance($courseorid); -} - -/** - * Base class for course formats - * - * Each course format must declare class - * class format_FORMATNAME extends format_base {} - * in file lib.php - * - * For each course just one instance of this class is created and it will always be returned by - * course_get_format($courseorid). Format may store it's specific course-dependent options in - * variables of this class. - * - * In rare cases instance of child class may be created just for format without course id - * i.e. to check if format supports AJAX. - * - * Also course formats may extend class section_info and overwrite - * format_base::build_section_cache() to return more information about sections. - * - * If you are upgrading from Moodle 2.3 start with copying the class format_legacy and renaming - * it to format_FORMATNAME, then move the code from your callback functions into - * appropriate functions of the class. - * - * @package core_course - * @copyright 2012 Marina Glancy - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -abstract class format_base { - /** @var int Id of the course in this instance (maybe 0) */ - protected $courseid; - /** @var string format used for this course. Please note that it can be different from - * course.format field if course referes to non-existing of disabled format */ - protected $format; - /** @var stdClass data for course object, please use {@link format_base::get_course()} */ - protected $course = false; - /** @var array caches format options, please use {@link format_base::get_format_options()} */ - protected $formatoptions = array(); - /** @var array cached instances */ - private static $instances = array(); - /** @var array plugin name => class name. */ - private static $classesforformat = array('site' => 'site'); - - /** - * Creates a new instance of class - * - * Please use {@link course_get_format($courseorid)} to get an instance of the format class - * - * @param string $format - * @param int $courseid - * @return format_base - */ - protected function __construct($format, $courseid) { - $this->format = $format; - $this->courseid = $courseid; - } - - /** - * Validates that course format exists and enabled and returns either itself or default format - * - * @param string $format - * @return string - */ - protected static final function get_format_or_default($format) { - global $CFG; - require_once($CFG->dirroot . '/course/lib.php'); - - if (array_key_exists($format, self::$classesforformat)) { - return self::$classesforformat[$format]; - } - - $plugins = get_sorted_course_formats(); - foreach ($plugins as $plugin) { - self::$classesforformat[$plugin] = $plugin; - } - - if (array_key_exists($format, self::$classesforformat)) { - return self::$classesforformat[$format]; - } - - if (PHPUNIT_TEST && class_exists('format_' . $format)) { - // Allow unittests to use non-existing course formats. - return $format; - } - - // Else return default format - $defaultformat = get_config('moodlecourse', 'format'); - if (!in_array($defaultformat, $plugins)) { - // when default format is not set correctly, use the first available format - $defaultformat = reset($plugins); - } - debugging('Format plugin format_'.$format.' is not found. Using default format_'.$defaultformat, DEBUG_DEVELOPER); - - self::$classesforformat[$format] = $defaultformat; - return $defaultformat; - } - - /** - * Get class name for the format - * - * If course format xxx does not declare class format_xxx, format_legacy will be returned. - * This function also includes lib.php file from corresponding format plugin - * - * @param string $format - * @return string - */ - protected static final function get_class_name($format) { - global $CFG; - static $classnames = array('site' => 'format_site'); - if (!isset($classnames[$format])) { - $plugins = core_component::get_plugin_list('format'); - $usedformat = self::get_format_or_default($format); - if (isset($plugins[$usedformat]) && file_exists($plugins[$usedformat].'/lib.php')) { - require_once($plugins[$usedformat].'/lib.php'); - } - $classnames[$format] = 'format_'. $usedformat; - if (!class_exists($classnames[$format])) { - require_once($CFG->dirroot.'/course/format/formatlegacy.php'); - $classnames[$format] = 'format_legacy'; - } - } - return $classnames[$format]; - } - - /** - * Returns an instance of the class - * - * @todo MDL-35727 use MUC for caching of instances, limit the number of cached instances - * - * @param int|stdClass $courseorid either course id or - * an object that has the property 'format' and may contain property 'id' - * @return format_base - */ - public static final function instance($courseorid) { - global $DB; - if (!is_object($courseorid)) { - $courseid = (int)$courseorid; - if ($courseid && isset(self::$instances[$courseid]) && count(self::$instances[$courseid]) == 1) { - $formats = array_keys(self::$instances[$courseid]); - $format = reset($formats); - } else { - $format = $DB->get_field('course', 'format', array('id' => $courseid), MUST_EXIST); - } - } else { - $format = $courseorid->format; - if (isset($courseorid->id)) { - $courseid = clean_param($courseorid->id, PARAM_INT); - } else { - $courseid = 0; - } - } - // validate that format exists and enabled, use default otherwise - $format = self::get_format_or_default($format); - if (!isset(self::$instances[$courseid][$format])) { - $classname = self::get_class_name($format); - self::$instances[$courseid][$format] = new $classname($format, $courseid); - } - return self::$instances[$courseid][$format]; - } - - /** - * Resets cache for the course (or all caches) - * To be called from {@link rebuild_course_cache()} - * - * @param int $courseid - */ - public static final function reset_course_cache($courseid = 0) { - if ($courseid) { - if (isset(self::$instances[$courseid])) { - foreach (self::$instances[$courseid] as $format => $object) { - // in case somebody keeps the reference to course format object - self::$instances[$courseid][$format]->course = false; - self::$instances[$courseid][$format]->formatoptions = array(); - } - unset(self::$instances[$courseid]); - } - } else { - self::$instances = array(); - } - } - - /** - * Returns the format name used by this course - * - * @return string - */ - public final function get_format() { - return $this->format; - } - - /** - * Returns id of the course (0 if course is not specified) - * - * @return int - */ - public final function get_courseid() { - return $this->courseid; - } - - /** - * Returns a record from course database table plus additional fields - * that course format defines - * - * @return stdClass - */ - public function get_course() { - global $DB; - if (!$this->courseid) { - return null; - } - if ($this->course === false) { - $this->course = get_course($this->courseid); - $options = $this->get_format_options(); - $dbcoursecolumns = null; - foreach ($options as $optionname => $optionvalue) { - if (isset($this->course->$optionname)) { - // Course format options must not have the same names as existing columns in db table "course". - if (!isset($dbcoursecolumns)) { - $dbcoursecolumns = $DB->get_columns('course'); - } - if (isset($dbcoursecolumns[$optionname])) { - debugging('The option name '.$optionname.' in course format '.$this->format. - ' is invalid because the field with the same name exists in {course} table', - DEBUG_DEVELOPER); - continue; - } - } - $this->course->$optionname = $optionvalue; - } - } - return $this->course; - } - - /** - * Method used in the rendered and during backup instead of legacy 'numsections' - * - * Default renderer will treat sections with sectionnumber greater that the value returned by this - * method as "orphaned" and not display them on the course page unless in editing mode. - * Backup will store this value as 'numsections'. - * - * This method ensures that 3rd party course format plugins that still use 'numsections' continue to - * work but at the same time we no longer expect formats to have 'numsections' property. - * - * @return int - */ - public function get_last_section_number() { - $course = $this->get_course(); - if (isset($course->numsections)) { - return $course->numsections; - } - $modinfo = get_fast_modinfo($course); - $sections = $modinfo->get_section_info_all(); - return (int)max(array_keys($sections)); - } - - /** - * Method used to get the maximum number of sections for this course format. - * @return int - */ - public function get_max_sections() { - $maxsections = get_config('moodlecourse', 'maxsections'); - if (!isset($maxsections) || !is_numeric($maxsections)) { - $maxsections = 52; - } - return $maxsections; - } - - /** - * Returns true if the course has a front page. - * - * This function is called to determine if the course has a view page, whether or not - * it contains a listing of activities. It can be useful to set this to false when the course - * format has only one activity and ignores the course page. Or if there are multiple - * activities but no page to see the centralised information. - * - * Initially this was created to know if forms should add a button to return to the course page. - * So if 'Return to course' does not make sense in your format your should probably return false. - * - * @return boolean - * @since Moodle 2.6 - */ - public function has_view_page() { - return true; - } - - /** - * Returns true if this course format uses sections - * - * This function may be called without specifying the course id - * i.e. in {@link course_format_uses_sections()} - * - * Developers, note that if course format does use sections there should be defined a language - * string with the name 'sectionname' defining what the section relates to in the format, i.e. - * $string['sectionname'] = 'Topic'; - * or - * $string['sectionname'] = 'Week'; - * - * @return bool - */ - public function uses_sections() { - return false; - } - - /** - * Returns a list of sections used in the course - * - * This is a shortcut to get_fast_modinfo()->get_section_info_all() - * @see get_fast_modinfo() - * @see course_modinfo::get_section_info_all() - * - * @return array of section_info objects - */ - public final function get_sections() { - if ($course = $this->get_course()) { - $modinfo = get_fast_modinfo($course); - return $modinfo->get_section_info_all(); - } - return array(); - } - - /** - * Returns information about section used in course - * - * @param int|stdClass $section either section number (field course_section.section) or row from course_section table - * @param int $strictness - * @return section_info - */ - public final function get_section($section, $strictness = IGNORE_MISSING) { - if (is_object($section)) { - $sectionnum = $section->section; - } else { - $sectionnum = $section; - } - $sections = $this->get_sections(); - if (array_key_exists($sectionnum, $sections)) { - return $sections[$sectionnum]; - } - if ($strictness == MUST_EXIST) { - throw new moodle_exception('sectionnotexist'); - } - return null; - } - - /** - * Returns the display name of the given section that the course prefers. - * - * @param int|stdClass $section Section object from database or just field course_sections.section - * @return Display name that the course format prefers, e.g. "Topic 2" - */ - public function get_section_name($section) { - if (is_object($section)) { - $sectionnum = $section->section; - } else { - $sectionnum = $section; - } - - if (get_string_manager()->string_exists('sectionname', 'format_' . $this->format)) { - return get_string('sectionname', 'format_' . $this->format) . ' ' . $sectionnum; - } - - // Return an empty string if there's no available section name string for the given format. - return ''; - } - - /** - * Returns the default section using format_base's implementation of get_section_name. - * - * @param int|stdClass $section Section object from database or just field course_sections section - * @return string The default value for the section name based on the given course format. - */ - public function get_default_section_name($section) { - return self::get_section_name($section); - } - - /** - * Returns the information about the ajax support in the given source format - * - * The returned object's property (boolean)capable indicates that - * the course format supports Moodle course ajax features. - * - * @return stdClass - */ - public function supports_ajax() { - // no support by default - $ajaxsupport = new stdClass(); - $ajaxsupport->capable = false; - return $ajaxsupport; - } - - /** - * Custom action after section has been moved in AJAX mode - * - * Used in course/rest.php - * - * @return array This will be passed in ajax respose - */ - public function ajax_section_move() { - return null; - } - - /** - * The URL to use for the specified course (with section) - * - * Please note that course view page /course/view.php?id=COURSEID is hardcoded in many - * places in core and contributed modules. If course format wants to change the location - * of the view script, it is not enough to change just this function. Do not forget - * to add proper redirection. - * - * @param int|stdClass $section Section object from database or just field course_sections.section - * if null the course view page is returned - * @param array $options options for view URL. At the moment core uses: - * 'navigation' (bool) if true and section has no separate page, the function returns null - * 'sr' (int) used by multipage formats to specify to which section to return - * @return null|moodle_url - */ - public function get_view_url($section, $options = array()) { - global $CFG; - $course = $this->get_course(); - $url = new moodle_url('/course/view.php', array('id' => $course->id)); - - if (array_key_exists('sr', $options)) { - $sectionno = $options['sr']; - } else if (is_object($section)) { - $sectionno = $section->section; - } else { - $sectionno = $section; - } - if (empty($CFG->linkcoursesections) && !empty($options['navigation']) && $sectionno !== null) { - // by default assume that sections are never displayed on separate pages - return null; - } - if ($this->uses_sections() && $sectionno !== null) { - $url->set_anchor('section-'.$sectionno); - } - return $url; - } - - /** - * Loads all of the course sections into the navigation - * - * This method is called from {@link global_navigation::load_course_sections()} - * - * By default the method {@link global_navigation::load_generic_course_sections()} is called - * - * When overwriting please note that navigationlib relies on using the correct values for - * arguments $type and $key in {@link navigation_node::add()} - * - * Example of code creating a section node: - * $sectionnode = $node->add($sectionname, $url, navigation_node::TYPE_SECTION, null, $section->id); - * $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH; - * - * Example of code creating an activity node: - * $activitynode = $sectionnode->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $activity->id, $icon); - * if (global_navigation::module_extends_navigation($activity->modname)) { - * $activitynode->nodetype = navigation_node::NODETYPE_BRANCH; - * } else { - * $activitynode->nodetype = navigation_node::NODETYPE_LEAF; - * } - * - * Also note that if $navigation->includesectionnum is not null, the section with this relative - * number needs is expected to be loaded - * - * @param global_navigation $navigation - * @param navigation_node $node The course node within the navigation - */ - public function extend_course_navigation($navigation, navigation_node $node) { - if ($course = $this->get_course()) { - $navigation->load_generic_course_sections($course, $node); - } - return array(); - } - - /** - * Returns the list of blocks to be automatically added for the newly created course - * - * @see blocks_add_default_course_blocks() - * - * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT - * each of values is an array of block names (for left and right side columns) - */ - public function get_default_blocks() { - global $CFG; - if (isset($CFG->defaultblocks)) { - return blocks_parse_default_blocks_list($CFG->defaultblocks); - } - $blocknames = array( - BLOCK_POS_LEFT => array(), - BLOCK_POS_RIGHT => array() - ); - return $blocknames; - } - - /** - * Returns the localised name of this course format plugin - * - * @return lang_string - */ - public final function get_format_name() { - return new lang_string('pluginname', 'format_'.$this->get_format()); - } - - /** - * Definitions of the additional options that this course format uses for course - * - * This function may be called often, it should be as fast as possible. - * Avoid using get_string() method, use "new lang_string()" instead - * It is not recommended to use dynamic or course-dependant expressions here - * This function may be also called when course does not exist yet. - * - * Option names must be different from fields in the {course} talbe or any form elements on - * course edit form, it may even make sence to use special prefix for them. - * - * Each option must have the option name as a key and the array of properties as a value: - * 'default' - default value for this option (assumed null if not specified) - * 'type' - type of the option value (PARAM_INT, PARAM_RAW, etc.) - * - * Additional properties used by default implementation of - * {@link format_base::create_edit_form_elements()} (calls this method with $foreditform = true) - * 'label' - localised human-readable label for the edit form - * 'element_type' - type of the form element, default 'text' - * 'element_attributes' - additional attributes for the form element, these are 4th and further - * arguments in the moodleform::addElement() method - * 'help' - string for help button. Note that if 'help' value is 'myoption' then the string with - * the name 'myoption_help' must exist in the language file - * 'help_component' - language component to look for help string, by default this the component - * for this course format - * - * This is an interface for creating simple form elements. If format plugin wants to use other - * methods such as disableIf, it can be done by overriding create_edit_form_elements(). - * - * Course format options can be accessed as: - * $this->get_course()->OPTIONNAME (inside the format class) - * course_get_format($course)->get_course()->OPTIONNAME (outside of format class) - * - * All course options are returned by calling: - * $this->get_format_options(); - * - * @param bool $foreditform - * @return array of options - */ - public function course_format_options($foreditform = false) { - return array(); - } - - /** - * Definitions of the additional options that this course format uses for section - * - * See {@link format_base::course_format_options()} for return array definition. - * - * Additionally section format options may have property 'cache' set to true - * if this option needs to be cached in {@link get_fast_modinfo()}. The 'cache' property - * is recommended to be set only for fields used in {@link format_base::get_section_name()}, - * {@link format_base::extend_course_navigation()} and {@link format_base::get_view_url()} - * - * For better performance cached options are recommended to have 'cachedefault' property - * Unlike 'default', 'cachedefault' should be static and not access get_config(). - * - * Regardless of value of 'cache' all options are accessed in the code as - * $sectioninfo->OPTIONNAME - * where $sectioninfo is instance of section_info, returned by - * get_fast_modinfo($course)->get_section_info($sectionnum) - * or get_fast_modinfo($course)->get_section_info_all() - * - * All format options for particular section are returned by calling: - * $this->get_format_options($section); - * - * @param bool $foreditform - * @return array - */ - public function section_format_options($foreditform = false) { - return array(); - } - - /** - * Returns the format options stored for this course or course section - * - * When overriding please note that this function is called from rebuild_course_cache() - * and section_info object, therefore using of get_fast_modinfo() and/or any function that - * accesses it may lead to recursion. - * - * @param null|int|stdClass|section_info $section if null the course format options will be returned - * otherwise options for specified section will be returned. This can be either - * section object or relative section number (field course_sections.section) - * @return array - */ - public function get_format_options($section = null) { - global $DB; - if ($section === null) { - $options = $this->course_format_options(); - } else { - $options = $this->section_format_options(); - } - if (empty($options)) { - // there are no option for course/sections anyway, no need to go further - return array(); - } - if ($section === null) { - // course format options will be returned - $sectionid = 0; - } else if ($this->courseid && isset($section->id)) { - // course section format options will be returned - $sectionid = $section->id; - } else if ($this->courseid && is_int($section) && - ($sectionobj = $DB->get_record('course_sections', - array('section' => $section, 'course' => $this->courseid), 'id'))) { - // course section format options will be returned - $sectionid = $sectionobj->id; - } else { - // non-existing (yet) section was passed as an argument - // default format options for course section will be returned - $sectionid = -1; - } - if (!array_key_exists($sectionid, $this->formatoptions)) { - $this->formatoptions[$sectionid] = array(); - // first fill with default values - foreach ($options as $optionname => $optionparams) { - $this->formatoptions[$sectionid][$optionname] = null; - if (array_key_exists('default', $optionparams)) { - $this->formatoptions[$sectionid][$optionname] = $optionparams['default']; - } - } - if ($this->courseid && $sectionid !== -1) { - // overwrite the default options values with those stored in course_format_options table - // nothing can be stored if we are interested in generic course ($this->courseid == 0) - // or generic section ($sectionid === 0) - $records = $DB->get_records('course_format_options', - array('courseid' => $this->courseid, - 'format' => $this->format, - 'sectionid' => $sectionid - ), '', 'id,name,value'); - foreach ($records as $record) { - if (array_key_exists($record->name, $this->formatoptions[$sectionid])) { - $value = $record->value; - if ($value !== null && isset($options[$record->name]['type'])) { - // this will convert string value to number if needed - $value = clean_param($value, $options[$record->name]['type']); - } - $this->formatoptions[$sectionid][$record->name] = $value; - } - } - } - } - return $this->formatoptions[$sectionid]; - } - - /** - * Adds format options elements to the course/section edit form - * - * This function is called from {@link course_edit_form::definition_after_data()} - * - * @param MoodleQuickForm $mform form the elements are added to - * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form - * @return array array of references to the added form elements - */ - public function create_edit_form_elements(&$mform, $forsection = false) { - $elements = array(); - if ($forsection) { - $options = $this->section_format_options(true); - } else { - $options = $this->course_format_options(true); - } - foreach ($options as $optionname => $option) { - if (!isset($option['element_type'])) { - $option['element_type'] = 'text'; - } - $args = array($option['element_type'], $optionname, $option['label']); - if (!empty($option['element_attributes'])) { - $args = array_merge($args, $option['element_attributes']); - } - $elements[] = call_user_func_array(array($mform, 'addElement'), $args); - if (isset($option['help'])) { - $helpcomponent = 'format_'. $this->get_format(); - if (isset($option['help_component'])) { - $helpcomponent = $option['help_component']; - } - $mform->addHelpButton($optionname, $option['help'], $helpcomponent); - } - if (isset($option['type'])) { - $mform->setType($optionname, $option['type']); - } - if (isset($option['default']) && !array_key_exists($optionname, $mform->_defaultValues)) { - // Set defaults for the elements in the form. - // Since we call this method after set_data() make sure that we don't override what was already set. - $mform->setDefault($optionname, $option['default']); - } - } - - if (!$forsection && empty($this->courseid)) { - // Check if course end date form field should be enabled by default. - // If a default date is provided to the form element, it is magically enabled by default in the - // MoodleQuickForm_date_time_selector class, otherwise it's disabled by default. - if (get_config('moodlecourse', 'courseenddateenabled')) { - // At this stage (this is called from definition_after_data) course data is already set as default. - // We can not overwrite what is in the database. - $mform->setDefault('enddate', $this->get_default_course_enddate($mform)); - } - } - - return $elements; - } - - /** - * Override if you need to perform some extra validation of the format options - * - * @param array $data array of ("fieldname"=>value) of submitted data - * @param array $files array of uploaded files "element_name"=>tmp_file_path - * @param array $errors errors already discovered in edit form validation - * @return array of "element_name"=>"error_description" if there are errors, - * or an empty array if everything is OK. - * Do not repeat errors from $errors param here - */ - public function edit_form_validation($data, $files, $errors) { - return array(); - } - - /** - * Prepares values of course or section format options before storing them in DB - * - * If an option has invalid value it is not returned - * - * @param array $rawdata associative array of the proposed course/section format options - * @param int|null $sectionid null if it is course format option - * @return array array of options that have valid values - */ - protected function validate_format_options(array $rawdata, int $sectionid = null) : array { - if (!$sectionid) { - $allformatoptions = $this->course_format_options(true); - } else { - $allformatoptions = $this->section_format_options(true); - } - $data = array_intersect_key($rawdata, $allformatoptions); - foreach ($data as $key => $value) { - $option = $allformatoptions[$key] + ['type' => PARAM_RAW, 'element_type' => null, 'element_attributes' => [[]]]; - $data[$key] = clean_param($value, $option['type']); - if ($option['element_type'] === 'select' && !array_key_exists($data[$key], $option['element_attributes'][0])) { - // Value invalid for select element, skip. - unset($data[$key]); - } - } - return $data; - } - - /** - * Validates format options for the course - * - * @param array $data data to insert/update - * @return array array of options that have valid values - */ - public function validate_course_format_options(array $data) : array { - return $this->validate_format_options($data); - } - - /** - * Updates format options for a course or section - * - * If $data does not contain property with the option name, the option will not be updated - * - * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data - * @param null|int null if these are options for course or section id (course_sections.id) - * if these are options for section - * @return bool whether there were any changes to the options values - */ - protected function update_format_options($data, $sectionid = null) { - global $DB; - $data = $this->validate_format_options((array)$data, $sectionid); - if (!$sectionid) { - $allformatoptions = $this->course_format_options(); - $sectionid = 0; - } else { - $allformatoptions = $this->section_format_options(); - } - if (empty($allformatoptions)) { - // nothing to update anyway - return false; - } - $defaultoptions = array(); - $cached = array(); - foreach ($allformatoptions as $key => $option) { - $defaultoptions[$key] = null; - if (array_key_exists('default', $option)) { - $defaultoptions[$key] = $option['default']; - } - $cached[$key] = ($sectionid === 0 || !empty($option['cache'])); - } - $records = $DB->get_records('course_format_options', - array('courseid' => $this->courseid, - 'format' => $this->format, - 'sectionid' => $sectionid - ), '', 'name,id,value'); - $changed = $needrebuild = false; - foreach ($defaultoptions as $key => $value) { - if (isset($records[$key])) { - if (array_key_exists($key, $data) && $records[$key]->value !== $data[$key]) { - $DB->set_field('course_format_options', 'value', - $data[$key], array('id' => $records[$key]->id)); - $changed = true; - $needrebuild = $needrebuild || $cached[$key]; - } - } else { - if (array_key_exists($key, $data) && $data[$key] !== $value) { - $newvalue = $data[$key]; - $changed = true; - $needrebuild = $needrebuild || $cached[$key]; - } else { - $newvalue = $value; - // we still insert entry in DB but there are no changes from user point of - // view and no need to call rebuild_course_cache() - } - $DB->insert_record('course_format_options', array( - 'courseid' => $this->courseid, - 'format' => $this->format, - 'sectionid' => $sectionid, - 'name' => $key, - 'value' => $newvalue - )); - } - } - if ($needrebuild) { - rebuild_course_cache($this->courseid, true); - } - if ($changed) { - // reset internal caches - if (!$sectionid) { - $this->course = false; - } - unset($this->formatoptions[$sectionid]); - } - return $changed; - } - - /** - * Updates format options for a course - * - * If $data does not contain property with the option name, the option will not be updated - * - * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data - * @param stdClass $oldcourse if this function is called from {@link update_course()} - * this object contains information about the course before update - * @return bool whether there were any changes to the options values - */ - public function update_course_format_options($data, $oldcourse = null) { - return $this->update_format_options($data); - } - - /** - * Updates format options for a section - * - * Section id is expected in $data->id (or $data['id']) - * If $data does not contain property with the option name, the option will not be updated - * - * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data - * @return bool whether there were any changes to the options values - */ - public function update_section_format_options($data) { - $data = (array)$data; - return $this->update_format_options($data, $data['id']); - } - - /** - * Return an instance of moodleform to edit a specified section - * - * Default implementation returns instance of editsection_form that automatically adds - * additional fields defined in {@link format_base::section_format_options()} - * - * Format plugins may extend editsection_form if they want to have custom edit section form. - * - * @param mixed $action the action attribute for the form. If empty defaults to auto detect the - * current url. If a moodle_url object then outputs params as hidden variables. - * @param array $customdata the array with custom data to be passed to the form - * /course/editsection.php passes section_info object in 'cs' field - * for filling availability fields - * @return moodleform - */ - public function editsection_form($action, $customdata = array()) { - global $CFG; - require_once($CFG->dirroot. '/course/editsection_form.php'); - $context = context_course::instance($this->courseid); - if (!array_key_exists('course', $customdata)) { - $customdata['course'] = $this->get_course(); - } - return new editsection_form($action, $customdata); - } - - /** - * Allows course format to execute code on moodle_page::set_course() - * - * @param moodle_page $page instance of page calling set_course - */ - public function page_set_course(moodle_page $page) { - } - - /** - * Allows course format to execute code on moodle_page::set_cm() - * - * Current module can be accessed as $page->cm (returns instance of cm_info) - * - * @param moodle_page $page instance of page calling set_cm - */ - public function page_set_cm(moodle_page $page) { - } - - /** - * Course-specific information to be output on any course page (usually above navigation bar) - * - * Example of usage: - * define - * class format_FORMATNAME_XXX implements renderable {} - * - * create format renderer in course/format/FORMATNAME/renderer.php, define rendering function: - * class format_FORMATNAME_renderer extends plugin_renderer_base { - * protected function render_format_FORMATNAME_XXX(format_FORMATNAME_XXX $xxx) { - * return html_writer::tag('div', 'This is my header/footer'); - * } - * } - * - * Return instance of format_FORMATNAME_XXX in this function, the appropriate method from - * plugin renderer will be called - * - * @return null|renderable null for no output or object with data for plugin renderer - */ - public function course_header() { - return null; - } - - /** - * Course-specific information to be output on any course page (usually in the beginning of - * standard footer) - * - * See {@link format_base::course_header()} for usage - * - * @return null|renderable null for no output or object with data for plugin renderer - */ - public function course_footer() { - return null; - } - - /** - * Course-specific information to be output immediately above content on any course page - * - * See {@link format_base::course_header()} for usage - * - * @return null|renderable null for no output or object with data for plugin renderer - */ - public function course_content_header() { - return null; - } - - /** - * Course-specific information to be output immediately below content on any course page - * - * See {@link format_base::course_header()} for usage - * - * @return null|renderable null for no output or object with data for plugin renderer - */ - public function course_content_footer() { - return null; - } - - /** - * Returns instance of page renderer used by this plugin - * - * @param moodle_page $page - * @return renderer_base - */ - public function get_renderer(moodle_page $page) { - return $page->get_renderer('format_'. $this->get_format()); - } - - /** - * Returns true if the specified section is current - * - * By default we analyze $course->marker - * - * @param int|stdClass|section_info $section - * @return bool - */ - public function is_section_current($section) { - if (is_object($section)) { - $sectionnum = $section->section; - } else { - $sectionnum = $section; - } - return ($sectionnum && ($course = $this->get_course()) && $course->marker == $sectionnum); - } - - /** - * Allows to specify for modinfo that section is not available even when it is visible and conditionally available. - * - * Note: affected user can be retrieved as: $section->modinfo->userid - * - * Course format plugins can override the method to change the properties $available and $availableinfo that were - * calculated by conditional availability. - * To make section unavailable set: - * $available = false; - * To make unavailable section completely hidden set: - * $availableinfo = ''; - * To make unavailable section visible with availability message set: - * $availableinfo = get_string('sectionhidden', 'format_xxx'); - * - * @param section_info $section - * @param bool $available the 'available' propery of the section_info as it was evaluated by conditional availability. - * Can be changed by the method but 'false' can not be overridden by 'true'. - * @param string $availableinfo the 'availableinfo' propery of the section_info as it was evaluated by conditional availability. - * Can be changed by the method - */ - public function section_get_available_hook(section_info $section, &$available, &$availableinfo) { - } - - /** - * Whether this format allows to delete sections - * - * If format supports deleting sections it is also recommended to define language string - * 'deletesection' inside the format. - * - * Do not call this function directly, instead use {@link course_can_delete_section()} - * - * @param int|stdClass|section_info $section - * @return bool - */ - public function can_delete_section($section) { - return false; - } - - /** - * Deletes a section - * - * Do not call this function directly, instead call {@link course_delete_section()} - * - * @param int|stdClass|section_info $section - * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it. - * @return bool whether section was deleted - */ - public function delete_section($section, $forcedeleteifnotempty = false) { - global $DB; - if (!$this->uses_sections()) { - // Not possible to delete section if sections are not used. - return false; - } - if (!is_object($section)) { - $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section), - 'id,section,sequence,summary'); - } - if (!$section || !$section->section) { - // Not possible to delete 0-section. - return false; - } - - if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) { - return false; - } - - $course = $this->get_course(); - - // Remove the marker if it points to this section. - if ($section->section == $course->marker) { - course_set_marker($course->id, 0); - } - - $lastsection = $DB->get_field_sql('SELECT max(section) from {course_sections} - WHERE course = ?', array($course->id)); - - // Find out if we need to descrease the 'numsections' property later. - $courseformathasnumsections = array_key_exists('numsections', - $this->get_format_options()); - $decreasenumsections = $courseformathasnumsections && ($section->section <= $course->numsections); - - // Move the section to the end. - move_section_to($course, $section->section, $lastsection, true); - - // Delete all modules from the section. - foreach (preg_split('/,/', $section->sequence, -1, PREG_SPLIT_NO_EMPTY) as $cmid) { - course_delete_module($cmid); - } - - // Delete section and it's format options. - $DB->delete_records('course_format_options', array('sectionid' => $section->id)); - $DB->delete_records('course_sections', array('id' => $section->id)); - rebuild_course_cache($course->id, true); - - // Delete section summary files. - $context = \context_course::instance($course->id); - $fs = get_file_storage(); - $fs->delete_area_files($context->id, 'course', 'section', $section->id); - - // Descrease 'numsections' if needed. - if ($decreasenumsections) { - $this->update_course_format_options(array('numsections' => $course->numsections - 1)); - } - - return true; - } - - /** - * Prepares the templateable object to display section name - * - * @param \section_info|\stdClass $section - * @param bool $linkifneeded - * @param bool $editable - * @param null|lang_string|string $edithint - * @param null|lang_string|string $editlabel - * @return \core\output\inplace_editable - */ - public function inplace_editable_render_section_name($section, $linkifneeded = true, - $editable = null, $edithint = null, $editlabel = null) { - global $USER, $CFG; - require_once($CFG->dirroot.'/course/lib.php'); - - if ($editable === null) { - $editable = !empty($USER->editing) && has_capability('moodle/course:update', - context_course::instance($section->course)); - } - - $displayvalue = $title = get_section_name($section->course, $section); - if ($linkifneeded) { - // Display link under the section name if the course format setting is to display one section per page. - $url = course_get_url($section->course, $section->section, array('navigation' => true)); - if ($url) { - $displayvalue = html_writer::link($url, $title); - } - $itemtype = 'sectionname'; - } else { - // If $linkifneeded==false, we never display the link (this is used when rendering the section header). - // Itemtype 'sectionnamenl' (nl=no link) will tell the callback that link should not be rendered - - // there is no other way callback can know where we display the section name. - $itemtype = 'sectionnamenl'; - } - if (empty($edithint)) { - $edithint = new lang_string('editsectionname'); - } - if (empty($editlabel)) { - $editlabel = new lang_string('newsectionname', '', $title); - } - - return new \core\output\inplace_editable('format_' . $this->format, $itemtype, $section->id, $editable, - $displayvalue, $section->name, $edithint, $editlabel); - } - - /** - * Updates the value in the database and modifies this object respectively. - * - * ALWAYS check user permissions before performing an update! Throw exceptions if permissions are not sufficient - * or value is not legit. - * - * @param stdClass $section - * @param string $itemtype - * @param mixed $newvalue - * @return \core\output\inplace_editable - */ - public function inplace_editable_update_section_name($section, $itemtype, $newvalue) { - if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') { - $context = context_course::instance($section->course); - external_api::validate_context($context); - require_capability('moodle/course:update', $context); - - $newtitle = clean_param($newvalue, PARAM_TEXT); - if (strval($section->name) !== strval($newtitle)) { - course_update_section($section->course, $section, array('name' => $newtitle)); - } - return $this->inplace_editable_render_section_name($section, ($itemtype === 'sectionname'), true); - } - } - - - /** - * Returns the default end date value based on the start date. - * - * This is the default implementation for course formats, it is based on - * moodlecourse/courseduration setting. Course formats like format_weeks for - * example can overwrite this method and return a value based on their internal options. - * - * @param moodleform $mform - * @param array $fieldnames The form - field names mapping. - * @return int - */ - public function get_default_course_enddate($mform, $fieldnames = array()) { - - if (empty($fieldnames)) { - $fieldnames = array('startdate' => 'startdate'); - } - - $startdate = $this->get_form_start_date($mform, $fieldnames); - $courseduration = intval(get_config('moodlecourse', 'courseduration')); - if (!$courseduration) { - // Default, it should be already set during upgrade though. - $courseduration = YEARSECS; - } - - return $startdate + $courseduration; - } - - /** - * Indicates whether the course format supports the creation of the Announcements forum. - * - * For course format plugin developers, please override this to return true if you want the Announcements forum - * to be created upon course creation. - * - * @return bool - */ - public function supports_news() { - // For backwards compatibility, check if default blocks include the news_items block. - $defaultblocks = $this->get_default_blocks(); - foreach ($defaultblocks as $blocks) { - if (in_array('news_items', $blocks)) { - return true; - } - } - // Return false by default. - return false; - } - - /** - * Get the start date value from the course settings page form. - * - * @param moodleform $mform - * @param array $fieldnames The form - field names mapping. - * @return int - */ - protected function get_form_start_date($mform, $fieldnames) { - $startdate = $mform->getElementValue($fieldnames['startdate']); - return $mform->getElement($fieldnames['startdate'])->exportValue($startdate); - } - - /** - * Returns whether this course format allows the activity to - * have "triple visibility state" - visible always, hidden on course page but available, hidden. - * - * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module) - * @param stdClass|section_info $section section where this module is located or will be added to - * @return bool - */ - public function allow_stealth_module_visibility($cm, $section) { - return false; - } - - /** - * Callback used in WS core_course_edit_section when teacher performs an AJAX action on a section (show/hide) - * - * Access to the course is already validated in the WS but the callback has to make sure - * that particular action is allowed by checking capabilities - * - * Course formats should register - * - * @param stdClass|section_info $section - * @param string $action - * @param int $sr - * @return null|array|stdClass any data for the Javascript post-processor (must be json-encodeable) - */ - public function section_action($section, $action, $sr) { - global $PAGE; - if (!$this->uses_sections() || !$section->section) { - // No section actions are allowed if course format does not support sections. - // No actions are allowed on the 0-section by default (overwrite in course format if needed). - throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action)); - } - - $course = $this->get_course(); - $coursecontext = context_course::instance($course->id); - switch($action) { - case 'hide': - case 'show': - require_capability('moodle/course:sectionvisibility', $coursecontext); - $visible = ($action === 'hide') ? 0 : 1; - course_update_section($course, $section, array('visible' => $visible)); - break; - default: - throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action)); - } - - $modules = []; - - $modinfo = get_fast_modinfo($course); - $coursesections = $modinfo->sections; - if (array_key_exists($section->section, $coursesections)) { - $courserenderer = $PAGE->get_renderer('core', 'course'); - $completioninfo = new completion_info($course); - foreach ($coursesections[$section->section] as $cmid) { - $cm = $modinfo->get_cm($cmid); - $modules[] = $courserenderer->course_section_cm_list_item($course, $completioninfo, $cm, $sr); - } - } - - return ['modules' => $modules]; - } - - /** - * Return the plugin config settings for external functions, - * in some cases the configs will need formatting or be returned only if the current user has some capabilities enabled. - * - * @return array the list of configs - * @since Moodle 3.5 - */ - public function get_config_for_external() { - return array(); - } + return course_format::instance($courseorid); } /** @@ -1335,7 +44,7 @@ abstract class format_base { * @copyright 2012 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class format_site extends format_base { +class format_site extends course_format { /** * Returns the display name of the given section that the course prefers. @@ -1399,4 +108,27 @@ class format_site extends format_base { public function allow_stealth_module_visibility($cm, $section) { return true; } + + /** + * Returns instance of page renderer used by the site page + * + * @param moodle_page $page the current page + * @return renderer_base + */ + public function get_renderer(moodle_page $page) { + global $CFG; + if (!class_exists('format_site_renderer')) { + require_once($CFG->dirroot.'/course/format/renderer.php'); + } + return new format_site_renderer($page, null); + } + + /** + * Site format uses only section 1. + * + * @return int + */ + public function get_section_number(): int { + return 1; + } } diff --git a/course/format/renderer.php b/course/format/renderer.php index b5884c36c5b..3fe14315bf8 100644 --- a/course/format/renderer.php +++ b/course/format/renderer.php @@ -25,6 +25,10 @@ defined('MOODLE_INTERNAL') || die(); +use core_course\course_format; + +global $CFG; +require_once($CFG->dirroot. '/course/renderer.php'); /** * This is a convenience renderer which can be used by section based formats @@ -36,13 +40,22 @@ defined('MOODLE_INTERNAL') || die(); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.3 */ -abstract class format_section_renderer_base extends plugin_renderer_base { +abstract class format_section_renderer_base extends core_course_renderer { - /** @var core_course_renderer contains instance of core course renderer */ + /** + * @var core_course_renderer contains an instance of core course renderer + * @deprecated since 4.0 - use $this to access course renderer methods + */ protected $courserenderer; /** - * Constructor method, calls the parent constructor + * Constructor method, calls the parent constructor. + * + * @deprecated since 4.0 + * + * Note: this method exists only for compatibilitiy with legacy course formats. Legacy formats + * depends on $this->courserenderer to access the course renderer methods. Since Moodle 4.0 + * format_section_renderer_base extends core_course_renderer and all metdhos can be used directly from $this. * * @param moodle_page $page * @param string $target one of rendering target constants @@ -53,22 +66,46 @@ abstract class format_section_renderer_base extends plugin_renderer_base { } /** - * Generate the starting container html for a list of sections - * @return string HTML to output. + * Renders the provided widget and returns the HTML to display it. + * + * Course format templates uses a similar subfolder structure to the renderable classes. + * This method find out the specific template for a course widget. That's the reason why + * this render method is different from the normal plugin renderer one. + * + * course format templatables can be rendered using the core_course/local/* templates. + * Format plugins are free to override the default template location using render_xxx methods as usual. + * + * @param renderable $widget instance with renderable interface + * @return string the widget HTML */ - abstract protected function start_section_list(); + public function render(renderable $widget) { + global $CFG; + $fullpath = str_replace('\\', '/', get_class($widget)); + $classparts = explode('/', $fullpath); + // Strip namespaces. + $classname = array_pop($classparts); + // Remove _renderable suffixes. + $classname = preg_replace('/_renderable$/', '', $classname); - /** - * Generate the closing container html for a list of sections - * @return string HTML to output. - */ - abstract protected function end_section_list(); + $rendermethod = 'render_' . $classname; + if (method_exists($this, $rendermethod)) { + return $this->$rendermethod($widget); + } + // Check for special course format templatables. + if ($widget instanceof templatable) { + // Templatables from both core_course\output\xxx_format\* and format_xxx\output\xxx_format\* + // use core_crouse/local/xxx_format templates by default. + $specialrenderers = '/^(?core_course|format_.+)\/output\/(?