From 23856b108683e2f72973e0d61435a22155ff40c0 Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Fri, 11 Feb 2022 16:16:45 +0100 Subject: [PATCH 1/2] MDL-73762 core_courseformat: refactor cm and section outputs --- .../output/local/content/addsection.php | 137 +++++---- .../classes/output/local/content/cm.php | 284 +++++++++++------- .../output/local/content/cm/availability.php | 135 +++++---- .../classes/output/local/content/section.php | 281 +++++++++++------ .../local/content/section/availability.php | 39 ++- lib/modinfolib.php | 31 ++ mod/label/lib.php | 9 + 7 files changed, 596 insertions(+), 320 deletions(-) diff --git a/course/format/classes/output/local/content/addsection.php b/course/format/classes/output/local/content/addsection.php index e7ba7f1aef8..6c1c5e38f15 100644 --- a/course/format/classes/output/local/content/addsection.php +++ b/course/format/classes/output/local/content/addsection.php @@ -54,11 +54,16 @@ class addsection implements renderable, templatable { /** * 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 + * @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 { + // If no editor must be displayed, just return an empty structure. + if (!$this->format->show_editor()) { + return new stdClass(); + } + $format = $this->format; $course = $format->get_course(); $options = $format->get_format_options(); @@ -66,63 +71,14 @@ class addsection implements renderable, templatable { $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; - } - // Component based formats handle add section button in the frontend. - $show = ($lastsection < $maxsections) || course_get_format($course)->supports_components(); + $show = ($lastsection < $maxsections) || $format->supports_components(); $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()] - ), - ]; - } - + $data = $this->get_num_sections_data($output, $lastsection, $maxsections); } else if (course_get_format($course)->uses_sections() && $show) { - // 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, - ]; + $data = $this->get_add_section_data($output, $lastsection, $maxsections); } if (count((array)$data)) { @@ -131,4 +87,79 @@ class addsection implements renderable, templatable { return $data; } + + /** + * Get the legacy num section add/remove section buttons data. + * + * 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). + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @param int $lastsection the last section number + * @param int $maxsections the maximum number of sections + * @return stdClass data context for a mustache template + */ + protected function get_num_sections_data(\renderer_base $output, int $lastsection, int $maxsections): stdClass { + $format = $this->format; + $course = $format->get_course(); + $data = new stdClass(); + + 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()] + ), + ]; + } + return $data; + } + + /** + * Get the add section button data. + * + * 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'. + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @param int $lastsection the last section number + * @param int $maxsections the maximum number of sections + * @return stdClass data context for a mustache template + */ + protected function get_add_section_data(\renderer_base $output, int $lastsection, int $maxsections): stdClass { + $format = $this->format; + $course = $format->get_course(); + $data = new stdClass(); + + 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, + ]; + return $data; + } } diff --git a/course/format/classes/output/local/content/cm.php b/course/format/classes/output/local/content/cm.php index 919053d66a1..05c671a6d00 100644 --- a/course/format/classes/output/local/content/cm.php +++ b/course/format/classes/output/local/content/cm.php @@ -25,16 +25,16 @@ namespace core_courseformat\output\local\content; use cm_info; -use context_module; use core\activity_dates; +use core_availability\info_module; use core_completion\cm_completion_details; use core_courseformat\base as course_format; use core_course\output\activity_information; use renderable; +use renderer_base; use section_info; use stdClass; use templatable; -use \core_availability\info_module; /** * Base class to render a course module inside a course format. @@ -84,9 +84,12 @@ class cm implements renderable, templatable { $this->format = $format; $this->section = $section; $this->mod = $mod; - $this->displayoptions = $displayoptions; + // Add extra display options. $this->load_classes(); + $displayoptions['linkclasses'] = $this->get_link_classes(); + $displayoptions['textclasses'] = $this->get_text_classes(); + $this->displayoptions = $displayoptions; // Get the necessary classes. $this->cmnameclass = $format->get_output_classname('content\\cm\\cmname'); @@ -97,138 +100,206 @@ class cm implements renderable, templatable { /** * 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 + * @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; - $section = $this->section; + public function export_for_template(renderer_base $output): stdClass { $mod = $this->mod; $displayoptions = $this->displayoptions; - $course = $mod->get_course(); - - // Fetch completion details. - $showcompletionconditions = $course->showcompletionconditions == COMPLETION_SHOW_CONDITIONS; - $completiondetails = cm_completion_details::get_instance($mod, $USER->id, $showcompletionconditions); - - // Fetch activity dates. - $activitydates = []; - if ($course->showactivitydates) { - $activitydates = activity_dates::get_dates_for_module($mod, $USER->id); - } - - $displayoptions['linkclasses'] = $this->get_link_classes(); - $displayoptions['textclasses'] = $this->get_text_classes(); - - // Grouping activity. - $groupinglabel = $mod->get_grouping_label($displayoptions['textclasses']); - - $activityinfodata = (object) ['hasdates' => false, 'hascompletion' => false]; - // - There are activity dates to be shown; or - // - Completion info needs to be displayed - // * The activity tracks completion; AND - // * The showcompletionconditions setting is enabled OR an activity that tracks manual completion needs the manual - // completion button to be displayed on the course homepage. - $showcompletioninfo = $completiondetails->has_completion() && ($showcompletionconditions || - (!$completiondetails->is_automatic() && $completiondetails->show_manual_completion())); - if ($showcompletioninfo || !empty($activitydates)) { - $activityinfo = new activity_information($mod, $completiondetails, $activitydates); - $activityinfodata = $activityinfo->export_for_template($output); - } - - // Mod inplace name editable. - $cmname = new $this->cmnameclass( - $format, - $this->section, - $mod, - $format->show_editor(), - $this->displayoptions - ); - - // Mod availability. - $availability = new $this->availabilityclass( - $format, - $this->section, - $mod, - $this->displayoptions - ); - - $modavailability = $availability->export_for_template($output); $data = (object)[ - 'cmname' => $cmname->export_for_template($output), - 'grouping' => $groupinglabel, - 'afterlink' => $mod->afterlink, - 'altcontent' => $mod->get_formatted_content(['overflowdiv' => true, 'noclean' => true]), - 'modavailability' => $mod->visible ? $modavailability : null, + 'grouping' => $mod->get_grouping_label($displayoptions['textclasses']), 'modname' => get_string('pluginname', 'mod_' . $mod->modname), 'url' => $mod->url, - 'activityinfo' => $activityinfodata, 'activityname' => $mod->get_formatted_name(), 'textclasses' => $displayoptions['textclasses'], - 'classlist' => [] + 'classlist' => [], ]; - if (!empty($mod->indent) && $format->uses_indentation()) { - $data->indent = $mod->indent; - if ($mod->indent > 15) { - $data->hugeindent = true; - } - } - - $data->altcontent = (empty($data->altcontent)) ? false : $data->altcontent; - - $data->hasname = !empty($data->cmname['displayvalue']); + // Add partial data segments. + $haspartials = []; + $haspartials['cmname'] = $this->add_cm_name_data($data, $output); + $haspartials['availability'] = $this->add_availability_data($data, $output); + $haspartials['alternative'] = $this->add_alternative_content_data($data, $output); + $haspartials['completion'] = $this->add_completion_data($data, $output); + $haspartials['editor'] = $this->add_editor_data($data, $output); + $this->add_format_data($data, $haspartials, $output); + // Calculated fields. if (!empty($data->url)) { $data->hasurl = true; } - if (!$mod->visible) { + return $data; + } + + /** + * Add course module name attributes to the data structure. + * + * @param stdClass $data the current cm data reference + * @param renderer_base $output typically, the renderer that's calling this function + * @return bool if the cm has name data + */ + protected function add_cm_name_data(stdClass &$data, renderer_base $output): bool { + // Mod inplace name editable. + $cmname = new $this->cmnameclass( + $this->format, + $this->section, + $this->mod, + $this->format->show_editor(), + $this->displayoptions + ); + $data->cmname = $cmname->export_for_template($output); + $data->hasname = !empty($data->cmname['displayvalue']); + return $data->hasname; + } + + /** + * Add the module availability to the data structure. + * + * @param stdClass $data the current cm data reference + * @param renderer_base $output typically, the renderer that's calling this function + * @return bool if the cm has mod availability + */ + protected function add_availability_data(stdClass &$data, renderer_base $output): bool { + if (!$this->mod->visible) { + $data->modavailability = null; + return false; + } + // Mod availability output class. + $availability = new $this->availabilityclass( + $this->format, + $this->section, + $this->mod, + $this->displayoptions + ); + $modavailability = $availability->export_for_template($output); + $data->modavailability = $modavailability; + return $availability->has_availability($output); + } + + /** + * Add the alternative content to the data structure. + * + * @param stdClass $data the current cm data reference + * @param renderer_base $output typically, the renderer that's calling this function + * @return bool if the cm has alternative content + */ + protected function add_alternative_content_data(stdClass &$data, renderer_base $output): bool { + $altcontent = $this->mod->get_formatted_content( + ['overflowdiv' => true, 'noclean' => true] + ); + $data->altcontent = (empty($altcontent)) ? false : $altcontent; + $data->afterlink = $this->mod->afterlink; + return !empty($data->altcontent); + } + + /** + * Add activity completion information to the data structure. + * + * @param stdClass $data the current cm data reference + * @param renderer_base $output typically, the renderer that's calling this function + * @return bool the module has completion information + */ + protected function add_completion_data(stdClass &$data, renderer_base $output): bool { + global $USER; + $course = $this->mod->get_course(); + // Fetch completion details. + $showcompletionconditions = $course->showcompletionconditions == COMPLETION_SHOW_CONDITIONS; + $completiondetails = cm_completion_details::get_instance($this->mod, $USER->id, $showcompletionconditions); + + // Fetch activity dates. + $activitydates = []; + if ($course->showactivitydates) { + $activitydates = activity_dates::get_dates_for_module($this->mod, $USER->id); + } + + $activityinfodata = (object) ['hasdates' => false, 'hascompletion' => false]; + // There are activity dates to be shown; or + // Completion info needs to be displayed + // * The activity tracks completion; AND + // * The showcompletionconditions setting is enabled OR an activity that tracks manual + // completion needs the manual completion button to be displayed on the course homepage. + $showcompletioninfo = $completiondetails->has_completion() && ($showcompletionconditions || + (!$completiondetails->is_automatic() && $completiondetails->show_manual_completion())); + if ($showcompletioninfo || !empty($activitydates)) { + $activityinfo = new activity_information($this->mod, $completiondetails, $activitydates); + $activityinfodata = $activityinfo->export_for_template($output); + } + + $data->activityinfo = $activityinfodata; + return $activityinfodata->hascompletion; + } + + /** + * Add activity information to the data structure. + * + * @param stdClass $data the current cm data reference + * @param bool[] $haspartials the result of loading partial data elements + * @param renderer_base $output typically, the renderer that's calling this function + * @return bool if the cm has format data + */ + protected function add_format_data(stdClass &$data, array $haspartials, renderer_base $output): bool { + $result = false; + // Legacy indentation. + if (!empty($this->mod->indent) && $this->format->uses_indentation()) { + $data->indent = $this->mod->indent; + if ($this->mod->indent > 15) { + $data->hugeindent = true; + $result = true; + } + } + // Stealth and hidden from student. + if (!$this->mod->visible) { // This module is hidden but current user has capability to see it. $data->modhiddenfromstudents = true; - } else if ($mod->is_stealth()) { + $result = true; + } else if ($this->mod->is_stealth()) { // This module is available but is normally not displayed on the course page // (this user can see it because they can manage it). $data->modstealth = true; + $result = true; } - - if ($mod->modname == 'label' && - !$modavailability->hasmodavailability && - !$activityinfodata->hascompletion && + // Special inline activity format. + if ( + $this->mod->has_custom_cmlist_item() && + !$haspartials['availability'] && + !$haspartials['completion'] && !isset($data->modhiddenfromstudents) && !isset($data->modstealth) && - !$format->show_editor() - ) { + !$this->format->show_editor() + ) { $data->modinline = true; + $result = true; } + return $result; + } - $returnsection = $format->get_section_number(); - - if ($format->show_editor()) { - // Edit actions. - $controlmenu = new $this->controlmenuclass( - $format, - $this->section, - $mod, - $this->displayoptions - ); - $data->controlmenu = $controlmenu->export_for_template($output); - - if ($format->supports_components()) { - $data->moveicon = $output->pix_icon('i/dragdrop', '', 'moodle', ['class' => 'editing_move dragicon']); - } else { - // Add the legacy YUI move link. - $data->moveicon = course_get_cm_move($mod, $returnsection); - } - - // Move and select options. - $modcontext = context_module::instance($mod->id); + /** + * Add course editor attributes to the data structure. + * + * @param stdClass $data the current cm data reference + * @param renderer_base $output typically, the renderer that's calling this function + * @return bool if the cm has editor data + */ + protected function add_editor_data(stdClass &$data, renderer_base $output): bool { + if (!$this->format->show_editor()) { + return false; } - - return $data; + $returnsection = $this->format->get_section_number(); + // Edit actions. + $controlmenu = new $this->controlmenuclass( + $this->format, + $this->section, + $this->mod, + $this->displayoptions + ); + $data->controlmenu = $controlmenu->export_for_template($output); + if (!$this->format->supports_components()) { + // Add the legacy YUI move link. + $data->moveicon = course_get_cm_move($this->mod, $returnsection); + } + return true; } /** @@ -236,7 +307,6 @@ class cm implements renderable, templatable { * */ protected function load_classes() { - $mod = $this->mod; $linkclasses = ''; diff --git a/course/format/classes/output/local/content/cm/availability.php b/course/format/classes/output/local/content/cm/availability.php index efe1c0c29ab..dbe249f6e01 100644 --- a/course/format/classes/output/local/content/cm/availability.php +++ b/course/format/classes/output/local/content/cm/availability.php @@ -53,6 +53,12 @@ class availability extends section_avalability { /** @var array optional display options */ protected $displayoptions; + /** @var bool the has availability attribute name */ + protected $hasavailabilityname; + + /** @var stdClass|null the instance export data */ + protected $data = null; + /** * Constructor. * @@ -66,86 +72,91 @@ class availability extends section_avalability { $this->section = $section; $this->mod = $mod; $this->displayoptions = $displayoptions; + $this->hasavailabilityname = 'hasmodavailability'; } /** - * Export this data so it can be used as the context for a mustache template. + * Get the availability data to 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 { - - $data = (object)[ - 'info' => $this->get_info($output), - 'hasmodavailability' => false, - ]; - - if (!empty($data->info)) { - $data->hasmodavailability = true; - } - - return $data; - } - - /** - * Get the availability HTML form the course renderer. - * - * @param \renderer_base $output typically, the renderer that's calling this function - * @return string the availability HTML + * @return array the availability data. */ protected function get_info(\renderer_base $output): array { - global $CFG; + if (!$this->mod->is_visible_on_course_page()) { + // Nothing to be displayed to the user. + return []; + } + if (!$this->mod->uservisible) { + return $this->user_availability_info($output); + } - $format = $this->format; - $mod = $this->mod; - $section = $this->section; + return $this->conditional_availability_info($output); + } + + /** + * Get the current user availability data. + * + * This is a student who is not allowed to see the module but might be allowed + * to see availability info (i.e. "Available from ..."). + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @return array the availability data. + */ + protected function user_availability_info(\renderer_base $output): array { + if (empty($this->mod->availableinfo)) { + return []; + } $info = []; + $formattedinfo = \core_availability\info::format_info( + $this->mod->availableinfo, + $this->mod->get_course() + ); + $info[] = $this->availability_info($formattedinfo, 'isrestricted'); + return $info; + } - if (!$mod->is_visible_on_course_page()) { - // Nothing to be displayed to the user. - return $info; - } - - if (!$mod->uservisible) { - // This is a student who is not allowed to see the module but might be allowed - // to see availability info (i.e. "Available from ..."). - if (!empty($mod->availableinfo)) { - $formattedinfo = \core_availability\info::format_info( - $mod->availableinfo, - $mod->get_course() - ); - $info[] = $this->availability_info($formattedinfo, 'isrestricted'); - } - return $info; - } + /** + * Get the activity availability data to display. + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @return array the availability data. + */ + protected function conditional_availability_info(\renderer_base $output): array { + global $CFG; // This is a teacher who is allowed to see module but still should see the // information that module is not available to all/some students. + $mod = $this->mod; $modcontext = $mod->context; $canviewhidden = has_capability('moodle/course:viewhiddenactivities', $modcontext); - - if ($canviewhidden && !empty($CFG->enableavailability)) { - // Display information about conditional availability. - // Don't add availability information if user is not editing and activity is hidden. - if ($mod->visible || $format->show_editor()) { - $hidinfoclass = 'isrestricted isfullinfo'; - if (!$mod->visible) { - $hidinfoclass .= ' hide'; - } - $ci = new info_module($mod); - $fullinfo = $ci->get_full_information(); - if ($fullinfo) { - $formattedinfo = info::format_info( - $fullinfo, - $mod->get_course() - ); - $info[] = $this->availability_info($formattedinfo, $hidinfoclass); - } - } + if (!$canviewhidden || empty($CFG->enableavailability)) { + return []; } + // Display information about conditional availability. + // Don't add availability information if user is not editing and activity is hidden. + if (!$mod->visible && !$this->format->show_editor()) { + return []; + } + + $ci = new info_module($mod); + $fullinfo = $ci->get_full_information(); + if (!$fullinfo) { + return []; + } + + $info = []; + $hidinfoclass = 'isrestricted isfullinfo'; + if (!$mod->visible) { + $hidinfoclass .= ' hide'; + } + $formattedinfo = info::format_info( + $fullinfo, + $mod->get_course() + ); + $info[] = $this->availability_info($formattedinfo, $hidinfoclass); + return $info; } } diff --git a/course/format/classes/output/local/content/section.php b/course/format/classes/output/local/content/section.php index 3b521bc238b..8a90497b66b 100644 --- a/course/format/classes/output/local/content/section.php +++ b/course/format/classes/output/local/content/section.php @@ -27,9 +27,10 @@ namespace core_courseformat\output\local\content; use core_courseformat\base as course_format; use context_course; use renderable; -use templatable; +use renderer_base; use section_info; use stdClass; +use templatable; /** * Base class to render a course section. @@ -44,7 +45,7 @@ class section implements renderable, templatable { protected $format; /** @var section_info the section info */ - protected $thissection; + protected $section; /** @var section header output class */ protected $headerclass; @@ -73,16 +74,23 @@ class section implements renderable, templatable { /** @var bool if the title is hidden for some reason */ protected $hidecontrols = false; + /** @var bool if the section is considered stealth */ + protected $isstealth = false; + /** * Constructor. * * @param course_format $format the course format - * @param section_info $thissection the section info + * @param section_info $section the section info */ - public function __construct(course_format $format, section_info $thissection) { + public function __construct(course_format $format, section_info $section) { $this->format = $format; - $this->thissection = $thissection; + $this->section = $section; + + if ($section->section > $format->get_last_section_number()) { + $this->isstealth = true; + } // Load output classes names from format. $this->headerclass = $format->get_output_classname('content\\section\\header'); @@ -118,44 +126,191 @@ class section implements renderable, templatable { * @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 { + public function export_for_template(renderer_base $output): stdClass { global $USER; $format = $this->format; $course = $format->get_course(); - $thissection = $this->thissection; - $singlesection = $format->get_section_number(); - $context = context_course::instance($course->id); + $section = $this->section; - $summary = new $this->summaryclass($format, $thissection); - $availability = new $this->availabilityclass($format, $thissection); + $summary = new $this->summaryclass($format, $section); $data = (object)[ - 'num' => $thissection->section ?? '0', - 'id' => $thissection->id, - 'sectionreturnid' => $singlesection, + 'num' => $section->section ?? '0', + 'id' => $section->id, + 'sectionreturnid' => $format->get_section_number(), 'insertafter' => false, 'summary' => $summary->export_for_template($output), - 'availability' => $availability->export_for_template($output), - 'restrictionlock' => !empty($thissection->availableinfo), 'highlightedlabel' => $format->get_section_highlighted_name(), + 'sitehome' => $course->id == SITEID, ]; - // Check if it is a stealth sections (orphaned). - if ($thissection->section > $format->get_last_section_number()) { - $data->isstealth = true; - $data->ishidden = true; + $haspartials = []; + $haspartials['availability'] = $this->add_availability_data($data, $output); + $haspartials['visibility'] = $this->add_visibility_data($data, $output); + $haspartials['editor'] = $this->add_editor_data($data, $output); + $haspartials['header'] = $this->add_header_data($data, $output); + $haspartials['cm'] = $this->add_cm_data($data, $output); + $this->add_format_data($data, $haspartials, $output); + + return $data; + } + + /** + * Add the section header to the data structure. + * + * @param stdClass $data the current cm data reference + * @param renderer_base $output typically, the renderer that's calling this function + * @return bool if the cm has name data + */ + protected function add_header_data(stdClass &$data, renderer_base $output): bool { + if (!empty($this->hidetitle)) { + return false; } - 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, $singlesection); + $section = $this->section; + $format = $this->format; + + $header = new $this->headerclass($format, $section); + $headerdata = $header->export_for_template($output); + + // When a section is displayed alone the title goes over the section, not inside it. + if ($section->section != 0 && $section->section == $format->get_section_number()) { + $data->singleheader = $headerdata; + } else { + $data->header = $headerdata; + } + return true; + } + + /** + * Add the section cm list to the data structure. + * + * @param stdClass $data the current cm data reference + * @param renderer_base $output typically, the renderer that's calling this function + * @return bool if the cm has name data + */ + protected function add_cm_data(stdClass &$data, renderer_base $output): bool { + $result = false; + + $section = $this->section; + $format = $this->format; + $course = $this->format->get_course(); + + $showsummary = ($section->section != 0 && + $section->section != $format->get_section_number() && + $course->coursedisplay == COURSE_DISPLAY_MULTIPAGE && + !$format->show_editor() + ); + + $showcmlist = $section->uservisible; + + // Add activities summary if necessary. + if ($showsummary) { + $cmsummary = new $this->cmsummaryclass($format, $section); + $data->cmsummary = $cmsummary->export_for_template($output); + $data->onlysummary = true; + $result = true; + + if (!$format->is_section_current($section)) { + // In multipage, only the current section (and the section zero) has elements. + $showcmlist = false; } } + // Add the cm list. + if ($showcmlist) { + $cmlist = new $this->cmlistclass($format, $section); + $data->cmlist = $cmlist->export_for_template($output); + $result = true; + + } + return $result; + } + + /** + * Add the section availability to the data structure. + * + * @param stdClass $data the current cm data reference + * @param renderer_base $output typically, the renderer that's calling this function + * @return bool if the cm has name data + */ + protected function add_availability_data(stdClass &$data, renderer_base $output): bool { + $availability = new $this->availabilityclass($this->format, $this->section); + $data->availability = $availability->export_for_template($output); + $data->restrictionlock = !empty($this->section->availableinfo); + $data->hasavailability = $availability->has_availability($output); + return true; + } + + /** + * Add the section vibility information to the data structure. + * + * @param stdClass $data the current cm data reference + * @param renderer_base $output typically, the renderer that's calling this function + * @return bool if the cm has name data + */ + protected function add_visibility_data(stdClass &$data, renderer_base $output): bool { + global $USER; + $result = false; + $course = $this->format->get_course(); + $context = context_course::instance($course->id); + // Check if it is a stealth sections (orphaned). + if ($this->isstealth) { + $data->isstealth = true; + $data->ishidden = true; + $result = true; + } + if (!$this->section->visible) { + $data->ishidden = true; + $data->notavailable = true; + if (has_capability('moodle/course:viewhiddensections', $context, $USER)) { + $data->hiddenfromstudents = true; + $data->notavailable = false; + $result = true; + } + } + return $result; + } + + /** + * Add the section editor attributes to the data structure. + * + * @param stdClass $data the current cm data reference + * @param renderer_base $output typically, the renderer that's calling this function + * @return bool if the cm has name data + */ + protected function add_editor_data(stdClass &$data, renderer_base $output): bool { + if (!$this->format->show_editor()) { + return false; + } + + $course = $this->format->get_course(); + if (empty($this->hidecontrols)) { + $controlmenu = new $this->controlmenuclass($this->format, $this->section); + $data->controlmenu = $controlmenu->export_for_template($output); + } + if (!$this->isstealth) { + $data->cmcontrols = $output->course_section_add_cm_control( + $course, + $this->section->section, + $this->format->get_section_number() + ); + } + return true; + } + + /** + * Add the section format attributes to the data structure. + * + * @param stdClass $data the current cm data reference + * @param bool[] $haspartials the result of loading partial data elements + * @param renderer_base $output typically, the renderer that's calling this function + * @return bool if the cm has name data + */ + protected function add_format_data(stdClass &$data, array $haspartials, renderer_base $output): bool { + $section = $this->section; + $format = $this->format; + $course = $this->format->get_course(); $coursedisplay = $course->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE; $data->iscoursedisplaymultipage = ($coursedisplay == COURSE_DISPLAY_MULTIPAGE); @@ -164,83 +319,21 @@ class section implements renderable, templatable { $data->collapsemenu = true; } - if ($course->id == SITEID) { - $data->sitehome = true; - } - $data->contentcollapsed = false; $preferences = $format->get_sections_preferences(); - if (isset($preferences[$thissection->id])) { - $sectionpreferences = $preferences[$thissection->id]; + if (isset($preferences[$section->id])) { + $sectionpreferences = $preferences[$section->id]; if (!empty($sectionpreferences->contentcollapsed)) { $data->contentcollapsed = true; } } - 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); - - $header = new $this->headerclass($format, $thissection); - if (empty($this->hidetitle)) { - $data->header = $header->export_for_template($output); - } - 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->visible) { - $data->ishidden = true; - $data->notavailable = true; - if (has_capability('moodle/course:viewhiddensections', $context, $USER)) { - $data->hiddenfromstudents = true; - $data->notavailable = false; - } - } - - 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); - } - - $data->hasavailability = false; - if (isset($data->availability->hasavailability)) { - $data->hasavailability = $data->availability->hasavailability; - } - - if ($format->is_section_current($thissection)) { + if ($format->is_section_current($section)) { $data->highlighted = true; $data->currentlink = get_accesshide( - get_string('currentsection', 'format_'.$format->get_format()) + get_string('currentsection', 'format_' . $format->get_format()) ); } - - return $data; + return true; } } diff --git a/course/format/classes/output/local/content/section/availability.php b/course/format/classes/output/local/content/section/availability.php index 86954c2129b..1979543907a 100644 --- a/course/format/classes/output/local/content/section/availability.php +++ b/course/format/classes/output/local/content/section/availability.php @@ -48,6 +48,12 @@ class availability implements renderable, templatable { /** @var section_info the section object */ protected $section; + /** @var string the has availability attribute name */ + protected $hasavailabilityname; + + /** @var stdClass|null the instance export data */ + protected $data = null; + /** * Constructor. * @@ -57,6 +63,7 @@ class availability implements renderable, templatable { public function __construct(course_format $format, section_info $section) { $this->format = $format; $this->section = $section; + $this->hasavailabilityname = 'hasavailability'; } /** @@ -66,16 +73,40 @@ class availability implements renderable, templatable { * @return stdClass data context for a mustache template */ public function export_for_template(\renderer_base $output): stdClass { + $this->build_export_data($output); + return $this->data; + } + + /** + * Returns if the output has availability info to display. + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @return bool if the element has availability data to display + */ + public function has_availability(\renderer_base $output): bool { + $this->build_export_data($output); + $attributename = $this->hasavailabilityname; + return $this->data->$attributename; + } + + /** + * Protected method to build the export data. + * + * @param \renderer_base $output typically, the renderer that's calling this function + */ + protected function build_export_data(\renderer_base $output) { + if (!empty($this->data)) { + return; + } $data = (object)[ 'info' => $this->get_info($output), ]; - if (!empty($data->info)) { - $data->hasavailability = true; - } + $attributename = $this->hasavailabilityname; + $data->$attributename = !empty($data->info); - return $data; + $this->data = $data; } /** diff --git a/lib/modinfolib.php b/lib/modinfolib.php index b962a133385..fdd183234f3 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -1122,6 +1122,11 @@ class cm_info implements IteratorAggregate { */ private $contentisformatted; + /** + * @var bool True if the content has a special course item display like labels. + */ + private $customcmlistitem; + /** * @var string */ @@ -1194,6 +1199,7 @@ class cm_info implements IteratorAggregate { 'course' => 'get_course_id', 'coursegroupmode' => 'get_course_groupmode', 'coursegroupmodeforce' => 'get_course_groupmodeforce', + 'customcmlistitem' => 'has_custom_cmlist_item', 'effectivegroupmode' => 'get_effective_groupmode', 'extra' => false, 'groupingid' => false, @@ -1405,6 +1411,19 @@ class cm_info implements IteratorAggregate { return format_text($this->content, FORMAT_HTML, $options); } + /** + * Return the module custom cmlist item flag. + * + * Activities like label uses this flag to indicate that it should be + * displayed as a custom course item instead of a tipical activity card. + * + * @return bool + */ + public function has_custom_cmlist_item(): bool { + $this->obtain_view_data(); + return $this->customcmlistitem ?? false; + } + /** * Getter method for property $name, ensures that dynamic data is obtained. * @@ -1793,6 +1812,18 @@ class cm_info implements IteratorAggregate { $this->uservisible = $uservisible; } + /** + * Sets the 'customcmlistitem' flag + * + * This can be used (by setting true) to prevent the course from rendering the + * activity item as a regular activity card. This is applied to activities like labels. + * + * @param bool $customcmlistitem if the cmlist item of that activity has a special dysplay other than a card. + */ + public function set_custom_cmlist_item(bool $customcmlistitem) { + $this->customcmlistitem = $customcmlistitem; + } + /** * Sets the 'available' flag and related details. This flag is normally used to make * course modules unavailable until a certain date or condition is met. (When a course diff --git a/mod/label/lib.php b/mod/label/lib.php index c435b7a43ce..03c2f712584 100644 --- a/mod/label/lib.php +++ b/mod/label/lib.php @@ -70,6 +70,15 @@ function label_add_instance($label) { return $id; } +/** + * Sets the special label display on course page. + * + * @param cm_info $cm Course-module object + */ +function label_cm_info_view(cm_info $cm) { + $cm->set_custom_cmlist_item(true); +} + /** * Given an object containing all the necessary data, * (defined by the form in mod_form.php) this function From 479faca4283956faadd30e8c2182ae25d1a6ee3b Mon Sep 17 00:00:00 2001 From: Ferran Recio Date: Wed, 16 Feb 2022 10:49:30 +0100 Subject: [PATCH 2/2] MDL-73762 core_courseformat: fix coursedisplay dependency For legacy reasons, the course renderings uses a format setting to know if the page is multiple sections per page or a single section per page. This format specific dependency is not solved using a get_course_display format method any plugin can extend. --- course/format/classes/base.php | 14 ++++++++++++++ course/format/classes/output/local/content.php | 2 +- .../classes/output/local/content/section.php | 7 ++----- .../output/local/content/section/header.php | 2 +- course/format/classes/output/section_renderer.php | 2 +- course/format/formatlegacy.php | 6 +++--- course/format/topics/lib.php | 2 +- course/format/upgrade.txt | 1 + course/format/weeks/lib.php | 4 ++-- 9 files changed, 26 insertions(+), 14 deletions(-) diff --git a/course/format/classes/base.php b/course/format/classes/base.php index 079cc4acf2c..7b4f47905a9 100644 --- a/course/format/classes/base.php +++ b/course/format/classes/base.php @@ -277,6 +277,20 @@ abstract class base { return $this->course; } + /** + * Get the course display value for the current course. + * + * Formats extending topics or weeks will use coursedisplay as this setting name + * so they don't need to override the method. However, if the format uses a different + * display logic it must override this method to ensure the core renderers know + * if a COURSE_DISPLAY_MULTIPAGE or COURSE_DISPLAY_SINGLEPAGE is being used. + * + * @return int The current value (COURSE_DISPLAY_MULTIPAGE or COURSE_DISPLAY_SINGLEPAGE) + */ + public function get_course_display(): int { + return $this->get_course()->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE; + } + /** * Return the current course modinfo. * diff --git a/course/format/classes/output/local/content.php b/course/format/classes/output/local/content.php index 9c864fb7b4c..78492f09e63 100644 --- a/course/format/classes/output/local/content.php +++ b/course/format/classes/output/local/content.php @@ -158,7 +158,7 @@ class content implements renderable, templatable { // 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); + (!$thissection->visible && !($course->hiddensections ?? false)); if (!$showsection) { continue; } diff --git a/course/format/classes/output/local/content/section.php b/course/format/classes/output/local/content/section.php index 8a90497b66b..dd8f09f5420 100644 --- a/course/format/classes/output/local/content/section.php +++ b/course/format/classes/output/local/content/section.php @@ -195,11 +195,10 @@ class section implements renderable, templatable { $section = $this->section; $format = $this->format; - $course = $this->format->get_course(); $showsummary = ($section->section != 0 && $section->section != $format->get_section_number() && - $course->coursedisplay == COURSE_DISPLAY_MULTIPAGE && + $format->get_course_display() == COURSE_DISPLAY_MULTIPAGE && !$format->show_editor() ); @@ -310,10 +309,8 @@ class section implements renderable, templatable { protected function add_format_data(stdClass &$data, array $haspartials, renderer_base $output): bool { $section = $this->section; $format = $this->format; - $course = $this->format->get_course(); - $coursedisplay = $course->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE; - $data->iscoursedisplaymultipage = ($coursedisplay == COURSE_DISPLAY_MULTIPAGE); + $data->iscoursedisplaymultipage = ($format->get_course_display() == COURSE_DISPLAY_MULTIPAGE); if ($data->num === 0 && !$data->iscoursedisplaymultipage) { $data->collapsemenu = true; diff --git a/course/format/classes/output/local/content/section/header.php b/course/format/classes/output/local/content/section/header.php index 840a09d68e0..e70a64edd73 100644 --- a/course/format/classes/output/local/content/section/header.php +++ b/course/format/classes/output/local/content/section/header.php @@ -75,7 +75,7 @@ class header implements renderable, templatable { $data->title = $output->section_title_without_link($section, $course); - $coursedisplay = $course->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE; + $coursedisplay = $format->get_course_display(); $data->headerdisplaymultipage = false; if ($coursedisplay == COURSE_DISPLAY_MULTIPAGE) { $data->headerdisplaymultipage = true; diff --git a/course/format/classes/output/section_renderer.php b/course/format/classes/output/section_renderer.php index f84ff3cf1d2..bbee28019bc 100644 --- a/course/format/classes/output/section_renderer.php +++ b/course/format/classes/output/section_renderer.php @@ -1043,7 +1043,7 @@ abstract class section_renderer extends core_course_renderer { continue; } - if (!$format->show_editor() && $course->coursedisplay == COURSE_DISPLAY_MULTIPAGE) { + if (!$format->show_editor() && $format->get_course_display() == COURSE_DISPLAY_MULTIPAGE) { // Display section summary only. echo $this->section_summary($thissection, $course, null); } else { diff --git a/course/format/formatlegacy.php b/course/format/formatlegacy.php index 2b7af9315d0..318b4e1ea13 100644 --- a/course/format/formatlegacy.php +++ b/course/format/formatlegacy.php @@ -126,7 +126,7 @@ class format_legacy extends core_courseformat\base { $usercoursedisplay = COURSE_DISPLAY_SINGLEPAGE; } } else { - $usercoursedisplay = $course->coursedisplay; + $usercoursedisplay = $course->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE; } if ($sectionno != 0 && $usercoursedisplay == COURSE_DISPLAY_MULTIPAGE) { $url->param('section', $sectionno); @@ -268,11 +268,11 @@ class format_legacy extends core_courseformat\base { 'type' => PARAM_INT, ), 'hiddensections' => array( - 'default' => $courseconfig->hiddensections, + 'default' => $courseconfig->hiddensections ?? 0, 'type' => PARAM_INT, ), 'coursedisplay' => array( - 'default' => $courseconfig->coursedisplay, + 'default' => $courseconfig->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE, 'type' => PARAM_INT, ), ); diff --git a/course/format/topics/lib.php b/course/format/topics/lib.php index 0ff47a2aabc..efda54a16a5 100644 --- a/course/format/topics/lib.php +++ b/course/format/topics/lib.php @@ -135,7 +135,7 @@ class format_topics extends core_courseformat\base { $usercoursedisplay = COURSE_DISPLAY_SINGLEPAGE; } } else { - $usercoursedisplay = $course->coursedisplay; + $usercoursedisplay = $course->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE; } if ($sectionno != 0 && $usercoursedisplay == COURSE_DISPLAY_MULTIPAGE) { $url->param('section', $sectionno); diff --git a/course/format/upgrade.txt b/course/format/upgrade.txt index 39cd43fc94c..8707452361c 100644 --- a/course/format/upgrade.txt +++ b/course/format/upgrade.txt @@ -6,6 +6,7 @@ Overview of this plugin type at http://docs.moodle.org/dev/Course_formats * New core_courseformat\base::uses_course_index() to define whether the course format uses course index or not. * New core_courseformat\base::supports_components() to specify if the format is compatible with reactive components. * New core_courseformat\base::uses_indentation() to specify if the format is compatible with activity indentation. +* New core_courseformat\base::get_course_display() if the course uses COURSE_DISPLAY_SINGLEPAGE or COURSE_DISPLAY_MULTIPAGE layout. === 3.10 === * Added the missing callback supports_ajax() to format_social. diff --git a/course/format/weeks/lib.php b/course/format/weeks/lib.php index c0916fc1030..fd265ad81fe 100644 --- a/course/format/weeks/lib.php +++ b/course/format/weeks/lib.php @@ -145,7 +145,7 @@ class format_weeks extends core_courseformat\base { $usercoursedisplay = COURSE_DISPLAY_SINGLEPAGE; } } else { - $usercoursedisplay = $course->coursedisplay; + $usercoursedisplay = $course->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE; } if ($sectionno != 0 && $usercoursedisplay == COURSE_DISPLAY_MULTIPAGE) { $url->param('section', $sectionno); @@ -268,7 +268,7 @@ class format_weeks extends core_courseformat\base { 'type' => PARAM_INT, ), 'coursedisplay' => array( - 'default' => $courseconfig->coursedisplay, + 'default' => $courseconfig->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE, 'type' => PARAM_INT, ), 'automaticenddate' => array(