diff --git a/course/format/classes/output/local/content/basecontrolmenu.php b/course/format/classes/output/local/content/basecontrolmenu.php index 062edd12be02a..7e2e3218d98a5 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; 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 + ): ?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 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..e7c6a9b2ae8a9 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; +use core\output\action_menu\link_secondary; +use core\output\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,332 @@ 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; + } + + /** + * Generates the edit settings item for a course module. + * + * @return link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_edit_item(): ?link { + if (!$this->canmanageactivities) { + return null; + } + + $url = new url( + '/course/mod.php', + [ + 'sesskey' => sesskey(), + 'update' => $this->mod->id, + ], + ); + if ($this->format->get_sectionnum() !== null) { + $url->param('sr', $this->format->get_sectionnum()); + } + + return new link_secondary( + url: $url, + icon: new pix_icon('i/settings', ''), + text: get_string('editsettings'), + attributes: [ + 'class' => 'editing_update', + ], + ); + } + + /** + * Generates the move item for a course module. + * + * @return link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_move_item(): ?link { + // Only show the move link if we are not already in the section view page. + if (!$this->canmanageactivities) { + return null; + } + return new 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, + ], + ); + } + + /** + * Check if the course module can be indented. + * + * @return bool + */ + 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(); + } + + /** + * Generates the move right item for a course module. + * + * @return link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_moveend_item(): ?link { + if (!$this->can_indent_cm() || $this->mod->indent > 0) { + return null; + } + + $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 link_secondary( + url: $url, + 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, + ], + ); + } + + /** + * Generates the move left item for a course module. + * + * @return link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_movestart_item(): ?link { + if (!$this->can_indent_cm() || $this->mod->indent <= 0) { + return null; + } + + $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 link_secondary( + url: $url, + 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, + ], + ); + } + + /** + * Generates the visibility item for a course module. + * + * @return link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_visibility_item(): link_secondary|subpanel|null { + 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 $output */ + $output = new $outputclass($this->format, $this->section, $this->mod); + return $output->get_menu_item(); + } + + /** + * Generates the duplicate item for a course module. + * + * @return link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_duplicate_item(): ?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 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, + ], + ); + } + + /** + * Generates the assign roles item for a course module. + * + * @return link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_assign_item(): ?link { + if ( + !has_capability('moodle/role:assign', $this->modcontext) + || sectiondelegate::has_delegate_class('mod_'.$this->mod->modname) + ) { + return null; + } + + return new 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-sectionreturn' => $this->format->get_sectionnum(), + ], + ); + } + + /** + * Generates the group mode item for a course module. + * + * @return subpanel|null The menu item if applicable, otherwise null. + */ + protected function get_cm_groupmode_item(): ?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 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 link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_delete_item(): ?link { + if (!$this->canmanageactivities) { + return null; + } + + $url = new url( + '/course/mod.php', + [ + 'sesskey' => sesskey(), + 'delete' => $this->mod->id, + 'sr' => $this->mod->sectionnum, + ], + ); + + return new 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)) { @@ -133,7 +455,7 @@ public function get_action_menu(\renderer_base $output): ?action_menu { $menu->set_owner_selector($ownerselector); foreach ($controls as $control) { - if ($control instanceof action_menu_link) { + if ($control instanceof link) { $control->add_class('cm-edit-action'); } $menu->add($control); @@ -151,6 +473,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..204e5412c2db3 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; +use core\output\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,238 @@ 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 link|null The menu item if applicable, otherwise null. + */ + protected function get_section_view_item(): ?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 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 link|null The menu item if applicable, otherwise null. + */ + protected function get_section_edit_item(): ?link { + if (!has_capability('moodle/course:update', $this->coursecontext)) { + return null; + } + $params = ['id' => $this->section->id]; + $params['sr'] = $this->section->section; + + return new 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 link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_move_item(): ?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 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 link|null The menu item if applicable, otherwise null. + */ + protected function get_section_visibility_item(): ?link { + // To avoid exponential complexity, we only allow subsection visibility actions + // when the parent section is visible. + $parentsection = $this->mod->get_section_info(); + if ( + $this->section->sectionnum == 0 + || !$parentsection->visible + || !has_capability('moodle/course:sectionvisibility', $this->coursecontext) + || !has_capability('moodle/course:activityvisibility', $this->modcontext) + ) { + 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 link_secondary( + url: $url, + icon: new pix_icon($icon, ''), + text: $name, + attributes: $attributes, + ); + } + + /** + * Retrieves the permalink item for the section control menu. + * + * @return link|null The menu item if applicable, otherwise null. + */ + protected function get_section_permalink_item(): ?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 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 link|null The menu item if applicable, otherwise null. + */ + protected function get_cm_delete_item(): ?link { + if (!$this->canmanageactivities) { + return null; + } + + $url = new url( + '/course/mod.php', + [ + 'sesskey' => sesskey(), + 'delete' => $this->mod->id, + 'sr' => $this->mod->sectionnum, + ], + ); + + return new 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 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 +309,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 +320,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 +339,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 +397,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 +437,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..3b07e8e0bad54 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; +use core\output\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 link|null The menu item if applicable, otherwise null. + */ + protected function get_section_view_item(): ?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 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 link|null The menu item if applicable, otherwise null. + */ + protected function get_section_edit_item(): ?link { + if (!has_capability('moodle/course:update', $this->coursecontext)) { + return null; + } + $params = ['id' => $this->section->id]; + $params['sr'] = $this->section->section; + + return new 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 link|null The menu item if applicable, otherwise null. + */ + protected function get_section_duplicate_item(): ?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 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 link|null The menu item if applicable, otherwise null. + */ + protected function get_section_visibility_item(): ?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 link_secondary( + url: $url, + icon: new pix_icon($icon, ''), + text: $name, + attributes: $attributes, + ); + } + + /** + * Retrieves the move item for the section control menu. + * + * @return link|null The menu item if applicable, otherwise null. + */ + protected function get_section_movesection_item(): ?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 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 link|null The menu item if applicable, otherwise null. + */ + protected function get_section_permalink_item(): ?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 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 link|null The menu item if applicable, otherwise null. + */ + protected function get_section_delete_item(): ?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 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);