From c6a9177252194a98f585624f873673b58be2e5d6 Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Tue, 8 Oct 2024 12:45:58 +0200 Subject: [PATCH 1/7] MDL-82349 core_courseformat: fix home section zero name --- course/format/lib.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/course/format/lib.php b/course/format/lib.php index c64d61ae065..5d51b17d037 100644 --- a/course/format/lib.php +++ b/course/format/lib.php @@ -58,9 +58,12 @@ class format_site extends course_format { if ((string)$section->name !== '') { // Return the name the user set. return format_string($section->name, true, array('context' => context_course::instance($this->courseid))); - } else { - return get_string('site'); } + // The section zero is located in a block. + if ($section->sectionnum == 0) { + return get_string('block'); + } + return get_string('site'); } /** From c5133ddc4771a4f71dca34e5c61c3eb3118eb063 Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Wed, 9 Oct 2024 09:05:44 +0200 Subject: [PATCH 2/7] MDL-82349 courseformat: add reactivity to frontpage topic --- .../output/local/content/frontpagesection.php | 1 + course/format/lib.php | 22 ++++++++++++++++++- .../local/content/frontpagesection.mustache | 13 +++++++++-- course/lib.php | 2 +- index.php | 14 +++++++----- 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/course/format/classes/output/local/content/frontpagesection.php b/course/format/classes/output/local/content/frontpagesection.php index ac34c59ac6c..b9f3b551fd3 100644 --- a/course/format/classes/output/local/content/frontpagesection.php +++ b/course/format/classes/output/local/content/frontpagesection.php @@ -89,6 +89,7 @@ class frontpagesection implements named_templatable, renderable { } $data = (object)[ + 'editing' => $format->show_editor(), 'sections' => [$sectionoutput->export_for_template($output)], ]; diff --git a/course/format/lib.php b/course/format/lib.php index 5d51b17d037..c90e725733f 100644 --- a/course/format/lib.php +++ b/course/format/lib.php @@ -51,7 +51,7 @@ class format_site extends course_format { * Returns the display name of the given section that the course prefers. * * @param int|stdClass $section Section object from database or just field section.section - * @return Display name that the course format prefers, e.g. "Topic 2" + * @return string Display name that the course format prefers, e.g. "Topic 2" */ function get_section_name($section) { $section = $this->get_section($section); @@ -88,6 +88,26 @@ class format_site extends course_format { return blocks_get_default_site_course_blocks(); } + #[\Override] + public function supports_ajax() { + // All home page is rendered in the backend, we only need an ajax editor components in edit mode. + // This will also prevent redirectng to the login page when a guest tries to access the site, + // and will make the home page loading faster. + $ajaxsupport = new stdClass(); + $ajaxsupport->capable = $this->show_editor(); + return $ajaxsupport; + } + + #[\Override] + public function supports_components() { + return true; + } + + #[\Override] + public function uses_sections() { + return true; + } + /** * Definitions of the additional options that site uses * diff --git a/course/format/templates/local/content/frontpagesection.mustache b/course/format/templates/local/content/frontpagesection.mustache index 126e745c7ed..18d7106eca8 100644 --- a/course/format/templates/local/content/frontpagesection.mustache +++ b/course/format/templates/local/content/frontpagesection.mustache @@ -21,6 +21,7 @@ Example context (json): { + "editing": true, "sections": [ "
  • This is the section content
  • " ], @@ -28,7 +29,7 @@ "settingsurl": "#" } }} -
    + +{{#js}} +{{! The home page should be as fast as possible, we only load the editor when needed.}} +{{#editing}} +require(['core_courseformat/local/content'], function(component) { + component.init('courseformat-frontpage-main-topic', {}); +}); +{{/editing}} +{{/js}} diff --git a/course/lib.php b/course/lib.php index 1d9265d3c3e..e526417dcb4 100644 --- a/course/lib.php +++ b/course/lib.php @@ -2867,7 +2867,7 @@ function include_course_editor(course_format $format) { $course = $format->get_course(); - if ($SITE->id === $course->id) { + if (!$format->supports_ajax()?->capable) { return; } diff --git a/index.php b/index.php index b7076287727..801ffcf1dd3 100644 --- a/index.php +++ b/index.php @@ -109,6 +109,14 @@ $PAGE->set_title(get_string('home')); $PAGE->set_heading($SITE->fullname); $PAGE->set_secondary_active_tab('coursehome'); +$siteformatoptions = course_get_format($SITE)->get_format_options(); +$modinfo = get_fast_modinfo($SITE); +$modnamesused = $modinfo->get_used_module_names(); + +// The home page can have acitvities in the block aside. We should +// initialize the course editor before the page structure is rendered. +include_course_ajax($SITE, $modnamesused); + $courserenderer = $PAGE->get_renderer('core', 'course'); if ($hassiteconfig) { @@ -119,10 +127,6 @@ if ($hassiteconfig) { echo $OUTPUT->header(); -$siteformatoptions = course_get_format($SITE)->get_format_options(); -$modinfo = get_fast_modinfo($SITE); -$modnamesused = $modinfo->get_used_module_names(); - // Print Section or custom info. if (!empty($CFG->customfrontpageinclude)) { // Pre-fill some variables that custom front page might use. @@ -135,8 +139,6 @@ if (!empty($CFG->customfrontpageinclude)) { } else if ($siteformatoptions['numsections'] > 0) { echo $courserenderer->frontpage_section1(); } -// Include course AJAX. -include_course_ajax($SITE, $modnamesused); echo $courserenderer->frontpage(); From 2f13ffcfb1fa103ff75819b490c271580ece1142 Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Wed, 9 Oct 2024 09:05:57 +0200 Subject: [PATCH 3/7] MDL-82349 courseformat: add reactivity to main menu block --- .../site_main_menu/block_site_main_menu.php | 214 ++---------------- .../classes/output/mainsection.php | 72 ++++++ blocks/site_main_menu/styles.css | 46 ++-- .../templates/mainsection.mustache | 85 +++++++ 4 files changed, 197 insertions(+), 220 deletions(-) create mode 100644 blocks/site_main_menu/classes/output/mainsection.php create mode 100644 blocks/site_main_menu/templates/mainsection.mustache diff --git a/blocks/site_main_menu/block_site_main_menu.php b/blocks/site_main_menu/block_site_main_menu.php index d7d7461c9c1..35316ec64ad 100644 --- a/blocks/site_main_menu/block_site_main_menu.php +++ b/blocks/site_main_menu/block_site_main_menu.php @@ -21,8 +21,7 @@ * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -class block_site_main_menu extends block_list { +class block_site_main_menu extends block_base { function init() { $this->title = get_string('pluginname', 'block_site_main_menu'); } @@ -32,224 +31,37 @@ class block_site_main_menu extends block_list { } function get_content() { - global $USER, $CFG, $DB, $OUTPUT; - if ($this->content !== NULL) { return $this->content; } $this->content = new stdClass(); - $this->content->items = array(); - $this->content->icons = array(); + $this->content->text = ''; $this->content->footer = ''; if (empty($this->instance)) { return $this->content; } - require_once($CFG->dirroot . '/course/lib.php'); - $course = get_site(); - $format = course_get_format($course); - $courserenderer = $format->get_renderer($this->page); - $context = context_course::instance($course->id); - $isediting = $this->page->user_is_editing() && has_capability('moodle/course:manageactivities', $context); - - // Output classes. - $cmnameclass = $format->get_output_classname('content\\cm\\cmname'); - $controlmenuclass = $format->get_output_classname('content\\cm\\controlmenu'); - - $badgeattributes = [ - 'class' => 'badge rounded-pill bg-warning text-dark mt-2', - 'data-region' => 'visibility' - ]; - - // Extra fast view mode. - if (!$isediting) { - $modinfo = get_fast_modinfo($course); - if (!empty($modinfo->sections[0])) { - foreach($modinfo->sections[0] as $cmid) { - $cm = $modinfo->cms[$cmid]; - if (!$cm->uservisible || !$cm->is_visible_on_course_page()) { - continue; - } - - if ($cm->indent > 0) { - $indent = '
    '; - } else { - $indent = ''; - } - - $badges = ''; - if (!$cm->visible) { - $badges = html_writer::tag( - 'span', - get_string('hiddenfromstudents'), - $badgeattributes - ); - } - - if ($cm->is_stealth()) { - $badges = html_writer::tag( - 'span', - get_string('hiddenoncoursepage'), - $badgeattributes - ); - } - - if (!$cm->url) { - $activitybasis = html_writer::div( - $indent . $cm->get_formatted_content(['overflowdiv' => true, 'noclean' => true]), - 'activity-basis d-flex align-items-center'); - $content = html_writer::div( - $activitybasis . $badges, - 'contentwithoutlink activity-item activity', - ['data-activityname' => $cm->name] - ); - } else { - $cmname = new $cmnameclass($format, $cm->get_section_info(), $cm); - $activitybasis = html_writer::div( - $indent . $courserenderer->render($cmname), - 'activity-basis d-flex align-items-center'); - $content = html_writer::div( - $activitybasis . $badges, - 'activity-item activity', - ['data-activityname' => $cm->name] - ); - } - - $this->content->items[] = html_writer::div($content, 'main-menu-content section'); - } - } - return $this->content; - } - - // Slow & hacky editing mode. - $ismoving = ismoving($course->id); course_create_sections_if_missing($course, 0); - $modinfo = get_fast_modinfo($course); + $format = course_get_format($course); + $modinfo = $format->get_modinfo(); $section = $modinfo->get_section_info(0); - if ($ismoving) { - $strmovefull = strip_tags(get_string('movefull', '', "'$USER->activitycopyname'")); - $strcancel= get_string('cancel'); - } else { - $strmove = get_string('move'); - } + $courserenderer = $format->get_renderer($this->page); - if ($ismoving) { - $this->content->icons[] = $OUTPUT->pix_icon('t/move', get_string('move')); - $this->content->items[] = $USER->activitycopyname.' (
    '.$strcancel.')'; - } + $output = new block_site_main_menu\output\mainsection($format, $section); - if (!empty($modinfo->sections[0])) { - foreach ($modinfo->sections[0] as $modnumber) { - $mod = $modinfo->cms[$modnumber]; - if (!$mod->uservisible || !$mod->is_visible_on_course_page()) { - continue; - } - if (!$ismoving) { + $this->content->text = $courserenderer->render($output); - $controlmenu = new $controlmenuclass( - $format, - $mod->get_section_info(), - $mod - ); - - $menu = $controlmenu->get_action_menu($OUTPUT); - - $moveaction = html_writer::link( - new moodle_url('/course/mod.php', ['sesskey' => sesskey(), 'copy' => $mod->id]), - $OUTPUT->pix_icon('i/dragdrop', $strmove), - ['class' => 'editing_move_activity'] - ); - - $editbuttons = html_writer::tag( - 'div', - $courserenderer->render($controlmenu), - ['class' => 'buttons activity-actions ms-auto'] - ); - } else { - $editbuttons = ''; - $moveaction = ''; - } - - if ($mod->visible || has_capability('moodle/course:viewhiddenactivities', $mod->context)) { - if ($ismoving) { - if ($mod->id == $USER->activitycopy) { - continue; - } - $movingurl = new moodle_url('/course/mod.php', array('moveto' => $mod->id, 'sesskey' => sesskey())); - $this->content->items[] = html_writer::link($movingurl, '', array('title' => $strmovefull, - 'class' => 'movehere')); - $this->content->icons[] = ''; - } - - if ($mod->indent > 0) { - $indent = '
    '; - } else { - $indent = ''; - } - - $badges = ''; - if (!$mod->visible) { - $badges = html_writer::tag( - 'span', - get_string('hiddenfromstudents'), - $badgeattributes - ); - } - - if ($mod->is_stealth()) { - $badges = html_writer::tag( - 'span', - get_string('hiddenoncoursepage'), - $badgeattributes - ); - } - - if (!$mod->url) { - $activitybasis = html_writer::div( - $moveaction . - $indent . - $mod->get_formatted_content(['overflowdiv' => true, 'noclean' => true]) . - $editbuttons, - 'activity-basis d-flex align-items-center'); - $content = html_writer::div( - $activitybasis . $badges, - 'contentwithoutlink activity-item activity', - ['data-activityname' => $mod->name] - ); - } else { - $cmname = new $cmnameclass($format, $mod->get_section_info(), $mod); - $activitybasis = html_writer::div( - $moveaction . - $indent . - $courserenderer->render($cmname) . - $editbuttons, - 'activity-basis d-flex align-items-center'); - $content = html_writer::div( - $activitybasis . $badges, - 'activity-item activity', - ['data-activityname' => $mod->name] - ); - } - $this->content->items[] = html_writer::div($content, 'main-menu-content'); - } - } - } - - if ($ismoving) { - $movingurl = new moodle_url('/course/mod.php', array('movetosection' => $section->id, 'sesskey' => sesskey())); - $this->content->items[] = html_writer::link($movingurl, '', array('title' => $strmovefull, 'class' => 'movehere')); - $this->content->icons[] = ''; - } - - if ($this->page->course->id === SITEID) { - $this->content->footer = $courserenderer->course_section_add_cm_control($course, - 0, null, array('inblock' => true)); - } + $this->content->footer = $courserenderer->course_section_add_cm_control( + course: $course, + section: 0, + sectionreturn: null, + displayoptions: ['inblock' => true], + ); return $this->content; } } diff --git a/blocks/site_main_menu/classes/output/mainsection.php b/blocks/site_main_menu/classes/output/mainsection.php new file mode 100644 index 00000000000..77db566abce --- /dev/null +++ b/blocks/site_main_menu/classes/output/mainsection.php @@ -0,0 +1,72 @@ +. + +namespace block_site_main_menu\output; + +use core_courseformat\base as courseformat; +use renderable; +use section_info; +use templatable; + +/** + * Class mainsection + * + * @package block_site_main_menu + * @copyright 2024 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mainsection implements renderable, templatable { + + /** + * The class constructor. + * + * @param courseformat $format the course format instance + * @param section_info $section the section to render + */ + public function __construct( + /** @var courseformat $format the course format instance. */ + protected courseformat $format, + /** @var section_info $section the section to render. */ + protected section_info $section, + ) { + } + + /** + * Export for template. + * + * @param \renderer_base $output + * @return array + */ + public function export_for_template(\renderer_base $output) { + $format = $this->format; + $course = $format->get_course(); + $section = $this->section; + + $sectionoutputclass = $format->get_output_classname('content\\section\\cmlist'); + $sectionoutput = new $sectionoutputclass($format, $section); + + $cmlist = $output->render($sectionoutput); + + return [ + 'siteid' => $course->id, + 'cmlist' => $cmlist, + 'sectionid' => $section->id, + 'sectionname' => $format->get_section_name($section), + 'sectionnum' => $section->sectionnum, + 'editing' => $format->show_editor(), + ]; + } +} diff --git a/blocks/site_main_menu/styles.css b/blocks/site_main_menu/styles.css index 4e1c6ecad1d..9b3a06ae7d0 100644 --- a/blocks/site_main_menu/styles.css +++ b/blocks/site_main_menu/styles.css @@ -1,26 +1,34 @@ -.block_site_main_menu li { - clear: both; +/* Imitate mobile grid for activity card. */ +.block_site_main_menu .activity-item .activity-grid { + grid-template-columns: min-content 1fr min-content min-content min-content; + grid-template-rows: 1fr repeat(4, min-content); + grid-template-areas: + "icon name actions" + "visibility visibility visibility" + "dates dates dates" + "completion completion completion" + "altcontent altcontent altcontent" + "afterlink afterlink afterlink" + "availability availability availability"; } -.block_site_main_menu.block .content > .unlist > li > .column { - /* Made specific to win over .block.list_block .unlist > li > .column. */ - width: 100%; - display: table; - margin-bottom: 0.5rem; +.block_site_main_menu .activity-item .activity-grid.noname-grid { + grid-template-columns: 1fr min-content; + grid-template-areas: + "actions" + "visibility" + "altcontent" + "groupmode" + "afterlink" + "completion" + "availability"; } -.block_site_main_menu li .buttons a img { - vertical-align: text-bottom; +.block_site_main_menu .activity-item .activity-grid.noname-grid .activity-actions { + justify-self: end; } -.block_site_main_menu .footer { - margin-top: 1em; -} - -.block_site_main_menu .section_add_menus noscript div { - display: inline; -} - -.block_site_main_menu .instancename { - word-break: break-all; +/* Hide extra edit elements in block space. */ +.block_site_main_menu .activity-groupmode-info { + display: none; } diff --git a/blocks/site_main_menu/templates/mainsection.mustache b/blocks/site_main_menu/templates/mainsection.mustache new file mode 100644 index 00000000000..7e91520ba65 --- /dev/null +++ b/blocks/site_main_menu/templates/mainsection.mustache @@ -0,0 +1,85 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_site_main_menu/mainsection + + This mustache emulates a course single section structure inside a block. + + It requires to include several divs to emulate the structure of a course format. + + Example context (json): + { + "cmlist": "Sample", + "siteid": 1, + "sectionid": 1, + "sectionname": "Sample", + "sectionnum": 1, + "editing": true + } +}} +
    + {{! The section list is used by the content module to init the sections.}} +
    + {{! The section need some bottom padding and margin for the dropzone. + Otherwise the activities will cover the area. }} +
    + {{#editing}} + {{! The section header is used as a dropzone when the section is empty.}} +
     
    + {{/editing}} + {{{cmlist}}} +
    +
    +
    +{{#js}} +{{! The block should be fast to load, we only load the editor when needed.}} +{{#editing}} +require( + [ + 'core_courseformat/local/content', + 'core_courseformat/courseeditor' + ], + function( + Component, + Courseeditor + ) { + {{! The block could be included static in other courses so we use Courseeditor.getCourseEditor. }} + new Component({ + element: document.getElementById('block_site_main_menu_section'), + reactive: Courseeditor.getCourseEditor({{siteid}}), + }); + } +); +{{/editing}} +{{/js}} From a5ba68c97f185c5d235db6c236877265ddecbf1c Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Tue, 8 Oct 2024 18:03:26 +0200 Subject: [PATCH 4/7] MDL-82349 block_site_main_menu: fix behats --- .../behat/behat_block_site_main_menu.php | 119 ----------- .../behat_block_site_main_menu_deprecated.php | 194 ++++++++++++++++++ .../tests/behat/edit_activities.feature | 124 ++++++++++- 3 files changed, 312 insertions(+), 125 deletions(-) create mode 100644 blocks/site_main_menu/tests/behat/behat_block_site_main_menu_deprecated.php diff --git a/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php b/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php index 00190399429..0580e24e0c5 100644 --- a/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php +++ b/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php @@ -27,10 +27,6 @@ require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); -use Behat\Mink\Exception\ExpectationException as ExpectationException, - Behat\Mink\Exception\DriverException as DriverException, - Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; - /** * Behat steps definitions for block site main menu * @@ -40,121 +36,6 @@ use Behat\Mink\Exception\ExpectationException as ExpectationException, * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_block_site_main_menu extends behat_base { - - /** - * Returns the DOM node of the activity in the site menu block - * - * @throws ElementNotFoundException Thrown by behat_base::find - * @param string $activityname The activity name - * @return NodeElement - */ - protected function get_site_menu_activity_node($activityname) { - $activityname = behat_context_helper::escape($activityname); - $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]//li[contains(., $activityname)]"; - - return $this->find('xpath', $xpath); - } - - /** - * Checks that the specified activity's action menu contains an item. - * - * @Then /^"(?P(?:[^"]|\\")*)" activity in site main menu block should have "(?P(?:[^"]|\\")*)" editing icon$/ - * @param string $activityname - * @param string $iconname - */ - public function activity_in_site_main_menu_block_should_have_editing_icon($activityname, $iconname) { - $activitynode = $this->get_site_menu_activity_node($activityname); - - $notfoundexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' . - $iconname . '" editing icon', $this->getSession()); - $this->find('named_partial', array('link', $iconname), $notfoundexception, $activitynode); - } - - /** - * Checks that the specified activity's action menu contains an item. - * - * @Then /^"(?P(?:[^"]|\\")*)" activity in site main menu block should not have "(?P(?:[^"]|\\")*)" editing icon$/ - * @param string $activityname - * @param string $iconname - */ - public function activity_in_site_main_menu_block_should_not_have_editing_icon($activityname, $iconname) { - $activitynode = $this->get_site_menu_activity_node($activityname); - - try { - $this->find('named_partial', array('link', $iconname), false, $activitynode); - throw new ExpectationException('"' . $activityname . '" has a "' . $iconname . - '" editing icon when it should not', $this->getSession()); - } catch (ElementNotFoundException $e) { - // This is good, the menu item should not be there. - } - } - - /** - * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on. - * - * @Given /^I click on "(?P(?:[^"]|\\")*)" "(?P(?:[^"]|\\")*)" in the "(?P(?:[^"]|\\")*)" activity in site main menu block$/ - * @param string $element - * @param string $selectortype - * @param string $activityname - */ - public function i_click_on_in_the_activity_in_site_main_menu_block($element, $selectortype, $activityname) { - $element = $this->get_site_menu_activity_element($element, $selectortype, $activityname); - $element->click(); - } - - /** - * Clicks on the specified element inside the activity container. - * - * @throws ElementNotFoundException - * @param string $element - * @param string $selectortype - * @param string $activityname - * @return NodeElement - */ - protected function get_site_menu_activity_element($element, $selectortype, $activityname) { - $activitynode = $this->get_site_menu_activity_node($activityname); - - $exception = new ElementNotFoundException($this->getSession(), "'{$element}' '{$selectortype}' in '{$activityname}'"); - return $this->find($selectortype, $element, $exception, $activitynode); - } - - /** - * Checks that the specified activity is hidden. - * - * @Then /^"(?P(?:[^"]|\\")*)" activity in site main menu block should be hidden$/ - * @param string $activityname - */ - public function activity_in_site_main_menu_block_should_be_hidden($activityname) { - $activitynode = $this->get_site_menu_activity_node($activityname); - $exception = new ExpectationException('"' . $activityname . '" is not hidden', $this->getSession()); - $this->find('named_partial', array('badge', get_string('hiddenfromstudents')), $exception, $activitynode); - } - - /** - * Checks that the specified activity is hidden. - * - * @Then /^"(?P(?:[^"]|\\")*)" activity in site main menu block should be available but hidden from course page$/ - * @param string $activityname - */ - public function activity_in_site_main_menu_block_should_be_available_but_hidden_from_course_page($activityname) { - $activitynode = $this->get_site_menu_activity_node($activityname); - $exception = new ExpectationException('"' . $activityname . '" is not hidden but available', $this->getSession()); - $this->find('named_partial', array('badge', get_string('hiddenoncoursepage')), $exception, $activitynode); - } - - /** - * Opens an activity actions menu if it is not already opened. - * - * @Given /^I open "(?P(?:[^"]|\\")*)" actions menu in site main menu block$/ - * @throws DriverException The step is not available when Javascript is disabled - * @param string $activityname - */ - public function i_open_actions_menu_in_site_main_menu_block($activityname) { - $activityname = behat_context_helper::escape($activityname); - $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]//li[contains(., $activityname)]"; - $this->execute('behat_action_menu::i_open_the_action_menu_in', [$xpath, 'xpath_element']); - } - /** * Return the list of partial named selectors. * diff --git a/blocks/site_main_menu/tests/behat/behat_block_site_main_menu_deprecated.php b/blocks/site_main_menu/tests/behat/behat_block_site_main_menu_deprecated.php new file mode 100644 index 00000000000..9cd001d07fe --- /dev/null +++ b/blocks/site_main_menu/tests/behat/behat_block_site_main_menu_deprecated.php @@ -0,0 +1,194 @@ +. + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +require_once(__DIR__ . '/../../../../lib/behat/behat_deprecated_base.php'); + +use Behat\Mink\Element\NodeElement; +use Behat\Mink\Exception\ExpectationException as ExpectationException; +use Behat\Mink\Exception\DriverException as DriverException; +use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; + +/** + * Behat steps in plugin block_site_main_menu + * + * @package block_site_main_menu + * @category test + * @copyright 2024 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_block_site_main_menu_deprecated extends behat_deprecated_base { + + /** + * Returns the DOM node of the activity in the site menu block + * + * @todo MDL-78077 This will be deleted in Moodle 6.0. + * @throws ElementNotFoundException Thrown by behat_base::find + * @param string $activityname The activity name + * @return NodeElement + */ + protected function get_site_menu_activity_node($activityname) { + $activityname = behat_context_helper::escape($activityname); + $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]//li[contains(., $activityname)]"; + + return $this->find('xpath', $xpath); + } + + /** + * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on. + * + * @todo MDL-78077 This will be deleted in Moodle 6.0. + * @deprecated since 5.0 + * + * @Given /^I click on "(?P(?:[^"]|\\")*)" "(?P(?:[^"]|\\")*)" in the "(?P(?:[^"]|\\")*)" activity in site main menu block$/ + * @param string $element + * @param string $selectortype + * @param string $activityname + */ + public function i_click_on_in_the_activity_in_site_main_menu_block($element, $selectortype, $activityname) { + $this->deprecated_message([ + 'behat_block_site_main_menu::i_click_on_in_the_activity_in_site_main_menu_block is deprecated', + 'Use: I open ACTIVITYNAME actions menu & I choose OPTIONTEXT in the open action menu', + ]); + $element = $this->get_site_menu_activity_element($element, $selectortype, $activityname); + $element->click(); + } + + /** + * Clicks on the specified element inside the activity container. + * + * @todo MDL-78077 This will be deleted in Moodle 6.0. + * @throws ElementNotFoundException + * @param string $element + * @param string $selectortype + * @param string $activityname + * @return NodeElement + */ + protected function get_site_menu_activity_element($element, $selectortype, $activityname) { + $activitynode = $this->get_site_menu_activity_node($activityname); + + $exception = new ElementNotFoundException($this->getSession(), "'{$element}' '{$selectortype}' in '{$activityname}'"); + return $this->find($selectortype, $element, $exception, $activitynode); + } + + /** + * Checks that the specified activity's action menu contains an item. + * + * @todo MDL-78077 This will be deleted in Moodle 6.0. + * @deprecated since 5.0 + * + * @Then /^"(?P(?:[^"]|\\")*)" activity in site main menu block should have "(?P(?:[^"]|\\")*)" editing icon$/ + * @param string $activityname + * @param string $iconname + */ + public function activity_in_site_main_menu_block_should_have_editing_icon($activityname, $iconname) { + $this->deprecated_message([ + 'behat_block_site_main_menu::activity_in_site_main_menu_block_should_have_editing_icon is deprecated', + 'Use: I should see WHATEVER in the ACTIVITYNAME "activity"', + ]); + + $activitynode = $this->get_site_menu_activity_node($activityname); + + $notfoundexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' . + $iconname . '" editing icon', $this->getSession()); + $this->find('named_partial', ['link', $iconname], $notfoundexception, $activitynode); + } + + /** + * Checks that the specified activity's action menu contains an item. + * + * @todo MDL-78077 This will be deleted in Moodle 6.0. + * @deprecated since 5.0 + * + * @Then /^"(?P(?:[^"]|\\")*)" activity in site main menu block should not have "(?P(?:[^"]|\\")*)" editing icon$/ + * @param string $activityname + * @param string $iconname + */ + public function activity_in_site_main_menu_block_should_not_have_editing_icon($activityname, $iconname) { + $this->deprecated_message([ + 'behat_block_site_main_menu::activity_in_site_main_menu_block_should_not_have_editing_icon is deprecated', + 'Use: I should not see WHATEVER in the ACTIVITYNAME "activity"', + ]); + $activitynode = $this->get_site_menu_activity_node($activityname); + + try { + $this->find('named_partial', array('link', $iconname), false, $activitynode); + throw new ExpectationException('"' . $activityname . '" has a "' . $iconname . + '" editing icon when it should not', $this->getSession()); + } catch (ElementNotFoundException $e) { + // This is good, the menu item should not be there. + } + } + + /** + * Checks that the specified activity is hidden. + * + * @todo MDL-78077 This will be deleted in Moodle 6.0. + * @deprecated since 5.0 + * + * @Then /^"(?P(?:[^"]|\\")*)" activity in site main menu block should be hidden$/ + * @param string $activityname + */ + public function activity_in_site_main_menu_block_should_be_hidden($activityname) { + $this->deprecated_message([ + 'behat_block_site_main_menu::activity_in_site_main_menu_block_should_be_hidden is deprecated', + 'Use: I should see "Hidden from students" in the "ACTIVITYNAME" "core_courseformat > Activity visibility"', + ]); + $activitynode = $this->get_site_menu_activity_node($activityname); + $exception = new ExpectationException('"' . $activityname . '" is not hidden', $this->getSession()); + $this->find('named_partial', ['badge', get_string('hiddenfromstudents')], $exception, $activitynode); + } + + /** + * Checks that the specified activity is hidden. + * + * @todo MDL-78077 This will be deleted in Moodle 6.0. + * @deprecated since 5.0 + * + * @Then /^"(?P(?:[^"]|\\")*)" activity in site main menu block should be available but hidden from course page$/ + * @param string $activityname + */ + public function activity_in_site_main_menu_block_should_be_available_but_hidden_from_course_page($activityname) { + $this->deprecated_message([ + 'behat_block_site_main_menu::activity_in_site_main_menu_block_should_be_available_but_hidden_from_course_page is deprecated', + 'Use: I should see "Available but not shown on course page" in the "ACTIVITYNAME" "core_courseformat > Activity visibility"', + ]); + $activitynode = $this->get_site_menu_activity_node($activityname); + $exception = new ExpectationException('"' . $activityname . '" is not hidden but available', $this->getSession()); + $this->find('named_partial', ['badge', get_string('hiddenoncoursepage')], $exception, $activitynode); + } + + /** + * Opens an activity actions menu if it is not already opened. + * + * @todo MDL-78077 This will be deleted in Moodle 6.0. + * @deprecated since 5.0 + * + * @Given /^I open "(?P(?:[^"]|\\")*)" actions menu in site main menu block$/ + * @throws DriverException The step is not available when Javascript is disabled + * @param string $activityname + */ + public function i_open_actions_menu_in_site_main_menu_block($activityname) { + $this->deprecated_message([ + 'behat_block_site_main_menu::i_open_actions_menu_in_site_main_menu_block is deprecated', + 'Use: I open "ACTIVITYNAME" actions menu', + ]); + $activityname = behat_context_helper::escape($activityname); + $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]//li[contains(., $activityname)]"; + $this->execute('behat_action_menu::i_open_the_action_menu_in', [$xpath, 'xpath_element']); + } +} diff --git a/blocks/site_main_menu/tests/behat/edit_activities.feature b/blocks/site_main_menu/tests/behat/edit_activities.feature index 336c14bc652..1617417fcd6 100644 --- a/blocks/site_main_menu/tests/behat/edit_activities.feature +++ b/blocks/site_main_menu/tests/behat/edit_activities.feature @@ -17,7 +17,7 @@ Feature: Edit activities in main menu block And I log in as "admin" And I am on site homepage And I turn editing mode on - When I set the field "Edit title" in the "My forum name" "block_site_main_menu > Activity" to "New forum name" + When I set the field "Edit title" in the "My forum name" "activity" to "New forum name" Then I should not see "My forum name" And I should see "New forum name" And I follow "New forum name" @@ -38,20 +38,132 @@ Feature: Edit activities in main menu block And I log in as "admin" And I am on site homepage And I turn editing mode on - When I open "My forum name" actions menu in site main menu block + When I open "My forum name" actions menu And I choose "Availability > Make available but don't show on course page" in the open action menu Then I should see "Available but not shown on course page" in the "My forum name" "core_courseformat > Activity visibility" # Make sure that "Availability" dropdown in the edit menu has three options. - And I open "My forum name" actions menu in site main menu block - And I click on "Edit settings" "link" in the "My forum name" activity in site main menu block + And I open "My forum name" actions menu + And I choose "Edit settings" in the open action menu And I expand all fieldsets And the "Availability" select box should contain "Show on course page" And the "Availability" select box should contain "Hide on course page" And the field "Availability" matches value "Make available but don't show on course page" And I press "Save and return to course" - And "My forum name" activity in site main menu block should be available but hidden from course page + And I should see "Available but not shown on course page" in the "My forum name" "core_courseformat > Activity visibility" And I turn editing mode off - And "My forum name" activity in site main menu block should be available but hidden from course page + And I should see "Available but not shown on course page" in the "My forum name" "core_courseformat > Activity visibility" And I log out And I should not see "My forum name" in the "Main menu" "block" And I should see "Visible forum" in the "Main menu" "block" + + @javascript + Scenario: The move activity modal allow to move from the main menu block to the main content + Given the following "activity" exists: + | activity | forum | + | course | Acceptance test site | + | name | My forum name | + | idnumber | forum | + And the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | + | site_main_menu | System | 1 | site-index | side-pre | + And I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I should see "My forum name" in the "block_site_main_menu_section" "region" + And I should not see "My forum name" in the "region-main" "region" + When I open "My forum name" actions menu + And I click on "Move" "link" in the "My forum name" activity + And I should see "My forum name" in the "Move activity" "dialogue" + And I should see "Block" in the "Move activity" "dialogue" + And I should see "Site" in the "Move activity" "dialogue" + And I click on "Site" "link" in the "Move activity" "dialogue" + Then I should see "My forum name" in the "region-main" "region" + And I should not see "My forum name" in the "block_site_main_menu_section" "region" + + @javascript + Scenario: The move activity modal allow to move from the main content to the main menu block + Given the following "activity" exists: + | activity | forum | + | course | Acceptance test site | + | name | My forum name | + | idnumber | forum | + | section | 1 | + And the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | + | site_main_menu | System | 1 | site-index | side-pre | + And I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I should not see "My forum name" in the "block_site_main_menu_section" "region" + And I should see "My forum name" in the "region-main" "region" + When I open "My forum name" actions menu + And I click on "Move" "link" in the "My forum name" activity + And I should see "My forum name" in the "Move activity" "dialogue" + And I should see "Block" in the "Move activity" "dialogue" + And I should see "Site" in the "Move activity" "dialogue" + And I click on "Block" "link" in the "Move activity" "dialogue" + Then I should not see "My forum name" in the "region-main" "region" + And I should see "My forum name" in the "block_site_main_menu_section" "region" + + @javascript + Scenario: Admin can delete an activity in the main menu block + Given the following "activity" exists: + | activity | forum | + | course | Acceptance test site | + | name | My forum name | + | idnumber | forum | + And the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | + | site_main_menu | System | 1 | site-index | side-pre | + And I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I should see "My forum name" in the "block_site_main_menu_section" "region" + When I open "My forum name" actions menu + And I choose "Delete" in the open action menu + And I click on "Delete" "button" in the "Delete activity?" "dialogue" + Then I should not see "My forum name" in the "block_site_main_menu_section" "region" + + @javascript + Scenario: Admin can duplicate an activity in the main menu block + Given the following "activity" exists: + | activity | forum | + | course | Acceptance test site | + | name | My forum name | + | idnumber | forum | + And the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | + | site_main_menu | System | 1 | site-index | side-pre | + And I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I should see "My forum name" in the "block_site_main_menu_section" "region" + When I open "My forum name" actions menu + And I choose "Duplicate" in the open action menu + Then I should see "My forum name (copy)" in the "block_site_main_menu_section" "region" + + @javascript + Scenario: Admin can move right and left an activity in the main menu block + Given the following "activity" exists: + | activity | forum | + | course | Acceptance test site | + | name | My forum name | + | idnumber | forum | + And the following "blocks" exist: + | blockname | contextlevel | reference | pagetypepattern | defaultregion | + | site_main_menu | System | 1 | site-index | side-pre | + And I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I should see "My forum name" in the "block_site_main_menu_section" "region" + When I open "My forum name" actions menu + And "Move right" "link" should be visible + And "Move left" "link" should not be visible + And I choose "Move right" in the open action menu + Then I open "My forum name" actions menu + And "Move right" "link" should not be visible + And "Move left" "link" should be visible + And I choose "Move left" in the open action menu + And I open "My forum name" actions menu + And "Move right" "link" should be visible + And "Move left" "link" should not be visible From cccacbdc04f01c3e76ba265c8a1c29ad8cb41ef7 Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Tue, 8 Oct 2024 18:36:16 +0200 Subject: [PATCH 5/7] MDL-82349 course: add frontpage behats --- .../behat/frontpage_topic_section.feature | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/course/tests/behat/frontpage_topic_section.feature b/course/tests/behat/frontpage_topic_section.feature index 646bb1d1c8f..1f1fa00c3f1 100644 --- a/course/tests/behat/frontpage_topic_section.feature +++ b/course/tests/behat/frontpage_topic_section.feature @@ -42,3 +42,75 @@ Feature: Site home activities section And I should see "New section description" in the "region-main" "region" Then I turn editing mode off And I should see "New section description" in the "region-main" "region" + + @javascript + Scenario: Admin can change the activity visibility in the frontpage + Given the following config values are set as admin: + | allowstealth | 1 | + And the following "activities" exist: + | activity | course | section | name | intro | idnumber | + | assign | Acceptance test site | 1 | Frontpage assignment | Assignment description | assign0 | + When I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I should see "Frontpage assignment" in the "region-main" "region" + Then I open "Frontpage assignment" actions menu + And I choose "Availability > Make available but don't show on course page" in the open action menu + And I should see "Available but not shown on course page" in the "Frontpage assignment" "core_courseformat > Activity visibility" + And I open "Frontpage assignment" actions menu + And I choose "Availability > Show on course page" in the open action menu + And I should not see "Available but not shown on course page" in the "Frontpage assignment" "activity" + And I should not see "Hidden from students" in the "Frontpage assignment" "activity" + And I open "Frontpage assignment" actions menu + And I choose "Availability > Hide on course page" in the open action menu + And I should not see "Available but not shown on course page" in the "Frontpage assignment" "activity" + And I should see "Hidden from students" in the "Frontpage assignment" "core_courseformat > Activity visibility" + + @javascript + Scenario: Admin can delete an activity in the frontpage + Given the following "activities" exist: + | activity | course | section | name | intro | idnumber | + | assign | Acceptance test site | 1 | Frontpage assignment | Assignment description | assign0 | + When I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I should see "Frontpage assignment" in the "region-main" "region" + Then I open "Frontpage assignment" actions menu + And I choose "Delete" in the open action menu + And I click on "Delete" "button" in the "Delete activity?" "dialogue" + And I should not see "Frontpage assignment" in the "region-main" "region" + + @javascript + Scenario: Admin can duplicate an activity in the frontpage + Given the following "activities" exist: + | activity | course | section | name | intro | idnumber | + | assign | Acceptance test site | 1 | Frontpage assignment | Assignment description | assign0 | + When I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I should see "Frontpage assignment" in the "region-main" "region" + Then I open "Frontpage assignment" actions menu + And I choose "Duplicate" in the open action menu + And I should see "Frontpage assignment (copy)" in the "region-main" "region" + + @javascript + Scenario: Admin can move an activity lefts and right in the frontpage + Given the following "activities" exist: + | activity | course | section | name | intro | idnumber | + | assign | Acceptance test site | 1 | Frontpage assignment | Assignment description | assign0 | + | assign | Acceptance test site | 1 | Frontpage assignment | Assignment description | assign1 | + And I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I should see "Frontpage assignment" in the "region-main" "region" + When I open "Frontpage assignment" actions menu + And "Move right" "link" should be visible + And "Move left" "link" should not be visible + And I choose "Move right" in the open action menu + Then I open "Frontpage assignment" actions menu + And "Move right" "link" should not be visible + And "Move left" "link" should be visible + And I choose "Move left" in the open action menu + And I open "Frontpage assignment" actions menu + And "Move right" "link" should be visible + And "Move left" "link" should not be visible From 0d04567882ee888b494060822c4d6639cc5619a6 Mon Sep 17 00:00:00 2001 From: ferranrecio Date: Tue, 8 Oct 2024 20:09:31 +0200 Subject: [PATCH 6/7] MDL-82349 course: fix guest access to course --- .../amd/build/local/courseeditor/courseeditor.min.js | 4 ++-- .../build/local/courseeditor/courseeditor.min.js.map | 2 +- .../format/amd/src/local/courseeditor/courseeditor.js | 11 +++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/course/format/amd/build/local/courseeditor/courseeditor.min.js b/course/format/amd/build/local/courseeditor/courseeditor.min.js index e6f2f0436c6..bd42ce87bed 100644 --- a/course/format/amd/build/local/courseeditor/courseeditor.min.js +++ b/course/format/amd/build/local/courseeditor/courseeditor.min.js @@ -1,4 +1,4 @@ -define("core_courseformat/local/courseeditor/courseeditor",["exports","core/str","core/reactive","core/notification","core_courseformat/local/courseeditor/exporter","core/log","core/ajax","core/sessionstorage","core_courseformat/local/courseeditor/fileuploader"],(function(_exports,_str,_reactive,_notification,_exporter,_log,_ajax,Storage,_fileuploader){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj} +define("core_courseformat/local/courseeditor/courseeditor",["exports","core/config","core/str","core/reactive","core/notification","core_courseformat/local/courseeditor/exporter","core/log","core/ajax","core/sessionstorage","core_courseformat/local/courseeditor/fileuploader"],(function(_exports,_config,_str,_reactive,_notification,_exporter,_log,_ajax,Storage,_fileuploader){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj} /** * Main course editor module. * @@ -9,6 +9,6 @@ define("core_courseformat/local/courseeditor/courseeditor",["exports","core/str" * @class core_courseformat/local/courseeditor/courseeditor * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=_interopRequireDefault(_notification),_exporter=_interopRequireDefault(_exporter),_log=_interopRequireDefault(_log),_ajax=_interopRequireDefault(_ajax),Storage=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Storage);class _default extends _reactive.Reactive{constructor(){super(...arguments),_defineProperty(this,"stateKey",1),_defineProperty(this,"sectionReturn",null)}async loadCourse(courseId,serverStateKey){if(this.courseId)throw new Error("Cannot load ".concat(courseId,", course already loaded with id ").concat(this.courseId));let stateData;serverStateKey||(serverStateKey="invalidStateKey_".concat(Date.now())),this._editing=!1,this._supportscomponents=!1,this._fileHandlers=null,this.courseId=courseId;const storeStateKey=Storage.get("course/".concat(courseId,"/stateKey"));try{this.isEditing||serverStateKey!=storeStateKey||(stateData=JSON.parse(Storage.get("course/".concat(courseId,"/staticState")))),stateData||(stateData=await this.getServerCourseState())}catch(error){return _log.default.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR"),void _log.default.error(error)}if(stateData.bulk={enabled:!1,selectedType:"",selection:[]},this.setInitialState(stateData),this.isEditing)this.stateKey=null;else{const newState=JSON.stringify(stateData);var _stateData$course$sta,_stateData,_stateData$course;if(Storage.get("course/".concat(courseId,"/staticState"))!==newState||storeStateKey!==serverStateKey)Storage.set("course/".concat(courseId,"/staticState"),newState),Storage.set("course/".concat(courseId,"/stateKey"),null!==(_stateData$course$sta=null===(_stateData=stateData)||void 0===_stateData||null===(_stateData$course=_stateData.course)||void 0===_stateData$course?void 0:_stateData$course.statekey)&&void 0!==_stateData$course$sta?_stateData$course$sta:serverStateKey);this.stateKey=Storage.get("course/".concat(courseId,"/stateKey"))}this._loadFileHandlers(),this._pageAnchorCmInfo=this._scanPageAnchorCmInfo()}_loadFileHandlers(){this._fileHandlersPromise=new Promise((resolve=>{if(!this.isEditing)return void resolve([]);const handlersCacheKey="course/".concat(this.courseId,"/fileHandlers"),cacheValue=Storage.get(handlersCacheKey);if(cacheValue)try{const cachedHandlers=JSON.parse(cacheValue);return void resolve(cachedHandlers)}catch(error){_log.default.error("ERROR PARSING CACHED FILE HANDLERS")}_ajax.default.call([{methodname:"core_courseformat_file_handlers",args:{courseid:this.courseId}}])[0].then((handlers=>{Storage.set(handlersCacheKey,JSON.stringify(handlers)),resolve(handlers)})).catch((error=>{_log.default.error(error),resolve([])}))}))}setViewFormat(setup){var _setup$editing,_setup$supportscompon,_setup$overriddenStri;this._editing=null!==(_setup$editing=setup.editing)&&void 0!==_setup$editing&&_setup$editing,this._supportscomponents=null!==(_setup$supportscompon=setup.supportscomponents)&&void 0!==_setup$supportscompon&&_setup$supportscompon;const overriddenStrings=null!==(_setup$overriddenStri=setup.overriddenStrings)&&void 0!==_setup$overriddenStri?_setup$overriddenStri:[];this._overriddenStrings=overriddenStrings.reduce(((indexed,currentValue)=>indexed.set(currentValue.key,currentValue)),new Map)}getFormatString(key,param){if(this._overriddenStrings.has(key)){var _override$component;const override=this._overriddenStrings.get(key);return(0,_str.getString)(key,null!==(_override$component=override.component)&&void 0!==_override$component?_override$component:"core_courseformat",param)}return(0,_str.getString)(key,"core_courseformat",param)}async getServerCourseState(){const courseState=await _ajax.default.call([{methodname:"core_courseformat_get_state",args:{courseid:this.courseId}}])[0];return{course:{},section:[],cm:[],...JSON.parse(courseState)}}get isEditing(){var _this$_editing;return null!==(_this$_editing=this._editing)&&void 0!==_this$_editing&&_this$_editing}getExporter(){return new _exporter.default(this)}get supportComponents(){var _this$_supportscompon;return null!==(_this$_supportscompon=this._supportscomponents)&&void 0!==_this$_supportscompon&&_this$_supportscompon}async getFileHandlersPromise(){var _this$_fileHandlersPr;return null!==(_this$_fileHandlersPr=this._fileHandlersPromise)&&void 0!==_this$_fileHandlersPr?_this$_fileHandlersPr:[]}uploadFiles(sectionId,sectionNum,files){return(0,_fileuploader.uploadFilesToCourse)(this.courseId,sectionId,sectionNum,files)}getStorageValue(key){if(this.isEditing||!this.stateKey)return!1;const dataJson=Storage.get("course/".concat(this.courseId,"/").concat(key));if(!dataJson)return!1;try{const data=JSON.parse(dataJson);return(null==data?void 0:data.stateKey)===this.stateKey&&data.value}catch(error){return!1}}setStorageValue(key,value){if(this.isEditing)return!1;const data={stateKey:this.stateKey,value:value};return Storage.set("course/".concat(this.courseId,"/").concat(key),JSON.stringify(data))}getFilesDraggableData(dataTransfer){return this.getExporter().fileDraggableData(this.state,dataTransfer)}async dispatch(){try{await super.dispatch(...arguments)}catch(error){_notification.default.exception(error),super.dispatch("unlockAll")}}_scanPageAnchorCmInfo(){const anchor=new URL(window.location.href).hash;if(!anchor.startsWith("#module-"))return null;const cmid=anchor.split("-")[1];return this.stateManager.get("cm",parseInt(cmid))}getPageAnchorCmInfo(){return this._pageAnchorCmInfo}}return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_config=_interopRequireDefault(_config),_notification=_interopRequireDefault(_notification),_exporter=_interopRequireDefault(_exporter),_log=_interopRequireDefault(_log),_ajax=_interopRequireDefault(_ajax),Storage=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Storage);class _default extends _reactive.Reactive{constructor(){super(...arguments),_defineProperty(this,"stateKey",1),_defineProperty(this,"sectionReturn",null)}async loadCourse(courseId,serverStateKey){if(this.courseId)throw new Error("Cannot load ".concat(courseId,", course already loaded with id ").concat(this.courseId));let stateData;serverStateKey||(serverStateKey="invalidStateKey_".concat(Date.now())),this._editing=!1,this._supportscomponents=!1,this._fileHandlers=null,this.courseId=courseId;const storeStateKey=Storage.get("course/".concat(courseId,"/stateKey"));try{this.isEditing||serverStateKey!=storeStateKey||(stateData=JSON.parse(Storage.get("course/".concat(courseId,"/staticState")))),stateData||(stateData=await this.getServerCourseState())}catch(error){return _log.default.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR"),void _log.default.error(error)}if(stateData.bulk={enabled:!1,selectedType:"",selection:[]},this.setInitialState(stateData),this.isEditing)this.stateKey=null;else{const newState=JSON.stringify(stateData);var _stateData$course$sta,_stateData,_stateData$course;if(Storage.get("course/".concat(courseId,"/staticState"))!==newState||storeStateKey!==serverStateKey)Storage.set("course/".concat(courseId,"/staticState"),newState),Storage.set("course/".concat(courseId,"/stateKey"),null!==(_stateData$course$sta=null===(_stateData=stateData)||void 0===_stateData||null===(_stateData$course=_stateData.course)||void 0===_stateData$course?void 0:_stateData$course.statekey)&&void 0!==_stateData$course$sta?_stateData$course$sta:serverStateKey);this.stateKey=Storage.get("course/".concat(courseId,"/stateKey"))}this._loadFileHandlers(),this._pageAnchorCmInfo=this._scanPageAnchorCmInfo()}_loadFileHandlers(){this._fileHandlersPromise=new Promise((resolve=>{if(!this.isEditing)return void resolve([]);const handlersCacheKey="course/".concat(this.courseId,"/fileHandlers"),cacheValue=Storage.get(handlersCacheKey);if(cacheValue)try{const cachedHandlers=JSON.parse(cacheValue);return void resolve(cachedHandlers)}catch(error){_log.default.error("ERROR PARSING CACHED FILE HANDLERS")}_ajax.default.call([{methodname:"core_courseformat_file_handlers",args:{courseid:this.courseId}}])[0].then((handlers=>{Storage.set(handlersCacheKey,JSON.stringify(handlers)),resolve(handlers)})).catch((error=>{_log.default.error(error),resolve([])}))}))}setViewFormat(setup){var _setup$editing,_setup$supportscompon,_setup$overriddenStri;this._editing=null!==(_setup$editing=setup.editing)&&void 0!==_setup$editing&&_setup$editing,this._supportscomponents=null!==(_setup$supportscompon=setup.supportscomponents)&&void 0!==_setup$supportscompon&&_setup$supportscompon;const overriddenStrings=null!==(_setup$overriddenStri=setup.overriddenStrings)&&void 0!==_setup$overriddenStri?_setup$overriddenStri:[];this._overriddenStrings=overriddenStrings.reduce(((indexed,currentValue)=>indexed.set(currentValue.key,currentValue)),new Map)}getFormatString(key,param){if(this._overriddenStrings.has(key)){var _override$component;const override=this._overriddenStrings.get(key);return(0,_str.getString)(key,null!==(_override$component=override.component)&&void 0!==_override$component?_override$component:"core_courseformat",param)}return(0,_str.getString)(key,"core_courseformat",param)}async getServerCourseState(){if(0==_config.default.userId)return{course:{},section:[],cm:[]};const courseState=await _ajax.default.call([{methodname:"core_courseformat_get_state",args:{courseid:this.courseId}}])[0];return{course:{},section:[],cm:[],...JSON.parse(courseState)}}get isEditing(){var _this$_editing;return null!==(_this$_editing=this._editing)&&void 0!==_this$_editing&&_this$_editing}getExporter(){return new _exporter.default(this)}get supportComponents(){var _this$_supportscompon;return null!==(_this$_supportscompon=this._supportscomponents)&&void 0!==_this$_supportscompon&&_this$_supportscompon}async getFileHandlersPromise(){var _this$_fileHandlersPr;return null!==(_this$_fileHandlersPr=this._fileHandlersPromise)&&void 0!==_this$_fileHandlersPr?_this$_fileHandlersPr:[]}uploadFiles(sectionId,sectionNum,files){return(0,_fileuploader.uploadFilesToCourse)(this.courseId,sectionId,sectionNum,files)}getStorageValue(key){if(this.isEditing||!this.stateKey)return!1;const dataJson=Storage.get("course/".concat(this.courseId,"/").concat(key));if(!dataJson)return!1;try{const data=JSON.parse(dataJson);return(null==data?void 0:data.stateKey)===this.stateKey&&data.value}catch(error){return!1}}setStorageValue(key,value){if(this.isEditing)return!1;const data={stateKey:this.stateKey,value:value};return Storage.set("course/".concat(this.courseId,"/").concat(key),JSON.stringify(data))}getFilesDraggableData(dataTransfer){return this.getExporter().fileDraggableData(this.state,dataTransfer)}async dispatch(){try{await super.dispatch(...arguments)}catch(error){_notification.default.exception(error),super.dispatch("unlockAll")}}_scanPageAnchorCmInfo(){const anchor=new URL(window.location.href).hash;if(!anchor.startsWith("#module-"))return null;const cmid=anchor.split("-")[1];return this.stateManager.get("cm",parseInt(cmid))}getPageAnchorCmInfo(){return this._pageAnchorCmInfo}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=courseeditor.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/courseeditor/courseeditor.min.js.map b/course/format/amd/build/local/courseeditor/courseeditor.min.js.map index b84053be786..18578444ac9 100644 --- a/course/format/amd/build/local/courseeditor/courseeditor.min.js.map +++ b/course/format/amd/build/local/courseeditor/courseeditor.min.js.map @@ -1 +1 @@ -{"version":3,"file":"courseeditor.min.js","sources":["../../../src/local/courseeditor/courseeditor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {getString} from 'core/str';\nimport {Reactive} from 'core/reactive';\nimport notification from 'core/notification';\nimport Exporter from 'core_courseformat/local/courseeditor/exporter';\nimport log from 'core/log';\nimport ajax from 'core/ajax';\nimport * as Storage from 'core/sessionstorage';\nimport {uploadFilesToCourse} from 'core_courseformat/local/courseeditor/fileuploader';\n\n/**\n * Main course editor module.\n *\n * All formats can register new components on this object to create new reactive\n * UI components that watch the current course state.\n *\n * @module core_courseformat/local/courseeditor/courseeditor\n * @class core_courseformat/local/courseeditor/courseeditor\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class extends Reactive {\n\n /**\n * The current state cache key\n *\n * The state cache is considered dirty if the state changes from the last page or\n * if the page has editing mode on.\n *\n * @attribute stateKey\n * @type number|null\n * @default 1\n * @package\n */\n stateKey = 1;\n\n /**\n * The current page section return\n * @attribute sectionReturn\n * @type number\n * @default null\n */\n sectionReturn = null;\n\n /**\n * Set up the course editor when the page is ready.\n *\n * The course can only be loaded once per instance. Otherwise an error is thrown.\n *\n * The backend can inform the module of the current state key. This key changes every time some\n * update in the course affect the current user state. Some examples are:\n * - The course content has been edited\n * - The user marks some activity as completed\n * - The user collapses or uncollapses a section (it is stored as a user preference)\n *\n * @param {number} courseId course id\n * @param {string} serverStateKey the current backend course cache reference\n */\n async loadCourse(courseId, serverStateKey) {\n\n if (this.courseId) {\n throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);\n }\n\n if (!serverStateKey) {\n // The server state key is not provided, we use a invalid statekey to force reloading.\n serverStateKey = `invalidStateKey_${Date.now()}`;\n }\n\n // Default view format setup.\n this._editing = false;\n this._supportscomponents = false;\n this._fileHandlers = null;\n\n this.courseId = courseId;\n\n let stateData;\n\n const storeStateKey = Storage.get(`course/${courseId}/stateKey`);\n try {\n // Check if the backend state key is the same we have in our session storage.\n if (!this.isEditing && serverStateKey == storeStateKey) {\n stateData = JSON.parse(Storage.get(`course/${courseId}/staticState`));\n }\n if (!stateData) {\n stateData = await this.getServerCourseState();\n }\n\n } catch (error) {\n log.error(\"EXCEPTION RAISED WHILE INIT COURSE EDITOR\");\n log.error(error);\n return;\n }\n\n // The bulk editing only applies to the frontend and the state data is not created in the backend.\n stateData.bulk = {\n enabled: false,\n selectedType: '',\n selection: [],\n };\n\n this.setInitialState(stateData);\n\n // In editing mode, the session cache is considered dirty always.\n if (this.isEditing) {\n this.stateKey = null;\n } else {\n // Check if the last state is the same as the cached one.\n const newState = JSON.stringify(stateData);\n const previousState = Storage.get(`course/${courseId}/staticState`);\n if (previousState !== newState || storeStateKey !== serverStateKey) {\n Storage.set(`course/${courseId}/staticState`, newState);\n Storage.set(`course/${courseId}/stateKey`, stateData?.course?.statekey ?? serverStateKey);\n }\n this.stateKey = Storage.get(`course/${courseId}/stateKey`);\n }\n\n this._loadFileHandlers();\n\n this._pageAnchorCmInfo = this._scanPageAnchorCmInfo();\n }\n\n /**\n * Load the file hanlders promise.\n */\n _loadFileHandlers() {\n // Load the course file extensions.\n this._fileHandlersPromise = new Promise((resolve) => {\n if (!this.isEditing) {\n resolve([]);\n return;\n }\n // Check the cache.\n const handlersCacheKey = `course/${this.courseId}/fileHandlers`;\n\n const cacheValue = Storage.get(handlersCacheKey);\n if (cacheValue) {\n try {\n const cachedHandlers = JSON.parse(cacheValue);\n resolve(cachedHandlers);\n return;\n } catch (error) {\n log.error(\"ERROR PARSING CACHED FILE HANDLERS\");\n }\n }\n // Call file handlers webservice.\n ajax.call([{\n methodname: 'core_courseformat_file_handlers',\n args: {\n courseid: this.courseId,\n }\n }])[0].then((handlers) => {\n Storage.set(handlersCacheKey, JSON.stringify(handlers));\n resolve(handlers);\n return;\n }).catch(error => {\n log.error(error);\n resolve([]);\n return;\n });\n });\n }\n\n /**\n * Setup the current view settings\n *\n * @param {Object} setup format, page and course settings\n * @param {boolean} setup.editing if the page is in edit mode\n * @param {boolean} setup.supportscomponents if the format supports components for content\n * @param {string} setup.cacherev the backend cached state revision\n * @param {Array} setup.overriddenStrings optional overridden strings\n */\n setViewFormat(setup) {\n this._editing = setup.editing ?? false;\n this._supportscomponents = setup.supportscomponents ?? false;\n const overriddenStrings = setup.overriddenStrings ?? [];\n this._overriddenStrings = overriddenStrings.reduce(\n (indexed, currentValue) => indexed.set(currentValue.key, currentValue),\n new Map()\n );\n }\n\n /**\n * Execute a get string for a possible format overriden editor string.\n *\n * Return the proper getString promise for an editor string using the core_courseformat\n * of the format_PLUGINNAME compoment depending on the current view format setup.\n * @param {String} key the string key\n * @param {string|undefined} param The param for variable expansion in the string.\n * @returns {Promise} a getString promise\n */\n getFormatString(key, param) {\n if (this._overriddenStrings.has(key)) {\n const override = this._overriddenStrings.get(key);\n return getString(key, override.component ?? 'core_courseformat', param);\n }\n // All format overridable strings are from core_courseformat lang file.\n return getString(key, 'core_courseformat', param);\n }\n\n /**\n * Load the current course state from the server.\n *\n * @returns {Object} the current course state\n */\n async getServerCourseState() {\n const courseState = await ajax.call([{\n methodname: 'core_courseformat_get_state',\n args: {\n courseid: this.courseId,\n }\n }])[0];\n\n const stateData = JSON.parse(courseState);\n\n return {\n course: {},\n section: [],\n cm: [],\n ...stateData,\n };\n }\n\n /**\n * Return the current edit mode.\n *\n * Components should use this method to check if edit mode is active.\n *\n * @return {boolean} if edit is enabled\n */\n get isEditing() {\n return this._editing ?? false;\n }\n\n /**\n * Return a data exporter to transform state part into mustache contexts.\n *\n * @return {Exporter} the exporter class\n */\n getExporter() {\n return new Exporter(this);\n }\n\n /**\n * Return if the current course support components to refresh the content.\n *\n * @returns {boolean} if the current content support components\n */\n get supportComponents() {\n return this._supportscomponents ?? false;\n }\n\n /**\n * Return the course file handlers promise.\n * @returns {Promise} the promise for file handlers.\n */\n async getFileHandlersPromise() {\n return this._fileHandlersPromise ?? [];\n }\n\n /**\n * Upload a file list to the course.\n *\n * This method is a wrapper to the course file uploader.\n *\n * @param {number} sectionId the section id\n * @param {number} sectionNum the section number\n * @param {Array} files and array of files\n * @return {Promise} the file queue promise\n */\n uploadFiles(sectionId, sectionNum, files) {\n return uploadFilesToCourse(this.courseId, sectionId, sectionNum, files);\n }\n\n /**\n * Get a value from the course editor static storage if any.\n *\n * The course editor static storage uses the sessionStorage to store values from the\n * components. This is used to prevent unnecesary template loadings on every page. However,\n * the storage does not work if no sessionStorage can be used (in debug mode for example),\n * if the page is in editing mode or if the initial state change from the last page.\n *\n * @param {string} key the key to get\n * @return {boolean|string} the storage value or false if cannot be loaded\n */\n getStorageValue(key) {\n if (this.isEditing || !this.stateKey) {\n return false;\n }\n const dataJson = Storage.get(`course/${this.courseId}/${key}`);\n if (!dataJson) {\n return false;\n }\n // Check the stateKey.\n try {\n const data = JSON.parse(dataJson);\n if (data?.stateKey !== this.stateKey) {\n return false;\n }\n return data.value;\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Stores a value into the course editor static storage if available\n *\n * @param {String} key the key to store\n * @param {*} value the value to store (must be compatible with JSON,stringify)\n * @returns {boolean} true if the value is stored\n */\n setStorageValue(key, value) {\n // Values cannot be stored on edit mode.\n if (this.isEditing) {\n return false;\n }\n const data = {\n stateKey: this.stateKey,\n value,\n };\n return Storage.set(`course/${this.courseId}/${key}`, JSON.stringify(data));\n }\n\n /**\n * Convert a file dragging event into a proper dragging file list.\n * @param {DataTransfer} dataTransfer the event to convert\n * @return {Array} of file list info.\n */\n getFilesDraggableData(dataTransfer) {\n const exporter = this.getExporter();\n return exporter.fileDraggableData(this.state, dataTransfer);\n }\n\n /**\n * Dispatch a change in the state.\n *\n * Usually reactive modules throw an error directly to the components when something\n * goes wrong. However, course editor can directly display a notification.\n *\n * @method dispatch\n * @param {mixed} args any number of params the mutation needs.\n */\n async dispatch(...args) {\n try {\n await super.dispatch(...args);\n } catch (error) {\n // Display error modal.\n notification.exception(error);\n // Force unlock all elements.\n super.dispatch('unlockAll');\n }\n }\n\n /**\n * Calculate the cm info from the current page anchor.\n *\n * @returns {Object|null} the cm info or null if not found.\n */\n _scanPageAnchorCmInfo() {\n const anchor = new URL(window.location.href).hash;\n if (!anchor.startsWith('#module-')) {\n return null;\n }\n // The anchor is always #module-CMID.\n const cmid = anchor.split('-')[1];\n return this.stateManager.get('cm', parseInt(cmid));\n }\n\n /**\n * Return the current page anchor cm info.\n */\n getPageAnchorCmInfo() {\n return this._pageAnchorCmInfo;\n }\n}\n"],"names":["Reactive","courseId","serverStateKey","this","Error","stateData","Date","now","_editing","_supportscomponents","_fileHandlers","storeStateKey","Storage","get","isEditing","JSON","parse","getServerCourseState","error","bulk","enabled","selectedType","selection","setInitialState","stateKey","newState","stringify","set","_stateData","course","_stateData$course","statekey","_loadFileHandlers","_pageAnchorCmInfo","_scanPageAnchorCmInfo","_fileHandlersPromise","Promise","resolve","handlersCacheKey","cacheValue","cachedHandlers","call","methodname","args","courseid","then","handlers","catch","setViewFormat","setup","editing","supportscomponents","overriddenStrings","_overriddenStrings","reduce","indexed","currentValue","key","Map","getFormatString","param","has","override","component","courseState","ajax","section","cm","getExporter","Exporter","supportComponents","uploadFiles","sectionId","sectionNum","files","getStorageValue","dataJson","data","value","setStorageValue","getFilesDraggableData","dataTransfer","fileDraggableData","state","super","dispatch","exception","anchor","URL","window","location","href","hash","startsWith","cmid","split","stateManager","parseInt","getPageAnchorCmInfo"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"courseeditor.min.js","sources":["../../../src/local/courseeditor/courseeditor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport Config from 'core/config';\nimport {getString} from 'core/str';\nimport {Reactive} from 'core/reactive';\nimport notification from 'core/notification';\nimport Exporter from 'core_courseformat/local/courseeditor/exporter';\nimport log from 'core/log';\nimport ajax from 'core/ajax';\nimport * as Storage from 'core/sessionstorage';\nimport {uploadFilesToCourse} from 'core_courseformat/local/courseeditor/fileuploader';\n\n/**\n * Main course editor module.\n *\n * All formats can register new components on this object to create new reactive\n * UI components that watch the current course state.\n *\n * @module core_courseformat/local/courseeditor/courseeditor\n * @class core_courseformat/local/courseeditor/courseeditor\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class extends Reactive {\n\n /**\n * The current state cache key\n *\n * The state cache is considered dirty if the state changes from the last page or\n * if the page has editing mode on.\n *\n * @attribute stateKey\n * @type number|null\n * @default 1\n * @package\n */\n stateKey = 1;\n\n /**\n * The current page section return\n * @attribute sectionReturn\n * @type number\n * @default null\n */\n sectionReturn = null;\n\n /**\n * Set up the course editor when the page is ready.\n *\n * The course can only be loaded once per instance. Otherwise an error is thrown.\n *\n * The backend can inform the module of the current state key. This key changes every time some\n * update in the course affect the current user state. Some examples are:\n * - The course content has been edited\n * - The user marks some activity as completed\n * - The user collapses or uncollapses a section (it is stored as a user preference)\n *\n * @param {number} courseId course id\n * @param {string} serverStateKey the current backend course cache reference\n */\n async loadCourse(courseId, serverStateKey) {\n\n if (this.courseId) {\n throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);\n }\n\n if (!serverStateKey) {\n // The server state key is not provided, we use a invalid statekey to force reloading.\n serverStateKey = `invalidStateKey_${Date.now()}`;\n }\n\n // Default view format setup.\n this._editing = false;\n this._supportscomponents = false;\n this._fileHandlers = null;\n\n this.courseId = courseId;\n\n let stateData;\n\n const storeStateKey = Storage.get(`course/${courseId}/stateKey`);\n try {\n // Check if the backend state key is the same we have in our session storage.\n if (!this.isEditing && serverStateKey == storeStateKey) {\n stateData = JSON.parse(Storage.get(`course/${courseId}/staticState`));\n }\n if (!stateData) {\n stateData = await this.getServerCourseState();\n }\n\n } catch (error) {\n log.error(\"EXCEPTION RAISED WHILE INIT COURSE EDITOR\");\n log.error(error);\n return;\n }\n\n // The bulk editing only applies to the frontend and the state data is not created in the backend.\n stateData.bulk = {\n enabled: false,\n selectedType: '',\n selection: [],\n };\n\n this.setInitialState(stateData);\n\n // In editing mode, the session cache is considered dirty always.\n if (this.isEditing) {\n this.stateKey = null;\n } else {\n // Check if the last state is the same as the cached one.\n const newState = JSON.stringify(stateData);\n const previousState = Storage.get(`course/${courseId}/staticState`);\n if (previousState !== newState || storeStateKey !== serverStateKey) {\n Storage.set(`course/${courseId}/staticState`, newState);\n Storage.set(`course/${courseId}/stateKey`, stateData?.course?.statekey ?? serverStateKey);\n }\n this.stateKey = Storage.get(`course/${courseId}/stateKey`);\n }\n\n this._loadFileHandlers();\n\n this._pageAnchorCmInfo = this._scanPageAnchorCmInfo();\n }\n\n /**\n * Load the file hanlders promise.\n */\n _loadFileHandlers() {\n // Load the course file extensions.\n this._fileHandlersPromise = new Promise((resolve) => {\n if (!this.isEditing) {\n resolve([]);\n return;\n }\n // Check the cache.\n const handlersCacheKey = `course/${this.courseId}/fileHandlers`;\n\n const cacheValue = Storage.get(handlersCacheKey);\n if (cacheValue) {\n try {\n const cachedHandlers = JSON.parse(cacheValue);\n resolve(cachedHandlers);\n return;\n } catch (error) {\n log.error(\"ERROR PARSING CACHED FILE HANDLERS\");\n }\n }\n // Call file handlers webservice.\n ajax.call([{\n methodname: 'core_courseformat_file_handlers',\n args: {\n courseid: this.courseId,\n }\n }])[0].then((handlers) => {\n Storage.set(handlersCacheKey, JSON.stringify(handlers));\n resolve(handlers);\n return;\n }).catch(error => {\n log.error(error);\n resolve([]);\n return;\n });\n });\n }\n\n /**\n * Setup the current view settings\n *\n * @param {Object} setup format, page and course settings\n * @param {boolean} setup.editing if the page is in edit mode\n * @param {boolean} setup.supportscomponents if the format supports components for content\n * @param {string} setup.cacherev the backend cached state revision\n * @param {Array} setup.overriddenStrings optional overridden strings\n */\n setViewFormat(setup) {\n this._editing = setup.editing ?? false;\n this._supportscomponents = setup.supportscomponents ?? false;\n const overriddenStrings = setup.overriddenStrings ?? [];\n this._overriddenStrings = overriddenStrings.reduce(\n (indexed, currentValue) => indexed.set(currentValue.key, currentValue),\n new Map()\n );\n }\n\n /**\n * Execute a get string for a possible format overriden editor string.\n *\n * Return the proper getString promise for an editor string using the core_courseformat\n * of the format_PLUGINNAME compoment depending on the current view format setup.\n * @param {String} key the string key\n * @param {string|undefined} param The param for variable expansion in the string.\n * @returns {Promise} a getString promise\n */\n getFormatString(key, param) {\n if (this._overriddenStrings.has(key)) {\n const override = this._overriddenStrings.get(key);\n return getString(key, override.component ?? 'core_courseformat', param);\n }\n // All format overridable strings are from core_courseformat lang file.\n return getString(key, 'core_courseformat', param);\n }\n\n /**\n * Load the current course state from the server.\n *\n * @returns {Object} the current course state\n */\n async getServerCourseState() {\n // Only logged users can get the course state. Filtering here will prevent unnecessary\n // calls to the server and login page redirects. Especially for home activities with\n // guest access.\n if (Config.userId == 0) {\n return {\n course: {},\n section: [],\n cm: [],\n };\n }\n const courseState = await ajax.call([{\n methodname: 'core_courseformat_get_state',\n args: {\n courseid: this.courseId,\n }\n }])[0];\n\n const stateData = JSON.parse(courseState);\n\n return {\n course: {},\n section: [],\n cm: [],\n ...stateData,\n };\n }\n\n /**\n * Return the current edit mode.\n *\n * Components should use this method to check if edit mode is active.\n *\n * @return {boolean} if edit is enabled\n */\n get isEditing() {\n return this._editing ?? false;\n }\n\n /**\n * Return a data exporter to transform state part into mustache contexts.\n *\n * @return {Exporter} the exporter class\n */\n getExporter() {\n return new Exporter(this);\n }\n\n /**\n * Return if the current course support components to refresh the content.\n *\n * @returns {boolean} if the current content support components\n */\n get supportComponents() {\n return this._supportscomponents ?? false;\n }\n\n /**\n * Return the course file handlers promise.\n * @returns {Promise} the promise for file handlers.\n */\n async getFileHandlersPromise() {\n return this._fileHandlersPromise ?? [];\n }\n\n /**\n * Upload a file list to the course.\n *\n * This method is a wrapper to the course file uploader.\n *\n * @param {number} sectionId the section id\n * @param {number} sectionNum the section number\n * @param {Array} files and array of files\n * @return {Promise} the file queue promise\n */\n uploadFiles(sectionId, sectionNum, files) {\n return uploadFilesToCourse(this.courseId, sectionId, sectionNum, files);\n }\n\n /**\n * Get a value from the course editor static storage if any.\n *\n * The course editor static storage uses the sessionStorage to store values from the\n * components. This is used to prevent unnecesary template loadings on every page. However,\n * the storage does not work if no sessionStorage can be used (in debug mode for example),\n * if the page is in editing mode or if the initial state change from the last page.\n *\n * @param {string} key the key to get\n * @return {boolean|string} the storage value or false if cannot be loaded\n */\n getStorageValue(key) {\n if (this.isEditing || !this.stateKey) {\n return false;\n }\n const dataJson = Storage.get(`course/${this.courseId}/${key}`);\n if (!dataJson) {\n return false;\n }\n // Check the stateKey.\n try {\n const data = JSON.parse(dataJson);\n if (data?.stateKey !== this.stateKey) {\n return false;\n }\n return data.value;\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Stores a value into the course editor static storage if available\n *\n * @param {String} key the key to store\n * @param {*} value the value to store (must be compatible with JSON,stringify)\n * @returns {boolean} true if the value is stored\n */\n setStorageValue(key, value) {\n // Values cannot be stored on edit mode.\n if (this.isEditing) {\n return false;\n }\n const data = {\n stateKey: this.stateKey,\n value,\n };\n return Storage.set(`course/${this.courseId}/${key}`, JSON.stringify(data));\n }\n\n /**\n * Convert a file dragging event into a proper dragging file list.\n * @param {DataTransfer} dataTransfer the event to convert\n * @return {Array} of file list info.\n */\n getFilesDraggableData(dataTransfer) {\n const exporter = this.getExporter();\n return exporter.fileDraggableData(this.state, dataTransfer);\n }\n\n /**\n * Dispatch a change in the state.\n *\n * Usually reactive modules throw an error directly to the components when something\n * goes wrong. However, course editor can directly display a notification.\n *\n * @method dispatch\n * @param {mixed} args any number of params the mutation needs.\n */\n async dispatch(...args) {\n try {\n await super.dispatch(...args);\n } catch (error) {\n // Display error modal.\n notification.exception(error);\n // Force unlock all elements.\n super.dispatch('unlockAll');\n }\n }\n\n /**\n * Calculate the cm info from the current page anchor.\n *\n * @returns {Object|null} the cm info or null if not found.\n */\n _scanPageAnchorCmInfo() {\n const anchor = new URL(window.location.href).hash;\n if (!anchor.startsWith('#module-')) {\n return null;\n }\n // The anchor is always #module-CMID.\n const cmid = anchor.split('-')[1];\n return this.stateManager.get('cm', parseInt(cmid));\n }\n\n /**\n * Return the current page anchor cm info.\n */\n getPageAnchorCmInfo() {\n return this._pageAnchorCmInfo;\n }\n}\n"],"names":["Reactive","courseId","serverStateKey","this","Error","stateData","Date","now","_editing","_supportscomponents","_fileHandlers","storeStateKey","Storage","get","isEditing","JSON","parse","getServerCourseState","error","bulk","enabled","selectedType","selection","setInitialState","stateKey","newState","stringify","set","_stateData","course","_stateData$course","statekey","_loadFileHandlers","_pageAnchorCmInfo","_scanPageAnchorCmInfo","_fileHandlersPromise","Promise","resolve","handlersCacheKey","cacheValue","cachedHandlers","call","methodname","args","courseid","then","handlers","catch","setViewFormat","setup","editing","supportscomponents","overriddenStrings","_overriddenStrings","reduce","indexed","currentValue","key","Map","getFormatString","param","has","override","component","Config","userId","section","cm","courseState","ajax","getExporter","Exporter","supportComponents","uploadFiles","sectionId","sectionNum","files","getStorageValue","dataJson","data","value","setStorageValue","getFilesDraggableData","dataTransfer","fileDraggableData","state","super","dispatch","exception","anchor","URL","window","location","href","hash","startsWith","cmid","split","stateManager","parseInt","getPageAnchorCmInfo"],"mappings":";;;;;;;;;;;w9BAoC6BA,qFAad,wCAQK,uBAgBCC,SAAUC,mBAEnBC,KAAKF,eACC,IAAIG,4BAAqBH,oDAA2CE,KAAKF,eAe/EI,UAZCH,iBAEDA,yCAAoCI,KAAKC,aAIxCC,UAAW,OACXC,qBAAsB,OACtBC,cAAgB,UAEhBT,SAAWA,eAIVU,cAAgBC,QAAQC,qBAAcZ,2BAGnCE,KAAKW,WAAaZ,gBAAkBS,gBACrCN,UAAYU,KAAKC,MAAMJ,QAAQC,qBAAcZ,4BAE5CI,YACDA,gBAAkBF,KAAKc,wBAG7B,MAAOC,2BACDA,MAAM,+DACNA,MAAMA,UAKdb,UAAUc,KAAO,CACbC,SAAS,EACTC,aAAc,GACdC,UAAW,SAGVC,gBAAgBlB,WAGjBF,KAAKW,eACAU,SAAW,SACb,OAEGC,SAAWV,KAAKW,UAAUrB,qEACVO,QAAQC,qBAAcZ,4BACtBwB,UAAYd,gBAAkBT,eAChDU,QAAQe,qBAAc1B,yBAAwBwB,UAC9Cb,QAAQe,qBAAc1B,uEAAqBI,2DAAAuB,WAAWC,2CAAXC,kBAAmBC,gEAAY7B,qBAEzEsB,SAAWZ,QAAQC,qBAAcZ,4BAGrC+B,yBAEAC,kBAAoB9B,KAAK+B,wBAMlCF,yBAESG,qBAAuB,IAAIC,SAASC,cAChClC,KAAKW,sBACNuB,QAAQ,UAINC,kCAA6BnC,KAAKF,0BAElCsC,WAAa3B,QAAQC,IAAIyB,qBAC3BC,qBAEUC,eAAiBzB,KAAKC,MAAMuB,wBAClCF,QAAQG,gBAEV,MAAOtB,oBACDA,MAAM,oDAIbuB,KAAK,CAAC,CACPC,WAAY,kCACZC,KAAM,CACFC,SAAUzC,KAAKF,aAEnB,GAAG4C,MAAMC,WACTlC,QAAQe,IAAIW,iBAAkBvB,KAAKW,UAAUoB,WAC7CT,QAAQS,aAETC,OAAM7B,qBACDA,MAAMA,OACVmB,QAAQ,UAepBW,cAAcC,2EACLzC,gCAAWyC,MAAMC,uDACjBzC,kDAAsBwC,MAAME,iFAC3BC,gDAAoBH,MAAMG,yEAAqB,QAChDC,mBAAqBD,kBAAkBE,QACxC,CAACC,QAASC,eAAiBD,QAAQ5B,IAAI6B,aAAaC,IAAKD,eACzD,IAAIE,KAaZC,gBAAgBF,IAAKG,UACbzD,KAAKkD,mBAAmBQ,IAAIJ,KAAM,+BAC5BK,SAAW3D,KAAKkD,mBAAmBxC,IAAI4C,YACtC,kBAAUA,gCAAKK,SAASC,6DAAa,oBAAqBH,cAG9D,kBAAUH,IAAK,oBAAqBG,uCAYtB,GAAjBI,gBAAOC,aACA,CACHpC,OAAQ,GACRqC,QAAS,GACTC,GAAI,UAGNC,kBAAoBC,cAAK5B,KAAK,CAAC,CACjCC,WAAY,8BACZC,KAAM,CACFC,SAAUzC,KAAKF,aAEnB,SAIG,CACH4B,OAAQ,GACRqC,QAAS,GACTC,GAAI,MALUpD,KAAKC,MAAMoD,cAiB7BtD,iEACOX,KAAKK,mDAQhB8D,qBACW,IAAIC,kBAASpE,MAQpBqE,uFACOrE,KAAKM,0KAQLN,KAAKgC,4EAAwB,GAaxCsC,YAAYC,UAAWC,WAAYC,cACxB,qCAAoBzE,KAAKF,SAAUyE,UAAWC,WAAYC,OAcrEC,gBAAgBpB,QACRtD,KAAKW,YAAcX,KAAKqB,gBACjB,QAELsD,SAAWlE,QAAQC,qBAAcV,KAAKF,qBAAYwD,UACnDqB,gBACM,YAIDC,KAAOhE,KAAKC,MAAM8D,iBACpBC,MAAAA,YAAAA,KAAMvD,YAAarB,KAAKqB,UAGrBuD,KAAKC,MACd,MAAO9D,cACE,GAWf+D,gBAAgBxB,IAAKuB,UAEb7E,KAAKW,iBACE,QAELiE,KAAO,CACTvD,SAAUrB,KAAKqB,SACfwD,MAAAA,cAEGpE,QAAQe,qBAAcxB,KAAKF,qBAAYwD,KAAO1C,KAAKW,UAAUqD,OAQxEG,sBAAsBC,qBACDhF,KAAKmE,cACNc,kBAAkBjF,KAAKkF,MAAOF,yCAcpCG,MAAMC,uBACd,MAAOrE,6BAEQsE,UAAUtE,aAEjBqE,SAAS,cASvBrD,8BACUuD,OAAS,IAAIC,IAAIC,OAAOC,SAASC,MAAMC,SACxCL,OAAOM,WAAW,mBACZ,WAGLC,KAAOP,OAAOQ,MAAM,KAAK,UACxB9F,KAAK+F,aAAarF,IAAI,KAAMsF,SAASH,OAMhDI,6BACWjG,KAAK8B"} \ No newline at end of file diff --git a/course/format/amd/src/local/courseeditor/courseeditor.js b/course/format/amd/src/local/courseeditor/courseeditor.js index 6be871a155d..76c9203c38f 100644 --- a/course/format/amd/src/local/courseeditor/courseeditor.js +++ b/course/format/amd/src/local/courseeditor/courseeditor.js @@ -13,6 +13,7 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +import Config from 'core/config'; import {getString} from 'core/str'; import {Reactive} from 'core/reactive'; import notification from 'core/notification'; @@ -218,6 +219,16 @@ export default class extends Reactive { * @returns {Object} the current course state */ async getServerCourseState() { + // Only logged users can get the course state. Filtering here will prevent unnecessary + // calls to the server and login page redirects. Especially for home activities with + // guest access. + if (Config.userId == 0) { + return { + course: {}, + section: [], + cm: [], + }; + } const courseState = await ajax.call([{ methodname: 'core_courseformat_get_state', args: { From b0683da8779f82e156a7da867b5508cdafa8554a Mon Sep 17 00:00:00 2001 From: ferran Date: Wed, 23 Oct 2024 10:11:50 +0200 Subject: [PATCH 7/7] MDL-82349 core_courseformat: new get_generic_section_name method The string "sectionname" was an unnecessary coupling between formats and other plugins. Now the generic name for a section should be obtained using $format->get_generic_section_name. This allow formats to use an alternative string for nameing sections. This is especially important for rare formats like the frontpage one that does not have a plugin lang file. --- .upgradenotes/MDL-82349-2024111211161866.yml | 10 ++++++++ course/format/classes/base.php | 12 +++++++++ .../output/local/content/bulkedittoggler.php | 4 +-- course/format/tests/base_test.php | 25 +++++++++++++++++++ course/resources.php | 3 ++- course/section.php | 4 +-- course/view.php | 4 +-- mod/assign/locallib.php | 2 +- mod/bigbluebuttonbn/classes/output/index.php | 2 +- mod/book/index.php | 2 +- mod/chat/index.php | 3 +-- mod/choice/index.php | 4 +-- mod/data/index.php | 3 +-- mod/feedback/index.php | 2 +- mod/folder/index.php | 2 +- mod/forum/index.php | 2 +- mod/glossary/index.php | 3 +-- mod/imscp/index.php | 2 +- mod/lesson/index.php | 2 +- mod/lti/index.php | 2 +- mod/page/index.php | 2 +- mod/quiz/index.php | 2 +- mod/resource/index.php | 2 +- mod/scorm/index.php | 4 +-- mod/survey/index.php | 4 +-- mod/url/index.php | 2 +- mod/wiki/index.php | 2 +- mod/workshop/index.php | 2 +- 28 files changed, 77 insertions(+), 36 deletions(-) create mode 100644 .upgradenotes/MDL-82349-2024111211161866.yml diff --git a/.upgradenotes/MDL-82349-2024111211161866.yml b/.upgradenotes/MDL-82349-2024111211161866.yml new file mode 100644 index 00000000000..4b68ee38048 --- /dev/null +++ b/.upgradenotes/MDL-82349-2024111211161866.yml @@ -0,0 +1,10 @@ +issueNumber: MDL-82349 +notes: + core_courseformat: + - message: >- + A new core_courseformat\base::get_generic_section_name method is + created to know how a specific format name the sections. + This method is also used by plugins to know how to name the sections + instead of using using a direct get_string on "sectionnamer" that + may not exists. + type: improved diff --git a/course/format/classes/base.php b/course/format/classes/base.php index 663aba3a0c6..97810344112 100644 --- a/course/format/classes/base.php +++ b/course/format/classes/base.php @@ -551,6 +551,18 @@ abstract class base { return self::get_section_name($section); } + /** + * Returns the generic name for sections in this course format. + * + * @return string + */ + public function get_generic_section_name() { + if (get_string_manager()->string_exists('sectionname', 'format_' . $this->format)) { + return get_string('sectionname', 'format_' . $this->format); + } + return get_string('section'); + } + /** * Returns the name for the highlighted section. * diff --git a/course/format/classes/output/local/content/bulkedittoggler.php b/course/format/classes/output/local/content/bulkedittoggler.php index e0975cd75a3..59450f5ac2b 100644 --- a/course/format/classes/output/local/content/bulkedittoggler.php +++ b/course/format/classes/output/local/content/bulkedittoggler.php @@ -60,8 +60,8 @@ class bulkedittoggler implements named_templatable, renderable { ]; if ($section) { - $data->sectionname = get_string('sectionname', "format_$course->format"); - $data->sectiontitle = get_section_name($course, $section); + $data->sectionname = $format->get_generic_section_name(); + $data->sectiontitle = $format->get_section_name($section); } return $data; diff --git a/course/format/tests/base_test.php b/course/format/tests/base_test.php index a0f9b2238e4..efd55c536eb 100644 --- a/course/format/tests/base_test.php +++ b/course/format/tests/base_test.php @@ -996,6 +996,31 @@ class base_test extends advanced_testcase { $this->assertFalse($format->is_section_visible($modinfostudent->get_section_info(1))); $this->assertFalse($format->is_section_visible($modinfostudent->get_section_info(2))); } + + /** + * Test for the get_generic_section_name method. + * + * @covers ::get_generic_section_name + */ + public function test_get_generic_section_name(): void { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course1 = $generator->create_course(['format' => 'topics']); + $course2 = $generator->create_course(['format' => 'theunittest']); + + $format = course_get_format($course1); + $this->assertEquals( + get_string('sectionname', 'format_topics'), + $format->get_generic_section_name() + ); + + $format = course_get_format($course2); + $this->assertEquals( + get_string('section'), + $format->get_generic_section_name() + ); + } } /** diff --git a/course/resources.php b/course/resources.php index 350c9bd65d7..43f053938f1 100644 --- a/course/resources.php +++ b/course/resources.php @@ -99,7 +99,8 @@ $table = new html_table(); $table->attributes['class'] = 'generaltable mod_index'; if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strintro); $table->align = array ('center', 'left', 'left'); } else { diff --git a/course/section.php b/course/section.php index 0fe81a5b6e2..e1f6162acbd 100644 --- a/course/section.php +++ b/course/section.php @@ -148,8 +148,8 @@ $editingtitle = ''; if ($PAGE->user_is_editing()) { $editingtitle = 'editing'; } -$sectionname = get_string('sectionname', "format_$course->format"); -$sectiontitle = get_section_name($course, $section); +$sectionname = $format->get_generic_section_name(); +$sectiontitle = $format->get_section_name($section); $PAGE->set_title( get_string( 'coursesectiontitle' . $editingtitle, diff --git a/course/view.php b/course/view.php index 900607f0112..ecf7fa4ed9b 100644 --- a/course/view.php +++ b/course/view.php @@ -284,8 +284,8 @@ if ($PAGE->user_is_editing()) { // If viewing a section, make the title more specific. if ($section && $section > 0 && course_format_uses_sections($course->format)) { - $sectionname = get_string('sectionname', "format_$course->format"); - $sectiontitle = get_section_name($course, $section); + $sectionname = $format->get_generic_section_name(); + $sectiontitle = $format->get_section_name($section); $PAGE->set_title( get_string( 'coursesectiontitle' . $editingtitle, diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 46588ccbd1f..cd05752a2e8 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -3280,7 +3280,7 @@ class assign { $modinfo = get_fast_modinfo($course); if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $sections = $modinfo->get_section_info_all(); } $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname); diff --git a/mod/bigbluebuttonbn/classes/output/index.php b/mod/bigbluebuttonbn/classes/output/index.php index 2bc8012bcbd..c57e6f875c9 100644 --- a/mod/bigbluebuttonbn/classes/output/index.php +++ b/mod/bigbluebuttonbn/classes/output/index.php @@ -61,7 +61,7 @@ class index implements renderable { $table = new html_table(); if (course_format_uses_sections($this->course->format)) { - $sectionheading = get_string('sectionname', "format_{$this->course->format}"); + $sectionheading = course_get_format($this->course)->get_generic_section_name(); } else { $sectionheading = ''; } diff --git a/mod/book/index.php b/mod/book/index.php index 3c91e8ed892..6fc2867c5b1 100644 --- a/mod/book/index.php +++ b/mod/book/index.php @@ -61,7 +61,7 @@ $table = new html_table(); $table->attributes['class'] = 'generaltable mod_index'; if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strintro); $table->align = array ('center', 'left', 'left'); } else { diff --git a/mod/chat/index.php b/mod/chat/index.php index be83bd57595..3c89547d889 100644 --- a/mod/chat/index.php +++ b/mod/chat/index.php @@ -62,7 +62,7 @@ $strname = get_string('name'); $table = new html_table(); if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname); $table->align = array ('center', 'left'); } else { @@ -103,4 +103,3 @@ echo html_writer::table($table); // Finish the page. echo $OUTPUT->footer(); - diff --git a/mod/choice/index.php b/mod/choice/index.php index 5616a1c89fa..2ed8ea84621 100644 --- a/mod/choice/index.php +++ b/mod/choice/index.php @@ -51,7 +51,7 @@ $table = new html_table(); if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, get_string("question"), get_string("answer")); $table->align = array ("center", "left", "left"); } else { @@ -103,5 +103,3 @@ echo html_writer::table($table); echo $OUTPUT->footer(); - - diff --git a/mod/data/index.php b/mod/data/index.php index b2c603c678e..8cbf07dad72 100644 --- a/mod/data/index.php +++ b/mod/data/index.php @@ -75,7 +75,7 @@ $strnumnotapproved = get_string('numnotapproved', 'data'); $table = new html_table(); if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strdescription, $strentries, $strnumnotapproved); $table->align = array ('center', 'center', 'center', 'center', 'center'); } else { @@ -149,4 +149,3 @@ foreach ($datas as $data) { echo "
    "; echo html_writer::tag('div', html_writer::table($table), array('class'=>'no-overflow')); echo $OUTPUT->footer(); - diff --git a/mod/feedback/index.php b/mod/feedback/index.php index 704b74a4e46..3f582171f81 100644 --- a/mod/feedback/index.php +++ b/mod/feedback/index.php @@ -77,7 +77,7 @@ $strresponses = get_string('responses', 'feedback'); $table = new html_table(); if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); if (has_capability('mod/feedback:viewreports', $context)) { $table->head = array ($strsectionname, $strname, $strresponses); $table->align = array ("center", "left", 'center'); diff --git a/mod/folder/index.php b/mod/folder/index.php index 7bafe8c1987..f5e567fedf3 100644 --- a/mod/folder/index.php +++ b/mod/folder/index.php @@ -66,7 +66,7 @@ $table = new html_table(); $table->attributes['class'] = 'generaltable mod_index'; if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strintro); $table->align = array ('center', 'left', 'left'); } else { diff --git a/mod/forum/index.php b/mod/forum/index.php index 0b2191c7c94..b16354094a6 100644 --- a/mod/forum/index.php +++ b/mod/forum/index.php @@ -349,7 +349,7 @@ if ($show_rss = (($showsubscriptioncolumns || $course->id == SITEID) && // Now let's process the learning forums. if ($course->id != SITEID) { // Only real courses have learning forums // 'format_.'$course->format only applicable when not SITEID (format_site is not a format) - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); // Add extra field for section number, at the front array_unshift($learningtable->head, $strsectionname); array_unshift($learningtable->align, 'center'); diff --git a/mod/glossary/index.php b/mod/glossary/index.php index 50245774733..fafb91eb1ec 100644 --- a/mod/glossary/index.php +++ b/mod/glossary/index.php @@ -58,7 +58,7 @@ $strentries = get_string("entries", "glossary"); $table = new html_table(); if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strentries); $table->align = array ('center', 'left', 'center'); } else { @@ -139,4 +139,3 @@ echo html_writer::table($table); /// Finish the page echo $OUTPUT->footer(); - diff --git a/mod/imscp/index.php b/mod/imscp/index.php index f2c7257ceb7..2c78249cf28 100644 --- a/mod/imscp/index.php +++ b/mod/imscp/index.php @@ -61,7 +61,7 @@ $table = new html_table(); $table->attributes['class'] = 'generaltable mod_index'; if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strintro); $table->align = array ('center', 'left', 'left'); } else { diff --git a/mod/lesson/index.php b/mod/lesson/index.php index 3da15f96df3..10fed60a097 100644 --- a/mod/lesson/index.php +++ b/mod/lesson/index.php @@ -78,7 +78,7 @@ $strnodeadline = get_string("nodeadline", "lesson"); $table = new html_table(); if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strgrade, $strdeadline); $table->align = array ("center", "left", "center", "center"); } else { diff --git a/mod/lti/index.php b/mod/lti/index.php index 1a0e97b6b62..f3582f41966 100644 --- a/mod/lti/index.php +++ b/mod/lti/index.php @@ -87,7 +87,7 @@ $table = new html_table(); $table->attributes['class'] = 'generaltable mod_index'; if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname); $table->align = array ("center", "left"); } else { diff --git a/mod/page/index.php b/mod/page/index.php index 53138b2b945..108aa86f8f2 100644 --- a/mod/page/index.php +++ b/mod/page/index.php @@ -60,7 +60,7 @@ $table = new html_table(); $table->attributes['class'] = 'generaltable mod_index'; if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strintro); $table->align = array ('center', 'left', 'left'); } else { diff --git a/mod/quiz/index.php b/mod/quiz/index.php index 640bddaf3a9..766549c8933 100644 --- a/mod/quiz/index.php +++ b/mod/quiz/index.php @@ -74,7 +74,7 @@ array_push($headings, get_string('quizcloses', 'quiz')); array_push($align, 'left'); if (course_format_uses_sections($course->format)) { - array_unshift($headings, get_string('sectionname', 'format_'.$course->format)); + array_unshift($headings, course_get_format($course)->get_generic_section_name()); } else { array_unshift($headings, ''); } diff --git a/mod/resource/index.php b/mod/resource/index.php index 17598411545..e176a103a5e 100644 --- a/mod/resource/index.php +++ b/mod/resource/index.php @@ -41,7 +41,7 @@ $event->trigger(); $strresource = get_string('modulename', 'resource'); $strresources = get_string('modulenameplural', 'resource'); -$strsectionname = get_string('sectionname', 'format_'.$course->format); +$strsectionname = course_get_format($course)->get_generic_section_name(); $strname = get_string('name'); $strintro = get_string('moduleintro'); $strlastmodified = get_string('lastmodified'); diff --git a/mod/scorm/index.php b/mod/scorm/index.php index 95e9f9da295..cca951a4991 100644 --- a/mod/scorm/index.php +++ b/mod/scorm/index.php @@ -65,7 +65,7 @@ if (! $scorms = get_all_instances_in_course("scorm", $course)) { $table = new html_table(); if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strsummary, $strreport); $table->align = array ("center", "left", "left", "left"); } else { @@ -116,4 +116,4 @@ echo html_writer::empty_tag('br'); echo html_writer::table($table); -echo $OUTPUT->footer(); \ No newline at end of file +echo $OUTPUT->footer(); diff --git a/mod/survey/index.php b/mod/survey/index.php index edbdf079d74..f979543b6c7 100644 --- a/mod/survey/index.php +++ b/mod/survey/index.php @@ -44,7 +44,7 @@ $table = new html_table(); if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strstatus); } else { $table->head = array ($strname, $strstatus); @@ -89,5 +89,3 @@ echo "
    "; echo html_writer::table($table); echo $OUTPUT->footer(); - - diff --git a/mod/url/index.php b/mod/url/index.php index 869eed21a3e..e44411e55e2 100644 --- a/mod/url/index.php +++ b/mod/url/index.php @@ -65,7 +65,7 @@ $table = new html_table(); $table->attributes['class'] = 'generaltable mod_index'; if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strintro); $table->align = array ('center', 'left', 'left'); } else { diff --git a/mod/wiki/index.php b/mod/wiki/index.php index b9039bb8aff..0bdcc8b4c69 100644 --- a/mod/wiki/index.php +++ b/mod/wiki/index.php @@ -75,7 +75,7 @@ $strname = get_string("name"); $table = new html_table(); if ($usesections) { - $strsectionname = get_string('sectionname', 'format_' . $course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array($strsectionname, $strname); } else { $table->head = array($strname); diff --git a/mod/workshop/index.php b/mod/workshop/index.php index eba17ae1627..281a526ebf2 100644 --- a/mod/workshop/index.php +++ b/mod/workshop/index.php @@ -63,7 +63,7 @@ $strname = get_string('name'); $table = new html_table(); if ($usesections) { - $strsectionname = get_string('sectionname', 'format_'.$course->format); + $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname); $table->align = array ('center', 'left'); } else {