diff --git a/course/format/classes/output/local/content.php b/course/format/classes/output/local/content.php
index ea226af7ea3..b5121a5a2a7 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 */
@@ -54,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;
@@ -74,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;
@@ -125,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/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 {
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;
+ }
+ }
}