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\/(?