MDL-82510 course: Add new delegated action menu

This commit is contained in:
Amaia Anabitarte 2024-07-11 15:38:16 +02:00
parent 554a790bf0
commit ff6edc2e9f
9 changed files with 513 additions and 113 deletions

View File

@ -0,0 +1,9 @@
issueNumber: MDL-82510
notes:
core_courseformat:
- message: >-
New \core_courseformat\output\local\content\basecontrolmenu class
has been created. Existing \core_courseformat\output\local\content\cm\controlmenu
and \core_courseformat\output\local\content\section\controlmenu classes extend
the new \core_courseformat\output\local\content\basecontrolmenu class.
type: improved

View File

@ -0,0 +1,9 @@
issueNumber: MDL-82510
notes:
core_courseformat:
- message: >-
New \core_courseformat\output\local\content\cm\delegatedcontrolmenu class
has been created extending
\core_courseformat\output\local\content\basecontrolmenu class to render
delegated section action menu combining section and module action menu.
type: improved

View File

@ -0,0 +1,163 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_courseformat\output\local\content;
use action_menu;
use action_menu_link_secondary;
use core\output\named_templatable;
use core_courseformat\base as course_format;
use core_courseformat\output\local\courseformat_named_templatable;
use moodle_url;
use pix_icon;
use renderable;
use section_info;
use cm_info;
use stdClass;
/**
* Base class to render course element controls.
*
* @package core_courseformat
* @copyright 2024 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class basecontrolmenu implements named_templatable, renderable {
use courseformat_named_templatable;
/** @var course_format the course format class */
protected $format;
/** @var section_info the course section class */
protected $section;
/** @var cm_info the course module class */
protected $mod;
/** @var string the menu ID */
protected $menuid;
/** @var action_menu the action menu */
protected $menu;
/**
* Constructor.
*
* @param course_format $format the course format
* @param section_info $section the section info
* @param cm_info|null $mod the module info
* @param string $menuid the ID value for the menu
*/
public function __construct(course_format $format, section_info $section, ?cm_info $mod = null, string $menuid = '') {
$this->format = $format;
$this->section = $section;
$this->mod = $mod;
$this->menuid = $menuid;
}
/**
* 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
* @return array data context for a mustache template
*/
public function export_for_template(\renderer_base $output): stdClass {
$menu = $this->get_action_menu($output);
if (empty($menu)) {
return new stdClass();
}
$data = (object)[
'menu' => $output->render($menu),
'hasmenu' => true,
'id' => $this->menuid,
];
return $data;
}
/**
* Generate the action menu element.
*
* @param \renderer_base $output typically, the renderer that's calling this function
* @return action_menu|null the action menu or null if no action menu is available
*/
public function get_action_menu(\renderer_base $output): ?action_menu {
if (!empty($this->menu)) {
return $this->menu;
}
$this->menu = $this->get_default_action_menu($output);
return $this->menu;
}
/**
* Generate the default 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 action menu
*/
public function get_default_action_menu(\renderer_base $output): ?action_menu {
return null;
}
/**
* Format control array into an action_menu.
*
* @param \renderer_base $output typically, the renderer that's calling this function
* @return action_menu|null the action menu
*/
protected function format_controls(array $controls): ?action_menu {
if (empty($controls)) {
return null;
}
$menu = new action_menu();
$menu->set_kebab_trigger(get_string('edit'));
$menu->attributes['class'] .= ' section-actions';
$menu->attributes['data-sectionid'] = $this->section->id;
foreach ($controls as $value) {
$url = empty($value['url']) ? '' : $value['url'];
$icon = empty($value['icon']) ? '' : $value['icon'];
$name = empty($value['name']) ? '' : $value['name'];
$attr = empty($value['attr']) ? [] : $value['attr'];
$class = empty($value['pixattr']['class']) ? '' : $value['pixattr']['class'];
$al = new action_menu_link_secondary(
new moodle_url($url),
new pix_icon($icon, '', null, ['class' => "smallicon " . $class]),
$name,
$attr
);
$menu->add($al);
}
return $menu;
}
/**
* Generate the edit control items of a section.
*
* This method must remain public until the final deprecation of section_edit_control_items.
*
* @return array of edit control items
*/
public function section_control_items() {
return [];
}
}

View File

@ -27,10 +27,9 @@ namespace core_courseformat\output\local\content\cm;
use action_menu;
use action_menu_link;
use cm_info;
use core\output\named_templatable;
use core_courseformat\base as course_format;
use core_courseformat\output\local\content\basecontrolmenu;
use core_courseformat\output\local\courseformat_named_templatable;
use renderable;
use section_info;
use stdClass;
@ -41,21 +40,7 @@ use stdClass;
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class controlmenu implements named_templatable, renderable {
use courseformat_named_templatable;
/** @var course_format the course format */
protected $format;
/** @var section_info the section object */
private $section;
/** @var action_menu the activity aciton menu */
protected $menu;
/** @var cm_info the course module instance */
protected $mod;
class controlmenu extends basecontrolmenu {
/** @var array optional display options */
protected $displayoptions;
@ -69,9 +54,7 @@ class controlmenu implements named_templatable, renderable {
* @param array $displayoptions optional extra display options
*/
public function __construct(course_format $format, section_info $section, cm_info $mod, array $displayoptions = []) {
$this->format = $format;
$this->section = $section;
$this->mod = $mod;
parent::__construct($format, $section, $mod, $mod->id);
$this->displayoptions = $displayoptions;
}
@ -94,7 +77,7 @@ class controlmenu implements named_templatable, renderable {
$data = (object)[
'menu' => $menu->export_for_template($output),
'hasmenu' => true,
'id' => $mod->id,
'id' => $this->menuid,
];
// After icons.
@ -120,6 +103,14 @@ class controlmenu implements named_templatable, renderable {
$mod = $this->mod;
// In case module is delegating a section, we should return delegated section action menu.
if ($delegated = $mod->get_delegated_section_info()) {
$controlmenuclass = $this->format->get_output_classname('content\\cm\\delegatedcontrolmenu');
$controlmenu = new $controlmenuclass($this->format, $delegated, $mod);
return $controlmenu->get_action_menu($output);
}
$controls = $this->cm_control_items();
if (empty($controls)) {

View File

@ -0,0 +1,170 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_courseformat\output\local\content\cm;
use action_menu;
use context_course;
use core_courseformat\base as course_format;
use core_courseformat\output\local\content\basecontrolmenu;
use moodle_url;
use section_info;
use cm_info;
/**
* Base class to render delegated section controls.
*
* @package core_courseformat
* @copyright 2024 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class delegatedcontrolmenu extends basecontrolmenu {
/**
* Constructor.
*
* @param course_format $format the course format
* @param section_info $section the section info
* @param cm_info $mod the module info
*/
public function __construct(course_format $format, section_info $section, cm_info $mod) {
parent::__construct($format, $section, $mod, $section->id);
}
/**
* Generate the default delegated 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 action menu
*/
public function get_default_action_menu(\renderer_base $output): ?action_menu {
$controls = $this->delegated_control_items();
return $this->format_controls($controls);
}
/**
* Generate the edit control items of a section.
*
* It is not clear this kind of controls are still available in 4.0 so, for now, this
* method is almost a clone of the previous section_control_items from the course/renderer.php.
*
* This method must remain public until the final deprecation of section_edit_control_items.
*
* @return array of edit control items
*/
public function delegated_control_items() {
global $USER;
$format = $this->format;
$section = $this->section;
$cm = $this->mod;
$course = $format->get_course();
$sectionreturn = !is_null($format->get_sectionid()) ? $format->get_sectionnum() : null;
$user = $USER;
$usecomponents = $format->supports_components();
$coursecontext = context_course::instance($course->id);
$baseurl = course_get_url($course, $sectionreturn);
$baseurl->param('sesskey', sesskey());
$cmbaseurl = new moodle_url('/course/mod.php');
$cmbaseurl->param('sesskey', sesskey());
$hasmanageactivities = has_capability('moodle/course:manageactivities', $coursecontext);
$isheadersection = $format->get_sectionid() == $section->id;
$controls = [];
// Only show the view link if we are not already in the section view page.
if (!$isheadersection) {
$controls['view'] = [
'url' => new moodle_url('/course/section.php', ['id' => $section->id]),
'icon' => 'i/viewsection',
'name' => get_string('view'),
'pixattr' => ['class' => ''],
'attr' => ['class' => 'view'],
];
}
if (has_capability('moodle/course:update', $coursecontext, $user)) {
$params = ['id' => $section->id];
$params['sr'] = $section->section;
if (get_string_manager()->string_exists('editsection', 'format_'.$format->get_format())) {
$streditsection = get_string('editsection', 'format_'.$format->get_format());
} else {
$streditsection = get_string('editsection');
}
// Edit settings goes to section settings form.
$controls['edit'] = [
'url' => new moodle_url('/course/editsection.php', $params),
'icon' => 'i/settings',
'name' => $streditsection,
'pixattr' => ['class' => ''],
'attr' => ['class' => 'edit'],
];
}
// Delete deletes the module.
// Only show the view link if we are not already in the section view page.
if (!$isheadersection && $hasmanageactivities) {
$url = clone($cmbaseurl);
$url->param('delete', $cm->id);
$url->param('sr', $cm->sectionnum);
$controls['delete'] = [
'url' => $url,
'icon' => 't/delete',
'name' => get_string('delete'),
'pixattr' => ['class' => ''],
'attr' => [
'class' => 'editing_delete text-danger',
'data-action' => ($usecomponents) ? 'cmDelete' : 'delete',
'data-sectionreturn' => $sectionreturn,
'data-id' => $cm->id,
],
];
}
// Add section page permalink.
if (
has_any_capability([
'moodle/course:movesections',
'moodle/course:update',
'moodle/course:sectionvisibility',
], $coursecontext)
) {
$sectionlink = new moodle_url(
'/course/section.php',
['id' => $section->id]
);
$controls['permalink'] = [
'url' => $sectionlink,
'icon' => 'i/link',
'name' => get_string('sectionlink', 'course'),
'pixattr' => ['class' => ''],
'attr' => [
'data-action' => 'permalink',
],
];
}
return $controls;
}
}

View File

@ -14,27 +14,14 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the default section controls output class.
*
* @package core_courseformat
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_courseformat\output\local\content\section;
use action_menu;
use action_menu_link_secondary;
use context_course;
use core\output\named_templatable;
use core_courseformat\base as course_format;
use core_courseformat\output\local\courseformat_named_templatable;
use core_courseformat\output\local\content\basecontrolmenu;
use moodle_url;
use pix_icon;
use renderable;
use section_info;
use stdClass;
/**
* Base class to render section controls.
@ -43,15 +30,7 @@ use stdClass;
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class controlmenu implements named_templatable, renderable {
use courseformat_named_templatable;
/** @var course_format the course format class */
protected $format;
/** @var section_info the course section class */
protected $section;
class controlmenu extends basecontrolmenu {
/**
* Constructor.
@ -60,40 +39,23 @@ class controlmenu implements named_templatable, renderable {
* @param section_info $section the section info
*/
public function __construct(course_format $format, section_info $section) {
$this->format = $format;
$this->section = $section;
}
/**
* 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
* @return array data context for a mustache template
*/
public function export_for_template(\renderer_base $output): stdClass {
$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;
parent::__construct($format, $section, null, $section->id);
}
/**
* Generate the action menu element depending on the section.
*
* Sections controlled by a plugin will delegate the control menu to the plugin.
* Sections controlled by a plugin will delegate the control menu to the delegated section class.
*
* @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
* @return action_menu|null the section action menu or null if no action menu is available
*/
public function get_action_menu(\renderer_base $output): ?action_menu {
if (!empty($this->menu)) {
return $this->menu;
}
$sectiondelegate = $this->section->get_component_instance();
if ($sectiondelegate) {
return $sectiondelegate->get_section_action_menu($this->format, $this, $output);
@ -107,34 +69,11 @@ class controlmenu implements named_templatable, renderable {
* 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
* @return action_menu|null the section 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'));
$menu->attributes['class'] .= ' section-actions';
$menu->attributes['data-sectionid'] = $this->section->id;
foreach ($controls as $value) {
$url = empty($value['url']) ? '' : $value['url'];
$icon = empty($value['icon']) ? '' : $value['icon'];
$name = empty($value['name']) ? '' : $value['name'];
$attr = empty($value['attr']) ? [] : $value['attr'];
$class = empty($value['pixattr']['class']) ? '' : $value['pixattr']['class'];
$al = new action_menu_link_secondary(
new moodle_url($url),
new pix_icon($icon, '', null, ['class' => "smallicon " . $class]),
$name,
$attr
);
$menu->add($al);
}
return $menu;
return $this->format_controls($controls);
}
/**

View File

@ -1,8 +1,8 @@
@mod @mod_subsection
Feature: The module menu replaces the section menu when accessing the subsection page
Feature: The module menu replaces the delegated section menu
In order to use subsections
As an teacher
I need to see the module action menu in the section page.
I need to see the delegated section action menu instead of module menu.
Background:
Given I enable "subsection" "mod" plugin
@ -10,8 +10,8 @@ Feature: The module menu replaces the section menu when accessing the subsection
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | numsections |
| Course 1 | C1 | 0 | 2 |
| fullname | shortname | category | numsections | initsections |
| Course 1 | C1 | 0 | 2 | 1 |
And the following "activity" exists:
| activity | subsection |
| name | Subsection1 |
@ -31,22 +31,129 @@ Feature: The module menu replaces the section menu when accessing the subsection
When I click on "Edit" "icon" in the "[data-region='header-actions-container']" "css_element"
Then I should not see "Move right"
And I should not see "Assign roles"
And I should not see "Permalink"
And I should not see "Highlight"
And I should see "Edit settings"
And I should see "Move"
And I should see "Hide"
And I should see "Duplicate"
And I should see "Delete"
# Duplicate, Move and Show/Hide are not implemented yet.
And I should not see "Move"
And I should not see "Duplicate"
And I should not see "Hide"
# Delete option for subsection page is not implemented yet.
And I should not see "Delete"
And I should see "Permalink"
@javascript
Scenario: The action menu for subsection module has less options thant a regular activity
Scenario: The action menu for subsection module has less options than a regular activity
Given I turn editing mode on
When I open "Subsection1" actions menu
Then I should not see "Move right"
And I should not see "Assign roles"
And I should not see "Highlight"
And I should see "View"
And I should see "Edit settings"
And I should see "Move"
And I should see "Hide"
And I should see "Duplicate"
# Duplicate, Move and Show/Hide are not implemented yet.
And I should not see "Move"
And I should not see "Duplicate"
And I should not see "Hide"
And I should see "Delete"
And I should see "Permalink"
@javascript
Scenario: The action menu for subsection module in section page has less options than a regular activity
Given I click on "Section 1" "link"
And I turn editing mode on
When I open "Subsection1" actions menu
Then I should not see "Move right"
And I should not see "Assign roles"
And I should not see "Highlight"
And I should see "View"
And I should see "Edit settings"
# Duplicate, Move and Show/Hide are not implemented yet.
And I should not see "Move"
And I should not see "Duplicate"
And I should not see "Hide"
And I should see "Delete"
And I should see "Permalink"
@javascript
Scenario: View option in subsection action menu
Given I turn editing mode on
And I open "Subsection1" actions menu
When I choose "View" in the open action menu
# Subsection page. Subsection name should be the title.
Then I should see "Subsection1" in the "h1" "css_element"
And "Section 1" "text" should exist in the ".breadcrumb" "css_element"
# Open the section header action menu.
And I click on "Edit" "icon" in the "[data-region='header-actions-container']" "css_element"
And "View" "link" should not exist in the "[data-region='header-actions-container']" "css_element"
And I click on "Section 1" "link" in the ".breadcrumb" "css_element"
# Section page. Section name should be the title.
And I should see "Section 1" in the "h1" "css_element"
And "Subsection1" "text" should not exist in the ".breadcrumb" "css_element"
# Open the section header action menu.
And I open "Subsection1" actions menu
And I choose "View" in the open action menu
And I should see "Subsection1" in the "h1" "css_element"
@javascript
Scenario: Edit settings option in subsection action menu
Given I turn editing mode on
And I open "Subsection1" actions menu
When I choose "Edit settings" in the open action menu
And the field "Section name" matches value "Subsection1"
And I click on "Cancel" "button"
And I am on the "Subsection1" "subsection activity" page
# Subsection page. Open the section header action menu.
And I click on "Edit" "icon" in the "[data-region='header-actions-container']" "css_element"
And I choose "Edit settings" in the open action menu
And the field "Section name" matches value "Subsection1"
And I click on "Cancel" "button"
And I am on the "C1 > Section 1" "course > section" page
# Section page. Open Subsection1 module action menu.
And I open "Subsection1" actions menu
And I choose "Edit settings" in the open action menu
And the field "Section name" matches value "Subsection1"
@javascript
Scenario: Permalink option in subsection action menu
Given I turn editing mode on
And I open "Subsection1" actions menu
When I choose "Permalink" in the open action menu
Then I click on "Copy to clipboard" "link"
And I should see "Text copied to clipboard"
And I am on the "Subsection1" "subsection activity" page
# Subsection page. Open the section header action menu.
And I click on "Edit" "icon" in the "[data-region='header-actions-container']" "css_element"
And I choose "Permalink" in the open action menu
And I click on "Copy to clipboard" "link"
And I should see "Text copied to clipboard"
And I am on the "C1 > Section 1" "course > section" page
# Section page. Open Subsection1 module action menu.
And I open "Subsection1" actions menu
And I choose "Permalink" in the open action menu
And I click on "Copy to clipboard" "link"
And I should see "Text copied to clipboard"
@javascript
Scenario: Delete option in subsection action menu
Given the following "activities" exist:
| activity | course | idnumber | name | intro | section |
| subsection | C1 | subsection2 | Subsection2 | Test Subsection2 | 1 |
| subsection | C1 | subsection3 | Subsection3 | Test Subsection3 | 1 |
Given I turn editing mode on
And "Subsection1" "link" should exist
And "Subsection2" "link" should exist
And "Subsection3" "link" should exist
And I open "Subsection1" actions menu
When I choose "Delete" in the open action menu
And I click on "Delete" "button" in the "Delete activity?" "dialogue"
And "Subsection1" "link" should not exist in the "#region-main-box" "css_element"
And I am on the "C1 > Section 1" "course > section" page
# Section page. Open Subsection2 module action menu.
And I open "Subsection2" actions menu
And I choose "Delete" in the open action menu
And I click on "Delete" "button" in the "Delete activity?" "dialogue"
And "Subsection2" "link" should not exist in the "#region-main-box" "css_element"
And I am on the "Subsection3" "subsection activity" page
# Subsection page. Open the section header action menu.
And I click on "Edit" "icon" in the "[data-region='header-actions-container']" "css_element"
And "Delete" "link" should not exist in the "[data-region='header-actions-container']" "css_element"

View File

@ -32,12 +32,12 @@ Feature: Teachers can rename subsections
And I should see "New name" in the "page" "region"
And I should see "Subactivity" in the "region-main" "region"
Scenario: Renaming the activity using the settings form rename the subsection name
Scenario: Renaming the subsection using the settings form renames the module
Given I should see "Subsection activity" in the "page-content" "region"
When I click on "Edit settings" "link" in the "Subsection activity" "activity"
And I set the following fields to these values:
| Name | New name |
And I press "Save and display"
| Section name | New name |
And I press "Save changes"
Then I should see "New name" in the "page" "region"
And I should see "Subactivity" in the "region-main" "region"
And I am on "Course 1" course homepage

View File

@ -64,12 +64,24 @@ final class sectiondelegate_test extends \advanced_testcase {
$controlmenu = new $outputclass($format, $sectioninfo);
$renderer = $PAGE->get_renderer('format_' . $course->format);
// Highlight is only present in section menu (not module), so they shouldn't be found in the result.
// Duplicate is not implemented yet, so they shouldn't be found in the result.
// The possible options are: View, Edit, Delete and Permalink.
if (get_string_manager()->string_exists('editsection', 'format_'.$format->get_format())) {
$streditsection = get_string('editsection', 'format_'.$format->get_format());
} else {
$streditsection = get_string('editsection');
}
$allowedoptions = [
get_string('view'),
$streditsection,
get_string('delete'),
get_string('sectionlink', 'course'),
];
// The default section menu should be different for the delegated section menu.
$result = $delegated->get_section_action_menu($format, $controlmenu, $renderer);
foreach ($result->get_secondary_actions() as $secondaryaction) {
// Highlight and Permalink are only present in section menu (not module), so they shouldn't be find in the result.
$this->assertNotEquals(get_string('highlight'), $secondaryaction->text);
$this->assertNotEquals(get_string('sectionlink', 'course'), $secondaryaction->text);
$this->assertContains($secondaryaction->text, $allowedoptions);
}
}
}