diff --git a/course/format/classes/base.php b/course/format/classes/base.php index 97810344112a6..a91a16cafa70d 100644 --- a/course/format/classes/base.php +++ b/course/format/classes/base.php @@ -33,8 +33,8 @@ use section_info; use context_course; use editsection_form; -use moodle_exception; -use coding_exception; +use core\exception\moodle_exception; +use core\exception\coding_exception; use moodle_url; use lang_string; use core_external\external_api; @@ -373,6 +373,37 @@ public function get_modinfo(): course_modinfo { return $this->modinfo; } + /** + * Return a format state updates instance. + */ + public function get_stateupdates_instance(): \core_courseformat\stateupdates { + $defaultupdatesclass = 'core_courseformat\\stateupdates'; + $updatesclass = 'format_' . $this->format . '\\courseformat\\stateupdates'; + if (!class_exists($updatesclass)) { + $updatesclass = $defaultupdatesclass; + } + + $updates = new $updatesclass($this); + if (!is_a($updates, $defaultupdatesclass)) { + throw new coding_exception("The \"$updatesclass\" class must extend \"$defaultupdatesclass\""); + } + + return $updates; + } + + /** + * Return a format state actions instance. + * @return \core_courseformat\stateactions + */ + public function get_stateactions_instance(): \core_courseformat\stateactions { + // Get the actions class from the course format. + $actionsclass = 'format_'. $this->format.'\\courseformat\\stateactions'; + if (!class_exists($actionsclass)) { + $actionsclass = 'core_courseformat\\stateactions'; + } + return new $actionsclass(); + } + /** * Method used in the rendered and during backup instead of legacy 'numsections' * @@ -872,7 +903,7 @@ public function ajax_section_move() { * of the view script, it is not enough to change just this function. Do not forget * to add proper redirection. * - * @param int|stdClass|section_info $section Section object from database or just field course_sections.section + * @param int|stdClass|section_info|null $section Section object from database or just field course_sections.section * if null the course view page is returned * @param array $options options for view URL. At the moment core uses: * 'navigation' (bool) if true and section not empty, the function returns section page; otherwise, it returns course page. @@ -911,6 +942,47 @@ public function get_view_url($section, $options = array()) { return $url; } + /** + * The URL to update the course format. + * + * If no section is specified, the update will redirect to the general course page. + * + * @param moodle_url|null $returnurl optional custom return url + * @return \moodle_url + */ + public function get_update_url( + string $action, + array $ids = [], + ?int $targetsectionid = null, + ?int $targetcmid = null, + ?moodle_url $returnurl = null + ): moodle_url { + $params = [ + 'courseid' => $this->get_courseid(), + 'sesskey' => sesskey(), + 'action' => $action, + ]; + + if (count($ids) === 1) { + $params['id'] = reset($ids); + } else { + foreach ($ids as $key => $id) { + $params["ids[]"] = $id; + } + } + + if ($targetsectionid) { + $params['sectionid'] = $targetsectionid; + } + if ($targetcmid) { + $params['cmid'] = $targetcmid; + } + if ($returnurl) { + $params['pageurl'] = $returnurl->out_as_local_url(); + } + return new moodle_url('/course/format/update.php', $params); + } + /** * Return the old non-ajax activity action url. * @@ -918,30 +990,38 @@ public function get_view_url($section, $options = array()) { * so we must translate to an old non-ajax url while non-ajax * course editing is still supported. * + * @deprecated since Moodle 5.0 + * @todo Remove this method in Moodle 6.0 (MDL-83530). + * * @param string $action action name the reactive action * @param cm_info $cm course module * @return moodle_url */ + #[\core\attribute\deprecated( + replacement: 'core_courseformat\output\local\content\cm\controlmenu', + since: '5.0', + mdl: 'MDL-83527', + reason: 'Replaced by get_update_url.', + final: true, + )] public function get_non_ajax_cm_action_url(string $action, cm_info $cm): moodle_url { + \core\deprecation::emit_deprecation_if_present([$this, __FUNCTION__]); $nonajaxactions = [ - 'cmDelete' => 'delete', - 'cmDuplicate' => 'duplicate', - 'cmHide' => 'hide', - 'cmShow' => 'show', - 'cmStealth' => 'stealth', + 'cmDelete' => 'cm_delete', + 'cmDuplicate' => 'cm_duplicate', + 'cmHide' => 'cm_hide', + 'cmShow' => 'cm_show', + 'cmStealth' => 'cm_stealth', ]; if (!isset($nonajaxactions[$action])) { throw new coding_exception('Unknown activity action: ' . $action); } $nonajaxaction = $nonajaxactions[$action]; - $nonajaxurl = new moodle_url( - '/course/mod.php', - ['sesskey' => sesskey(), $nonajaxaction => $cm->id] + return $this->get_update_url( + action: $nonajaxaction, + ids: [$cm->id], + returnurl: $this->get_view_url($this->get_sectionnum(), ['navigation' => true]), ); - if (!is_null($this->get_sectionid())) { - $nonajaxurl->param('sr', $this->get_sectionnum()); - } - return $nonajaxurl; } /** diff --git a/course/format/classes/external/update_course.php b/course/format/classes/external/update_course.php index 7552484774fe4..134c050f12941 100644 --- a/course/format/classes/external/update_course.php +++ b/course/format/classes/external/update_course.php @@ -16,12 +16,11 @@ namespace core_courseformat\external; +use core\exception\moodle_exception; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_value; -use moodle_exception; -use coding_exception; use context_course; use core_courseformat\base as course_format; @@ -106,32 +105,16 @@ public static function execute(string $action, int $courseid, array $ids = [], self::validate_context(context_course::instance($courseid)); - $courseformat = course_get_format($courseid); + $format = course_get_format($courseid); - // Create a course changes tracker object. - $defaultupdatesclass = 'core_courseformat\\stateupdates'; - $updatesclass = 'format_' . $courseformat->get_format() . '\\courseformat\\stateupdates'; - if (!class_exists($updatesclass)) { - $updatesclass = $defaultupdatesclass; - } - $updates = new $updatesclass($courseformat); - - if (!is_a($updates, $defaultupdatesclass)) { - throw new coding_exception("The \"$updatesclass\" class must extend \"$defaultupdatesclass\""); - } - - // Get the actions class from the course format. - $actionsclass = 'format_'. $courseformat->get_format().'\\courseformat\\stateactions'; - if (!class_exists($actionsclass)) { - $actionsclass = 'core_courseformat\\stateactions'; - } - $actions = new $actionsclass(); + $updates = $format->get_stateupdates_instance(); + $actions = $format->get_stateactions_instance(); if (!is_callable([$actions, $action])) { throw new moodle_exception("Invalid course state action $action in ".get_class($actions)); } - $course = $courseformat->get_course(); + $course = $format->get_course(); // Execute the action. $actions->$action($updates, $course, $ids, $targetsectionid, $targetcmid); diff --git a/course/format/classes/output/local/content/cm/controlmenu.php b/course/format/classes/output/local/content/cm/controlmenu.php index eea4e70eafdf5..97853f40cb9f6 100644 --- a/course/format/classes/output/local/content/cm/controlmenu.php +++ b/course/format/classes/output/local/content/cm/controlmenu.php @@ -238,14 +238,10 @@ protected function get_cm_moveend_item(): ?action_menu_link { return null; } - $url = new url( - '/course/mod.php', - [ - 'sesskey' => sesskey(), - 'sr' => $this->mod->sectionnum, - 'id' => $this->mod->id, - 'indent' => 1, - ], + $url = $this->format->get_update_url( + action: 'cm_moveright', + ids: [$this->mod->id], + returnurl: $this->baseurl, ); $icon = (right_to_left()) ? 't/left' : 't/right'; @@ -274,14 +270,10 @@ protected function get_cm_movestart_item(): ?action_menu_link { return null; } - $url = new url( - '/course/mod.php', - [ - 'sesskey' => sesskey(), - 'sr' => $this->mod->sectionnum, - 'id' => $this->mod->id, - 'indent' => -1, - ], + $url = $this->format->get_update_url( + action: 'cm_moveleft', + ids: [$this->mod->id], + returnurl: $this->baseurl, ); $icon = (right_to_left()) ? 't/right' : 't/left'; @@ -332,8 +324,14 @@ protected function get_cm_duplicate_item(): ?action_menu_link { return null; } + $url = $this->format->get_update_url( + action: 'cm_duplicate', + ids: [$this->mod->id], + returnurl: $this->baseurl, + ); + return new action_menu_link_secondary( - url: new url($this->baseurl, ['duplicate' => $this->mod->id]), + url: $url, icon: new pix_icon('t/copy', ''), text: get_string('duplicate'), attributes: [ @@ -403,13 +401,10 @@ protected function get_cm_delete_item(): ?action_menu_link { return null; } - $url = new url( - '/course/mod.php', - [ - 'sesskey' => sesskey(), - 'delete' => $this->mod->id, - 'sr' => $this->mod->sectionnum, - ], + $url = $this->format->get_update_url( + action: 'cm_delete', + ids: [$this->mod->id], + returnurl: $this->baseurl, ); return new action_menu_link_secondary( diff --git a/course/format/classes/output/local/content/cm/delegatedcontrolmenu.php b/course/format/classes/output/local/content/cm/delegatedcontrolmenu.php index 5d59eb607553f..84a0f6b14c3f6 100644 --- a/course/format/classes/output/local/content/cm/delegatedcontrolmenu.php +++ b/course/format/classes/output/local/content/cm/delegatedcontrolmenu.php @@ -178,13 +178,12 @@ protected function get_section_visibility_item(): ?action_menu_link { } $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); + $action = 'section_hide'; $icon = 'i/show'; $name = $strhide; $attributes = [ @@ -197,7 +196,7 @@ protected function get_section_visibility_item(): ?action_menu_link { 'data-swapicon' => 'i/hide', ]; } else { - $url->param('show', $this->section->sectionnum); + $action = 'section_show'; $icon = 'i/hide'; $name = $strshow; $attributes = [ @@ -211,6 +210,12 @@ protected function get_section_visibility_item(): ?action_menu_link { ]; } + $url = $this->format->get_update_url( + action: $action, + ids: [$this->section->id], + returnurl: $this->baseurl, + ); + return new action_menu_link_secondary( url: $url, icon: new pix_icon($icon, ''), @@ -262,13 +267,10 @@ protected function get_cm_delete_item(): ?action_menu_link { return null; } - $url = new url( - '/course/mod.php', - [ - 'sesskey' => sesskey(), - 'delete' => $this->mod->id, - 'sr' => $this->mod->sectionnum, - ], + $url = $this->format->get_update_url( + action: 'cm_delete', + ids: [$this->mod->id], + returnurl: $this->baseurl, ); return new action_menu_link_secondary( diff --git a/course/format/classes/output/local/content/cm/groupmode.php b/course/format/classes/output/local/content/cm/groupmode.php index 21434749ae633..f28d92ea18eb6 100644 --- a/course/format/classes/output/local/content/cm/groupmode.php +++ b/course/format/classes/output/local/content/cm/groupmode.php @@ -158,17 +158,17 @@ public function get_choice_list(): choicelist { $choice->add_option( NOGROUPS, get_string('groupsnone', 'group'), - $this->get_option_data(null, 'cmNoGroups', $this->mod->id) + $this->get_option_data(null, 'cmNoGroups', 'cm_nogroups') ); $choice->add_option( SEPARATEGROUPS, get_string('groupsseparate', 'group'), - $this->get_option_data('groupsseparate', 'cmSeparateGroups', $this->mod->id) + $this->get_option_data('groupsseparate', 'cmSeparateGroups', 'cm_separategroups') ); $choice->add_option( VISIBLEGROUPS, get_string('groupsvisible', 'group'), - $this->get_option_data('groupsvisible', 'cmVisibleGroups', $this->mod->id) + $this->get_option_data('groupsvisible', 'cmVisibleGroups', 'cm_visiblegroups') ); $choice->set_selected_value($this->mod->effectivegroupmode); return $choice; @@ -177,18 +177,26 @@ public function get_choice_list(): choicelist { /** * Get the data for the option. * @param string|null $name the name of the option - * @param string $action the state action of the option - * @param int $id the id of the module + * @param string $mutation the mutation name + * @param string $stateaction the state action name * @return array */ - private function get_option_data(?string $name, string $action, int $id): array { + private function get_option_data(?string $name, string $mutation, string $stateaction): array { + $format = $this->format; + $nonajaxurl = $format->get_update_url( + action: $stateaction, + ids: [$this->mod->id], + returnurl: $format->get_view_url($format->get_sectionnum(), ['navigation' => true]), + ); + return [ 'description' => ($name) ? get_string("groupmode_{$name}_help", 'group') : null, // The dropdown icons are decorative, so we don't need to provide alt text. - 'icon' => $this->get_action_icon($action), + 'icon' => $this->get_action_icon($mutation), + 'url' => $nonajaxurl, 'extras' => [ - 'data-id' => $id, - 'data-action' => $action, + 'data-id' => $this->mod->id, + 'data-action' => $mutation, ] ]; } diff --git a/course/format/classes/output/local/content/cm/visibility.php b/course/format/classes/output/local/content/cm/visibility.php index 2af03a27bd43c..51dbd76ad7c35 100644 --- a/course/format/classes/output/local/content/cm/visibility.php +++ b/course/format/classes/output/local/content/cm/visibility.php @@ -236,20 +236,20 @@ protected function create_choice_list(): choicelist { $choice->add_option( 'show', get_string("availability_{$label}", 'core_courseformat'), - $this->get_option_data($label, 'cmShow') + $this->get_option_data($label, 'cmShow', 'cm_show') ); } $choice->add_option( 'hide', get_string('availability_hide', 'core_courseformat'), - $this->get_option_data('hide', 'cmHide') + $this->get_option_data('hide', 'cmHide', 'cm_hide') ); if ($CFG->allowstealth && $this->format->allow_stealth_module_visibility($this->mod, $this->section)) { $choice->add_option( 'stealth', get_string('availability_stealth', 'core_courseformat'), - $this->get_option_data('stealth', 'cmStealth') + $this->get_option_data('stealth', 'cmStealth', 'cm_stealth') ); } return $choice; @@ -258,19 +258,25 @@ protected function create_choice_list(): choicelist { /** * Get the data for the option. * @param string $name the name of the option - * @param string $action the state action of the option + * @param string $mutation the mutation name + * @param string $stateaction the state action name * @return array */ - private function get_option_data(string $name, string $action): array { + private function get_option_data(string $name, string $mutation, string $stateaction): array { + $format = $this->format; + $nonajaxurl = $format->get_update_url( + action: $stateaction, + ids: [$this->mod->id], + returnurl: $format->get_view_url($format->get_sectionnum(), ['navigation' => true]), + ); + return [ 'description' => get_string("availability_{$name}_help", 'core_courseformat'), 'icon' => $this->get_icon($name), - // Non-ajax behat is not smart enough to discrimante hidden links - // so we need to keep providing the non-ajax links. - 'url' => $this->format->get_non_ajax_cm_action_url($action, $this->mod), + 'url' => $nonajaxurl, 'extras' => [ 'data-id' => $this->mod->id, - 'data-action' => $action, + 'data-action' => $mutation, ] ]; } diff --git a/course/format/classes/output/local/content/section/controlmenu.php b/course/format/classes/output/local/content/section/controlmenu.php index 377f9bddcffd4..ce817d3c3148d 100644 --- a/course/format/classes/output/local/content/section/controlmenu.php +++ b/course/format/classes/output/local/content/section/controlmenu.php @@ -186,13 +186,11 @@ protected function get_section_visibility_item(): ?action_menu_link { } $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); + $stateaction = 'section_hide'; $icon = 'i/show'; $name = $strhide; $attributes = [ @@ -205,7 +203,7 @@ protected function get_section_visibility_item(): ?action_menu_link { 'data-swapicon' => 'i/hide', ]; } else { - $url->param('show', $this->section->sectionnum); + $stateaction = 'section_show'; $icon = 'i/hide'; $name = $strshow; $attributes = [ @@ -219,6 +217,12 @@ protected function get_section_visibility_item(): ?action_menu_link { ]; } + $url = $this->format->get_update_url( + action: $stateaction, + ids: [$this->section->id], + returnurl: $this->baseurl, + ); + return new action_menu_link_secondary( url: $url, icon: new pix_icon($icon, ''), @@ -300,17 +304,12 @@ protected function get_section_delete_item(): ?action_menu_link { return null; } - $params = [ - 'id' => $this->section->id, - 'delete' => 1, - 'sesskey' => sesskey(), - ]; - $params['sr'] ??= $this->format->get_sectionnum(); - - $url = new url( - '/course/editsection.php', - $params, + $url = $this->format->get_update_url( + action: 'section_delete', + ids: [$this->section->id], + returnurl: $this->baseurl, ); + return new action_menu_link_secondary( url: $url, icon: new pix_icon('i/delete', ''), diff --git a/course/format/classes/output/local/content/section/visibility.php b/course/format/classes/output/local/content/section/visibility.php index dbc38e45d6cd9..fe6446729f09e 100644 --- a/course/format/classes/output/local/content/section/visibility.php +++ b/course/format/classes/output/local/content/section/visibility.php @@ -119,12 +119,12 @@ protected function get_visibility_dropdown(\renderer_base $output): array { $choice->add_option( 'show', get_string('availability_show', 'core_courseformat'), - $this->get_option_data('show', 'sectionShow') + $this->get_option_data('show', 'sectionShow', 'section_show') ); $choice->add_option( 'hide', get_string('availability_hide', 'core_courseformat'), - $this->get_option_data('hide', 'sectionHide') + $this->get_option_data('hide', 'sectionHide', 'section_hide') ); $choice->set_selected_value('hide'); @@ -140,23 +140,25 @@ protected function get_visibility_dropdown(\renderer_base $output): array { * Get the data for the option. * * @param string $name the name of the option - * @param string $action the state action of the option + * @param string $mutation the mutation name + * @param string $stateaction the state action name * @return array */ - private function get_option_data(string $name, string $action): array { - $baseurl = course_get_url($this->section->course, $this->section); - $baseurl->param('sesskey', sesskey()); - $baseurl->param($action, $this->section->section); + private function get_option_data(string $name, string $mutation, string $stateaction): array { + $format = $this->format; + $nonajaxurl = $format->get_update_url( + action: $stateaction, + ids: [$this->section->id], + returnurl: $format->get_view_url($format->get_sectionnum(), ['navigation' => true]), + ); return [ 'description' => get_string("availability_{$name}_help", 'core_courseformat'), 'icon' => $this->get_icon($name), - // Non-ajax behat is not smart enough to discrimante hidden links - // so we need to keep providing the non-ajax links. - 'url' => $baseurl, + 'url' => $nonajaxurl, 'extras' => [ 'data-id' => $this->section->id, - 'data-action' => $action, + 'data-action' => $mutation, ], ]; } diff --git a/course/format/classes/output/local/courseupdate.php b/course/format/classes/output/local/courseupdate.php new file mode 100644 index 0000000000000..9e221f398d67a --- /dev/null +++ b/course/format/classes/output/local/courseupdate.php @@ -0,0 +1,188 @@ +. + +namespace core_courseformat\output\local; + +use core_courseformat\base as course_format; +use core\output\renderer_base; +use core\output\single_button; +use core\url; +use stdClass; + +/** + * Support UIs for non-ajax course updates alternatives. + * + * This class is used from course/format/update.php to provide confirmation + * dialogs for specific actions that require user confirmation. + * + * All protected methods has the same parameters as the core_courseformat\stateactions + * even if they are not used for a specific action. + * + * @package core_courseformat + * @copyright 2024 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class courseupdate { + use courseformat_named_templatable; + + /** + * Constructor. + * + * @param course_format $format the course format class. + */ + public function __construct( + /** @var course_format the course format class */ + protected course_format $format, + /** @var url the current action url */ + protected url $actionurl, + /** @var url the return url if the user cancel the action */ + protected url $returnurl, + ) { + } + + /** + * Check if a specific action requires confirmation. + * + * Format plugins can override this method to provide confirmation + * dialogs for specific actions. + * + * @param string $action + * @return bool + */ + public function is_confirmation_required( + string $action, + ): bool { + $methodname = $action . '_confirmation_dialog'; + return method_exists($this, $methodname); + } + + public function get_confirmation_dialog( + renderer_base $output, + stdClass $course, + string $action, + array $ids = [], + ?int $targetsectionid = null, + ?int $targetcmid = null, + ): string { + $methodname = $action . '_confirmation_dialog'; + if (method_exists($this, $methodname)) { + return $this->$methodname( + output: $output, + course: $course, + ids: $ids, + targetsectionid: $targetsectionid, + targetcmid: $targetcmid, + ); + } + return ''; + } + + /** + * Render the section delete confirmation dialog. + * + * @param renderer_base $output the course renderer + * @param stdClass $course + * @param array $ids the action ids. + * @param int $targetsectionid the target section id (not used) + * @param int $targetcmid the target cm id (not used) + * @return string the HTML output + */ + protected function section_delete_confirmation_dialog( + renderer_base $output, + stdClass $course, + array $ids = [], + ?int $targetsectionid = null, + ?int $targetcmid = null, + ): string { + if (count($ids) == 1) { + $modinfo = $this->format->get_modinfo(); + $section = $modinfo->get_section_info_by_id($ids[0]); + $title = get_string('sectiondelete_title', 'core_courseformat'); + $message = get_string( + 'sectiondelete_info', + 'core_courseformat', + ['name' => $this->format->get_section_name($section)] + ); + } else { + $title = get_string('sectionsdelete_title', 'core_courseformat'); + $message = get_string('sectionsdelete_info', 'core_courseformat', ['count' => count($ids)]); + } + + return $output->confirm( + message: $message, + cancel: $this->returnurl, + continue: new url($this->actionurl, ['confirm' => 1]), + displayoptions: [ + 'confirmtitle' => $title, + 'type' => single_button::BUTTON_DANGER, + ] + ); + } + + /** + * Render the cm delete confirmation dialog. + * + * @param renderer_base $output the course renderer + * @param stdClass $course + * @param array $ids the action ids. + * @param int $targetsectionid the target section id (not used) + * @param int $targetcmid the target cm id (not used) + * @return string the HTML output + */ + protected function cm_delete_confirmation_dialog( + renderer_base $output, + stdClass $course, + array $ids = [], + ?int $targetsectionid = null, + ?int $targetcmid = null, + ): string { + + if (count($ids) == 1) { + $modinfo = $this->format->get_modinfo(); + $cm = $modinfo->get_cm($ids[0]); + + if ($cm->get_delegated_section_info()) { + $title = get_string('cmdelete_subsectiontitle', 'core_courseformat'); + $meesagestr = 'sectiondelete_info'; + } else { + $title = get_string('cmdelete_title', 'core_courseformat'); + $meesagestr = 'cmdelete_info'; + } + + $message = get_string( + $meesagestr, + 'core_courseformat', + (object) [ + 'type' => get_string('pluginname', 'mod_' . $cm->modname), + 'name' => $cm->name + ], + ); + } else { + $title = get_string('cmsdelete_title', 'core_courseformat'); + $message = get_string('cmsdelete_info', 'core_courseformat', ['count' => count($ids)]); + } + + return $output->confirm( + message: $message, + cancel: $this->returnurl, + continue: new url($this->actionurl, ['confirm' => 1]), + displayoptions: [ + 'confirmtitle' => $title, + 'type' => single_button::BUTTON_DANGER, + ] + ); + } +} diff --git a/course/format/tests/behat/activity_nonajax_edit.feature b/course/format/tests/behat/activity_nonajax_edit.feature new file mode 100644 index 0000000000000..0b34ef2a154c7 --- /dev/null +++ b/course/format/tests/behat/activity_nonajax_edit.feature @@ -0,0 +1,108 @@ +@core @core_courseformat +Feature: Validate some activity editing has a non-ajax alternative + In order to edit the course activities faster + As a teacher + I need to be able use some edit tools without ajax. + + Background: + Given the following "course" exists: + | fullname | Course 1 | + | shortname | C1 | + | category | 0 | + | numsections | 3 | + | initsections | 1 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | section | + | assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + Scenario: Activity settings can be accessed without ajax + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I click on "Edit settings" "link" in the "Activity sample 1" "activity" + Then I should see "Assignment name" + And I set the field "Assignment name" to "New name" + And I press "Save and return to course" + And I should see "New name" + + Scenario: Indent an activity can be done without ajax + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I should not see "Move left" + When I click on "Move right" "link" in the "Activity sample 1" "activity" + Then I should see "Move left" + And I should not see "Move right" + And I click on "Move left" "link" in the "Activity sample 1" "activity" + And I should not see "Move left" + And I should see "Move right" + + Scenario: Hide and show an activity can be done without ajax + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I should not see "Show" in the ".cm_action_menu" "css_element" + When I click on "Hide" "link" in the "Activity sample 1" "activity" + Then I should see "Show" in the ".cm_action_menu" "css_element" + And I should not see "Hide" in the ".cm_action_menu" "css_element" + And I click on "Show" "link" in the "Activity sample 1" "activity" + And I should not see "Show" in the ".cm_action_menu" "css_element" + And I should see "Hide" in the ".cm_action_menu" "css_element" + + Scenario: Activity visibility with stealth option can be changed without ajax + Given the following config values are set as admin: + | allowstealth | 1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I should see "Show on course page" + And I should see "Hide on course page" + And I should see "Make available but don't show on course page" + And ".activity-badges" "css_element" should not exist + When I click on "Hide on course page" "link" in the "Activity sample 1" "activity" + Then I should see "Hidden from students" in the "Activity sample 1" "core_courseformat > Activity visibility" + And I should not see "Available but not shown on course page" in the "Activity sample 1" "core_courseformat > Activity visibility" + And I click on "Make available but don't show on course page" "link" in the "Activity sample 1" "activity" + And I should not see "Hidden from students" in the "Activity sample 1" "core_courseformat > Activity visibility" + And I should see "Available but not shown on course page" in the "Activity sample 1" "core_courseformat > Activity visibility" + And I click on "Show on course page" "link" in the "Activity sample 1" "activity" + And ".activity-badges" "css_element" should not exist + + Scenario: Duplicate activity can be done without ajax + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I click on "Duplicate" "link" in the "Activity sample 1" "activity" + Then I should see "Activity sample 1 (copy)" + + Scenario: Delete activity can be done without ajax + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I click on "Delete" "link" in the "Activity sample 1" "activity" + And I should see "Delete activity?" + And I should see "This will delete Activity sample 1 and any user data it contains" + And I click on "Continue" "button" + Then I should not see "Activity sample 1" + + Scenario: The activity groupmode can be changed without ajax + Given the following "groups" exist: + | name | course | idnumber | + | G1 | C1 | GI1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And "No groups" "icon" should exist in the "Activity sample 1" "activity" + And "Visible groups" "icon" should not exist in the "Activity sample 1" "activity" + And "Separate groups" "icon" should not exist in the "Activity sample 1" "activity" + When I click on "Separate groups" "link" in the "Activity sample 1" "activity" + And "No groups" "icon" should not exist in the "Activity sample 1" "activity" + And "Visible groups" "icon" should not exist in the "Activity sample 1" "activity" + And "Separate groups" "icon" should exist in the "Activity sample 1" "activity" + And I click on "Visible groups" "link" in the "Activity sample 1" "activity" + And "No groups" "icon" should not exist in the "Activity sample 1" "activity" + And "Visible groups" "icon" should exist in the "Activity sample 1" "activity" + And "Separate groups" "icon" should not exist in the "Activity sample 1" "activity" + And I click on "No groups" "link" in the "Activity sample 1" "activity" + And "No groups" "icon" should exist in the "Activity sample 1" "activity" + And "Visible groups" "icon" should not exist in the "Activity sample 1" "activity" + And "Separate groups" "icon" should not exist in the "Activity sample 1" "activity" diff --git a/course/format/tests/behat/section_nonajax_edit.feature b/course/format/tests/behat/section_nonajax_edit.feature new file mode 100644 index 0000000000000..265aad918184a --- /dev/null +++ b/course/format/tests/behat/section_nonajax_edit.feature @@ -0,0 +1,52 @@ +@core @core_courseformat +Feature: Validate some section editing has a non-ajax alternative + In order to edit the course sections faster + As a teacher + I need to be able use some edit tools without ajax. + + Background: + Given the following "course" exists: + | fullname | Course 1 | + | shortname | C1 | + | category | 0 | + | numsections | 3 | + | initsections | 1 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | section | + | assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + Scenario: Section settings can be accessed without ajax + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I click on "Edit settings" "link" in the "Section 1" "core_courseformat > Section actions menu" + Then I should see "Section name" + And I set the field "Section name" to "New name" + And I press "Save changes" + And I should see "New name" + + Scenario: Hide and show a section can be done without ajax + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I should not see "Show" in the "Section 1" "core_courseformat > Section actions menu" + When I click on "Hide" "link" in the "Section 1" "core_courseformat > Section actions menu" + Then I should see "Show" in the "Section 1" "core_courseformat > Section actions menu" + And I should not see "Hide" in the "Section 1" "core_courseformat > Section actions menu" + And I click on "Show" "link" in the "Section 1" "core_courseformat > Section actions menu" + And I should not see "Show" in the "Section 1" "core_courseformat > Section actions menu" + And I should see "Hide" in the "Section 1" "core_courseformat > Section actions menu" + + Scenario: Delete a section can be done without ajax + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I click on "Delete" "link" in the "Section 1" "core_courseformat > Section actions menu" + Then I should see "Delete section?" + And I should see "This will delete Section 1 and all the activities it contains." + And I should see "Section 3" + And I click on "Continue" "button" + Then I should not see "Section 1" 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 8d72ef0dd8e74..92bc8f6421d7f 100644 --- a/course/format/topics/classes/output/courseformat/content/section/controlmenu.php +++ b/course/format/topics/classes/output/courseformat/content/section/controlmenu.php @@ -72,7 +72,14 @@ public function section_control_items() { * * @return url */ + #[\core\attribute\deprecated( + since: '5.0', + mdl: 'MDL-82767', + reason: 'Not used anymore, use $this->format->get_update_url instead', + final: true, + )] protected function get_course_url(): url { + \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]); $format = $this->format; $section = $this->section; $course = $format->get_course(); @@ -97,10 +104,6 @@ protected function get_section_highlight_item(): action_menu_link_secondary { $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'; @@ -109,7 +112,7 @@ protected function get_section_highlight_item(): action_menu_link_secondary { $highlightonicon = 'i/marker'; if ($course->marker == $section->sectionnum) { // Show the "light globe" on/off. - $url->param('marker', 0); + $action = 'section_unhighlight'; $icon = $highlightofficon; $name = $highlightoff; $attributes = [ @@ -122,7 +125,7 @@ protected function get_section_highlight_item(): action_menu_link_secondary { 'data-swapicon' => $highlightonicon, ]; } else { - $url->param('marker', $section->section); + $action = 'section_highlight'; $icon = $highlightonicon; $name = $highlighton; $attributes = [ @@ -135,6 +138,13 @@ protected function get_section_highlight_item(): action_menu_link_secondary { 'data-swapicon' => $highlightofficon, ]; } + + $url = $this->format->get_update_url( + action: $action, + ids: [$section->id], + returnurl: $this->baseurl, + ); + return new action_menu_link_secondary( url: $url, icon: new pix_icon($icon, ''), diff --git a/course/format/topics/tests/behat/highlight_sections.feature b/course/format/topics/tests/behat/highlight_sections.feature index 0a33f41776975..8c5f1dc286335 100644 --- a/course/format/topics/tests/behat/highlight_sections.feature +++ b/course/format/topics/tests/behat/highlight_sections.feature @@ -51,3 +51,11 @@ Feature: Sections can be highlighted When I open section "3" edit menu And I click on "Unhighlight" "link" in the "Section 3" "section" Then I should not see "Highlighted" in the "Section 3" "section" + + Scenario: Highlight and unhighlight a section can be done without ajax + # Without javascript hidden elements cannot be detected with a simple I should see step. + Given ".section.current" "css_element" should not exist + When I click on "Highlight" "link" in the "Section 2" "core_courseformat > Section actions menu" + Then ".section.current[data-number='2']" "css_element" should exist + And I click on "Unhighlight" "link" in the "Section 2" "core_courseformat > Section actions menu" + And ".section.current" "css_element" should not exist diff --git a/course/format/update.php b/course/format/update.php new file mode 100644 index 0000000000000..fd5255503037b --- /dev/null +++ b/course/format/update.php @@ -0,0 +1,129 @@ +. + +/** + * Execute an update action on a course format and structure. + * + * @package core_courseformat + * @copyright 2024 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->dirroot . '/course/lib.php'); + +use core\url; +use core\exception\moodle_exception; +use core_courseformat\base as course_format; + +$action = required_param('action', PARAM_ALPHANUMEXT); +$courseid = required_param('courseid', PARAM_INT); +$targetsectionid = optional_param('targetsectionid', null, PARAM_INT); +$targetcmid = optional_param('targetcmid', null, PARAM_INT); +$confirm = optional_param('confirm', false, PARAM_BOOL); + +$returnurl = optional_param('returnurl', null, PARAM_LOCALURL); + + +// All state updates are designed to be batch compatible. However, we also +// accept single id values for simplicity. +$ids = optional_param_array('ids', [], PARAM_INT); +if (empty($ids)) { + $ids = [required_param('id', PARAM_INT)]; +} +if (empty($ids)) { + throw new moodle_exception('missingparam', '', '', 'ids'); +} + +$format = course_get_format($courseid); +$course = $format->get_course(); + +if ($returnurl === null) { + $returnurl = new url('/course/view.php', ['id' => $course->id]); +} + +// Normalize the return URL. +$returnurl = new moodle_url($returnurl); + +$currenturl = new moodle_url( + '/course/format/update.php', + [ + 'action' => $action, + 'courseid' => $courseid, + // 'ids' => $ids, + 'targetsectionid' => $targetsectionid, + 'targetcmid' => $targetcmid, + 'returnurl' => $returnurl, + 'sesskey' => sesskey(), + ] +); +foreach ($ids as $key => $id) { + $currenturl->param("ids[]", $id); +} + +require_sesskey(); + +$PAGE->set_url($currenturl); +$PAGE->set_context($format->get_context()); +$PAGE->set_pagelayout('course'); +$PAGE->add_body_class('limitedwidth'); +$PAGE->set_heading($course->fullname); + +require_login($course); +require_all_capabilities( + ['moodle/course:update', 'moodle/course:sectionvisibility', 'moodle/course:activityvisibility'], + $format->get_context(), +); + +// Some actions may require a confirmation dialog. +$actionuiclass = $format->get_output_classname('courseupdate'); +/** @var core_courseformat\output\local\courseupdate $actionui */ +$actionui = new $actionuiclass($format, $currenturl, $returnurl); + +if ( + !$confirm + && $actionui->is_confirmation_required($action) +) { + /** @var \core_course_renderer $renderer */ + $renderer = $format->get_renderer($PAGE); + echo $renderer->header(); + echo "patata"; + echo $actionui->get_confirmation_dialog( + output: $renderer, + course: $course, + action: $action, + ids: $ids, + targetsectionid: $targetsectionid, + targetcmid: $targetcmid, + ); + echo $renderer->footer(); + die; +} + +$updates = $format->get_stateupdates_instance(); +$actions = $format->get_stateactions_instance(); + +if (!is_callable([$actions, $action])) { + throw new moodle_exception("Invalid course state action $action in ".get_class($actions)); +} + +// Execute the action. +$actions->$action($updates, $course, $ids, $targetsectionid, $targetcmid); + +// Any state action mark the state cache as dirty. +course_format::session_cache_reset($course); + +redirect($returnurl); diff --git a/course/mod.php b/course/mod.php index c365002524e0d..caa2309ad35a1 100644 --- a/course/mod.php +++ b/course/mod.php @@ -29,20 +29,20 @@ $sectionreturn = optional_param('sr', null, PARAM_INT); $add = optional_param('add', '', PARAM_ALPHANUM); $type = optional_param('type', '', PARAM_ALPHA); -$indent = optional_param('indent', 0, PARAM_INT); +$indent = optional_param('indent', 0, PARAM_INT); // TODO remove this param as part of MDL-83530. $update = optional_param('update', 0, PARAM_INT); -$duplicate = optional_param('duplicate', 0, PARAM_INT); -$hide = optional_param('hide', 0, PARAM_INT); -$stealth = optional_param('stealth', 0, PARAM_INT); -$show = optional_param('show', 0, PARAM_INT); +$duplicate = optional_param('duplicate', 0, PARAM_INT); // TODO remove this param as part of MDL-83530. +$hide = optional_param('hide', 0, PARAM_INT); // TODO remove this param as part of MDL-83530. +$stealth = optional_param('stealth', 0, PARAM_INT); // TODO remove this param as part of MDL-83530. +$show = optional_param('show', 0, PARAM_INT); // TODO remove this param as part of MDL-83530. $copy = optional_param('copy', 0, PARAM_INT); $moveto = optional_param('moveto', 0, PARAM_INT); $movetosection = optional_param('movetosection', 0, PARAM_INT); -$delete = optional_param('delete', 0, PARAM_INT); +$delete = optional_param('delete', 0, PARAM_INT); // TODO remove this param as part of MDL-83530. $course = optional_param('course', 0, PARAM_INT); -$groupmode = optional_param('groupmode', -1, PARAM_INT); +$groupmode = optional_param('groupmode', -1, PARAM_INT); // TODO remove this param as part of MDL-83530. $cancelcopy = optional_param('cancelcopy', 0, PARAM_BOOL); -$confirm = optional_param('confirm', 0, PARAM_BOOL); +$confirm = optional_param('confirm', 0, PARAM_BOOL); // TODO remove this param as part of MDL-83530. // This page should always redirect $url = new moodle_url('/course/mod.php'); @@ -118,6 +118,11 @@ ) ); } else if (!empty($duplicate) and confirm_sesskey()) { + // TODO remove this else if as part of MDL-83530. + debugging( + 'The duplicate param is deprecated. Please use action cm_duplicate in course/format/update.php instead.', + DEBUG_DEVELOPER + ); $cm = get_coursemodule_from_id('', $duplicate, 0, true, MUST_EXIST); $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); @@ -130,6 +135,11 @@ redirect(course_get_url($course, $cm->sectionnum, $urloptions)); } else if (!empty($delete)) { + // TODO remove this else if as part of MDL-83530. + debugging( + 'The delete param is deprecated. Please use action cm_delete in course/format/update.php instead.', + DEBUG_DEVELOPER + ); $cm = get_coursemodule_from_id('', $delete, 0, true, MUST_EXIST); $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); @@ -216,6 +226,11 @@ redirect(course_get_url($course, $section->section, $urloptions)); } else if (!empty($indent) and confirm_sesskey()) { + // TODO remove this else if as part of MDL-83530. + debugging( + 'The delete param deprecated. Please use action cm_moveleft and cm_moveright in course/format/update.php instead.', + DEBUG_DEVELOPER + ); $id = required_param('id', PARAM_INT); $cm = get_coursemodule_from_id('', $id, 0, true, MUST_EXIST); @@ -241,6 +256,11 @@ redirect(course_get_url($course, $cm->sectionnum, $urloptions)); } else if (!empty($hide) and confirm_sesskey()) { + // TODO remove this else if as part of MDL-83530. + debugging( + 'The hide param deprecated. Please use action cm_hide in course/format/update.php instead.', + DEBUG_DEVELOPER + ); $cm = get_coursemodule_from_id('', $hide, 0, true, MUST_EXIST); $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); @@ -255,6 +275,11 @@ redirect(course_get_url($course, $cm->sectionnum, $urloptions)); } else if (!empty($stealth) and confirm_sesskey()) { + // TODO remove this else if as part of MDL-83530. + debugging( + 'The stealth param deprecated. Please use action cm_stealth in course/format/update.php instead.', + DEBUG_DEVELOPER + ); list($course, $cm) = get_course_and_cm_from_cmid($stealth); require_login($course, false, $cm); require_capability('moodle/course:activityvisibility', $cm->context); @@ -265,6 +290,11 @@ redirect(course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn))); } else if (!empty($show) and confirm_sesskey()) { + // TODO remove this else if as part of MDL-83530. + debugging( + 'The show param deprecated. Please use action cm_show in course/format/update.php instead.', + DEBUG_DEVELOPER + ); list($course, $cm) = get_course_and_cm_from_cmid($show); require_login($course, false, $cm); require_capability('moodle/course:activityvisibility', $cm->context); @@ -276,6 +306,11 @@ redirect(course_get_url($course, $section->section, $urloptions)); } else if ($groupmode > -1 and confirm_sesskey()) { + // TODO remove this else if as part of MDL-83530. + debugging( + 'The gruopmode param deprecated. Please use the group mode actions in course/format/update.php instead.', + DEBUG_DEVELOPER + ); $id = required_param('id', PARAM_INT); $cm = get_coursemodule_from_id('', $id, 0, true, MUST_EXIST); diff --git a/course/view.php b/course/view.php index ff29d5ba103f9..1a71f20afe0a0 100644 --- a/course/view.php +++ b/course/view.php @@ -31,15 +31,15 @@ $id = optional_param('id', 0, PARAM_INT); $name = optional_param('name', '', PARAM_TEXT); $edit = optional_param('edit', -1, PARAM_BOOL); -$hide = optional_param('hide', 0, PARAM_INT); -$show = optional_param('show', 0, PARAM_INT); +$hide = optional_param('hide', 0, PARAM_INT); // TODO remove this param as part of MDL-83530. +$show = optional_param('show', 0, PARAM_INT); // TODO remove this param as part of MDL-83530. $duplicatesection = optional_param('duplicatesection', 0, PARAM_INT); $idnumber = optional_param('idnumber', '', PARAM_RAW); $sectionid = optional_param('sectionid', 0, PARAM_INT); $section = optional_param('section', null, PARAM_INT); $expandsection = optional_param('expandsection', -1, PARAM_INT); $move = optional_param('move', 0, PARAM_INT); -$marker = optional_param('marker', -1 , PARAM_INT); +$marker = optional_param('marker', -1 , PARAM_INT); // TODO remove this param as part of MDL-83530. $switchrole = optional_param('switchrole', -1, PARAM_INT); // Deprecated, use course/switchrole.php instead. $return = optional_param('return', 0, PARAM_LOCALURL); @@ -199,8 +199,13 @@ } } + // TODO remove this if as part of MDL-83530. if (has_capability('moodle/course:sectionvisibility', $context)) { if ($hide && confirm_sesskey()) { + debugging( + 'The hide param in course view is deprecated. Please use course/format/update.php instead.', + DEBUG_DEVELOPER + ); set_section_visible($course->id, $hide, '0'); if ($sectionid) { redirect(course_get_url($course, $section, ['navigation' => true])); @@ -210,6 +215,10 @@ } if ($show && confirm_sesskey()) { + debugging( + 'The show param in course view is deprecated. Please use course/format/update.php instead.', + DEBUG_DEVELOPER + ); set_section_visible($course->id, $show, '1'); if ($sectionid) { redirect(course_get_url($course, $section, ['navigation' => true])); @@ -219,7 +228,12 @@ } } + // TODO remove this if as part of MDL-83530. if ($marker >= 0 && confirm_sesskey()) { + debugging( + 'The marker param in course view is deprecated. Please use course/format/update.php instead.', + DEBUG_DEVELOPER + ); course_set_marker($course->id, $marker); if ($sectionid) { redirect(course_get_url($course, $section, ['navigation' => true]));