MDL-80203 courseformat: Set action menu for delegated sections

Co-authored by: Ferran Recio <[email protected]>
This commit is contained in:
Sara Arjona
2024-04-09 15:28:16 +02:00
parent dfb6a3ce16
commit e188a28add
7 changed files with 223 additions and 27 deletions
+24 -2
View File
@@ -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.
*
@@ -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);
}
@@ -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;
}
/**
+20
View File
@@ -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);
}
}
+65 -2
View File
@@ -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 <[email protected]>
* @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);
}
}
+8 -8
View File
@@ -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();
+62
View File
@@ -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;
}
}
}