From 08e06c45803a13f24c1ae54c769dcd6d526ca258 Mon Sep 17 00:00:00 2001 From: ferran Date: Wed, 23 Oct 2024 16:38:06 +0200 Subject: [PATCH] MDL-83527 core_courseformat: refactor course editor action menus As part of the course format cleanup epic, all course editor action menu output classes needs to be refactored to be more mantainable and reusable. Also, it added notes for the final deprecation of many lines of code in Moodle 6.0. --- .../output/local/content/basecontrolmenu.php | 125 +++++-- .../output/local/content/cm/controlmenu.php | 310 +++++++++++++++++- .../local/content/cm/delegatedcontrolmenu.php | 249 +++++++++++++- .../local/content/section/controlmenu.php | 272 ++++++++++++++- .../content/section/controlmenu.php | 106 ++++-- course/lib.php | 12 + 6 files changed, 983 insertions(+), 91 deletions(-) diff --git a/course/format/classes/output/local/content/basecontrolmenu.php b/course/format/classes/output/local/content/basecontrolmenu.php index 062edd12be02a..e771d0dfe5044 100644 --- a/course/format/classes/output/local/content/basecontrolmenu.php +++ b/course/format/classes/output/local/content/basecontrolmenu.php @@ -16,14 +16,16 @@ namespace core_courseformat\output\local\content; -use action_menu; -use action_menu_link_secondary; +use core\context\course as context_course; +use core\output\action_menu; +use core\output\action_menu\link_secondary as action_menu_link_secondary; use core\output\named_templatable; +use core\output\pix_icon; +use core\output\renderable; +use core\output\renderer_base; use core_courseformat\base as course_format; use core_courseformat\output\local\courseformat_named_templatable; -use moodle_url; -use pix_icon; -use renderable; +use core\url; use section_info; use cm_info; use stdClass; @@ -48,12 +50,21 @@ abstract class basecontrolmenu implements named_templatable, renderable { /** @var cm_info the course module class */ protected $mod; + /** @var stdClass the course instance */ + protected stdClass $course; + + /** @var context_course the course context */ + protected $coursecontext; + /** @var string the menu ID */ protected $menuid; /** @var action_menu the action menu */ protected $menu; + /** @var url The base URL for the course or the section */ + protected url $baseurl; + /** * Constructor. * @@ -67,15 +78,18 @@ public function __construct(course_format $format, section_info $section, ?cm_in $this->section = $section; $this->mod = $mod; $this->menuid = $menuid; + $this->course = $format->get_course(); + $this->coursecontext = $format->get_context(); + $this->baseurl = $format->get_view_url($format->get_sectionnum(), ['navigation' => true]); } /** * 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 + * @param renderer_base $output typically, the renderer that's calling this function * @return null|array data context for a mustache template */ - public function export_for_template(\renderer_base $output): ?stdClass { + public function export_for_template(renderer_base $output): ?stdClass { $menu = $this->get_action_menu($output); if (empty($menu)) { return new stdClass(); @@ -93,10 +107,10 @@ public function export_for_template(\renderer_base $output): ?stdClass { /** * Generate the action menu element. * - * @param \renderer_base $output typically, the renderer that's calling this function + * @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 { + public function get_action_menu(renderer_base $output): ?action_menu { if (!empty($this->menu)) { return $this->menu; @@ -111,10 +125,10 @@ public function get_action_menu(\renderer_base $output): ?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 + * @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 { + public function get_default_action_menu(renderer_base $output): ?action_menu { return null; } @@ -133,23 +147,57 @@ protected function format_controls(array $controls): ?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); + foreach ($controls as $item) { + // Actions not available for the user can be null. + if ($item === null) { + continue; + } + + // TODO remove this if as part of MDL-83530. + if (is_array($item)) { + // Some third party formats from 4.5 and older can use array to define the action menu items. + $item = $this->normalize_action_menu_link($item); + } + + $menu->add($item); } return $menu; } + /** + * Nromalize the action menu item, or return null if it is not possible. + * + * Traditionally, this class uses array to define the action menu items, + * for backward compatibility, this method will normalize the array into + * te correct action_menu_link object. + * + * @todo Remove this method in Moodle 6.0 (MDL-83530). + * @param array|null $itemdata the item data + * @return void + */ + private function normalize_action_menu_link( + array|null $itemdata + ): ?action_menu_link_secondary { + debugging( + "Using arrays as action menu items is deprecated, use a compatible menu item instead.", + DEBUG_DEVELOPER + ); + if (empty($itemdata)) { + return null; + } + $url = empty($itemdata['url']) ? '' : $itemdata['url']; + $icon = empty($itemdata['icon']) ? '' : $itemdata['icon']; + $name = empty($itemdata['name']) ? '' : $itemdata['name']; + $attr = empty($itemdata['attr']) ? [] : $itemdata['attr']; + $class = empty($itemdata['pixattr']['class']) ? '' : $itemdata['pixattr']['class']; + return new action_menu_link_secondary( + url: new url($url), + icon: new pix_icon($icon, '', null, ['class' => "smallicon " . $class]), + text: $name, + attributes: $attr, + ); + } + /** * Generate the edit control items of a section. * @@ -160,4 +208,33 @@ protected function format_controls(array $controls): ?action_menu { public function section_control_items() { return []; } + + /** + * Adds a new control item after a given control item. + * + * If the control item is not found, the new control item is added at the beginning. + * + * @param array $controls array of edit control items + * @param string $aftername name of the control item after which the new control item will be added + * @param string $newkey key of the new control item + * @param mixed $newcontrol new control item to be added (anything compatible with an action menu or null) + */ + protected function add_control_after(array $controls, string $aftername, string $newkey, mixed $newcontrol): array { + if (!array_key_exists($aftername, $controls)) { + return array_merge([$newkey => $newcontrol], $controls); + } + $newcontrols = []; + $found = false; + foreach ($controls as $keyname => $control) { + $newcontrols[$keyname] = $control; + if ($keyname === $aftername) { + $newcontrols[$newkey] = $newcontrol; + $found = true; + } + } + if (!$found) { + $newcontrols[$newkey] = $newcontrol; + } + return $newcontrols; + } } diff --git a/course/format/classes/output/local/content/cm/controlmenu.php b/course/format/classes/output/local/content/cm/controlmenu.php index 7e4a8e47f84e9..c0dd0c10f7342 100644 --- a/course/format/classes/output/local/content/cm/controlmenu.php +++ b/course/format/classes/output/local/content/cm/controlmenu.php @@ -24,12 +24,18 @@ namespace core_courseformat\output\local\content\cm; -use action_menu; -use action_menu_link; use cm_info; +use core\context\module as context_module; +use core\output\action_menu; +use core\output\action_menu\link as action_menu_link; +use core\output\action_menu\link_secondary as action_menu_link_secondary; +use core\output\action_menu\subpanel as action_menu_subpanel; +use core\output\pix_icon; +use core\output\renderer_base; use core_courseformat\base as course_format; use core_courseformat\output\local\content\basecontrolmenu; -use core_courseformat\output\local\courseformat_named_templatable; +use core_courseformat\sectiondelegate; +use core\url; use section_info; use stdClass; @@ -45,6 +51,12 @@ class controlmenu extends basecontrolmenu { /** @var array optional display options */ protected $displayoptions; + /** @var context_module|null modcontext the module context if any */ + protected ?context_module $modcontext = null; + + /** @var bool $canmanageactivities Optimization to know if the user can manage activities */ + protected bool $canmanageactivities; + /** * Constructor. * @@ -56,15 +68,18 @@ class controlmenu extends basecontrolmenu { public function __construct(course_format $format, section_info $section, cm_info $mod, array $displayoptions = []) { parent::__construct($format, $section, $mod, $mod->id); $this->displayoptions = $displayoptions; + + $this->modcontext = context_module::instance($mod->id); + $this->canmanageactivities = has_capability('moodle/course:manageactivities', $this->modcontext); } /** * 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 + * @param renderer_base $output typically, the renderer that's calling this function * @return stdClass|null data context for a mustache template */ - public function export_for_template(\renderer_base $output): ?stdClass { + public function export_for_template(renderer_base $output): ?stdClass { $mod = $this->mod; @@ -96,25 +111,295 @@ public function export_for_template(\renderer_base $output): ?stdClass { * 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 + * @param renderer_base $output typically, the renderer that's calling this function * @return action_menu|null the activity action menu */ - public function get_action_menu(\renderer_base $output): ?action_menu { + public function get_action_menu(renderer_base $output): ?action_menu { if (!empty($this->menu)) { return $this->menu; } - $mod = $this->mod; - // In case module is delegating a section, we should return delegated section action menu. - if ($delegated = $mod->get_delegated_section_info()) { + if ($delegated = $this->mod->get_delegated_section_info()) { $controlmenuclass = $this->format->get_output_classname('content\\cm\\delegatedcontrolmenu'); - $controlmenu = new $controlmenuclass($this->format, $delegated, $mod); - + $controlmenu = new $controlmenuclass($this->format, $delegated, $this->mod); return $controlmenu->get_action_menu($output); } + // TODO remove this if as part of MDL-83530. + if (!$this->format->supports_components()) { + $this->menu = $this->get_action_menu_legacy($output); + return $this->menu; + } + + $controls = $this->get_cm_control_items(); + return $this->format_controls($controls); + } + + /** + * Generate the edit control items of a course module. + * + * This method uses course_get_cm_edit_actions function to get the cm actions. + * However, format plugins can override the method to add or remove elements + * from the menu. + * + * @return array of edit control items + */ + protected function get_cm_control_items(): ?array { + $controls = []; + + $controls['update'] = $this->get_cm_edit_item(); + $controls['move'] = $this->get_cm_move_item(); + $controls['moveright'] = $this->get_cm_moveend_item(); + $controls['moveleft'] = $this->get_cm_movestart_item(); + $controls['availability'] = $this->get_cm_visibility_item(); + $controls['duplicate'] = $this->get_cm_duplicate_item(); + $controls['assign'] = $this->get_cm_assign_item(); + $controls['groupmode'] = $this->get_cm_groupmode_item(); + $controls['delete'] = $this->get_cm_delete_item(); + + return $controls; + } + + protected function get_cm_edit_item(): ?action_menu_link { + if (!$this->canmanageactivities) { + return null; + } + + $url = new url( + '/course/mod.php', + [ + 'sesskey' => sesskey(), + 'sr' => $this->mod->sectionnum, + 'update' => $this->mod->id, + ], + ); + + return new action_menu_link_secondary( + url: $url, + icon: new pix_icon('i/settings', ''), + text: get_string('editsettings'), + attributes: [ + 'class' => 'editing_update', + 'data-action' => 'update', + ], + ); + } + + /** + * Generates the move item for a course module. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_move_item(): ?action_menu_link { + // Only show the move link if we are not already in the section view page. + if (!$this->canmanageactivities) { + return null; + } + return new action_menu_link_secondary( + url: $this->baseurl, + icon: new pix_icon('i/dragdrop', ''), + text: get_string('move'), + attributes: [ + // This tool requires ajax and will appear only when the frontend state is ready. + 'class' => 'editing_movecm waitstate', + 'data-action' => 'moveCm', + 'data-id' => $this->mod->id, + ], + ); + } + + protected function can_indent_cm(): bool { + return $this->canmanageactivities + && !sectiondelegate::has_delegate_class('mod_'.$this->mod->modname) + && empty($this->displayoptions['disableindentation']) + && $this->format->uses_indentation(); + } + + protected function get_cm_moveend_item(): ?action_menu_link { + if (!$this->can_indent_cm() || $this->mod->indent > 0) { + return null; + } + + // NOPE + $url = new url( + '/course/mod.php', + [ + 'sesskey' => sesskey(), + 'sr' => $this->mod->sectionnum, + 'id' => $this->mod->id, + 'indent' => 1, + ], + ); + + $icon = (right_to_left()) ? 't/left' : 't/right'; + + return new action_menu_link_secondary( + url: new url($this->baseurl, ['id' => $this->mod->id, 'indent' => '1']), + icon: new pix_icon($icon, ''), + text: get_string('moveright'), + attributes: [ + 'class' => 'editing_moveright', + 'data-action' => 'cmMoveRight', + 'data-keepopen' => true, + 'data-sectionreturn' => $this->format->get_sectionnum(), + 'data-id' => $this->mod->id, + ], + ); + } + + protected function get_cm_movestart_item(): ?action_menu_link { + if (!$this->can_indent_cm() || $this->mod->indent <= 0) { + return null; + } + + // NOPE + $url = new url( + '/course/mod.php', + [ + 'sesskey' => sesskey(), + 'sr' => $this->mod->sectionnum, + 'id' => $this->mod->id, + 'indent' => -1, + ], + ); + + $icon = (right_to_left()) ? 't/right' : 't/left'; + + return new action_menu_link_secondary( + url: new url($this->baseurl, ['id' => $this->mod->id, 'indent' => '-1']), + icon: new pix_icon($icon, ''), + text: get_string('moveleft'), + attributes: [ + 'class' => 'editing_moveleft', + 'data-action' => 'cmMoveLeft', + 'data-keepopen' => true, + 'data-sectionreturn' => $this->format->get_sectionnum(), + 'data-id' => $this->mod->id, + ], + ); + } + + protected function get_cm_visibility_item(): ?action_menu_link { + if (!has_capability('moodle/course:activityvisibility', $this->modcontext)) { + return null; + } + $outputclass = $this->format->get_output_classname('content\\cm\\visibility'); + /** @var \core_courseformat\output\local\content\cm\visibility $availability */ + $output = new $outputclass($this->format, $this->section, $this->mod); + return $output->get_menu_item(); + } + + protected function get_cm_duplicate_item(): ?action_menu_link { + if ( + !has_all_capabilities( + ['moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport'], + $this->coursecontext + ) + || !plugin_supports('mod', $this->mod->modname, FEATURE_BACKUP_MOODLE2) + || !course_allowed_module($this->mod->get_course(), $this->mod->modname) + ) { + return null; + } + + return new action_menu_link_secondary( + url: new url($this->baseurl, ['duplicate' => $this->mod->id]), + icon: new pix_icon('t/copy', ''), + text: get_string('duplicate'), + attributes: [ + 'class' => 'editing_duplicate', + 'data-action' => 'cmDuplicate', + 'data-sectionreturn' => $this->format->get_sectionnum(), + 'data-id' => $this->mod->id, + ], + ); + } + + protected function get_cm_assign_item(): ?action_menu_link { + if ( + !has_capability('moodle/role:assign', $this->modcontext) + || sectiondelegate::has_delegate_class('mod_'.$this->mod->modname) + ) { + return null; + } + + return new action_menu_link_secondary( + url: new url('/admin/roles/assign.php', ['contextid' => $this->modcontext->id]), + icon: new pix_icon('t/assignroles', ''), + text: get_string('assignroles', 'role'), + attributes: [ + 'class' => 'editing_assign', + 'data-action' => 'assignroles', + 'data-sectionreturn' => $this->format->get_sectionnum(), + ], + ); + } + + // get_cm_groupmode_item + protected function get_cm_groupmode_item(): ?action_menu_subpanel { + if ( + !$this->format->show_groupmode($this->mod) + || $this->mod->coursegroupmodeforce + ) { + return null; + } + + $groupmodeclass = $this->format->get_output_classname('content\\cm\\groupmode'); + /** @var \core_courseformat\output\local\content\cm\groupmode $groupmode */ + $groupmode = new $groupmodeclass($this->format, $this->section, $this->mod); + return new action_menu_subpanel( + text: get_string('groupmode', 'group'), + subpanel: $groupmode->get_choice_list(), + attributes: ['class' => 'editing_groupmode'], + icon: new pix_icon('t/groupv', '', 'moodle', ['class' => 'iconsmall']), + ); + } + + /** + * Generates the delete item for a course module. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_delete_item(): ?action_menu_link { + if (!$this->canmanageactivities) { + return null; + } + + $url = new url( + '/course/mod.php', + [ + 'sesskey' => sesskey(), + 'delete' => $this->mod->id, + 'sr' => $this->mod->sectionnum, + ], + ); + + return new action_menu_link_secondary( + url: $url, + icon: new pix_icon('t/delete', ''), + text: get_string('delete'), + attributes: [ + 'class' => 'editing_delete text-danger', + 'data-action' => 'cmDelete', + 'data-sectionreturn' => $this->format->get_sectionnum(), + 'data-id' => $this->mod->id, + ], + ); + } + + /** + * Generate the action menu element for old course formats. + * + * This method is public in case some block needs to modify the menu before output it. + * + * @todo Remove this method in Moodle 6.0 (MDL-83530). + * @param \renderer_base $output typically, the renderer that's calling this function + * @return action_menu|null the activity action menu + */ + private function get_action_menu_legacy(\renderer_base $output): ?action_menu { + $mod = $this->mod; + $controls = $this->cm_control_items(); if (empty($controls)) { @@ -151,6 +436,7 @@ public function get_action_menu(\renderer_base $output): ?action_menu { * However, format plugins can override the method to add or remove elements * from the menu. * + * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return array of edit control items */ protected function cm_control_items() { diff --git a/course/format/classes/output/local/content/cm/delegatedcontrolmenu.php b/course/format/classes/output/local/content/cm/delegatedcontrolmenu.php index bcf07fcf697ce..f5732bec21495 100644 --- a/course/format/classes/output/local/content/cm/delegatedcontrolmenu.php +++ b/course/format/classes/output/local/content/cm/delegatedcontrolmenu.php @@ -16,13 +16,18 @@ namespace core_courseformat\output\local\content\cm; -use action_menu; -use context_course; +use cm_info; +use core\context\course as context_course; +use core\context\module as context_module; +use core\output\action_menu; +use core\output\action_menu\link as action_menu_link; +use core\output\action_menu\link_secondary as action_menu_link_secondary; +use core\output\renderer_base; use core_courseformat\base as course_format; use core_courseformat\output\local\content\basecontrolmenu; -use moodle_url; +use core\output\pix_icon; +use core\url; use section_info; -use cm_info; /** * Base class to render delegated section controls. @@ -32,6 +37,11 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delegatedcontrolmenu extends basecontrolmenu { + /** @var context_module|null modcontext the module context if any */ + protected ?context_module $modcontext = null; + + /** @var bool $canmanageactivities Optimization to know if the user can manage activities */ + protected bool $canmanageactivities; /** * Constructor. @@ -42,6 +52,9 @@ class delegatedcontrolmenu extends basecontrolmenu { */ public function __construct(course_format $format, section_info $section, cm_info $mod) { parent::__construct($format, $section, $mod, $section->id); + + $this->modcontext = context_module::instance($mod->id); + $this->canmanageactivities = has_capability('moodle/course:manageactivities', $this->modcontext); } /** @@ -49,25 +62,233 @@ public function __construct(course_format $format, section_info $section, cm_inf * * 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 + * @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 { + 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. + * + * @return array of edit control items + */ + public function delegated_control_items() { + // TODO remove this if as part of MDL-83530. + if (!$this->format->supports_components()) { + return $this->delegated_control_items_legacy(); + } + + $controls = []; + $controls['view'] = $this->get_section_view_item(); + $controls['edit'] = $this->get_section_edit_item(); + $controls['visibility'] = $this->get_section_visibility_item(); + $controls['movesection'] = $this->get_cm_move_item(); + $controls['permalink'] = $this->get_section_permalink_item(); + $controls['delete'] = $this->get_cm_delete_item(); + + return $controls; + } + + /** + * Retrieves the view item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_view_item(): ?action_menu_link { + // Only show the view link if we are not already in the section view page. + if ($this->format->get_sectionid() == $this->section->id) { + return null; + } + return new action_menu_link_secondary( + url: new url('/course/section.php', ['id' => $this->section->id]), + icon: new pix_icon('i/viewsection', ''), + text: get_string('view'), + attributes: ['class' => 'view'], + ); + } + + /** + * Retrieves the edit item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_edit_item(): ?action_menu_link { + if (!has_capability('moodle/course:update', $this->coursecontext)) { + return null; + } + $params = ['id' => $this->section->id]; + $params['sr'] = $this->section->section; + + return new action_menu_link_secondary( + url: new url('/course/editsection.php', $params), + icon: new pix_icon('i/settings', ''), + text: get_string('editsection'), + attributes: ['class' => 'edit'], + ); + } + + + + /** + * Generates the move item for a course module. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_move_item(): ?action_menu_link { + // Only show the move link if we are not already in the section view page. + if ( + !$this->canmanageactivities + || $this->format->get_sectionid() == $this->section->id + ) { + return null; + } + + return new action_menu_link_secondary( + url: $this->baseurl, + icon: new pix_icon('i/dragdrop', ''), + text: get_string('move'), + attributes: [ + // This tool requires ajax and will appear only when the frontend state is ready. + 'class' => 'editing_movecm waitstate', + 'data-action' => 'moveCm', + 'data-id' => $this->mod->id, + ], + ); + } + + /** + * Retrieves the get_section_visibility_menu_item item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_visibility_item(): ?action_menu_link { + // Disabling show and hide when the parent section is not visible + // reduces the complexity of the delegated section visibility logic. + $parentsection = $this->mod->get_section_info(); + if ( + !$parentsection->visible + || !has_capability('moodle/course:activityvisibility', $this->modcontext) + ) { + return null; + } + + $outputclass = $this->format->get_output_classname('content\\cm\\visibility'); + /** @var \core_courseformat\output\local\content\cm\visibility $availability */ + $output = new $outputclass($this->format, $this->section, $this->mod); + return $output->get_menu_item(); + } + + /** + * Retrieves the permalink item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_permalink_item(): ?action_menu_link { + if (!has_any_capability( + [ + 'moodle/course:movesections', + 'moodle/course:update', + 'moodle/course:sectionvisibility', + ], + $this->coursecontext + ) + ) { + return null; + } + + $url = new url( + '/course/section.php', + ['id' => $this->section->id] + ); + return new action_menu_link_secondary( + url: $url, + icon: new pix_icon('i/link', ''), + text: get_string('sectionlink', 'course'), + attributes: [ + 'class' => 'permalink', + 'data-action' => 'permalink', + ], + ); + } + + /** + * Generates the delete item for a course module. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_delete_item(): ?action_menu_link { + if (!$this->canmanageactivities) { + return null; + } + + $url = new url( + '/course/mod.php', + [ + 'sesskey' => sesskey(), + 'delete' => $this->mod->id, + 'sr' => $this->mod->sectionnum, + ], + ); + + return new action_menu_link_secondary( + url: $url, + icon: new pix_icon('t/delete', ''), + text: get_string('delete'), + attributes: [ + 'class' => 'editing_delete text-danger', + 'data-action' => 'cmDelete', + 'data-sectionreturn' => $this->format->get_sectionnum(), + 'data-id' => $this->mod->id, + ], + ); + } + + /** + * Retrieves the delete item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_delete_item(): ?action_menu_link { + if (!course_can_delete_section($this->format->get_course(), $this->section)) { + return null; + } + + $params = [ + 'id' => $this->section->id, + 'delete' => 1, + 'sesskey' => sesskey(), + ]; + $params['sr'] ??= $this->format->get_sectionnum(); + + $url = new url( + '/course/editsection.php', + $params, + ); + return new action_menu_link_secondary( + url: $url, + icon: new pix_icon('i/delete', ''), + text: get_string('delete'), + attributes: [ + 'class' => 'editing_delete text-danger', + 'data-action' => 'deleteSection', + 'data-id' => $this->section->id, + ], + ); + } + /** * 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. - * + * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return array of edit control items */ - public function delegated_control_items() { + protected function delegated_control_items_legacy(): array { global $USER; $format = $this->format; @@ -83,7 +304,7 @@ public function delegated_control_items() { $baseurl = course_get_url($course, $sectionreturn); $baseurl->param('sesskey', sesskey()); - $cmbaseurl = new moodle_url('/course/mod.php'); + $cmbaseurl = new url('/course/mod.php'); $cmbaseurl->param('sesskey', sesskey()); $hasmanageactivities = has_capability('moodle/course:manageactivities', $coursecontext); @@ -94,7 +315,7 @@ public function delegated_control_items() { // 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]), + 'url' => new url('/course/section.php', ['id' => $section->id]), 'icon' => 'i/viewsection', 'name' => get_string('view'), 'pixattr' => ['class' => ''], @@ -113,7 +334,7 @@ public function delegated_control_items() { // Edit settings goes to section settings form. $controls['edit'] = [ - 'url' => new moodle_url('/course/editsection.php', $params), + 'url' => new url('/course/editsection.php', $params), 'icon' => 'i/settings', 'name' => $streditsection, 'pixattr' => ['class' => ''], @@ -171,7 +392,7 @@ public function delegated_control_items() { // Move (only for component compatible formats). if (!$isheadersection && $hasmanageactivities && $usecomponents) { $controls['move'] = [ - 'url' => new moodle_url('/course/mod.php', ['copy' => $cm->id]), + 'url' => new url('/course/mod.php', ['copy' => $cm->id]), 'icon' => 'i/dragdrop', 'name' => get_string('move'), 'pixattr' => ['class' => ''], @@ -211,7 +432,7 @@ public function delegated_control_items() { 'moodle/course:sectionvisibility', ], $coursecontext) ) { - $sectionlink = new moodle_url( + $sectionlink = new url( '/course/section.php', ['id' => $section->id] ); diff --git a/course/format/classes/output/local/content/section/controlmenu.php b/course/format/classes/output/local/content/section/controlmenu.php index ba0c5b07f1c3b..377f9bddcffd4 100644 --- a/course/format/classes/output/local/content/section/controlmenu.php +++ b/course/format/classes/output/local/content/section/controlmenu.php @@ -16,11 +16,15 @@ namespace core_courseformat\output\local\content\section; -use action_menu; -use context_course; +use core\context\course as context_course; +use core\output\action_menu; +use core\output\action_menu\link as action_menu_link; +use core\output\action_menu\link_secondary as action_menu_link_secondary; +use core\output\pix_icon; +use core\output\renderer_base; use core_courseformat\base as course_format; use core_courseformat\output\local\content\basecontrolmenu; -use moodle_url; +use core\url; use section_info; /** @@ -47,10 +51,10 @@ public function __construct(course_format $format, section_info $section) { * * 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 + * @param renderer_base $output typically, the renderer that's calling this function * @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 { + public function get_action_menu(renderer_base $output): ?action_menu { if (!empty($this->menu)) { return $this->menu; @@ -68,14 +72,257 @@ public function get_action_menu(\renderer_base $output): ?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 + * @param renderer_base $output typically, the renderer that's calling this function * @return action_menu|null the section action menu */ - public function get_default_action_menu(\renderer_base $output): ?action_menu { + public function get_default_action_menu(renderer_base $output): ?action_menu { $controls = $this->section_control_items(); return $this->format_controls($controls); } + /** + * Generate the edit control items of a section. + * + * @return array of edit control items + */ + public function section_control_items() { + // TODO remove this if as part of MDL-83530. + if (!$this->format->supports_components()) { + return $this->section_control_items_legacy(); + } + + $controls = []; + + $controls['view'] = $this->get_section_view_item(); + + if (!$this->section->is_orphan()) { + $controls['edit'] = $this->get_section_edit_item(); + $controls['duplicate'] = $this->get_section_duplicate_item(); + $controls['visibility'] = $this->get_section_visibility_item(); + $controls['movesection'] = $this->get_section_movesection_item(); + $controls['permalink'] = $this->get_section_permalink_item(); + } + + $controls['delete'] = $this->get_section_delete_item(); + + return $controls; + } + + /** + * Retrieves the view item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_view_item(): ?action_menu_link { + // Only show the view link if we are not already in the section view page. + if ($this->format->get_sectionid() == $this->section->id) { + return null; + } + return new action_menu_link_secondary( + url: new url('/course/section.php', ['id' => $this->section->id]), + icon: new pix_icon('i/viewsection', ''), + text: get_string('view'), + attributes: ['class' => 'view'], + ); + } + + /** + * Retrieves the edit item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_edit_item(): ?action_menu_link { + if (!has_capability('moodle/course:update', $this->coursecontext)) { + return null; + } + $params = ['id' => $this->section->id]; + $params['sr'] = $this->section->section; + + return new action_menu_link_secondary( + url: new url('/course/editsection.php', $params), + icon: new pix_icon('i/settings', ''), + text: get_string('editsection'), + attributes: ['class' => 'edit'], + ); + } + + /** + * Retrieves the duplicate item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_duplicate_item(): ?action_menu_link { + if ( + $this->section->sectionnum == 0 + || !has_capability('moodle/course:update', $this->coursecontext) + ) { + return null; + } + + $url = clone($this->baseurl); + $url->param('sectionid', $this->section->id); + $url->param('duplicatesection', 1); + $url->param('sesskey', sesskey()); + + return new action_menu_link_secondary( + url: $url, + icon: new pix_icon('t/copy', ''), + text: get_string('duplicate'), + attributes: ['class' => 'duplicate'], + ); + } + + /** + * Retrieves the get_section_visibility_menu_item item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_visibility_item(): ?action_menu_link { + if ( + $this->section->sectionnum == 0 + || !has_capability('moodle/course:sectionvisibility', $this->coursecontext) + ) { + return null; + } + $sectionreturn = $this->format->get_sectionnum(); + + $url = clone($this->baseurl); + + $strhide = get_string('hide'); + $strshow = get_string('show'); + + if ($this->section->visible) { + $url->param('hide', $this->section->sectionnum); + $icon = 'i/show'; + $name = $strhide; + $attributes = [ + 'class' => 'icon editing_showhide', + 'data-sectionreturn' => $sectionreturn, + 'data-action' => 'sectionHide', + 'data-id' => $this->section->id, + 'data-icon' => 'i/show', + 'data-swapname' => $strshow, + 'data-swapicon' => 'i/hide', + ]; + } else { + $url->param('show', $this->section->sectionnum); + $icon = 'i/hide'; + $name = $strshow; + $attributes = [ + 'class' => 'editing_showhide', + 'data-sectionreturn' => $sectionreturn, + 'data-action' => 'sectionShow', + 'data-id' => $this->section->id, + 'data-icon' => 'i/hide', + 'data-swapname' => $strhide, + 'data-swapicon' => 'i/show', + ]; + } + + return new action_menu_link_secondary( + url: $url, + icon: new pix_icon($icon, ''), + text: $name, + attributes: $attributes, + ); + } + + /** + * Retrieves the move item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_movesection_item(): ?action_menu_link { + if ( + $this->section->sectionnum == 0 + || $this->format->get_sectionid() + || !has_capability('moodle/course:movesections', $this->coursecontext) + ) { + return null; + } + + $url = clone ($this->baseurl); + $url->param('movesection', $this->section->sectionnum); + $url->param('section', $this->section->sectionnum); + return new action_menu_link_secondary( + url: $url, + icon: new pix_icon('i/dragdrop', ''), + text: get_string('move'), + attributes: [ + // This tool requires ajax and will appear only when the frontend state is ready. + 'class' => 'move waitstate', + 'data-action' => 'moveSection', + 'data-id' => $this->section->id, + ], + ); + } + + /** + * Retrieves the permalink item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_permalink_item(): ?action_menu_link { + if (!has_any_capability( + [ + 'moodle/course:movesections', + 'moodle/course:update', + 'moodle/course:sectionvisibility', + ], + $this->coursecontext + ) + ) { + return null; + } + + $url = new url( + '/course/section.php', + ['id' => $this->section->id] + ); + return new action_menu_link_secondary( + url: $url, + icon: new pix_icon('i/link', ''), + text: get_string('sectionlink', 'course'), + attributes: [ + 'class' => 'permalink', + 'data-action' => 'permalink', + ], + ); + } + + /** + * Retrieves the delete item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_delete_item(): ?action_menu_link { + if (!course_can_delete_section($this->format->get_course(), $this->section)) { + return null; + } + + $params = [ + 'id' => $this->section->id, + 'delete' => 1, + 'sesskey' => sesskey(), + ]; + $params['sr'] ??= $this->format->get_sectionnum(); + + $url = new url( + '/course/editsection.php', + $params, + ); + return new action_menu_link_secondary( + url: $url, + icon: new pix_icon('i/delete', ''), + text: get_string('delete'), + attributes: [ + 'class' => 'editing_delete text-danger', + 'data-action' => 'deleteSection', + 'data-id' => $this->section->id, + ], + ); + } + /** * Generate the edit control items of a section. * @@ -84,9 +331,10 @@ public function get_default_action_menu(\renderer_base $output): ?action_menu { * * This method must remain public until the final deprecation of section_edit_control_items. * + * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return array of edit control items */ - public function section_control_items() { + protected function section_control_items_legacy(): array { global $USER, $PAGE; $format = $this->format; @@ -108,7 +356,7 @@ public function section_control_items() { // Only show the view link if we are not already in the section view page. if ($PAGE->pagetype !== 'course-view-section-' . $course->format) { $controls['view'] = [ - 'url' => new moodle_url('/course/section.php', ['id' => $section->id]), + 'url' => new url('/course/section.php', ['id' => $section->id]), 'icon' => 'i/viewsection', 'name' => get_string('view'), 'pixattr' => ['class' => ''], @@ -126,7 +374,7 @@ public function section_control_items() { } $controls['edit'] = [ - 'url' => new moodle_url('/course/editsection.php', $params), + 'url' => new url('/course/editsection.php', $params), 'icon' => 'i/settings', 'name' => $streditsection, 'pixattr' => ['class' => ''], @@ -259,7 +507,7 @@ public function section_control_items() { if (!is_null($sectionreturn)) { $params['sr'] = $sectionreturn; } - $url = new moodle_url( + $url = new url( '/course/editsection.php', $params, ); @@ -284,7 +532,7 @@ public function section_control_items() { 'moodle/course:sectionvisibility', ], $coursecontext) ) { - $sectionlink = new moodle_url( + $sectionlink = new url( '/course/section.php', ['id' => $section->id] ); diff --git a/course/format/topics/classes/output/courseformat/content/section/controlmenu.php b/course/format/topics/classes/output/courseformat/content/section/controlmenu.php index fc427ba73a1b7..8d72ef0dd8e74 100644 --- a/course/format/topics/classes/output/courseformat/content/section/controlmenu.php +++ b/course/format/topics/classes/output/courseformat/content/section/controlmenu.php @@ -24,8 +24,11 @@ namespace format_topics\output\courseformat\content\section; +use core\output\action_menu\link as action_menu_link; +use core\output\action_menu\link_secondary as action_menu_link_secondary; +use core\output\pix_icon; use core_courseformat\output\local\content\section\controlmenu as controlmenu_base; -use moodle_url; +use core\url; /** * Base class to render a course section menu. @@ -50,54 +53,33 @@ class controlmenu extends controlmenu_base { * @return array of edit control items */ public function section_control_items() { - - $format = $this->format; $section = $this->section; - $coursecontext = $format->get_context(); - $parentcontrols = parent::section_control_items(); - if ($section->is_orphan() || !$section->section) { + if ($section->is_orphan() || !$section->sectionnum) { return $parentcontrols; } - $controls = []; - if (has_capability('moodle/course:setcurrentsection', $coursecontext)) { - $controls['highlight'] = $this->get_highlight_control(); + if (!has_capability('moodle/course:setcurrentsection', $this->coursecontext)) { + return $parentcontrols; } - // If the edit key exists, we are going to insert our controls after it. - if (array_key_exists("edit", $parentcontrols)) { - $merged = []; - // We can't use splice because we are using associative arrays. - // Step through the array and merge the arrays. - foreach ($parentcontrols as $key => $action) { - $merged[$key] = $action; - if ($key == "edit") { - // If we have come to the edit key, merge these controls here. - $merged = array_merge($merged, $controls); - } - } - - return $merged; - } else { - return array_merge($controls, $parentcontrols); - } + return $this->add_control_after($parentcontrols, 'edit', 'highlight', $this->get_section_highlight_item()); } /** * Return the course url. * - * @return moodle_url + * @return url */ - protected function get_course_url(): moodle_url { + protected function get_course_url(): url { $format = $this->format; $section = $this->section; $course = $format->get_course(); $sectionreturn = $format->get_sectionnum(); if ($sectionreturn) { - $url = course_get_url($course, $section->section); + $url = course_get_url($course, $section->sectionnum); } else { $url = course_get_url($course); } @@ -105,12 +87,78 @@ protected function get_course_url(): moodle_url { return $url; } + /** + * Retrieves the view item for the section control menu. + * + * @return action_menu_link|null The menu item if applicable, otherwise null. + */ + protected function get_section_highlight_item(): action_menu_link_secondary { + $format = $this->format; + $section = $this->section; + $course = $format->get_course(); + $sectionreturn = $format->get_sectionnum(); + $url = $this->get_course_url(); + if (!is_null($sectionreturn)) { + $url->param('sectionid', $format->get_sectionid()); + } + + $highlightoff = get_string('highlightoff'); + $highlightofficon = 'i/marked'; + + $highlighton = get_string('highlight'); + $highlightonicon = 'i/marker'; + + if ($course->marker == $section->sectionnum) { // Show the "light globe" on/off. + $url->param('marker', 0); + $icon = $highlightofficon; + $name = $highlightoff; + $attributes = [ + 'class' => 'editing_highlight', + 'data-action' => 'sectionUnhighlight', + 'data-sectionreturn' => $sectionreturn, + 'data-id' => $section->id, + 'data-icon' => $highlightofficon, + 'data-swapname' => $highlighton, + 'data-swapicon' => $highlightonicon, + ]; + } else { + $url->param('marker', $section->section); + $icon = $highlightonicon; + $name = $highlighton; + $attributes = [ + 'class' => 'editing_highlight', + 'data-action' => 'sectionHighlight', + 'data-sectionreturn' => $sectionreturn, + 'data-id' => $section->id, + 'data-icon' => $highlightonicon, + 'data-swapname' => $highlightoff, + 'data-swapicon' => $highlightofficon, + ]; + } + return new action_menu_link_secondary( + url: $url, + icon: new pix_icon($icon, ''), + text: $name, + attributes: $attributes, + ); + } + /** * Return the specific section highlight action. * + * @deprecated since Moodle 5.0 + * @todo Remove this method in Moodle 6.0 (MDL-83530). * @return array the action element. */ + #[\core\attribute\deprecated( + replacement: 'get_section_highlight_item', + since: '5.0', + mdl: 'MDL-83527', + reason: 'Wrong return type', + final: true, + )] protected function get_highlight_control(): array { + \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]); $format = $this->format; $section = $this->section; $course = $format->get_course(); diff --git a/course/lib.php b/course/lib.php index 4b2f9208ef23c..f1e66ac754ad6 100644 --- a/course/lib.php +++ b/course/lib.php @@ -1327,14 +1327,26 @@ function moveto_module($mod, $section, $beforemod=NULL) { /** * Returns the list of all editing actions that current user can perform on the module * + * @deprecated since Moodle 5.0 + * @todo Remove this method in Moodle 6.0 (MDL-83530). + * * @param cm_info $mod The module to produce editing buttons for * @param int $indent The current indenting (default -1 means no move left-right actions) * @param int $sr The section to link back to (used for creating the links) * @return array array of action_link or pix_icon objects */ +#[\core\attribute\deprecated( + replacement: 'core_courseformat\output\local\content\cm\controlmenu', + since: '5.0', + mdl: 'MDL-83527', + reason: 'Replaced by an output class equivalent.', + final: true, +)] function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { global $COURSE, $SITE, $CFG; + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); + static $str; $coursecontext = context_course::instance($mod->course);