diff --git a/.upgradenotes/MDL-82845-2024091710272206.yml b/.upgradenotes/MDL-82845-2024091710272206.yml new file mode 100644 index 00000000000..002abf80894 --- /dev/null +++ b/.upgradenotes/MDL-82845-2024091710272206.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-82845 +notes: + core: + - message: >- + The global_navigation::load_section_activities method is now deprecated + and replaced by global_navigation::load_section_activities_navigation. + type: deprecated diff --git a/course/format/classes/base.php b/course/format/classes/base.php index 9a45304c2ef..663aba3a0c6 100644 --- a/course/format/classes/base.php +++ b/course/format/classes/base.php @@ -860,7 +860,7 @@ abstract class base { * 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 + * @param int|stdClass|section_info $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 not empty, the function returns section page; otherwise, it returns course page. diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 8c4991d0714..6a544aa6429 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -2204,7 +2204,7 @@ class global_navigation extends navigation_node { $activity = $this->load_stealth_activity($coursenode, get_fast_modinfo($course)); } } - } + } /** * Generates an array of sections and an array of activities for the given course. @@ -2220,29 +2220,29 @@ class global_navigation extends navigation_node { $modinfo = get_fast_modinfo($course); $sections = $modinfo->get_section_info_all(); + $format = course_get_format($course); // For course formats using 'numsections' trim the sections list - $courseformatoptions = course_get_format($course)->get_format_options(); + $courseformatoptions = $format->get_format_options(); if (isset($courseformatoptions['numsections'])) { $sections = array_slice($sections, 0, $courseformatoptions['numsections']+1, true); } - $activities = array(); + $activities = []; foreach ($sections as $key => $section) { // Clone and unset summary to prevent $SESSION bloat (MDL-31802). $sections[$key] = clone($section); unset($sections[$key]->summary); $sections[$key]->hasactivites = false; - if (!array_key_exists($section->section, $modinfo->sections)) { + if (!array_key_exists($section->sectionnum, $modinfo->sections)) { continue; } - foreach ($modinfo->sections[$section->section] as $cmid) { - $cm = $modinfo->cms[$cmid]; + foreach ($section->get_sequence_cm_infos() as $cm) { $activity = new stdClass; $activity->id = $cm->id; $activity->course = $course->id; - $activity->section = $section->section; + $activity->section = $section->sectionnum; $activity->name = $cm->name; $activity->icon = $cm->icon; $activity->iconcomponent = $cm->iconcomponent; @@ -2251,6 +2251,16 @@ class global_navigation extends navigation_node { $activity->nodetype = navigation_node::NODETYPE_LEAF; $activity->onclick = $cm->onclick; $url = $cm->url; + + // Activities witout url but with delegated section uses the section url. + $activity->delegatedsection = $cm->get_delegated_section_info(); + if (empty($cm->url) && $activity->delegatedsection) { + $url = $format->get_view_url( + $activity->delegatedsection->sectionnum, + ['navigation' => true] + ); + } + if (!$url) { $activity->url = null; $activity->display = false; @@ -2261,14 +2271,14 @@ class global_navigation extends navigation_node { $activity->nodetype = navigation_node::NODETYPE_BRANCH; } } - $activities[$cmid] = $activity; + $activities[$cm->id] = $activity; if ($activity->display) { $sections[$key]->hasactivites = true; } } } - return array($sections, $activities); + return [$sections, $activities]; } /** @@ -2284,42 +2294,233 @@ class global_navigation extends navigation_node { list($sections, $activities) = $this->generate_sections_and_activities($course); - $navigationsections = array(); + $navigationsections = []; foreach ($sections as $sectionid => $section) { - $section = clone($section); if ($course->id == $SITE->id) { - $this->load_section_activities($coursenode, $section->section, $activities); - } else { - if (!$section->uservisible || (!$this->showemptysections && - !$section->hasactivites && $this->includesectionnum !== $section->section)) { - continue; - } - - $parentnode = $coursenode; - - // Set the parent node to the parent section if this is a delegated section. - $parentsection = $section->get_component_instance()?->get_parent_section(); - if ($parentsection) { - $parentnode = $coursenode->find($parentsection->id, self::TYPE_SECTION) ?: $coursenode; - } - - $sectionname = get_section_name($course, $section); - $url = course_get_url($course, $section->section, array('navigation' => true)); - - $sectionnode = $parentnode->add($sectionname, $url, navigation_node::TYPE_SECTION, - null, $section->id, new pix_icon('i/section', '')); - $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH; - $sectionnode->hidden = (!$section->visible || !$section->available); - $sectionnode->add_attribute('data-section-name-for', $section->id); - if ($this->includesectionnum !== false && $this->includesectionnum == $section->section) { - $this->load_section_activities($sectionnode, $section->section, $activities); - } - $navigationsections[$sectionid] = $section; + $this->load_section_activities_navigation($coursenode, $section, $activities); + continue; } + + if ( + !$section->uservisible + || ( + !$this->showemptysections + && !$section->hasactivites + && $this->includesectionnum !== $section->section + ) + ) { + continue; + } + + // Delegated sections are added from the activity node. + if ($section->get_component_instance()) { + continue; + } + + $navigationsections[$sectionid] = $this->load_section_navigation( + parentnode: $coursenode, + section: $section, + activitiesdata: $activities, + ); } return $navigationsections; } + /** + * Returns true if the section is included in the breadcrumb. + * + * @param section_info $section + * @param moodle_url|null $sectionurl + * @return bool + */ + protected function is_section_in_breadcrumb(section_info $section, ?moodle_url $sectionurl): bool { + // Ajax requests uses includesectionnum as param. + if ($this->includesectionnum !== false && $this->includesectionnum == $section->sectionnum) { + return true; + } + + // If we are in a section page, we need to check for any child section. + $checkchildrenurls = false; + $format = null; + if ($sectionurl && $this->page->url->compare($sectionurl, URL_MATCH_BASE)) { + $checkchildrenurls = true; + $format = course_get_format($section->course); + } + + // Activities can have delegated sections that acts as a child section. + foreach ($section->get_sequence_cm_infos() as $cm) { + $delegatedsection = $cm->get_delegated_section_info(); + if (!$delegatedsection) { + continue; + } + // Check if the child node is requested via Ajax. + if ($this->includesectionnum == $delegatedsection->sectionnum) { + return true; + } + + if ($checkchildrenurls) { + $childurl = $format->get_view_url($delegatedsection, ['navigation' => true]); + if ($childurl && $this->page->url->compare($childurl, URL_MATCH_EXACT)) { + return true; + } + } + } + + return false; + } + + /** + * Loads a section into the navigation structure. + * + * @param navigation_node $parentnode + * @param section_info $section + * @param stdClass[] $activitiesdata Array of objects containing activities data indexed by cmid. + * @return navigation_node the section navigaiton node + */ + public function load_section_navigation($parentnode, $section, $activitiesdata): navigation_node { + $format = course_get_format($section->course); + $sectionname = $format->get_section_name($section); + $url = $format->get_view_url($section, ['navigation' => true]); + + $sectionnode = $parentnode->add( + text: $sectionname, + action: $url, + type: navigation_node::TYPE_SECTION, + key: $section->id, + icon: new pix_icon('i/section', ''), + ); + $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH; + $sectionnode->hidden = (!$section->visible || !$section->available); + $sectionnode->add_attribute('data-section-name-for', $section->id); + + // Sections content are usually loaded via ajax but the sections from the requested breadcrumb. + if ($this->is_section_in_breadcrumb($section, $url)) { + $this->load_section_activities_navigation($sectionnode, $section, $activitiesdata); + } + return $sectionnode; + } + + /** + * Loads the activities for a section into the navigation structure. + * + * This method is called from global_navigation::load_section_navigation(), + * It is not intended to be called directly. + * + * @param navigation_node $sectionnode + * @param section_info $section + * @param stdClass[] $activitiesdata Array of objects containing activities data indexed by cmid. + */ + protected function load_section_activities_navigation( + navigation_node $sectionnode, + section_info $section, + array $activitiesdata + ): array { + global $CFG, $SITE; + + $activitynodes = []; + if (empty($activitiesdata)) { + return $activitynodes; + } + + foreach ($section->get_sequence_cm_infos() as $cm) { + $activitydata = $activitiesdata[$cm->id]; + + // If activity is a delegated section, load a section node instead of the activity one. + if ($activitydata->delegatedsection) { + $activitynodes[$activitydata->id] = $this->load_section_navigation( + parentnode: $sectionnode, + section: $activitydata->delegatedsection, + activitiesdata: $activitiesdata, + ); + continue; + } + + $activitynodes[$activitydata->id] = $this->load_activity_navigation($sectionnode, $activitydata); + } + + return $activitynodes; + } + + /** + * Loads an activity into the navigation structure. + * + * This method is called from global_navigation::load_section_activities_navigation(), + * It is not intended to be called directly. + * + * @param navigation_node $sectionnode + * @param stdClass $activitydata The acitivy navigation data generated from generate_sections_and_activities + * @return navigation_node + */ + protected function load_activity_navigation( + navigation_node $sectionnode, + stdClass $activitydata, + ): navigation_node { + global $SITE, $CFG; + + $showactivities = ($activitydata->course != $SITE->id || !empty($CFG->navshowfrontpagemods)); + + $icon = new pix_icon( + $activitydata->icon ?: 'monologo', + get_string('modulename', $activitydata->modname), + $activitydata->modname + ); + + // Prepare the default name and url for the node. + $displaycontext = \context_helper::get_navigation_filter_context(context_module::instance($activitydata->id)); + $activityname = format_string($activitydata->name, true, ['context' => $displaycontext]); + + $activitynode = $sectionnode->add( + text: $activityname, + action: $this->get_activity_action($activitydata, $activityname), + type: navigation_node::TYPE_ACTIVITY, + key: $activitydata->id, + icon: $icon, + ); + $activitynode->title(get_string('modulename', $activitydata->modname)); + $activitynode->hidden = $activitydata->hidden; + $activitynode->display = $showactivities && $activitydata->display; + $activitynode->nodetype = $activitydata->nodetype; + + return $activitynode; + } + + /** + * Returns the action for the activity. + * + * @param stdClass $activitydata The acitivy navigation data generated from generate_sections_and_activities + * @param string $activityname + * @return moodle_url|action_link + */ + protected function get_activity_action(stdClass $activitydata, string $activityname): moodle_url|action_link { + // A static counter for JS function naming. + static $legacyonclickcounter = 0; + + $action = new moodle_url($activitydata->url); + + // Check if the onclick property is set (puke!). + if (!empty($activitydata->onclick)) { + // Increment the counter so that we have a unique number. + $legacyonclickcounter++; + // Generate the function name we will use. + $functionname = 'legacy_activity_onclick_handler_' . $legacyonclickcounter; + $propogrationhandler = ''; + // Check if we need to cancel propogation. Remember inline onclick + // events would return false if they wanted to prevent propogation and the + // default action. + if (strpos($activitydata->onclick, 'return false')) { + $propogrationhandler = 'e.halt();'; + } + // Decode the onclick - it has already been encoded for display (puke). + $onclick = htmlspecialchars_decode($activitydata->onclick, ENT_QUOTES); + // Build the JS function the click event will call. + $jscode = "function {$functionname}(e) { $propogrationhandler $onclick }"; + $this->page->requires->js_amd_inline($jscode); + // Override the default url with the new action link. + $action = new action_link($action, $activityname, new component_action('click', $functionname)); + } + return $action; + } + /** * Loads all of the activities for a section into the navigation structure. * @@ -2329,70 +2530,22 @@ class global_navigation extends navigation_node { * @param stdClass $course The course object the section and activities relate to. * @return array Array of activity nodes */ + #[\core\attribute\deprecated( + replacement: 'load_section_activities_navigation', + since: '4.5', + mdl: 'MDL-82845', + final: true, + )] protected function load_section_activities(navigation_node $sectionnode, $sectionnumber, array $activities, $course = null) { - global $CFG, $SITE; - // A static counter for JS function naming - static $legacyonclickcounter = 0; - - $activitynodes = array(); - if (empty($activities)) { - return $activitynodes; - } - + \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]); if (!is_object($course)) { $activity = reset($activities); $courseid = $activity->course; } else { $courseid = $course->id; } - $showactivities = ($courseid != $SITE->id || !empty($CFG->navshowfrontpagemods)); - - foreach ($activities as $activity) { - if ($activity->section != $sectionnumber) { - continue; - } - if ($activity->icon) { - $icon = new pix_icon($activity->icon, get_string('modulename', $activity->modname), $activity->iconcomponent); - } else { - $icon = new pix_icon('monologo', get_string('modulename', $activity->modname), $activity->modname); - } - - // Prepare the default name and url for the node - $displaycontext = \context_helper::get_navigation_filter_context(context_module::instance($activity->id)); - $activityname = format_string($activity->name, true, ['context' => $displaycontext]); - $action = new moodle_url($activity->url); - - // Check if the onclick property is set (puke!) - if (!empty($activity->onclick)) { - // Increment the counter so that we have a unique number. - $legacyonclickcounter++; - // Generate the function name we will use - $functionname = 'legacy_activity_onclick_handler_'.$legacyonclickcounter; - $propogrationhandler = ''; - // Check if we need to cancel propogation. Remember inline onclick - // events would return false if they wanted to prevent propogation and the - // default action. - if (strpos($activity->onclick, 'return false')) { - $propogrationhandler = 'e.halt();'; - } - // Decode the onclick - it has already been encoded for display (puke) - $onclick = htmlspecialchars_decode($activity->onclick, ENT_QUOTES); - // Build the JS function the click event will call - $jscode = "function {$functionname}(e) { $propogrationhandler $onclick }"; - $this->page->requires->js_amd_inline($jscode); - // Override the default url with the new action link - $action = new action_link($action, $activityname, new component_action('click', $functionname)); - } - - $activitynode = $sectionnode->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $activity->id, $icon); - $activitynode->title(get_string('modulename', $activity->modname)); - $activitynode->hidden = $activity->hidden; - $activitynode->display = $showactivities && $activity->display; - $activitynode->nodetype = $activity->nodetype; - $activitynodes[$activity->id] = $activitynode; - } - - return $activitynodes; + $sectionifo = get_fast_modinfo($courseid)->get_section_info($sectionnumber); + return $this->load_section_activities_navigation($sectionnode, $sectionifo, $activities); } /** * Loads a stealth module from unavailable section diff --git a/mod/subsection/tests/behat/subsection_navigation.feature b/mod/subsection/tests/behat/subsection_navigation.feature index e34012781a5..310bd4f6597 100644 --- a/mod/subsection/tests/behat/subsection_navigation.feature +++ b/mod/subsection/tests/behat/subsection_navigation.feature @@ -16,9 +16,11 @@ Feature: Teachers navigate to subsections | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: - | activity | name | course | idnumber | section | - | subsection | Subsection 1 | C1 | subsection1 | 1 | - | page | Page in Subsection 1 | C1 | page1 | 2 | + | activity | name | course | idnumber | section | + | assign | Assignment 1 | C1 | assignment1 | 1 | + | subsection | Subsection 1 | C1 | subsection1 | 1 | + | page | Page in Subsection 1 | C1 | page1 | 2 | + | assign | Assignment 2 | C1 | assignment2 | 1 | And I log in as "teacher1" Scenario: Subsection section page shows parent section in the breadcrumb @@ -34,11 +36,41 @@ Feature: Teachers navigate to subsections And "Subsection 1" "link" should exist in the ".breadcrumb" "css_element" And "Page in Subsection 1" "text" should exist in the ".breadcrumb" "css_element" - Scenario: Sections and Subsections are displayed in the navigation block + Scenario: Activity page shows Sections and Subsections in the navigation block Given the following config values are set as admin: | unaddableblocks | | theme_boost| And I turn editing mode on When I am on the "page1" "Activity" page And I add the "Navigation" block if not present - Then "Section 1" "link" should appear before "Subsection 1" "link" in the "Navigation" "block" + Then "Section 1" "link" should appear before "Assignment 1" "link" in the "Navigation" "block" + And "Assignment 1" "link" should appear before "Subsection 1" "link" in the "Navigation" "block" And "Subsection 1" "link" should appear before "Page in Subsection 1" "link" in the "Navigation" "block" + And "Page in Subsection 1" "link" should appear before "Assignment 2" "link" in the "Navigation" "block" + + @javascript + Scenario: Subsection page shows Sections and Subsections in the navigation block + Given the following config values are set as admin: + | unaddableblocks | | theme_boost | + And I turn editing mode on + When I am on the "C1 > Subsection 1" "course > section" page + And I add the "Navigation" block if not present + Then "Section 1" "link" should appear before "Assignment 1" "link" in the "Navigation" "block" + And "Assignment 1" "link" should appear before "Subsection 1" "link" in the "Navigation" "block" + And "Subsection 1" "link" should appear before "Page in Subsection 1" "link" in the "Navigation" "block" + And "Page in Subsection 1" "link" should appear before "Assignment 2" "link" in the "Navigation" "block" + + @javascript + Scenario: The navigation block can load subsections via ajax + Given the following config values are set as admin: + | unaddableblocks | | theme_boost | + And I am on "Course 1" course homepage with editing mode on + And I add the "Navigation" block if not present + # Open all navigation nodes via keyboard because it does not use buttons/links chevrons. + And I click on "Actions menu" "link" in the "Navigation" "block" + And I press the escape key + And I press the tab key + And I press the multiply key + Then "Section 1" "link" should appear before "Assignment 1" "link" in the "Navigation" "block" + And "Assignment 1" "link" should appear before "Subsection 1" "link" in the "Navigation" "block" + And "Subsection 1" "link" should appear before "Page in Subsection 1" "link" in the "Navigation" "block" + And "Page in Subsection 1" "link" should appear before "Assignment 2" "link" in the "Navigation" "block"