From dfb6a3ce160fd73b33cbf03e1e31342da45f302b Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Fri, 16 Feb 2024 16:24:12 +0100 Subject: [PATCH 1/2] MDL-80203 courseformat: Fix some typos and PHPDoc --- course/format/classes/output/local/content.php | 10 +--------- .../classes/output/local/content/cm/controlmenu.php | 6 +++--- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/course/format/classes/output/local/content.php b/course/format/classes/output/local/content.php index ea226af7ea3..8ff29d7a588 100644 --- a/course/format/classes/output/local/content.php +++ b/course/format/classes/output/local/content.php @@ -14,14 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Contains the main course format out class. - * - * @package core_courseformat - * @copyright 2020 Ferran Recio - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace core_courseformat\output\local; use core\output\named_templatable; @@ -39,7 +31,7 @@ use renderable; class content implements named_templatable, renderable { use courseformat_named_templatable; - /** @var core_courseformat\base the course format class */ + /** @var \core_courseformat\base the course format class */ protected $format; /** @var string the section format class */ diff --git a/course/format/classes/output/local/content/cm/controlmenu.php b/course/format/classes/output/local/content/cm/controlmenu.php index ce8f73b4081..f71a7938371 100644 --- a/course/format/classes/output/local/content/cm/controlmenu.php +++ b/course/format/classes/output/local/content/cm/controlmenu.php @@ -65,7 +65,7 @@ class controlmenu implements named_templatable, renderable { * * @param course_format $format the course format * @param section_info $section the section info - * @param cm_info $mod the course module ionfo + * @param cm_info $mod the course module info * @param array $displayoptions optional extra display options */ public function __construct(course_format $format, section_info $section, cm_info $mod, array $displayoptions = []) { @@ -106,11 +106,11 @@ class controlmenu implements named_templatable, renderable { } /** - * Generate the aciton menu element. + * Generate the action menu element. * * This method is public in case some block needs to modify the menu before output it. * @param \renderer_base $output typically, the renderer that's calling this function - * @return aciton_menu the activity action menu + * @return action_menu|null the activity action menu */ public function get_action_menu(\renderer_base $output): ?action_menu { From e188a28add43ac1b36d89d44bb6dd83483b5fe9f Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Fri, 16 Feb 2024 16:31:33 +0100 Subject: [PATCH 2/2] MDL-80203 courseformat: Set action menu for delegated sections Co-authored by: Ferran Recio --- .../format/classes/output/local/content.php | 26 ++++++- .../classes/output/local/content/section.php | 3 +- .../local/content/section/controlmenu.php | 56 ++++++++++++---- course/format/classes/sectiondelegate.php | 20 ++++++ course/format/tests/sectiondelegate_test.php | 67 ++++++++++++++++++- course/section.php | 16 ++--- lib/tests/fixtures/sectiondelegatetest.php | 62 +++++++++++++++++ 7 files changed, 223 insertions(+), 27 deletions(-) diff --git a/course/format/classes/output/local/content.php b/course/format/classes/output/local/content.php index 8ff29d7a588..b5121a5a2a7 100644 --- a/course/format/classes/output/local/content.php +++ b/course/format/classes/output/local/content.php @@ -46,6 +46,9 @@ class content implements named_templatable, renderable { /** @var string section selector class name */ protected $sectionselectorclass; + /** @var string the section control menu class */ + protected $sectioncontrolmenuclass; + /** @var string bulk editor bar toolbox */ protected $bulkedittoolsclass; @@ -66,13 +69,14 @@ class content implements named_templatable, renderable { $this->sectionnavigationclass = $format->get_output_classname('content\\sectionnavigation'); $this->sectionselectorclass = $format->get_output_classname('content\\sectionselector'); $this->bulkedittoolsclass = $format->get_output_classname('content\\bulkedittools'); + $this->sectioncontrolmenuclass = $format->get_output_classname('content\\section\\controlmenu'); } /** * 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 + * @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) { global $PAGE; @@ -117,6 +121,24 @@ class content implements named_templatable, renderable { return $data; } + /** + * Retrieves the action menu for the page header of the local content section. + * + * @param \renderer_base $output The renderer object used for rendering the action menu. + * @return string|null The rendered action menu HTML, null if page no action menu is available. + */ + public function get_page_header_action(\renderer_base $output): ?string { + $sectionid = $this->format->get_sectionid(); + if ($sectionid !== null) { + $modinfo = $this->format->get_modinfo(); + $sectioninfo = $modinfo->get_section_info_by_id($sectionid); + /** @var \core_courseformat\output\local\content\section\controlmenu */ + $controlmenu = new $this->sectioncontrolmenuclass($this->format, $sectioninfo); + return $output->render($controlmenu->get_action_menu($output)); + } + return null; + } + /** * Export sections array data. * diff --git a/course/format/classes/output/local/content/section.php b/course/format/classes/output/local/content/section.php index 75458fe44ac..7f1b1cba831 100644 --- a/course/format/classes/output/local/content/section.php +++ b/course/format/classes/output/local/content/section.php @@ -299,7 +299,8 @@ class section implements named_templatable, renderable { return false; } - if (empty($this->hidecontrols)) { + // In a single section page the control menu is located in the page header. + if (empty($this->hidecontrols) && $this->format->get_sectionid() != $this->section->id) { $controlmenu = new $this->controlmenuclass($this->format, $this->section); $data->controlmenu = $controlmenu->export_for_template($output); } diff --git a/course/format/classes/output/local/content/section/controlmenu.php b/course/format/classes/output/local/content/section/controlmenu.php index 63f2fa40d5f..3ae715f3e37 100644 --- a/course/format/classes/output/local/content/section/controlmenu.php +++ b/course/format/classes/output/local/content/section/controlmenu.php @@ -71,15 +71,50 @@ class controlmenu implements named_templatable, renderable { * @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)) { + $menu = $this->get_action_menu($output); + if (empty($menu)) { return new stdClass(); } + $data = (object)[ + 'menu' => $output->render($menu), + 'hasmenu' => true, + 'id' => $this->section->id, + ]; + + return $data; + } + + /** + * Generate the action menu element depending on the section. + * + * Sections controlled by a plugin will delegate the control menu to the plugin. + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @return action_menu|null the activity action menu or null if no action menu is available + */ + public function get_action_menu(\renderer_base $output): ?action_menu { + $sectiondelegate = $this->section->get_component_instance(); + if ($sectiondelegate) { + return $sectiondelegate->get_section_action_menu($this->format, $this, $output); + } + return $this->get_default_action_menu($output); + } + + /** + * Generate the default section action menu. + * + * This method is public in case some block needs to modify the menu before output it. + * + * @param \renderer_base $output typically, the renderer that's calling this function + * @return action_menu|null the activity action menu + */ + public function get_default_action_menu(\renderer_base $output): ?action_menu { + $controls = $this->section_control_items(); + if (empty($controls)) { + return null; + } + // Convert control array into an action_menu. $menu = new action_menu(); $menu->set_kebab_trigger(get_string('edit')); @@ -98,14 +133,7 @@ class controlmenu implements named_templatable, renderable { ); $menu->add($al); } - - $data = (object)[ - 'menu' => $output->render($menu), - 'hasmenu' => true, - 'id' => $section->id, - ]; - - return $data; + return $menu; } /** diff --git a/course/format/classes/sectiondelegate.php b/course/format/classes/sectiondelegate.php index ef7772af38e..2dcfcb17ee1 100644 --- a/course/format/classes/sectiondelegate.php +++ b/course/format/classes/sectiondelegate.php @@ -16,8 +16,12 @@ namespace core_courseformat; +use action_menu; +use renderer_base; use section_info; use core_courseformat\stateupdates; +use core_courseformat\output\local\content\section\controlmenu; +use core_courseformat\base as course_format; /** * Section delegate base class. @@ -104,4 +108,20 @@ abstract class sectiondelegate { public function put_section_state_extra_updates(section_info $section, stateupdates $updates): void { // By default, do nothing. } + + /** + * Allow delegate plugin to modify the available section menu. + * + * @param course_format $format The course format instance. + * @param controlmenu $controlmenu The control menu instance. + * @param renderer_base $output The renderer instance. + * @return action_menu|null The new action menu with the list of edit control items or null if no action menu is available. + */ + public function get_section_action_menu( + course_format $format, + controlmenu $controlmenu, + renderer_base $output, + ): ?action_menu { + return $controlmenu->get_default_action_menu($output); + } } diff --git a/course/format/tests/sectiondelegate_test.php b/course/format/tests/sectiondelegate_test.php index 06694296a3a..e3932342c0e 100644 --- a/course/format/tests/sectiondelegate_test.php +++ b/course/format/tests/sectiondelegate_test.php @@ -16,10 +16,12 @@ namespace core_courseformat; +use test_component\courseformat\sectiondelegate as testsectiondelegate; + /** - * Section delaegate tests. + * Section delegate tests. * - * @package core_course + * @package core_courseformat * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_courseformat\sectiondelegate @@ -73,9 +75,70 @@ class sectiondelegate_test extends \advanced_testcase { $this->assertNull(sectiondelegate::instance($sectioninfos[3])); } + /** + * Test has_delegate_class(). + * + * @covers ::has_delegate_class + */ public function test_has_delegate_class(): void { $this->assertFalse(sectiondelegate::has_delegate_class('missing_component')); $this->assertFalse(sectiondelegate::has_delegate_class('mod_label')); $this->assertTrue(sectiondelegate::has_delegate_class('test_component')); } + + /** + * Test get_section_action_menu(). + * + * @covers ::get_section_action_menu + */ + public function test_get_section_action_menu(): void { + global $DB, $PAGE; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]); + + $sectioninfo = formatactions::section($course)->create_delegated('test_component', 1); + + /** @var testsectiondelegate */ + $delegated = $sectioninfo->get_component_instance(); + + $format = course_get_format($course); + + $outputclass = $format->get_output_classname('content\\section\\controlmenu'); + /** @var \core_courseformat\output\local\content\section\controlmenu */ + $controlmenu = new $outputclass($format, $sectioninfo); + $renderer = $PAGE->get_renderer('format_' . $course->format); + $sectionmenu = $controlmenu->get_action_menu($renderer); + + // When the delegate class returns the same action menu, calculated from the given $controlmenu. + $result = $delegated->get_section_action_menu($format, $controlmenu, $renderer); + // The $result and $sectionmenu are the same but can't be compared directly because they have different ids. + $this->assertEquals( + count($result->get_primary_actions()), + count($sectionmenu->get_primary_actions()), + ); + $this->assertEquals( + count($result->get_secondary_actions()), + count($sectionmenu->get_secondary_actions()) + ); + $this->assertEquals( + $result->get_secondary_actions()[0]->url, + $sectionmenu->get_secondary_actions()[0]->url + ); + + // When the delegated class returns an empty action menu. + $delegated->set_section_action_menu(testsectiondelegate::MENUEMPTY); + $result = $delegated->get_section_action_menu($format, $controlmenu, $renderer); + // The $result and $sectionmenu are different. + $this->assertNotEquals( + count($result->get_secondary_actions()), + count($sectionmenu->get_secondary_actions()) + ); + + // When the delegated class return a null action menu. + $delegated->set_section_action_menu(null); + $result = $delegated->get_section_action_menu($format, $controlmenu, $renderer); + $this->assertNull($result); + } } diff --git a/course/section.php b/course/section.php index fbcd0dd1394..e7babd8666c 100644 --- a/course/section.php +++ b/course/section.php @@ -142,13 +142,15 @@ if (!empty($bulkbutton)) { $PAGE->add_header_action($bulkbutton); } +$outputclass = $format->get_output_classname('content'); +/** @var \core_courseformat\output\local\content */ +$sectionoutput = new $outputclass($format); + // Add to the header the control menu for the section. if ($format->show_editor()) { - $sectionclass = new \core_courseformat\output\local\content\section($format, $sectioninfo); - $renderable = $sectionclass->export_for_template($renderer); - if (property_exists($renderable->controlmenu, 'menu')) { - $controlmenuhtml = $renderable->controlmenu->menu; - $PAGE->add_header_action($controlmenuhtml); + $menu = $sectionoutput->get_page_header_action($renderer); + if ($menu) { + $PAGE->add_header_action($menu); } $sectionheading = $OUTPUT->container( $OUTPUT->render($format->inplace_editable_render_section_name($sectioninfo, false)), @@ -187,9 +189,7 @@ echo $renderer->container_start('course-content'); // Include course AJAX. include_course_ajax($course, $modinfo->get_used_module_names()); -$outputclass = $format->get_output_classname('content'); -$widget = new $outputclass($format); -echo $renderer->render($widget); +echo $renderer->render($sectionoutput); // Include course format javascript files. $jsfiles = $format->get_required_jsfiles(); diff --git a/lib/tests/fixtures/sectiondelegatetest.php b/lib/tests/fixtures/sectiondelegatetest.php index 9d5f706633f..1843bae90d2 100644 --- a/lib/tests/fixtures/sectiondelegatetest.php +++ b/lib/tests/fixtures/sectiondelegatetest.php @@ -16,9 +16,13 @@ namespace test_component\courseformat; +use core_courseformat\base as course_format; +use core_courseformat\output\local\content\section\controlmenu; use core_courseformat\sectiondelegate as sectiondelegatebase; use core_courseformat\stateupdates; use section_info; +use renderer_base; +use action_menu; /** * Test class for section delegate. @@ -28,6 +32,22 @@ use section_info; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sectiondelegate extends sectiondelegatebase { + + /** @var string force the default parent action menu. */ + public const MENUPARENT = 'parent'; + + /** @var string force an empty action menu. */ + public const MENUEMPTY = 'empty'; + + /** @var string force a null action menu. */ + public const MENUNULL = 'null'; + + /** + * @var string|null Status to define which action menu to return when calling get_section_action_menu(). + * Alternatively, different testing classes could be created, but it wasn't worth it for this case. + */ + protected ?string $actionmenustatus = self::MENUPARENT; + /** * Test method to fake preprocesses the section name by appending a suffix to it. * @@ -52,4 +72,46 @@ class sectiondelegate extends sectiondelegatebase { public function put_section_state_extra_updates(section_info $section, stateupdates $updates): void { $updates->add_cm_put($section->itemid); } + + /** + * Helper to change the behaviour of the get_section_action_menu(), for testing purposes, to return a different action menu + * based on the value of $actionmenustatus. For instance: + * - 'parent' returns the parent action menu. + * - 'empty' returns an empty action menu. + * - 'null' or null returns null action menu. + * + * @param string|null $actionmenustatus The status of the action menu. + */ + public function set_section_action_menu( + ?string $actionmenustatus, + ) { + $this->actionmenustatus = $actionmenustatus; + } + + /** + * Allow delegate plugin to modify the available section menu. + * By default, it returns the parent action menu. + * + * @param course_format $format The course format instance. + * @param controlmenu $controlmenu The control menu instance. + * @param renderer_base $output The renderer instance. + * @return action_menu|null The new action menu with the list of edit control items or null if no action menu is available. + */ + public function get_section_action_menu( + course_format $format, + controlmenu $controlmenu, + renderer_base $output, + ): ?action_menu { + switch ($this->actionmenustatus) { + case self::MENUPARENT: + return parent::get_section_action_menu($format, $controlmenu, $output); + + case self::MENUEMPTY: + return new action_menu(); + + case self::MENUNULL: + default: + return null; + } + } }