├── .gitattributes ├── pix ├── mergeup.png └── mergeup.svg ├── lang └── en │ ├── deprecated.txt │ └── format_flexsections.php ├── README.md ├── format.js ├── amd ├── build │ └── local │ │ ├── content │ │ ├── section.min.js │ │ └── section.min.js.map │ │ ├── courseindex │ │ ├── section.min.js │ │ ├── drawer.min.js │ │ ├── placeholder.min.js │ │ ├── drawer.min.js.map │ │ ├── section.min.js.map │ │ ├── placeholder.min.js.map │ │ └── courseindex.min.js │ │ └── courseeditor │ │ ├── mutations.min.js │ │ ├── exporter.min.js │ │ ├── mutations.min.js.map │ │ └── exporter.min.js.map └── src │ └── local │ ├── content │ └── section.js │ ├── courseindex │ ├── drawer.js │ ├── section.js │ └── placeholder.js │ └── courseeditor │ ├── mutations.js │ └── exporter.js ├── classes ├── constants.php ├── courseformat │ └── stateupdates.php ├── privacy │ └── provider.php ├── output │ ├── courseformat │ │ ├── content │ │ │ ├── cm │ │ │ │ ├── delegatedcontrolmenu.php │ │ │ │ └── controlmenu.php │ │ │ ├── bulkedittools.php │ │ │ ├── section │ │ │ │ └── header.php │ │ │ └── section.php │ │ ├── state │ │ │ ├── cm.php │ │ │ ├── course.php │ │ │ └── section.php │ │ └── content.php │ └── renderer.php └── local │ ├── hooks │ ├── output │ │ └── before_footer_html_generation.php │ └── before_activitychooserbutton_exported.php │ └── helpers │ └── preferences.php ├── db ├── upgrade.php └── hooks.php ├── version.php ├── styles.css ├── templates ├── local │ ├── courseindex │ │ ├── drawer.mustache │ │ ├── placeholders.mustache │ │ ├── courseindex.mustache │ │ └── section.mustache │ ├── content │ │ ├── addsection.mustache │ │ ├── movesection.mustache │ │ ├── movecm.mustache │ │ ├── section │ │ │ └── header.mustache │ │ ├── movesection_one.mustache │ │ ├── section.mustache │ │ └── movecm_one.mustache │ ├── navigate_back_to.mustache │ └── content.mustache └── back_link_in_cms.mustache ├── tests ├── generator │ ├── behat_format_flexsections_generator.php │ └── lib.php └── behat │ ├── delegated_sections.feature │ ├── move_section.feature │ ├── back_link_in_cms.feature │ ├── activity_chooser_plus.feature │ ├── course_crud.feature │ ├── edit_delete_sections.feature │ ├── behat_format_flexsections.php │ ├── delete_section.feature │ ├── indentation.feature │ └── add_sections.feature ├── format.php ├── .github └── workflows │ ├── moodle-release.yml │ └── moodle.yml ├── settings.php └── CHANGELOG.md /.gitattributes: -------------------------------------------------------------------------------- 1 | **/yui/build/** -diff 2 | **/amd/build/** -diff 3 | -------------------------------------------------------------------------------- /pix/mergeup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinaglancy/moodle-format_flexsections/MOODLE_500_STABLE/pix/mergeup.png -------------------------------------------------------------------------------- /lang/en/deprecated.txt: -------------------------------------------------------------------------------- 1 | addsubsectionfor,format_flexsections 2 | cancelmoving,format_flexsections 3 | removemarker,format_flexsections 4 | setmarker,format_flexsections 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flexible sections course format for Moodle 2 | 3 | In this course format: 4 | 5 | - sections can be added inside other sections 6 | - each section (regardless of its nesting level) can be shown either on the same page as parent or on a separate page. 7 | Teacher can change it in edit mode. 8 | - If section is displayed on a separate page, it's name is displayed as a link and on this page the link "Back to ... " 9 | is displayed 10 | - If teacher hides a section all nested sections and activities become hidden as well. 11 | 12 | Please note that if section has both activities and subsections activities are displayed first. 13 | 14 | See also https://moodle.org/plugins/format_flexsections 15 | -------------------------------------------------------------------------------- /format.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | // Javascript functions for Flexible sections course format. 3 | 4 | // This is no longer used but there are errors in console if this file is missing. 5 | 6 | M.course = M.course || {}; 7 | 8 | M.course.format = M.course.format || {}; 9 | 10 | /** 11 | * Get sections config for this format. 12 | * 13 | * The section structure is: 14 | * 19 | * 20 | * @return {object} section list configuration 21 | */ 22 | M.course.format.get_config = function() { 23 | return { 24 | container_node: 'ul', 25 | container_class: 'flexsections', 26 | section_node: 'li', 27 | section_class: 'section' 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /pix/mergeup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | ]> 6 | 10 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /amd/build/local/content/section.min.js: -------------------------------------------------------------------------------- 1 | define("format_flexsections/local/content/section",["exports","core_courseformat/local/content/section"],(function(_exports,_section){var obj; 2 | /** 3 | * Course section format component. 4 | * 5 | * @module format_flexsections/local/content/section 6 | * @copyright 2022 Marina Glancy 7 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 8 | */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_section=(obj=_section)&&obj.__esModule?obj:{default:obj};class _default extends _section.default{configDragDrop(headerComponent){super.configDragDrop(headerComponent),setTimeout((()=>{"function"==typeof headerComponent.dragdrop.parent.getDraggableData&&headerComponent.dragdrop.setDraggable(!1)}),1500)}}return _exports.default=_default,_exports.default})); 9 | 10 | //# sourceMappingURL=section.min.js.map -------------------------------------------------------------------------------- /amd/build/local/courseindex/section.min.js: -------------------------------------------------------------------------------- 1 | define("format_flexsections/local/courseindex/section",["exports","core_courseformat/local/courseindex/section"],(function(_exports,_section){var obj; 2 | /** 3 | * Course index section component. 4 | * 5 | * This component is used to control specific course section interactions like drag and drop. 6 | * 7 | * @module format_flexsections/local/courseindex/section 8 | * @copyright 2022 Marina Glancy 9 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 10 | */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_section=(obj=_section)&&obj.__esModule?obj:{default:obj};class Component extends _section.default{static init(target,selectors){return new Component({element:document.getElementById(target),selectors:selectors})}configDragDrop(sectionitem){sectionitem.draggable=!1,super.configDragDrop(sectionitem)}}return _exports.default=Component,_exports.default})); 11 | 12 | //# sourceMappingURL=section.min.js.map -------------------------------------------------------------------------------- /amd/build/local/courseindex/drawer.min.js: -------------------------------------------------------------------------------- 1 | define("format_flexsections/local/courseindex/drawer",["exports","core_courseformat/local/courseindex/drawer","core_courseformat/courseeditor","format_flexsections/local/courseeditor/exporter"],(function(_exports,_drawer,_courseeditor,_exporter){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} 2 | /** 3 | * Course format component 4 | * 5 | * @module format_flexsections/local/courseindex/drawer 6 | * @copyright 2022 Marina Glancy 7 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 8 | */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_drawer=_interopRequireDefault(_drawer),_exporter=_interopRequireDefault(_exporter);class Drawer extends _drawer.default{create(){this.name="courseindex-drawer-flexsections"}static init(target,selectors){const courseEditor=(0,_courseeditor.getCurrentCourseEditor)();return courseEditor.getExporter=()=>new _exporter.default(courseEditor),new Drawer({element:document.getElementById(target),reactive:courseEditor,selectors:selectors})}}return _exports.default=Drawer,_exports.default})); 9 | 10 | //# sourceMappingURL=drawer.min.js.map -------------------------------------------------------------------------------- /classes/constants.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections; 18 | 19 | /** 20 | * Constants 21 | * 22 | * @package format_flexsections 23 | * @copyright 2023 Marina Glancy 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | class constants { 27 | /** @var int */ 28 | const COURSEINDEX_FULL = 0; 29 | /** @var int */ 30 | const COURSEINDEX_SECTIONS = 1; 31 | /** @var int */ 32 | const COURSEINDEX_NONE = 2; 33 | } 34 | -------------------------------------------------------------------------------- /db/upgrade.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Upgrade scripts for Flexible sections course format. 19 | * 20 | * @package format_flexsections 21 | * @copyright 2022 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | /** 26 | * Upgrade script for Flexible sections course format. 27 | * 28 | * @param int|float $oldversion the version we are upgrading from 29 | * @return bool result 30 | */ 31 | function xmldb_format_flexsections_upgrade($oldversion) { 32 | global $CFG, $DB; 33 | 34 | return true; 35 | } 36 | -------------------------------------------------------------------------------- /classes/courseformat/stateupdates.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\courseformat; 18 | 19 | /** 20 | * class stateupdates 21 | * 22 | * @package format_flexsections 23 | * @copyright 2022 Marina Glancy 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | class stateupdates extends \core_courseformat\stateupdates { 27 | 28 | /** 29 | * Add track about a course module removed. 30 | * 31 | * @param int $cmid the affected course module id 32 | */ 33 | public function add_cm_remove(int $cmid): void { 34 | $this->add_update('cm', 'remove', (object)['id' => $cmid]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /version.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Version details. 19 | * 20 | * @package format_flexsections 21 | * @copyright 2022 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | $plugin->version = 2025100700; // The current plugin version (Date: YYYYMMDDXX). 28 | $plugin->requires = 2025041400.00; // Requires Moodle 5.0 or above. 29 | $plugin->release = "5.0.1"; 30 | $plugin->maturity = MATURITY_STABLE; 31 | $plugin->component = 'format_flexsections'; // Full name of the plugin (used for diagnostics). 32 | $plugin->supported = [500, 501]; 33 | 34 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .course-content ul.flexsections { 2 | padding: 0; 3 | margin: 0; 4 | list-style: none; 5 | } 6 | 7 | .course-content ul.flexsections li.section { 8 | padding-top: 1rem; 9 | padding-bottom: 1rem; 10 | } 11 | 12 | .course-content ul.flexsections li.section .content { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | @media (min-width: 576px) { 18 | .course-content ul.flexsections li.section .summary, 19 | .course-content ul.flexsections li.section .content > .availabilityinfo { 20 | margin-left: 25px; 21 | } 22 | } 23 | 24 | .course-content ul.flexsections li.section .left, 25 | .course-content ul.flexsections li.section .right { 26 | padding: 0 6px 0; 27 | text-align: right; 28 | width: auto; 29 | } 30 | 31 | @media (max-width: 767.98px) { 32 | body:not(.editing) .course-content ul.flexsections li.section .left, 33 | body:not(.editing) .course-content ul.flexsections li.section .right { 34 | display: none; 35 | } 36 | } 37 | 38 | [data-flexsections-accordion="1"] .section-collapsemenu.collapsed .expandall { 39 | display: none; 40 | } 41 | 42 | @media (min-width: 768px) { 43 | #page.drawers .flexsections-activity-page-navigate-back { 44 | padding-left: 15px; 45 | padding-right: 15px; 46 | } 47 | } 48 | 49 | .course-content ul.flexsections .course-section .sectionname > a { 50 | color: var(--bs-link-color); 51 | } 52 | -------------------------------------------------------------------------------- /classes/privacy/provider.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\privacy; 18 | 19 | use core_privacy\local\metadata\null_provider; 20 | 21 | /** 22 | * Privacy Subsystem for Flexible sections course format implementing null_provider. 23 | * 24 | * @package format_flexsections 25 | * @copyright 2022 Marina Glancy 26 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 | */ 28 | class provider implements null_provider { 29 | 30 | /** 31 | * Get the language string identifier with the component's language 32 | * file to explain why this plugin stores no data. 33 | * 34 | * @return string 35 | */ 36 | public static function get_reason(): string { 37 | return 'privacy:metadata'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /templates/local/courseindex/drawer.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/courseindex/drawer 19 | 20 | Overrides template core_courseformat/local/courseindex/drawer 21 | 22 | This template renders the course index drawer with the placeholder. 23 | 24 | The code from this file is just an stub as the final code will come from 25 | the new layouts for Moodle 4.0. 26 | 27 | Example context (json): 28 | {} 29 | }} 30 | 35 | {{#js}} 36 | require(['format_flexsections/local/courseindex/drawer'], function(component) { 37 | component.init('courseindex'); 38 | }); 39 | {{/js}} 40 | -------------------------------------------------------------------------------- /classes/output/courseformat/content/cm/delegatedcontrolmenu.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\output\courseformat\content\cm; 18 | 19 | /** 20 | * Class delegatedcontrolmenu 21 | * 22 | * @package format_flexsections 23 | * @copyright Marina Glancy 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | class delegatedcontrolmenu extends \core_courseformat\output\local\content\cm\delegatedcontrolmenu { 27 | 28 | /** 29 | * Generate the edit control items of a section. 30 | * 31 | * @return array of edit control items 32 | */ 33 | public function delegated_control_items() { 34 | $controls = parent::delegated_control_items(); 35 | unset($controls['view'], $controls['permalink']); 36 | return $controls; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /db/hooks.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Hook callbacks for Flexible sections format 19 | * 20 | * @package format_flexsections 21 | * @copyright 2024 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | $callbacks = [ 28 | 29 | [ 30 | 'hook' => core\hook\output\before_footer_html_generation::class, 31 | 'callback' => 'format_flexsections\local\hooks\output\before_footer_html_generation::callback', 32 | 'priority' => 0, 33 | ], 34 | 35 | [ 36 | 'hook' => core_course\hook\before_activitychooserbutton_exported::class, 37 | 'callback' => 'format_flexsections\local\hooks\before_activitychooserbutton_exported::callback', 38 | 'priority' => -100, 39 | ], 40 | ]; 41 | -------------------------------------------------------------------------------- /classes/output/courseformat/state/cm.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\output\courseformat\state; 18 | 19 | /** 20 | * Contains the ajax update course module structure. 21 | * 22 | * @package format_flexsections 23 | * @copyright 2022 Marina Glancy 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | class cm extends \core_courseformat\output\local\state\cm { 27 | 28 | /** 29 | * Export this data so it can be used as state object in the course editor. 30 | * 31 | * @param \renderer_base $output typically, the renderer that's calling this function 32 | * @return \stdClass data context for a mustache template 33 | */ 34 | public function export_for_template(\renderer_base $output): \stdClass { 35 | $data = parent::export_for_template($output); 36 | 37 | return $data; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /templates/back_link_in_cms.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/back_link_in_cms 19 | 20 | Inserted on top of the activity pages in the course in flexsections format 21 | 22 | Example context (json): 23 | { 24 | "backtosection": { 25 | "url": "/course/view.php?id=2§ion=1", 26 | "sectionname": "Section 1" 27 | } 28 | } 29 | }} 30 |
31 | {{> format_flexsections/local/navigate_back_to }} 32 |
33 | {{#js}} 34 | /* Move the widget to the header area */ 35 | const widget = document.querySelector(".flexsections-activity-page-navigate-back"); 36 | const maincontent = document.getElementById('maincontent'); 37 | if (maincontent) { 38 | maincontent.parentElement.insertBefore(widget, maincontent); 39 | } 40 | {{/js}} 41 | -------------------------------------------------------------------------------- /amd/build/local/courseeditor/mutations.min.js: -------------------------------------------------------------------------------- 1 | define("format_flexsections/local/courseeditor/mutations",["exports","core_courseformat/local/courseeditor/mutations"],(function(_exports,_mutations){var obj; 2 | /** 3 | * Mutations 4 | * 5 | * @module format_flexsections/local/courseeditor/mutations 6 | * @copyright 2022 Marina Glancy 7 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 8 | */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_mutations=(obj=_mutations)&&obj.__esModule?obj:{default:obj};class _default extends _mutations.default{async sectionMergeUp(stateManager,sectionId){this.sectionLock(stateManager,[sectionId],!0);const course=stateManager.get("course"),updates=await this._callEditWebservice("section_mergeup",course.id,[],sectionId);stateManager.processUpdates(updates),this.sectionLock(stateManager,[sectionId],!1)}async addSubSection(stateManager,parentSectionId){const course=stateManager.get("course"),updates=await this._callEditWebservice("section_add_subsection",course.id,[],parentSectionId);stateManager.processUpdates(updates)}async insertSubSection(stateManager,parentSectionId){const course=stateManager.get("course"),updates=await this._callEditWebservice("section_insert_subsection",course.id,[],parentSectionId);stateManager.processUpdates(updates)}async sectionSwitchCollapsed(stateManager,sectionId){const course=stateManager.get("course"),updates=await this._callEditWebservice("section_switch_collapsed",course.id,[sectionId]);stateManager.processUpdates(updates)}}return _exports.default=_default,_exports.default})); 9 | 10 | //# sourceMappingURL=mutations.min.js.map -------------------------------------------------------------------------------- /templates/local/content/addsection.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/content/addsection 19 | 20 | Link to inesert a section 21 | 22 | Example context (json): 23 | { 24 | } 25 | }} 26 | {{#showaddsection}} 27 |
28 | {{#addsections}} 29 | 43 | {{title}} 44 | 45 | {{/addsections}} 46 |
47 | {{/showaddsection}} 48 | -------------------------------------------------------------------------------- /tests/generator/behat_format_flexsections_generator.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Behat plugin generator 19 | * 20 | * @package format_flexsections 21 | * @category test 22 | * @copyright 2023 Marina Glancy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | class behat_format_flexsections_generator extends behat_generator_base { 26 | /** 27 | * Get a list of the entities that can be created for this component. 28 | * 29 | * @return array entity name => information about how to generate. 30 | */ 31 | protected function get_creatable_entities(): array { 32 | return [ 33 | 'sections' => [ 34 | 'singular' => 'section', 35 | 'datagenerator' => 'section', 36 | 'required' => ['name', 'course'], 37 | 'switchids' => ['course' => 'courseid'], 38 | ], 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /amd/build/local/courseindex/placeholder.min.js: -------------------------------------------------------------------------------- 1 | define("format_flexsections/local/courseindex/placeholder",["exports","core/templates","core_courseformat/courseeditor","core_courseformat/local/courseindex/placeholder","format_flexsections/local/courseeditor/exporter"],(function(_exports,_templates,_courseeditor,_placeholder,_exporter){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} 2 | /** 3 | * Course index placeholder replacer. 4 | * 5 | * @module format_flexsections/local/courseindex/placeholder 6 | * @copyright 2022 Marina Glancy 7 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 8 | */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),_placeholder=_interopRequireDefault(_placeholder),_exporter=_interopRequireDefault(_exporter);class Component extends _placeholder.default{static init(target,selectors){const courseEditor=(0,_courseeditor.getCurrentCourseEditor)();return courseEditor.getExporter=()=>new _exporter.default(courseEditor),new Component({element:document.getElementById(target),reactive:courseEditor,selectors:selectors})}async loadTemplateContent(state){const data=this.reactive.getExporter().course(state);try{const{html:html,js:js}=await _templates.default.renderForPromise("format_flexsections/local/courseindex/courseindex",data);_templates.default.replaceNode(this.element,html,js),this.pendingContent.resolve(),this.reactive.setStorageValue("courseIndex",{html:html,js:js})}catch(error){throw this.pendingContent.resolve(error),error}}}return _exports.default=Component,_exports.default})); 9 | 10 | //# sourceMappingURL=placeholder.min.js.map -------------------------------------------------------------------------------- /amd/src/local/content/section.js: -------------------------------------------------------------------------------- 1 | // This file is part of Moodle - http://moodle.org/ 2 | // 3 | // Moodle is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // Moodle is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with Moodle. If not, see . 15 | 16 | import Section from 'core_courseformat/local/content/section'; 17 | 18 | /** 19 | * Course section format component. 20 | * 21 | * @module format_flexsections/local/content/section 22 | * @copyright 2022 Marina Glancy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | export default class extends Section { 26 | // Extends course/format/amd/src/local/content/section.js 27 | // Extends course/format/amd/src/local/courseeditor/dndsection.js 28 | 29 | /** 30 | * Register state values and the drag and drop subcomponent. 31 | * 32 | * @param {BaseComponent} headerComponent section header component 33 | */ 34 | configDragDrop(headerComponent) { 35 | super.configDragDrop(headerComponent); 36 | // Disable drag and drop for the sections, it does not really work yet. 37 | setTimeout(() => { 38 | if (typeof headerComponent.dragdrop.parent.getDraggableData === 'function') { 39 | headerComponent.dragdrop.setDraggable(false); 40 | } 41 | }, 1500); 42 | } 43 | } -------------------------------------------------------------------------------- /templates/local/navigate_back_to.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/navigate_back_to 19 | 20 | Links to navigate back to course/section 21 | 22 | Example context (json): 23 | { 24 | "backtocourse": { 25 | "url": "/course/view.php?id=2", 26 | "coursename": "Course 1" 27 | }, 28 | "backtosection": { 29 | "url": "/course/view.php?id=2§ion=1", 30 | "sectionname": "Section 1" 31 | } 32 | } 33 | }} 34 | {{#backtocourse}} 35 | 41 | {{/backtocourse}} 42 | {{#backtosection}} 43 | 49 | {{/backtosection}} 50 | -------------------------------------------------------------------------------- /format.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Flexsections course format. Display the whole course as sections made of modules. 19 | * 20 | * @package format_flexsections 21 | * @copyright 2022 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | require_once($CFG->libdir.'/filelib.php'); 28 | require_once($CFG->libdir.'/completionlib.php'); 29 | 30 | // Retrieve course format option fields and add them to the $course object. 31 | /** @var format_flexsections $format */ 32 | $format = course_get_format($course); 33 | $course = $format->get_course(); 34 | $context = context_course::instance($course->id); 35 | 36 | // Make sure section 0 is created. 37 | course_create_sections_if_missing($course, 0); 38 | 39 | $renderer = $PAGE->get_renderer('format_flexsections'); 40 | 41 | if (!empty($displaysection)) { 42 | $format->set_sectionnum($displaysection); 43 | } 44 | $outputclass = $format->get_output_classname('content'); 45 | $widget = new $outputclass($format); 46 | echo $renderer->render($widget); 47 | 48 | // Include course format js module. 49 | $PAGE->requires->js('/course/format/flexsections/format.js'); 50 | -------------------------------------------------------------------------------- /amd/build/local/courseeditor/exporter.min.js: -------------------------------------------------------------------------------- 1 | define("format_flexsections/local/courseeditor/exporter",["exports","core_courseformat/local/courseeditor/exporter"],(function(_exports,_exporter){var obj; 2 | /** 3 | * Overriding default course format exporter 4 | * 5 | * @module format_flexsections/local/courseeditor/exporter 6 | * @copyright 2022 Marina Glancy 7 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 8 | */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_exporter=(obj=_exporter)&&obj.__esModule?obj:{default:obj};class _default extends _exporter.default{section(state,sectioninfo){const children=sectioninfo.children,section=super.section(state,sectioninfo);if(section.children=[],children&&children.length)for(let i=0;i{var _sectioninfo$cmlist;(null!==(_sectioninfo$cmlist=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist?_sectioninfo$cmlist:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid);items.push({type:"cm",id:cminfo.id,url:cminfo.url})}))},addChildItems=children=>{if(children&&children.length)for(let i=0;i{const sectioninfo=state.section.get(sectionid);items.push({type:"section",id:sectioninfo.id,url:sectioninfo.sectionurl}),addCms(sectioninfo),addChildItems(sectioninfo.children)})),items}}return _exports.default=_default,_exports.default})); 9 | 10 | //# sourceMappingURL=exporter.min.js.map -------------------------------------------------------------------------------- /tests/behat/delegated_sections.feature: -------------------------------------------------------------------------------- 1 | @format @format_flexsections @javascript 2 | Feature: Testing delegated sections in format_flexsections 3 | 4 | Background: 5 | Given the following "course" exists: 6 | | fullname | Course 1 | 7 | | shortname | C1 | 8 | | category | 0 | 9 | | numsections | 3 | 10 | | initsections | 1 | 11 | And the following "activities" exist: 12 | | activity | name | course | idnumber | section | 13 | | assign | First assignment | C1 | assign1 | 2 | 14 | And the following "activity" exists: 15 | | activity | subsection | 16 | | name | Subsection1 | 17 | | course | C1 | 18 | | idnumber | subsection1 | 19 | | section | 1 | 20 | And the following "users" exist: 21 | | username | firstname | lastname | email | 22 | | teacher1 | Teacher | 1 | teacher1@example.com | 23 | | student1 | Student | 1 | student1@example.com | 24 | And the following "course enrolments" exist: 25 | | user | course | role | 26 | | teacher1 | C1 | editingteacher | 27 | | student1 | C1 | student | 28 | 29 | Scenario: Viewing delegated sections 30 | When I am on the "Course 1" course page logged in as teacher1 31 | And I should see "Subsection1" 32 | And I navigate to "Settings" in current page administration 33 | And I expand all fieldsets 34 | And I set the field "Format" to "Flexible sections format" 35 | And I press "Save and display" 36 | And I should see "Subsection1" 37 | And I turn editing mode on 38 | And I click on "#action-menu-toggle-3" "css_element" 39 | And I choose "Delete" in the open action menu 40 | And I click on "Delete" "button" in the "Delete subsection?" "dialogue" 41 | And I should not see "Subsection1" in the "region-main" "region" 42 | And I reload the page 43 | And I should not see "Subsection1" 44 | -------------------------------------------------------------------------------- /amd/build/local/content/section.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"section.min.js","sources":["../../../src/local/content/section.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 Section from 'core_courseformat/local/content/section';\n\n/**\n * Course section format component.\n *\n * @module format_flexsections/local/content/section\n * @copyright 2022 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class extends Section {\n // Extends course/format/amd/src/local/content/section.js\n // Extends course/format/amd/src/local/courseeditor/dndsection.js\n\n /**\n * Register state values and the drag and drop subcomponent.\n *\n * @param {BaseComponent} headerComponent section header component\n */\n configDragDrop(headerComponent) {\n super.configDragDrop(headerComponent);\n // Disable drag and drop for the sections, it does not really work yet.\n setTimeout(() => {\n if (typeof headerComponent.dragdrop.parent.getDraggableData === 'function') {\n headerComponent.dragdrop.setDraggable(false);\n }\n }, 1500);\n }\n}"],"names":["Section","configDragDrop","headerComponent","setTimeout","dragdrop","parent","getDraggableData","setDraggable"],"mappings":";;;;;;;sKAwB6BA,iBASzBC,eAAeC,uBACLD,eAAeC,iBAErBC,YAAW,KACyD,mBAArDD,gBAAgBE,SAASC,OAAOC,kBACvCJ,gBAAgBE,SAASG,cAAa,KAE3C"} -------------------------------------------------------------------------------- /classes/output/courseformat/content/bulkedittools.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\output\courseformat\content; 18 | 19 | /** 20 | * Class bulkedittools 21 | * 22 | * @package format_flexsections 23 | * @copyright Marina Glancy 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | class bulkedittools extends \core_courseformat\output\local\content\bulkedittools { 27 | 28 | /** 29 | * Generate the bulk edit control items of a course module. 30 | * 31 | * Format plugins can override the method to add or remove elements 32 | * from the toolbar. 33 | * 34 | * @return array of edit control items 35 | */ 36 | protected function cm_control_items(): array { 37 | $items = parent::cm_control_items(); 38 | // TODO "Move" action from the parent class is not working with flexsections. 39 | unset($items['move']); 40 | return $items; 41 | } 42 | 43 | /** 44 | * Generate the bulk edit control items of a section. 45 | * 46 | * Format plugins can override the method to add or remove elements 47 | * from the toolbar. 48 | * 49 | * @return array of edit control items 50 | */ 51 | protected function section_control_items(): array { 52 | // TODO Section controls are not working with flexsections. 53 | return []; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /amd/src/local/courseindex/drawer.js: -------------------------------------------------------------------------------- 1 | // This file is part of Moodle - http://moodle.org/ 2 | // 3 | // Moodle is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // Moodle is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with Moodle. If not, see . 15 | 16 | import BaseDrawer from 'core_courseformat/local/courseindex/drawer'; 17 | import {getCurrentCourseEditor} from 'core_courseformat/courseeditor'; 18 | import Exporter from "format_flexsections/local/courseeditor/exporter"; 19 | 20 | /** 21 | * Course format component 22 | * 23 | * @module format_flexsections/local/courseindex/drawer 24 | * @copyright 2022 Marina Glancy 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | export default class Drawer extends BaseDrawer { 28 | // Extends course/format/amd/src/local/courseindex/drawer.js 29 | 30 | /** 31 | * Constructor hook. 32 | */ 33 | create() { 34 | // Optional component name for debugging. 35 | this.name = 'courseindex-drawer-flexsections'; 36 | } 37 | 38 | /** 39 | * Static method to create a component instance form the mustache template. 40 | * 41 | * @param {element|string} target the DOM main element or its ID 42 | * @param {object} selectors optional css selector overrides 43 | * @return {Component} 44 | */ 45 | static init(target, selectors) { 46 | const courseEditor = getCurrentCourseEditor(); 47 | courseEditor.getExporter = () => new Exporter(courseEditor); 48 | return new Drawer({ 49 | element: document.getElementById(target), 50 | reactive: courseEditor, 51 | selectors, 52 | }); 53 | } 54 | } -------------------------------------------------------------------------------- /amd/src/local/courseindex/section.js: -------------------------------------------------------------------------------- 1 | // This file is part of Moodle - http://moodle.org/ 2 | // 3 | // Moodle is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // Moodle is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with Moodle. If not, see . 15 | 16 | import BaseSection from "core_courseformat/local/courseindex/section"; 17 | 18 | /** 19 | * Course index section component. 20 | * 21 | * This component is used to control specific course section interactions like drag and drop. 22 | * 23 | * @module format_flexsections/local/courseindex/section 24 | * @copyright 2022 Marina Glancy 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | export default class Component extends BaseSection { 28 | // Extends course/format/amd/src/local/courseindex/section.js 29 | // Extends course/format/amd/src/local/courseeditor/dndsection.js 30 | 31 | /** 32 | * Static method to create a component instance form the mustahce template. 33 | * 34 | * @param {string} target the DOM main element or its ID 35 | * @param {object} selectors optional css selector overrides 36 | * @return {Component} 37 | */ 38 | static init(target, selectors) { 39 | return new Component({ 40 | element: document.getElementById(target), 41 | selectors, 42 | }); 43 | } 44 | 45 | /** 46 | * Register state values and the drag and drop subcomponent. 47 | * 48 | * @param {BaseComponent} sectionitem section item component 49 | */ 50 | configDragDrop(sectionitem) { 51 | sectionitem.draggable = false; // <---- my modification - disable drag&drop of sections for now. 52 | super.configDragDrop(sectionitem); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /classes/local/hooks/output/before_footer_html_generation.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\local\hooks\output; 18 | 19 | /** 20 | * Hook callbacks for format_flexsections 21 | * 22 | * @package format_flexsections 23 | * @copyright 2024 Marina Glancy 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | class before_footer_html_generation { 27 | 28 | /** 29 | * Callback allowing to add contetnt inside the region-main, in the very end 30 | * 31 | * If we are on activity page, add the "Back to section" link 32 | * 33 | * @param \core\hook\output\before_footer_html_generation $hook 34 | */ 35 | public static function callback(\core\hook\output\before_footer_html_generation $hook): void { 36 | global $OUTPUT, $CFG; 37 | require_once($CFG->dirroot.'/course/format/flexsections/lib.php'); 38 | 39 | if (during_initial_install() || isset($CFG->upgraderunning)) { 40 | // Do nothing during installation or upgrade. 41 | return; 42 | } 43 | 44 | if ($cm = format_flexsections_add_back_link_to_cm()) { 45 | $hook->add_html($OUTPUT->render_from_template('format_flexsections/back_link_in_cms', [ 46 | 'backtosection' => [ 47 | 'url' => course_get_url($cm->course, $cm->sectionnum)->out(false), 48 | 'sectionname' => get_section_name($cm->course, $cm->sectionnum), 49 | ], 50 | ])); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /classes/local/hooks/before_activitychooserbutton_exported.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\local\hooks; 18 | 19 | /** 20 | * Hook callbacks for format_flexsections 21 | * 22 | * @package format_flexsections 23 | * @copyright Marina Glancy 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | class before_activitychooserbutton_exported { 27 | 28 | /** 29 | * This hook is triggered when a activity chooser button is exported. 30 | * 31 | * @param \core_course\hook\before_activitychooserbutton_exported $hook 32 | */ 33 | public static function callback(\core_course\hook\before_activitychooserbutton_exported $hook): void { 34 | $activitychooserbutton = $hook->get_activitychooserbutton(); 35 | $section = $hook->get_section(); 36 | $format = course_get_format($section->course); 37 | 38 | if ($format->get_format() !== 'flexsections') { 39 | return; 40 | } 41 | 42 | // Remove action link added by submodule. Use Reflections to set protected property $activitychooserbutton->actionlinks. 43 | $refobject = new \ReflectionObject($activitychooserbutton); 44 | $refproperty = $refobject->getProperty('actionlinks'); 45 | $refproperty->setAccessible(true); 46 | $actionlinks = $refproperty->getValue($activitychooserbutton); 47 | $actionlinks = array_filter($actionlinks, fn($a) => ($a->attributes['data-modname'] ?? null) !== 'subsection'); 48 | $refproperty->setValue($activitychooserbutton, array_values($actionlinks)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/generator/lib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Data generator class 19 | * 20 | * @package format_flexsections 21 | * @category test 22 | * @copyright 2023 Marina Glancy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | class format_flexsections_generator extends component_generator_base { 26 | /** 27 | * Create a new course section. 28 | * 29 | * @param array $data properties such as: courseid, name, 30 | * summary, summaryformat, visible, 31 | * parent (= name of the parent section), 32 | * collapsed (= display as a link) 33 | * @return int 34 | */ 35 | public function create_section(array $data): int { 36 | global $DB; 37 | $courseid = $data['courseid']; 38 | $parentname = $data['parent'] ?? ''; 39 | unset($data['courseid'], $data['parent']); 40 | $lastsection = (int)$DB->get_field_sql( 41 | 'SELECT max(section) from {course_sections} WHERE course = ?', 42 | [$courseid]); 43 | $section = course_create_section($courseid, $lastsection + 1, true); 44 | course_update_section($courseid, $section, $data); 45 | if (strlen('' . $parentname)) { 46 | $parentsection = $DB->get_field('course_sections', 'section', 47 | ['course' => $courseid, 'name' => $parentname], MUST_EXIST); 48 | /** @var format_flexsections $format */ 49 | $format = course_get_format($courseid); 50 | $format->move_section($section->section, $parentsection); 51 | } 52 | return $section->id; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /templates/local/courseindex/placeholders.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/courseindex/placeholders 19 | 20 | This template renders the loading placeholders for the course index. 21 | 22 | Example context (json): 23 | {} 24 | }} 25 | 53 | {{#js}} 54 | require(['format_flexsections/local/courseindex/placeholder'], function(component) { 55 | component.init('{{uniqid}}-course-index-placeholder'); 56 | }); 57 | {{/js}} 58 | -------------------------------------------------------------------------------- /amd/build/local/courseindex/drawer.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"drawer.min.js","sources":["../../../src/local/courseindex/drawer.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 BaseDrawer from 'core_courseformat/local/courseindex/drawer';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport Exporter from \"format_flexsections/local/courseeditor/exporter\";\n\n/**\n * Course format component\n *\n * @module format_flexsections/local/courseindex/drawer\n * @copyright 2022 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class Drawer extends BaseDrawer {\n // Extends course/format/amd/src/local/courseindex/drawer.js\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex-drawer-flexsections';\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n const courseEditor = getCurrentCourseEditor();\n courseEditor.getExporter = () => new Exporter(courseEditor);\n return new Drawer({\n element: document.getElementById(target),\n reactive: courseEditor,\n selectors,\n });\n }\n}"],"names":["Drawer","BaseDrawer","create","name","target","selectors","courseEditor","getExporter","Exporter","element","document","getElementById","reactive"],"mappings":";;;;;;;+KA0BqBA,eAAeC,gBAMhCC,cAESC,KAAO,8CAUJC,OAAQC,iBACVC,cAAe,iDACrBA,aAAaC,YAAc,IAAM,IAAIC,kBAASF,cACvC,IAAIN,OAAO,CACdS,QAASC,SAASC,eAAeP,QACjCQ,SAAUN,aACVD,UAAAA"} -------------------------------------------------------------------------------- /amd/build/local/courseindex/section.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"section.min.js","sources":["../../../src/local/courseindex/section.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 BaseSection from \"core_courseformat/local/courseindex/section\";\n\n/**\n * Course index section component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module format_flexsections/local/courseindex/section\n * @copyright 2022 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class Component extends BaseSection {\n // Extends course/format/amd/src/local/courseindex/section.js\n // Extends course/format/amd/src/local/courseeditor/dndsection.js\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new Component({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Register state values and the drag and drop subcomponent.\n *\n * @param {BaseComponent} sectionitem section item component\n */\n configDragDrop(sectionitem) {\n sectionitem.draggable = false; // <---- my modification - disable drag&drop of sections for now.\n super.configDragDrop(sectionitem);\n }\n}\n"],"names":["Component","BaseSection","target","selectors","element","document","getElementById","configDragDrop","sectionitem","draggable"],"mappings":";;;;;;;;;qJA0BqBA,kBAAkBC,6BAWvBC,OAAQC,kBACT,IAAIH,UAAU,CACjBI,QAASC,SAASC,eAAeJ,QACjCC,UAAAA,YASRI,eAAeC,aACXA,YAAYC,WAAY,QAClBF,eAAeC"} -------------------------------------------------------------------------------- /tests/behat/move_section.feature: -------------------------------------------------------------------------------- 1 | @format @format_flexsections @javascript 2 | Feature: Moving sections in a course in flexsections format 3 | 4 | Scenario: Move sections in flexsections format 5 | Given the following "users" exist: 6 | | username | firstname | lastname | email | 7 | | teacher1 | Teacher | 1 | teacher1@example.com | 8 | And the following "courses" exist: 9 | | fullname | shortname | format | coursedisplay | numsections | 10 | | Course 1 | C1 | flexsections | 0 | 4 | 11 | And the following "course enrolments" exist: 12 | | user | course | role | 13 | | teacher1 | C1 | editingteacher | 14 | When I log in as "teacher1" 15 | And I am on "Course 1" course homepage with editing mode on 16 | 17 | And I set the field "Edit section name" in the "Topic 1" "section" to "Section A" 18 | And I set the field "Edit section name" in the "Topic 2" "section" to "Section B0" 19 | And I set the field "Edit section name" in the "Topic 3" "section" to "Section C" 20 | And I set the field "Edit section name" in the "Topic 4" "section" to "Section B1" 21 | 22 | And I open section "4" edit menu 23 | And I click on "Move" "link" in the "#section-4 .action-menu" "css_element" 24 | And I should see "Move Section B1 to this location:" in the "Move section" "dialogue" 25 | And I click on "As a subsection of 'Section B0'" "link" in the "Move section" "dialogue" 26 | 27 | Then "Section A" "text" should appear before "Section B0" "text" in the ".course-content" "css_element" 28 | And "Section B0" "text" should appear before "Section B1" "text" in the ".course-content" "css_element" 29 | And "Section B1" "text" should appear before "Section C" "text" in the ".course-content" "css_element" 30 | 31 | When I open section "1" edit menu 32 | And I click on "Move" "link" in the "#section-1 .action-menu" "css_element" 33 | And I should see "Move Section A to this location:" in the "Move section" "dialogue" 34 | And I click on "Before 'Section C'" "link" in the "Move section" "dialogue" 35 | 36 | Then "Section B0" "text" should appear before "Section B1" "text" in the ".course-content" "css_element" 37 | And "Section B1" "text" should appear before "Section A" "text" in the ".course-content" "css_element" 38 | And "Section A" "text" should appear before "Section C" "text" in the ".course-content" "css_element" 39 | -------------------------------------------------------------------------------- /.github/workflows/moodle-release.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Whenever a new tag starting with "v" is pushed, add the tagged version 3 | # to the Moodle Plugins directory at https://moodle.org/plugins 4 | # 5 | # revision: 2021070201 6 | # 7 | name: Releasing in the Plugins directory 8 | 9 | on: 10 | push: 11 | tags: 12 | - v* 13 | 14 | workflow_dispatch: 15 | inputs: 16 | tag: 17 | description: 'Tag to be released' 18 | required: true 19 | 20 | defaults: 21 | run: 22 | shell: bash 23 | 24 | jobs: 25 | release-at-moodle-org: 26 | runs-on: ubuntu-latest 27 | env: 28 | PLUGIN: format_flexsections 29 | CURL: curl -s 30 | ENDPOINT: https://moodle.org/webservice/rest/server.php 31 | TOKEN: ${{ secrets.MOODLE_ORG_TOKEN }} 32 | FUNCTION: local_plugins_add_version 33 | 34 | steps: 35 | - name: Call the service function 36 | id: add-version 37 | run: | 38 | if [[ ! -z "${{ github.event.inputs.tag }}" ]]; then 39 | TAGNAME="${{ github.event.inputs.tag }}" 40 | elif [[ $GITHUB_REF = refs/tags/* ]]; then 41 | TAGNAME="${GITHUB_REF##*/}" 42 | fi 43 | if [[ -z "${TAGNAME}" ]]; then 44 | echo "No tag name has been provided!" 45 | exit 1 46 | fi 47 | ZIPURL="https://api.github.com/repos/${{ github.repository }}/zipball/${TAGNAME}" 48 | RESPONSE=$(${CURL} ${ENDPOINT} --data-urlencode "wstoken=${TOKEN}" \ 49 | --data-urlencode "wsfunction=${FUNCTION}" \ 50 | --data-urlencode "moodlewsrestformat=json" \ 51 | --data-urlencode "frankenstyle=${PLUGIN}" \ 52 | --data-urlencode "zipurl=${ZIPURL}" \ 53 | --data-urlencode "vcssystem=git" \ 54 | --data-urlencode "vcsrepositoryurl=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \ 55 | --data-urlencode "vcstag=${TAGNAME}" \ 56 | --data-urlencode "changelogurl=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commits/${TAGNAME}" \ 57 | --data-urlencode "altdownloadurl=${ZIPURL}") 58 | echo "response=${RESPONSE}" >> $GITHUB_OUTPUT 59 | 60 | - name: Evaluate the response 61 | id: evaluate-response 62 | env: 63 | RESPONSE: ${{ steps.add-version.outputs.response }} 64 | run: | 65 | jq <<< ${RESPONSE} 66 | jq --exit-status ".id" <<< ${RESPONSE} > /dev/null 67 | -------------------------------------------------------------------------------- /templates/local/content/movesection.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/content/movesection 19 | 20 | Displays the target for section moving. 21 | 22 | Example context (json): 23 | { 24 | "sectionid": 23, 25 | "sectiontitle": "Section title", 26 | "sections": [ 27 | { 28 | "title": "Section 1", 29 | "id": 42, 30 | "number": 1, 31 | "sectionurl": "#", 32 | "haschildren": 1, 33 | "children": [ 34 | { 35 | "title": "Section 2", 36 | "id": "44", 37 | "number": "2", 38 | "sectionurl": "#", 39 | "haschildren": 0, 40 | "children": [] 41 | } 42 | ] 43 | }, 44 | { 45 | "title": "Section 3", 46 | "id": "43", 47 | "number": "3", 48 | "sectionurl": "#", 49 | "haschildren": 0, 50 | "children": [] 51 | } 52 | ] 53 | } 54 | 55 | }} 56 |

{{#str}} movefull, moodle, {{sectiontitle}} {{/str}}:

57 | 76 | -------------------------------------------------------------------------------- /templates/local/content/movecm.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/content/movecm 19 | 20 | Displays the course index. 21 | 22 | Example context (json): 23 | { 24 | "cmname": "Activity name", 25 | "cmid": 42, 26 | "sections": [ 27 | { 28 | "title": "General", 29 | "id": 42, 30 | "number": 1, 31 | "sectionurl": "#", 32 | "hascms": true, 33 | "cms": [ 34 | { 35 | "name": "Glossary of characters", 36 | "id": "10", 37 | "url": "#" 38 | }, 39 | { 40 | "name": "World Cinema forum", 41 | "id": "11", 42 | "url": "#" 43 | }, 44 | { 45 | "name": "Announcements", 46 | "id": "12", 47 | "url": "#" 48 | } 49 | ] 50 | }, 51 | { 52 | "title": "City of God or Cidade de Deus", 53 | "id": "43", 54 | "number": "2", 55 | "sectionurl": "#", 56 | "hascms": true, 57 | "cms": [ 58 | { 59 | "name": "Resources", 60 | "id": "13", 61 | "url": "#" 62 | }, 63 | { 64 | "name": "Studying City of God by Stephen Smith Bergman-Messerschmidt", 65 | "id": "14", 66 | "url": "#" 67 | }, 68 | { 69 | "name": "Film education study guide", 70 | "id": "15", 71 | "url": "#" 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | 78 | }} 79 |
80 | {{#sections}} 81 | {{> format_flexsections/local/content/movecm_one }} 82 | {{/sections}} 83 |
84 | -------------------------------------------------------------------------------- /classes/output/courseformat/content/cm/controlmenu.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\output\courseformat\content\cm; 18 | 19 | use action_menu; 20 | use action_menu_link; 21 | use action_menu_link_secondary; 22 | use cm_info; 23 | use core\output\named_templatable; 24 | use core_courseformat\base as course_format; 25 | use core_courseformat\output\local\courseformat_named_templatable; 26 | use moodle_url; 27 | use pix_icon; 28 | use renderable; 29 | use section_info; 30 | use stdClass; 31 | 32 | /** 33 | * Class to render a course module menu inside a course format. 34 | * 35 | * @package format_flexsections 36 | * @copyright 2022 Marina Glancy 37 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 | */ 39 | class controlmenu extends \core_courseformat\output\local\content\cm\controlmenu { 40 | 41 | /** @var \format_flexsections the course format */ 42 | protected $format; 43 | 44 | /** 45 | * Generate the edit control items of a course module. 46 | * 47 | * This method uses course_get_cm_edit_actions function to get the cm actions. 48 | * However, format plugins can override the method to add or remove elements 49 | * from the menu. 50 | * 51 | * @return array of edit control items 52 | */ 53 | protected function cm_control_items() { 54 | $actions = parent::cm_control_items(); 55 | 56 | $baseurl = new moodle_url('/course/mod.php', ['sesskey' => sesskey()]); 57 | $sr = (int)$this->format->get_sectionnum(); 58 | $mod = $this->mod; 59 | 60 | if ($sr !== null) { 61 | $baseurl->param('sr', $sr); 62 | } 63 | 64 | if (isset($actions['move'])) { 65 | $actions['move'] = new action_menu_link_secondary( 66 | new moodle_url($baseurl, ['sesskey' => sesskey(), 'copy' => $mod->id]), 67 | new pix_icon('i/dragdrop', '', 'moodle', ['class' => 'iconsmall']), 68 | get_string('move', 'moodle'), 69 | [ 70 | 'class' => 'editing_movecm', 71 | 'data-action-flexsections' => 'moveCm', 72 | 'data-id' => $mod->id, 73 | ] 74 | ); 75 | } 76 | 77 | return $actions; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /settings.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Settings for format_flexsections 19 | * 20 | * @package format_flexsections 21 | * @copyright 2023 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die; 26 | 27 | use format_flexsections\constants; 28 | 29 | if ($ADMIN->fulltree) { 30 | $url = new moodle_url('/admin/course/resetindentation.php', ['format' => 'flexsections']); 31 | $link = html_writer::link($url, get_string('resetindentation', 'admin')); 32 | $settings->add(new admin_setting_configcheckbox( 33 | 'format_flexsections/indentation', 34 | new lang_string('indentation', 'format_topics'), 35 | new lang_string('indentation_help', 'format_topics').'
'.$link, 36 | 1 37 | )); 38 | $settings->add(new admin_setting_configtext('format_flexsections/maxsectiondepth', 39 | get_string('maxsectiondepth', 'format_flexsections'), 40 | get_string('maxsectiondepthdesc', 'format_flexsections'), 10, PARAM_INT, 7)); 41 | $settings->add(new admin_setting_configcheckbox('format_flexsections/showsection0titledefault', 42 | get_string('showsection0titledefault', 'format_flexsections'), 43 | get_string('showsection0titledefaultdesc', 'format_flexsections'), 0)); 44 | $options = [ 45 | constants::COURSEINDEX_FULL => get_string('courseindexfull', 'format_flexsections'), 46 | constants::COURSEINDEX_SECTIONS => get_string('courseindexsections', 'format_flexsections'), 47 | constants::COURSEINDEX_NONE => get_string('courseindexnone', 'format_flexsections'), 48 | ]; 49 | $settings->add(new admin_setting_configselect('format_flexsections/courseindexdisplay', 50 | get_string('courseindexdisplay', 'format_flexsections'), 51 | get_string('courseindexdisplaydesc', 'format_flexsections'), 0, $options)); 52 | $settings->add(new admin_setting_configcheckbox('format_flexsections/accordion', 53 | get_string('accordion', 'format_flexsections'), 54 | get_string('accordiondesc', 'format_flexsections'), 0)); 55 | $settings->add(new admin_setting_configcheckbox('format_flexsections/cmbacklink', 56 | get_string('cmbacklink', 'format_flexsections'), 57 | get_string('cmbacklinkdesc', 'format_flexsections'), 0)); 58 | } 59 | -------------------------------------------------------------------------------- /amd/src/local/courseindex/placeholder.js: -------------------------------------------------------------------------------- 1 | // This file is part of Moodle - http://moodle.org/ 2 | // 3 | // Moodle is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // Moodle is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with Moodle. If not, see . 15 | 16 | import Templates from 'core/templates'; 17 | import {getCurrentCourseEditor} from 'core_courseformat/courseeditor'; 18 | import BasePlaceholder from 'core_courseformat/local/courseindex/placeholder'; 19 | import Exporter from "format_flexsections/local/courseeditor/exporter"; 20 | 21 | /** 22 | * Course index placeholder replacer. 23 | * 24 | * @module format_flexsections/local/courseindex/placeholder 25 | * @copyright 2022 Marina Glancy 26 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 | */ 28 | export default class Component extends BasePlaceholder { 29 | // Extends course/format/amd/src/local/courseindex/placeholder.js 30 | 31 | /** 32 | * Static method to create a component instance form the mustache template. 33 | * 34 | * @param {element|string} target the DOM main element or its ID 35 | * @param {object} selectors optional css selector overrides 36 | * @return {Component} 37 | */ 38 | static init(target, selectors) { 39 | const courseEditor = getCurrentCourseEditor(); 40 | courseEditor.getExporter = () => new Exporter(courseEditor); 41 | return new Component({ 42 | element: document.getElementById(target), 43 | reactive: courseEditor, 44 | selectors, 45 | }); 46 | } 47 | 48 | /** 49 | * Load the course index template. 50 | * 51 | * @param {Object} state the initial state 52 | */ 53 | async loadTemplateContent(state) { 54 | // Collect section information from the state. 55 | const exporter = this.reactive.getExporter(); 56 | const data = exporter.course(state); 57 | try { 58 | // To render an HTML into our component we just use the regular Templates module. 59 | const {html, js} = await Templates.renderForPromise( 60 | 'format_flexsections/local/courseindex/courseindex', 61 | data, 62 | ); 63 | Templates.replaceNode(this.element, html, js); 64 | this.pendingContent.resolve(); 65 | 66 | // Save the rendered template into the session cache. 67 | this.reactive.setStorageValue(`courseIndex`, {html, js}); 68 | } catch (error) { 69 | this.pendingContent.resolve(error); 70 | throw error; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /amd/src/local/courseeditor/mutations.js: -------------------------------------------------------------------------------- 1 | // This file is part of Moodle - http://moodle.org/ 2 | // 3 | // Moodle is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // Moodle is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with Moodle. If not, see . 15 | 16 | import Mutations from 'core_courseformat/local/courseeditor/mutations'; 17 | 18 | /** 19 | * Mutations 20 | * 21 | * @module format_flexsections/local/courseeditor/mutations 22 | * @copyright 2022 Marina Glancy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | export default class extends Mutations { 26 | // Extends: course/format/amd/src/local/courseeditor/mutations.js 27 | 28 | /** 29 | * Merge section with its parent. 30 | * 31 | * @param {StateManager} stateManager the current state manager 32 | * @param {number} sectionId 33 | */ 34 | async sectionMergeUp(stateManager, sectionId) { 35 | this.sectionLock(stateManager, [sectionId], true); 36 | const course = stateManager.get('course'); 37 | const updates = await this._callEditWebservice('section_mergeup', course.id, [], sectionId); 38 | stateManager.processUpdates(updates); 39 | this.sectionLock(stateManager, [sectionId], false); 40 | } 41 | 42 | /** 43 | * Add a new subsection to a specific section. 44 | * 45 | * @param {StateManager} stateManager the current state manager 46 | * @param {number} parentSectionId 47 | */ 48 | async addSubSection(stateManager, parentSectionId) { 49 | const course = stateManager.get('course'); 50 | const updates = await this._callEditWebservice('section_add_subsection', course.id, [], parentSectionId); 51 | stateManager.processUpdates(updates); 52 | } 53 | 54 | /** 55 | * Add a new section to a specific course location. 56 | * 57 | * @param {StateManager} stateManager the current state manager 58 | * @param {number} parentSectionId optional the target section id 59 | */ 60 | async insertSubSection(stateManager, parentSectionId) { 61 | const course = stateManager.get('course'); 62 | const updates = await this._callEditWebservice('section_insert_subsection', course.id, [], parentSectionId); 63 | stateManager.processUpdates(updates); 64 | } 65 | 66 | /** 67 | * Switch between section being displayed on a separate page vs on the same page 68 | * 69 | * @param {StateManager} stateManager the current state manager 70 | * @param {number} sectionId 71 | */ 72 | async sectionSwitchCollapsed(stateManager, sectionId) { 73 | const course = stateManager.get('course'); 74 | const updates = await this._callEditWebservice('section_switch_collapsed', course.id, [sectionId]); 75 | stateManager.processUpdates(updates); 76 | } 77 | } -------------------------------------------------------------------------------- /classes/output/courseformat/state/course.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\output\courseformat\state; 18 | 19 | /** 20 | * Contains the ajax update course structure. 21 | * 22 | * @package format_flexsections 23 | * @copyright 2022 Marina Glancy 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | class course extends \core_courseformat\output\local\state\course { 27 | 28 | /** @var \format_flexsections the course format class */ 29 | protected $format; 30 | 31 | /** 32 | * Export this data so it can be used as state object in the course editor. 33 | * 34 | * @param \renderer_base $output typically, the renderer that's calling this function 35 | * @return \stdClass data context for a mustache template 36 | */ 37 | public function export_for_template(\renderer_base $output): \stdClass { 38 | $data = parent::export_for_template($output); 39 | 40 | // Build list of first-level sections (used by courseindex). 41 | $data->sectionlist = array_values(array_filter($data->sectionlist, 42 | function($sectionid) { 43 | $section = $this->format->get_modinfo()->get_section_info_by_id($sectionid); 44 | return $section && !$section->parent; 45 | })); 46 | 47 | // Build sections hierarchy. 48 | $allsections = $this->format->get_modinfo()->get_section_info_all(); 49 | $res1 = $res2 = []; 50 | foreach ($allsections as $s) { 51 | if ($s->section && $this->format->is_section_visible($s)) { 52 | $children = []; 53 | foreach ($allsections as $ss) { 54 | if ($ss->parent == $s->section) { 55 | $children[] = $ss->id; 56 | } 57 | } 58 | if ($children) { 59 | $res1[] = ['id' => $s->id, 'section' => $s->section, 'children' => $children]; 60 | } else { 61 | $res2[] = ['id' => $s->id, 'section' => $s->section, 'children' => $children]; 62 | } 63 | } 64 | } 65 | // Function _fixOrder in lib/amd/src/local/reactive/basecomponent.js removes all existing children of empty lists 66 | // too early, before saving them in the 'dettachedelements'. To avoid accidentally losing sections during 67 | // reordering we pass the empty lists in the end. 68 | $data->hierarchy = array_merge($res1, $res2); 69 | $data->maxsectiondepth = $this->format->get_max_section_depth(); 70 | if ($this->format->get_accordion_setting()) { 71 | $data->accordion = 1; 72 | } 73 | 74 | return $data; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /classes/output/courseformat/state/section.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\output\courseformat\state; 18 | use format_flexsections\constants; 19 | 20 | /** 21 | * Contains the ajax update section structure. 22 | * 23 | * @package format_flexsections 24 | * @copyright 2022 Marina Glancy 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | class section extends \core_courseformat\output\local\state\section { 28 | 29 | /** @var \format_flexsections the course format class */ 30 | protected $format; 31 | 32 | /** 33 | * Export this data so it can be used as state object in the course editor. 34 | * 35 | * @param \renderer_base $output typically, the renderer that's calling this function 36 | * @return \stdClass data context for a mustache template 37 | */ 38 | public function export_for_template(\renderer_base $output): \stdClass { 39 | /** @var \stdClass $data */ 40 | $data = parent::export_for_template($output); 41 | $data->parent = $this->section->parent; 42 | $data->parentid = $this->section->parent ? $this->format->get_modinfo()->get_section_info($this->section->parent)->id : 0; 43 | 44 | // For sections that are displayed as a link do not print list of cms or controls. 45 | $showaslink = $this->section->collapsed == FORMAT_FLEXSECTIONS_COLLAPSED 46 | && $this->format->get_viewed_section() != $this->section->section; 47 | $data->showaslink = $showaslink; 48 | $data->children = []; 49 | if ($this->section->section) { 50 | foreach ($this->format->get_modinfo()->get_section_info_all() as $s) { 51 | if ($s->parent == $this->section->section && $this->format->is_section_visible($s)) { 52 | $data->children[] = (array)((new static($this->format, $s))->export_for_template($output)) + 53 | $this->default_section_properties(); 54 | } 55 | } 56 | } 57 | $data->haschildren = !empty($data->children); 58 | $data->singlesection = (int)($this->section->collapsed && $this->section->section == $this->format->get_viewed_section()); 59 | $courseindex = $this->format->get_course_index_display(); 60 | $data->hidecmsinindex = ($courseindex == constants::COURSEINDEX_SECTIONS); 61 | 62 | return $data; 63 | } 64 | 65 | /** 66 | * Since we display sections nested the values from the parent can propagate in templates 67 | * 68 | * @return array 69 | */ 70 | protected function default_section_properties(): array { 71 | return [ 72 | 'isstealth' => false, 'ishidden' => false, 'notavailable' => false, 'hiddenfromstudents' => false, 73 | 'cmlist' => [], 'hascms' => false, 'cms' => [], 74 | ]; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /amd/build/local/courseindex/placeholder.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"placeholder.min.js","sources":["../../../src/local/courseindex/placeholder.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 Templates from 'core/templates';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport BasePlaceholder from 'core_courseformat/local/courseindex/placeholder';\nimport Exporter from \"format_flexsections/local/courseeditor/exporter\";\n\n/**\n * Course index placeholder replacer.\n *\n * @module format_flexsections/local/courseindex/placeholder\n * @copyright 2022 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class Component extends BasePlaceholder {\n // Extends course/format/amd/src/local/courseindex/placeholder.js\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n const courseEditor = getCurrentCourseEditor();\n courseEditor.getExporter = () => new Exporter(courseEditor);\n return new Component({\n element: document.getElementById(target),\n reactive: courseEditor,\n selectors,\n });\n }\n\n /**\n * Load the course index template.\n *\n * @param {Object} state the initial state\n */\n async loadTemplateContent(state) {\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(state);\n try {\n // To render an HTML into our component we just use the regular Templates module.\n const {html, js} = await Templates.renderForPromise(\n 'format_flexsections/local/courseindex/courseindex',\n data,\n );\n Templates.replaceNode(this.element, html, js);\n this.pendingContent.resolve();\n\n // Save the rendered template into the session cache.\n this.reactive.setStorageValue(`courseIndex`, {html, js});\n } catch (error) {\n this.pendingContent.resolve(error);\n throw error;\n }\n }\n}\n"],"names":["Component","BasePlaceholder","target","selectors","courseEditor","getExporter","Exporter","element","document","getElementById","reactive","state","data","this","course","html","js","Templates","renderForPromise","replaceNode","pendingContent","resolve","setStorageValue","error"],"mappings":";;;;;;;uOA2BqBA,kBAAkBC,iCAUvBC,OAAQC,iBACVC,cAAe,iDACrBA,aAAaC,YAAc,IAAM,IAAIC,kBAASF,cACvC,IAAIJ,UAAU,CACjBO,QAASC,SAASC,eAAeP,QACjCQ,SAAUN,aACVD,UAAAA,sCASkBQ,aAGhBC,KADWC,KAAKH,SAASL,cACTS,OAAOH,iBAGnBI,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAC/B,oDACAN,yBAEMO,YAAYN,KAAKN,QAASQ,KAAMC,SACrCI,eAAeC,eAGfX,SAASY,8BAA+B,CAACP,KAAAA,KAAMC,GAAAA,KACtD,MAAOO,kBACAH,eAAeC,QAAQE,OACtBA"} -------------------------------------------------------------------------------- /amd/src/local/courseeditor/exporter.js: -------------------------------------------------------------------------------- 1 | // This file is part of Moodle - http://moodle.org/ 2 | // 3 | // Moodle is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // Moodle is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with Moodle. If not, see . 15 | 16 | import Exporter from "core_courseformat/local/courseeditor/exporter"; 17 | 18 | /** 19 | * Overriding default course format exporter 20 | * 21 | * @module format_flexsections/local/courseeditor/exporter 22 | * @copyright 2022 Marina Glancy 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | export default class extends Exporter { 26 | // Extends: course/format/amd/src/local/courseeditor/exporter.js 27 | 28 | /** 29 | * Generate a section export data from the state. 30 | * 31 | * @param {Object} state the current state. 32 | * @param {Object} sectioninfo the section state data. 33 | * @returns {Object} 34 | */ 35 | section(state, sectioninfo) { 36 | const children = sectioninfo.children; 37 | const section = super.section(state, sectioninfo); 38 | section.children = []; 39 | if (children && children.length) { 40 | for (let i = 0; i < children.length; i++) { 41 | section.children.push(this.section(state, children[i])); 42 | } 43 | } 44 | return section; 45 | } 46 | 47 | /** 48 | * Generate the course export data from the state. 49 | * 50 | * @param {Object} state the current state. 51 | * @returns {Object} 52 | */ 53 | course(state) { 54 | const course = super.course(state); 55 | course.maxsectiondepth = state.course.maxsectiondepth; 56 | return course; 57 | } 58 | 59 | /** 60 | * Return a sorted list of all sections and cms items in the state. 61 | * 62 | * @param {Object} state the current state. 63 | * @returns {Array} all sections and cms items in the state. 64 | */ 65 | allItemsArray(state) { 66 | const items = []; 67 | const sectionlist = state.course.sectionlist ?? []; 68 | 69 | const addCms = (sectioninfo) => { 70 | const cmlist = sectioninfo.cmlist ?? []; 71 | cmlist.forEach(cmid => { 72 | const cminfo = state.cm.get(cmid); 73 | items.push({type: 'cm', id: cminfo.id, url: cminfo.url}); 74 | }); 75 | }; 76 | const addChildItems = (children) => { 77 | if (children && children.length) { 78 | for (let i = 0; i < children.length; i++) { 79 | const child = children[i]; 80 | items.push({type: 'section', id: child.id, url: child.sectionurl}); 81 | addCms(child); 82 | addChildItems(child.children); 83 | } 84 | } 85 | }; 86 | sectionlist.forEach(sectionid => { 87 | const sectioninfo = state.section.get(sectionid); 88 | items.push({type: 'section', id: sectioninfo.id, url: sectioninfo.sectionurl}); 89 | addCms(sectioninfo); 90 | addChildItems(sectioninfo.children); 91 | }); 92 | return items; 93 | } 94 | } -------------------------------------------------------------------------------- /tests/behat/back_link_in_cms.feature: -------------------------------------------------------------------------------- 1 | @format @format_flexsections @javascript 2 | Feature: Return to section from activity in format flexsections 3 | 4 | Background: 5 | Given the following "courses" exist: 6 | | fullname | shortname | format | numsections | 7 | | Course 1 | C1 | flexsections | 0 | 8 | And the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | student | Sam | Student | student@example.com | 11 | | teacher | Tom | Teacher | teacher@example.com | 12 | And the following "course enrolments" exist: 13 | | user | course | role | 14 | | student | C1 | student | 15 | | teacher | C1 | editingteacher | 16 | And the following "format_flexsections > sections" exist: 17 | | name | course | parent | collapsed | 18 | | t100 | C1 | | 0 | 19 | | t200 | C1 | | 0 | 20 | | t300 | C1 | | 1 | 21 | | t110 | C1 | t100 | 0 | 22 | | t120 | C1 | t100 | 0 | 23 | | t121 | C1 | t120 | 0 | 24 | | t210 | C1 | t200 | 0 | 25 | | t211 | C1 | t210 | 0 | 26 | | t111 | C1 | t110 | 0 | 27 | | t220 | C1 | t200 | 0 | 28 | | t310 | C1 | t300 | 0 | 29 | | t320 | C1 | t300 | 0 | 30 | | t311 | C1 | t310 | 0 | 31 | | t312 | C1 | t310 | 0 | 32 | | t321 | C1 | t320 | 0 | 33 | And the following "activities" exist: 34 | | activity | course | idnumber | name | section | 35 | | page | C1 | p1 | Page in General section | 0 | 36 | | page | C1 | p2 | Page in first section | 1 | 37 | | page | C1 | p3 | Page in t300 section | 10 | 38 | | page | C1 | p4 | Page in t310 section | 11 | 39 | 40 | Scenario: Return to section from activity in format flexsections (default to no link) 41 | When I log in as "student" 42 | And I am on "Course 1" course homepage 43 | And I follow "Page in first section" 44 | Then I should not see "Back to '" 45 | 46 | Scenario: Return to section from activity in format flexsections (with a link) 47 | When I log in as "teacher" 48 | When I am on "Course 1" course homepage 49 | And I follow "Settings" 50 | And I set the following fields to these values: 51 | | cmbacklink | 1 | 52 | And I press "Save and display" 53 | And I log out 54 | When I log in as "student" 55 | And I am on "Course 1" course homepage 56 | And I follow "Page in General section" 57 | Then I should not see "Back to '" 58 | And I am on "Course 1" course homepage 59 | And I follow "Page in first section" 60 | And I wait "2" seconds 61 | And "Back to" "text" should appear before "Test page content" "text" 62 | Then I follow "Back to 't100'" 63 | And I should see "t100" in the "region-main" "region" 64 | And I should see "t110" in the "region-main" "region" 65 | And I should see "t200" in the "region-main" "region" 66 | And I follow "t300" 67 | And I follow "Page in t300 section" 68 | Then I follow "Back to 't300'" 69 | And I should see "t300" in the "region-main" "region" 70 | And I should see "t310" in the "region-main" "region" 71 | And I should see "t320" in the "region-main" "region" 72 | And I follow "Page in t310 section" 73 | Then I follow "Back to 't310'" 74 | And I should see "t300" in the "region-main" "region" 75 | And I should see "t310" in the "region-main" "region" 76 | And I should see "t320" in the "region-main" "region" 77 | -------------------------------------------------------------------------------- /amd/build/local/courseeditor/mutations.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"mutations.min.js","sources":["../../../src/local/courseeditor/mutations.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 Mutations from 'core_courseformat/local/courseeditor/mutations';\n\n/**\n * Mutations\n *\n * @module format_flexsections/local/courseeditor/mutations\n * @copyright 2022 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class extends Mutations {\n // Extends: course/format/amd/src/local/courseeditor/mutations.js\n\n /**\n * Merge section with its parent.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {number} sectionId\n */\n async sectionMergeUp(stateManager, sectionId) {\n this.sectionLock(stateManager, [sectionId], true);\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_mergeup', course.id, [], sectionId);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, [sectionId], false);\n }\n\n /**\n * Add a new subsection to a specific section.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {number} parentSectionId\n */\n async addSubSection(stateManager, parentSectionId) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_add_subsection', course.id, [], parentSectionId);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Add a new section to a specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {number} parentSectionId optional the target section id\n */\n async insertSubSection(stateManager, parentSectionId) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_insert_subsection', course.id, [], parentSectionId);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Switch between section being displayed on a separate page vs on the same page\n *\n * @param {StateManager} stateManager the current state manager\n * @param {number} sectionId\n */\n async sectionSwitchCollapsed(stateManager, sectionId) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_switch_collapsed', course.id, [sectionId]);\n stateManager.processUpdates(updates);\n }\n}"],"names":["Mutations","stateManager","sectionId","sectionLock","course","get","updates","this","_callEditWebservice","id","processUpdates","parentSectionId"],"mappings":";;;;;;;0KAwB6BA,wCASJC,aAAcC,gBAC1BC,YAAYF,aAAc,CAACC,YAAY,SACtCE,OAASH,aAAaI,IAAI,UAC1BC,cAAgBC,KAAKC,oBAAoB,kBAAmBJ,OAAOK,GAAI,GAAIP,WACjFD,aAAaS,eAAeJ,cACvBH,YAAYF,aAAc,CAACC,YAAY,uBAS5BD,aAAcU,uBACxBP,OAASH,aAAaI,IAAI,UAC1BC,cAAgBC,KAAKC,oBAAoB,yBAA0BJ,OAAOK,GAAI,GAAIE,iBACxFV,aAAaS,eAAeJ,gCASTL,aAAcU,uBAC3BP,OAASH,aAAaI,IAAI,UAC1BC,cAAgBC,KAAKC,oBAAoB,4BAA6BJ,OAAOK,GAAI,GAAIE,iBAC3FV,aAAaS,eAAeJ,sCASHL,aAAcC,iBACjCE,OAASH,aAAaI,IAAI,UAC1BC,cAAgBC,KAAKC,oBAAoB,2BAA4BJ,OAAOK,GAAI,CAACP,YACvFD,aAAaS,eAAeJ"} -------------------------------------------------------------------------------- /tests/behat/activity_chooser_plus.feature: -------------------------------------------------------------------------------- 1 | @format @format_flexsections @javascript 2 | Feature: Use the activity chooser to insert activities anywhere in a section in format_flexsections 3 | In order to add activities to a course 4 | As a teacher 5 | I should be able to add an activity anywhere in a section. 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | teacher | Teacher | 1 | teacher@example.com | 11 | And the following "courses" exist: 12 | | fullname | shortname | format | 13 | | Course | C | flexsections | 14 | And the following "course enrolments" exist: 15 | | user | course | role | 16 | | teacher | C | editingteacher | 17 | And the following "activities" exist: 18 | | activity | course | idnumber | intro | name | section | 19 | | page | C | p1 | x | Test Page | 1 | 20 | | forum | C | f1 | x | Test Forum | 1 | 21 | | label | C | l1 | x | Test Label | 1 | 22 | And I log in as "teacher" 23 | And I am on "Course" course homepage with editing mode on 24 | 25 | Scenario: The activity chooser icon is hidden by default and be made visible on hover 26 | Given I hover ".navbar-brand" "css_element" 27 | #And "[data-action='insert-before-Test Forum'] button" "css_element" should not be visible 28 | When I hover "Insert an activity or resource before 'Test Forum'" "button" 29 | Then "button[aria-label=\"Insert an activity or resource before 'Test Forum'\"]" "css_element" should be visible 30 | 31 | Scenario: The activity chooser can be used to insert modules before existing modules in 5.0 32 | Given the site is running Moodle version 5.0.99 or lower 33 | Given I hover "Insert an activity or resource before 'Test Forum'" "button" 34 | And I press "Insert an activity or resource before 'Test Forum'" 35 | And I should see "Add an activity or resource" in the ".modal-title" "css_element" 36 | When I click on "Add a new Assignment" "link" in the "Add an activity or resource" "dialogue" 37 | And I set the following fields to these values: 38 | | Assignment name | Test Assignment | 39 | And I press "Save and return to course" 40 | And I should see "Test Assignment" in the "Topic 1" "section" 41 | # Ensure the new assignment is in the middle of the two existing modules. 42 | Then "Test Page" "text" should appear before "Test Assignment" "text" in the "region-main" "region" 43 | And "Test Assignment" "text" should appear before "Test Forum" "text" in the "region-main" "region" 44 | 45 | Scenario: The activity chooser can be used to insert modules before existing modules in 5.1 and above 46 | Given the site is running Moodle version 5.1 or higher 47 | Given I hover "Insert an activity or resource before 'Test Forum'" "button" 48 | And I press "Insert an activity or resource before 'Test Forum'" 49 | And I should see "Add an activity or resource" in the ".modal-title" "css_element" 50 | When I click on "Add a new Assignment" "link" in the "Add an activity or resource" "dialogue" 51 | And I click on "Add selected activity" "button" in the "Add an activity or resource" "dialogue" 52 | And I set the following fields to these values: 53 | | Assignment name | Test Assignment | 54 | And I press "Save and return to course" 55 | And I should see "Test Assignment" in the "Topic 1" "section" 56 | # Ensure the new assignment is in the middle of the two existing modules. 57 | Then "Test Page" "text" should appear before "Test Assignment" "text" in the "region-main" "region" 58 | And "Test Assignment" "text" should appear before "Test Forum" "text" in the "region-main" "region" 59 | -------------------------------------------------------------------------------- /templates/local/content/section/header.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/content/section/header 19 | 20 | Displays a course section header. 21 | 22 | Example context (json): 23 | { 24 | "id": 123, 25 | "name": "Section title", 26 | "title": "Section title", 27 | "url": "#", 28 | "headerdisplaymultipage": true, 29 | "hidetitle": false, 30 | "editing": 0 31 | } 32 | }} 33 | 34 |
35 | {{#headerdisplaymultipage}} 36 | {{^hidetitle}} 37 | {{#indenttitle}} 38 | 39 | {{#pix}} t/collapsedchevron, core {{/pix}} 40 | {{#pix}} t/collapsedchevron_rtl, core {{/pix}} 41 | 42 | {{/indenttitle}} 43 |

45 | {{{title}}} 46 |

47 | {{/hidetitle}} 48 | {{/headerdisplaymultipage}} 49 | {{^headerdisplaymultipage}} 50 | {{^hidetitle}} 51 | 74 | {{/hidetitle}} 75 | {{/headerdisplaymultipage}} 76 |
77 | -------------------------------------------------------------------------------- /classes/output/renderer.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\output; 18 | 19 | use action_link; 20 | use core_courseformat\base as course_format; 21 | use core_courseformat\output\section_renderer; 22 | use format_flexsections_edit_control; 23 | use html_writer; 24 | use moodle_page; 25 | use pix_icon; 26 | use section_info; 27 | use stdClass; 28 | 29 | /** 30 | * Basic renderer for Flexsections format. 31 | * 32 | * @package format_flexsections 33 | * @copyright 2022 Marina Glancy 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class renderer extends section_renderer { 37 | 38 | /** 39 | * Constructor method, calls the parent constructor. 40 | * 41 | * @param moodle_page $page 42 | * @param string $target one of rendering target constants 43 | */ 44 | public function __construct(moodle_page $page, $target) { 45 | parent::__construct($page, $target); 46 | 47 | // Since format_flexsections_renderer::section_edit_control_items() only displays the 'Highlight' control 48 | // when editing mode is on we need to be sure that the link 'Turn editing mode on' is available for a user 49 | // who does not have any other managing capability. 50 | $page->set_other_editing_capability('moodle/course:setcurrentsection'); 51 | } 52 | 53 | /** 54 | * Generate the section title, wraps it in a link to the section page if page is to be displayed on a separate page. 55 | * 56 | * @param section_info|stdClass $section The course_section entry from DB 57 | * @param stdClass $course The course entry from DB 58 | * @return string HTML to output. 59 | */ 60 | public function section_title($section, $course) { 61 | return $this->render(course_get_format($course)->inplace_editable_render_section_name($section)); 62 | } 63 | 64 | /** 65 | * Generate the section title to be displayed on the section page, without a link. 66 | * 67 | * @param section_info|stdClass $section The course_section entry from DB 68 | * @param int|stdClass $course The course entry from DB 69 | * @return string HTML to output. 70 | */ 71 | public function section_title_without_link($section, $course) { 72 | return $this->render(course_get_format($course)->inplace_editable_render_section_name($section, false)); 73 | } 74 | 75 | /** 76 | * Get the course index drawer with placeholder. 77 | * 78 | * The default course index is loaded after the page is ready. Format plugins can override 79 | * this method to provide an alternative course index. 80 | * 81 | * If the format is not compatible with the course index, this method will return an empty string. 82 | * 83 | * @param course_format $format the course format 84 | * @return string the course index HTML. 85 | */ 86 | public function course_index_drawer(course_format $format): ?String { 87 | if ($format->uses_course_index()) { 88 | include_course_editor($format); 89 | return $this->render_from_template('format_flexsections/local/courseindex/drawer', []); 90 | } 91 | return ''; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /classes/output/courseformat/content/section/header.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\output\courseformat\content\section; 18 | 19 | /** 20 | * Contains the section header output class. 21 | * 22 | * @package format_flexsections 23 | * @copyright 2022 Marina Glancy 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | class header extends \core_courseformat\output\local\content\section\header { 27 | 28 | /** 29 | * Template name 30 | * 31 | * @param \renderer_base $renderer 32 | * @return string 33 | */ 34 | public function get_template_name(\renderer_base $renderer): string { 35 | // Mdlcode uses: template 'format_flexsections/local/content/section/header'. 36 | return 'format_flexsections/local/content/section/header'; 37 | } 38 | 39 | /** 40 | * Data exporter 41 | * 42 | * @param \renderer_base $output 43 | * @return \stdClass 44 | */ 45 | public function export_for_template(\renderer_base $output): \stdClass { 46 | 47 | $data = parent::export_for_template($output); 48 | $data->indenttitle = false; 49 | $data->hidetitle = false; 50 | $course = $this->format->get_course(); 51 | 52 | if ($this->section->collapsed == FORMAT_FLEXSECTIONS_COLLAPSED) { 53 | // Do not display the collapse/expand caret for sections that are meant to be shown on a separate page. 54 | $data->headerdisplaymultipage = true; 55 | if ($this->format->get_viewed_section() != $this->section->section) { 56 | // If the section is displayed as a link and we are not on this section's page, display it as a link. 57 | $data->title = $output->section_title($this->section, $course); 58 | $data->indenttitle = $this->title_needs_indenting(); 59 | } else { 60 | if (strpos(qualified_me(), '/course/section.php') !== false) { 61 | $data->hidetitle = true; 62 | } 63 | } 64 | } 65 | 66 | if (!$course->showsection0title && $this->section->section === 0) { 67 | // Do not display header title for the "General" section. 68 | $data->hidetitle = true; 69 | } 70 | 71 | $data->headerdisplaymultipage = !empty($data->headerdisplaymultipage); 72 | return $data; 73 | } 74 | 75 | /** 76 | * Title needs indenting 77 | * 78 | * Title displayed as a link needs indenting if some siblings are collpased and some are not. 79 | * 80 | * @return bool 81 | */ 82 | protected function title_needs_indenting(): bool { 83 | $hassections = [FORMAT_FLEXSECTIONS_COLLAPSED => false, FORMAT_FLEXSECTIONS_EXPANDED => 0]; 84 | foreach ($this->format->get_modinfo()->get_section_info_all() as $section) { 85 | if ($section->section && $section->parent == $this->section->parent && $this->format->is_section_visible($section)) { 86 | $hassections[$section->collapsed] = true; 87 | } 88 | } 89 | return $hassections[FORMAT_FLEXSECTIONS_EXPANDED] && $hassections[FORMAT_FLEXSECTIONS_COLLAPSED]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/behat/course_crud.feature: -------------------------------------------------------------------------------- 1 | @format @format_flexsections @javascript 2 | Feature: Creating, updating and deleting courses in flexsections format 3 | In order to use flexsections format 4 | As a teacher 5 | I need to test all basic funtionality 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | teacher1 | Terry | Teacher | teacher1@example.com | 11 | | manager1 | Mary | Manager | manager1@example.com | 12 | Given the following "categories" exist: 13 | | name | category | idnumber | 14 | | Cat 1 | 0 | CAT1 | 15 | And the following "system role assigns" exist: 16 | | user | role | contextlevel | reference | 17 | | manager1 | manager | Category | CAT1 | 18 | 19 | Scenario: Creating course in flexsections format when default format is topics 20 | When I log in as "manager1" 21 | And I am on site homepage 22 | When I press "Add a new course" 23 | And I set the following fields to these values: 24 | | Course full name | My first course | 25 | | Course short name | myfirstcourse | 26 | | Format | Flexible sections format | 27 | And I wait to be redirected 28 | And I expand all fieldsets 29 | And I set the field "Number of sections" to "5" 30 | And I press "Save and display" 31 | And I should see "Topic 1" 32 | And I should see "Topic 5" 33 | And I should not see "Topic 6" 34 | 35 | Scenario: Creating course in flexsections format when default format is flexsections 36 | Given the following config values are set as admin: 37 | | format | flexsections | moodlecourse | 38 | When I log in as "manager1" 39 | And I am on site homepage 40 | When I press "Add a new course" 41 | And I set the following fields to these values: 42 | | Course full name | My first course | 43 | | Course short name | myfirstcourse | 44 | And the following fields match these values: 45 | | Format | Flexible sections format | 46 | And I set the field "Number of sections" to "5" 47 | And I press "Save and display" 48 | And I should see "Topic 1" 49 | And I should see "Topic 5" 50 | And I should not see "Topic 6" 51 | 52 | Scenario: Changing course format from topics to flexsections 53 | Given the following "courses" exist: 54 | | fullname | shortname | format | numsections | 55 | | Course 1 | C1 | topics | 3 | 56 | When I log in as "manager1" 57 | And I am on "Course 1" course homepage 58 | And I select "Settings" from secondary navigation 59 | And I set the following fields to these values: 60 | | Format | Flexible sections format | 61 | And I wait to be redirected 62 | And I expand all fieldsets 63 | And I should not see "Number of sections" 64 | And I press "Save and display" 65 | And I should see "Topic 1" 66 | And I should see "Topic 3" 67 | And I should not see "Topic 4" 68 | 69 | Scenario: Deleting course in flexsections format 70 | And the following "courses" exist: 71 | | fullname | shortname | format | numsections | 72 | | Course 1 | C1 | flexsections | 0 | 73 | And the following "activities" exist: 74 | | activity | name | intro | course | idnumber | section | completion | 75 | | assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 | 1 | 76 | When I log in as "manager1" 77 | And I am on "Course 1" course homepage 78 | And I select "Settings" from secondary navigation 79 | And I press "Save and display" 80 | And I go to the courses management page 81 | And I click on "delete" action for "Course 1" in management course listing 82 | And I press "Delete" 83 | And I should see "Deleting C1" 84 | And I should see "C1 has been completely deleted" 85 | And I press "Continue" 86 | -------------------------------------------------------------------------------- /amd/build/local/courseindex/courseindex.min.js: -------------------------------------------------------------------------------- 1 | define("format_flexsections/local/courseindex/courseindex",["exports","core_courseformat/courseeditor","core_courseformat/local/courseindex/courseindex","format_flexsections/local/courseeditor/exporter"],(function(_exports,_courseeditor,_courseindex,_exporter){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} 2 | /** 3 | * Course index main component. 4 | * 5 | * @module format_flexsections/local/courseindex/courseindex 6 | * @copyright 2022 Marina Glancu 7 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 8 | */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_courseindex=_interopRequireDefault(_courseindex),_exporter=_interopRequireDefault(_exporter);class Component extends _courseindex.default{static init(target,selectors){const courseEditor=(0,_courseeditor.getCurrentCourseEditor)();return courseEditor.getExporter=()=>new _exporter.default(courseEditor),new Component({element:document.getElementById(target),reactive:courseEditor,selectors:selectors})}create(descriptor){super.create(descriptor),this.name="course_format_flexsections_courseindex",this.selectors.COURSE_SUBSECTIONLIST="[data-for='subsectionlist']"}getWatchers(){let res=super.getWatchers();return res.push({watch:"course.hierarchy:updated",handler:this._refreshCourseSectionlist}),res}_refreshCourseSectionlist(_ref){var _element$hierarchy,_element$sectionlist;let{element:element}=_ref;const hierarchy=null!==(_element$hierarchy=element.hierarchy)&&void 0!==_element$hierarchy?_element$hierarchy:[];let dettachedSections=[];for(let i=0;i{var _this$getElement;let item=null!==(_this$getElement=this.getElement(this.selectors.SECTION,itemid))&&void 0!==_this$getElement?_this$getElement:dettachedelements[itemid];if(void 0===item)return;const currentitem=container.children[index];void 0!==currentitem?currentitem!==item&&container.insertBefore(item,currentitem):container.append(item)}));container.children.length>neworder.length;){var _lastchild$dataset$id,_lastchild$dataset;const lastchild=container.lastChild;dettachedelements[null!==(_lastchild$dataset$id=null==lastchild||null===(_lastchild$dataset=lastchild.dataset)||void 0===_lastchild$dataset?void 0:_lastchild$dataset.id)&&void 0!==_lastchild$dataset$id?_lastchild$dataset$id:0]=lastchild,container.removeChild(lastchild)}neworder.length||container.classList.add("hidden")}}async _createSection(_ref3){var _this$getElement2;let{state:state,element:element}=_ref3;const sectionItem=null!==(_this$getElement2=this.getElement("section",element.id))&&void 0!==_this$getElement2?_this$getElement2:this._createFakeSection(this.element,element.id);const data=this.reactive.getExporter().section(state,element),newelement=(await this.renderComponent(sectionItem,"format_flexsections/local/courseindex/section",data)).getElement();this.sections[element.id]=newelement,sectionItem.parentNode.replaceChild(newelement,sectionItem)}_createFakeSection(container,sectionid){const fakeelement=document.createElement("div");return container.appendChild(fakeelement),fakeelement.classList.add("bg-pulse-grey","w-100"),fakeelement.dataset.for="section",fakeelement.dataset.id=sectionid,fakeelement.innerHTML=" ",fakeelement}}return _exports.default=Component,_exports.default})); 9 | 10 | //# sourceMappingURL=courseindex.min.js.map -------------------------------------------------------------------------------- /tests/behat/edit_delete_sections.feature: -------------------------------------------------------------------------------- 1 | @format @format_flexsections @javascript 2 | Feature: Sections can be edited and deleted in flexsections format 3 | In order to rearrange my course contents 4 | As a teacher 5 | I need to edit and Delete sections 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | teacher1 | Teacher | 1 | teacher1@example.com | 11 | And the following "courses" exist: 12 | | fullname | shortname | format | coursedisplay | numsections | 13 | | Course 1 | C1 | flexsections | 0 | 5 | 14 | And the following "activities" exist: 15 | | activity | name | intro | course | idnumber | section | 16 | | assign | Test assignment name | Test assignment description | C1 | assign1 | 0 | 17 | | book | Test book name | Test book description | C1 | book1 | 1 | 18 | | folder | Test folder name | Test folder description | C1 | folder1 | 4 | 19 | | choice | Test choice name | Test choice description | C1 | choice1 | 5 | 20 | And the following "course enrolments" exist: 21 | | user | course | role | 22 | | teacher1 | C1 | editingteacher | 23 | And I log in as "teacher1" 24 | And I am on "Course 1" course homepage with editing mode on 25 | 26 | Scenario: View the default name of the second section in flexsections format 27 | When I edit the section "2" 28 | And the field "Section name" matches expression "/^$/" 29 | 30 | Scenario: Edit section summary in flexsections format 31 | When I edit the section "2" and I fill the form with: 32 | | Description | Welcome to section 2 | 33 | Then I should see "Welcome to section 2" in the "Topic 2" "section" 34 | 35 | Scenario: Edit section default name in flexsections format 36 | When I edit the section "2" and I fill the form with: 37 | | Section name | This is the second topic | 38 | Then I should see "This is the second topic" in the "This is the second topic" "section" 39 | And I should not see "Topic 2" in the "region-main" "region" 40 | 41 | Scenario: Inline edit section name in flexsections format 42 | When I set the field "Edit section name" in the "Topic 1" "section" to "Midterm evaluation" 43 | Then I should not see "Topic 1" in the "region-main" "region" 44 | And "New name for section" "field" should not exist 45 | And I should see "Midterm evaluation" in the "Midterm evaluation" "section" 46 | And I am on "Course 1" course homepage 47 | And I should not see "Topic 1" in the "region-main" "region" 48 | And I should see "Midterm evaluation" in the "Midterm evaluation" "section" 49 | 50 | Scenario: Deleting the last section in flexsections format 51 | When I delete section "5" 52 | Then I should see "Are you sure you want to delete this section? All activities and subsections will also be deleted" 53 | And I click on "Yes" "button" in the "Confirm" "dialogue" 54 | And I should not see "Topic 5" in the "region-main" "region" 55 | And I should see "Topic 4" in the "region-main" "region" 56 | 57 | Scenario: Deleting the middle section in flexsections format 58 | When I delete section "4" 59 | And I click on "Yes" "button" in the "Confirm" "dialogue" 60 | Then I should not see "Topic 5" in the "region-main" "region" 61 | And I should not see "Test folder name" 62 | And I should see "Test choice name" in the "Topic 4" "section" 63 | And I should see "Topic 4" in the "region-main" "region" 64 | 65 | Scenario: Adding sections at the end of a flexsections format 66 | When I click on "Add section" "link" in the "Topic 5" "section" 67 | Then I should see "Topic 6" in the "Topic 6" "section" 68 | And I should see "Test choice name" in the "Topic 5" "section" 69 | 70 | Scenario: Copy section page permalink URL to clipboard 71 | When I open section "4" edit menu 72 | And I click on "Permalink" "link" in the "Topic 4" "section" 73 | And I click on "Copy to clipboard" "link" in the "Permalink" "dialogue" 74 | Then I should see "Text copied to clipboard" 75 | -------------------------------------------------------------------------------- /templates/local/courseindex/courseindex.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/courseindex/courseindex 19 | 20 | Displays the course index. 21 | 22 | Example context (json): 23 | { 24 | "editmode": true, 25 | "sections": [ 26 | { 27 | "title": "General", 28 | "id": 42, 29 | "number": 1, 30 | "sectionurl": "#", 31 | "indexcollapsed": 0, 32 | "cms": [ 33 | { 34 | "name": "Glossary of characters", 35 | "id": "10", 36 | "url": "#", 37 | "visible": 1, 38 | "isactive": 0, 39 | "uniqid": "0", 40 | "accessvisible": 1 41 | }, 42 | { 43 | "name": "World Cinema forum", 44 | "id": "11", 45 | "url": "#", 46 | "visible": 1, 47 | "isactive": 0, 48 | "uniqid": "0", 49 | "accessvisible": 1 50 | }, 51 | { 52 | "name": "Announcements", 53 | "id": "12", 54 | "url": "#", 55 | "visible": 1, 56 | "isactive": 1, 57 | "uniqid": "0", 58 | "accessvisible": 1 59 | } 60 | ] 61 | }, 62 | { 63 | "title": "City of God or Cidade de Deus", 64 | "id": "43", 65 | "number": "2", 66 | "sectionurl": "#", 67 | "indexcollapsed": 1, 68 | "cms": [ 69 | { 70 | "name": "Resources", 71 | "id": "13", 72 | "url": "#", 73 | "visible": 1, 74 | "isactive": 0, 75 | "uniqid": "0", 76 | "accessvisible": 1 77 | }, 78 | { 79 | "name": "Studying City of God by Stephen Smith Bergman-Messerschmidt", 80 | "id": "14", 81 | "url": "#", 82 | "visible": 1, 83 | "isactive": 0, 84 | "uniqid": "0", 85 | "accessvisible": 1 86 | }, 87 | { 88 | "name": "Film education study guide", 89 | "id": "15", 90 | "url": "#", 91 | "visible": 1, 92 | "isactive": 0, 93 | "uniqid": "0", 94 | "accessvisible": 1 95 | } 96 | ] 97 | } 98 | ] 99 | } 100 | 101 | }} 102 |
103 | {{#sections}} 104 | {{> format_flexsections/local/courseindex/section }} 105 | {{/sections}} 106 |
107 | {{#js}} 108 | require(['format_flexsections/local/courseindex/courseindex'], function(component) { 109 | component.init('{{uniqid}}-course-index'); 110 | }); 111 | {{/js}} 112 | -------------------------------------------------------------------------------- /tests/behat/behat_format_flexsections.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. 18 | 19 | require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php'); 20 | 21 | /** 22 | * Behat steps in plugin format_flexsections 23 | * 24 | * @package format_flexsections 25 | * @category test 26 | * @copyright Marina Glancy 27 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 28 | */ 29 | class behat_format_flexsections extends behat_base { 30 | 31 | /** 32 | * Opens the activity chooser and opens the activity/resource link form page. Sections 0 and 1 are also allowed on frontpage. 33 | * 34 | * This step require javascript enabled and it is used mainly to click activities or resources by name, 35 | * not by plugin name. Use the standard behat_course::i_add_to_course_section step instead unless the 36 | * plugin create extra entries into the activity chooser (like LTI). 37 | * 38 | * @Given I add a :activityname to section :sectionnum in flexsections using the activity chooser 39 | * @Given I add an :activityname to section :sectionnum in flexsections using the activity chooser 40 | * @param string $activityname 41 | * @param int $sectionnum 42 | */ 43 | public function i_add_to_section_in_flexsections_using_the_activity_chooser($activityname, $sectionnum) { 44 | global $CFG; 45 | 46 | $this->require_javascript('Please use the \'the following "activity" exists:\' data generator instead.'); 47 | 48 | $sectionxpath = "//li[@id='section-" . $sectionnum . "']"; 49 | 50 | // Clicks add activity or resource section link. 51 | $sectionnode = $this->find('xpath', $sectionxpath); 52 | $this->execute('behat_general::i_click_on_in_the', [ 53 | "//button[@data-action='open-chooser' and not(@data-beforemod)]", 54 | 'xpath', 55 | $sectionnode, 56 | 'NodeElement', 57 | ]); 58 | 59 | if ($CFG->branch >= 501) { 60 | // Clicks the selected activity if it exists. 61 | $activityliteral = behat_context_helper::escape(ucfirst($activityname)); 62 | $activityxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' modchooser ')]" . 63 | "/descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' optioninfo ')]" . 64 | "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optionname ')]" . 65 | "[normalize-space(.)=$activityliteral]" . 66 | "/parent::a"; 67 | 68 | $this->execute('behat_general::i_click_on', [$activityxpath, 'xpath']); 69 | 70 | $this->execute('behat_general::i_click_on_in_the', [ 71 | "Add selected activity", 72 | 'button', 73 | "Add an activity or resource", 74 | 'dialogue', 75 | ]); 76 | } else { 77 | // Clicks the selected activity if it exists. 78 | $activityliteral = behat_context_helper::escape(ucfirst($activityname)); 79 | $activityxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' modchooser ')]" . 80 | "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optioninfo ')]" . 81 | "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optionname ')]" . 82 | "[normalize-space(.)=$activityliteral]" . 83 | "/parent::a"; 84 | 85 | $this->execute('behat_general::i_click_on', [$activityxpath, 'xpath']); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /amd/build/local/courseeditor/exporter.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"exporter.min.js","sources":["../../../src/local/courseeditor/exporter.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 Exporter from \"core_courseformat/local/courseeditor/exporter\";\n\n/**\n * Overriding default course format exporter\n *\n * @module format_flexsections/local/courseeditor/exporter\n * @copyright 2022 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class extends Exporter {\n // Extends: course/format/amd/src/local/courseeditor/exporter.js\n\n /**\n * Generate a section export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} sectioninfo the section state data.\n * @returns {Object}\n */\n section(state, sectioninfo) {\n const children = sectioninfo.children;\n const section = super.section(state, sectioninfo);\n section.children = [];\n if (children && children.length) {\n for (let i = 0; i < children.length; i++) {\n section.children.push(this.section(state, children[i]));\n }\n }\n return section;\n }\n\n /**\n * Generate the course export data from the state.\n *\n * @param {Object} state the current state.\n * @returns {Object}\n */\n course(state) {\n const course = super.course(state);\n course.maxsectiondepth = state.course.maxsectiondepth;\n return course;\n }\n\n /**\n * Return a sorted list of all sections and cms items in the state.\n *\n * @param {Object} state the current state.\n * @returns {Array} all sections and cms items in the state.\n */\n allItemsArray(state) {\n const items = [];\n const sectionlist = state.course.sectionlist ?? [];\n\n const addCms = (sectioninfo) => {\n const cmlist = sectioninfo.cmlist ?? [];\n cmlist.forEach(cmid => {\n const cminfo = state.cm.get(cmid);\n items.push({type: 'cm', id: cminfo.id, url: cminfo.url});\n });\n };\n const addChildItems = (children) => {\n if (children && children.length) {\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n items.push({type: 'section', id: child.id, url: child.sectionurl});\n addCms(child);\n addChildItems(child.children);\n }\n }\n };\n sectionlist.forEach(sectionid => {\n const sectioninfo = state.section.get(sectionid);\n items.push({type: 'section', id: sectioninfo.id, url: sectioninfo.sectionurl});\n addCms(sectioninfo);\n addChildItems(sectioninfo.children);\n });\n return items;\n }\n}"],"names":["Exporter","section","state","sectioninfo","children","super","length","i","push","this","course","maxsectiondepth","allItemsArray","items","sectionlist","addCms","cmlist","forEach","cmid","cminfo","cm","get","type","id","url","addChildItems","child","sectionurl","sectionid"],"mappings":";;;;;;;wKAwB6BA,kBAUzBC,QAAQC,MAAOC,mBACLC,SAAWD,YAAYC,SACvBH,QAAUI,MAAMJ,QAAQC,MAAOC,gBACrCF,QAAQG,SAAW,GACfA,UAAYA,SAASE,WAChB,IAAIC,EAAI,EAAGA,EAAIH,SAASE,OAAQC,IACjCN,QAAQG,SAASI,KAAKC,KAAKR,QAAQC,MAAOE,SAASG,YAGpDN,QASXS,OAAOR,aACGQ,OAASL,MAAMK,OAAOR,cAC5BQ,OAAOC,gBAAkBT,MAAMQ,OAAOC,gBAC/BD,OASXE,cAAcV,uCACJW,MAAQ,GACRC,0CAAcZ,MAAMQ,OAAOI,mEAAe,GAE1CC,OAAUZ,mEACGA,YAAYa,0DAAU,IAC9BC,SAAQC,aACLC,OAASjB,MAAMkB,GAAGC,IAAIH,MAC5BL,MAAML,KAAK,CAACc,KAAM,KAAMC,GAAIJ,OAAOI,GAAIC,IAAKL,OAAOK,UAGrDC,cAAiBrB,cACfA,UAAYA,SAASE,WAChB,IAAIC,EAAI,EAAGA,EAAIH,SAASE,OAAQC,IAAK,OAChCmB,MAAQtB,SAASG,GACvBM,MAAML,KAAK,CAACc,KAAM,UAAWC,GAAIG,MAAMH,GAAIC,IAAKE,MAAMC,aACtDZ,OAAOW,OACPD,cAAcC,MAAMtB,mBAIhCU,YAAYG,SAAQW,kBACVzB,YAAcD,MAAMD,QAAQoB,IAAIO,WACtCf,MAAML,KAAK,CAACc,KAAM,UAAWC,GAAIpB,YAAYoB,GAAIC,IAAKrB,YAAYwB,aAClEZ,OAAOZ,aACPsB,cAActB,YAAYC,aAEvBS"} -------------------------------------------------------------------------------- /tests/behat/delete_section.feature: -------------------------------------------------------------------------------- 1 | @format @format_flexsections @javascript 2 | Feature: Deleting sections in flexsections format 3 | In order to organise the content in the course 4 | As a teacher 5 | I need to be able to delete sections 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | teacher1 | Terry | Teacher | teacher1@example.com | 11 | And the following "courses" exist: 12 | | fullname | shortname | format | numsections | 13 | | Course 1 | C1 | flexsections | 0 | 14 | And the following "course enrolments" exist: 15 | | user | course | role | 16 | | teacher1 | C1 | editingteacher | 17 | And I log in as "teacher1" 18 | And I am on "Course 1" course homepage 19 | And I should not see "Add section" 20 | And I turn editing mode on 21 | And I follow "Add section" 22 | And I should see "Topic 1" 23 | And I add an "Assignment" to section "1" in flexsections using the activity chooser 24 | And I set the following fields to these values: 25 | | Assignment name | First module | 26 | | Description | Test | 27 | And I press "Save and return to course" 28 | And I open section "1" edit menu 29 | And I click on "Add subsection" "link" in the "li#section-1" "css_element" 30 | And I add an "Forum" to section "2" in flexsections using the activity chooser 31 | And I set the following fields to these values: 32 | | Forum name | Second module | 33 | | Description | Test | 34 | And I press "Save and return to course" 35 | And I open section "1" edit menu 36 | And I click on "Add subsection" "link" in the "li#section-1" "css_element" 37 | And I add an "Forum" to section "3" in flexsections using the activity chooser 38 | And I set the following fields to these values: 39 | | Forum name | Third module | 40 | | Description | Test | 41 | And I press "Save and return to course" 42 | And I click on "Add section" "link" in the "li#section-1" "css_element" 43 | And I click on "Add section" "link" in the "li#section-4" "css_element" 44 | And I add an "Forum" to section "5" in flexsections using the activity chooser 45 | And I set the following fields to these values: 46 | | Forum name | Fourth module | 47 | | Description | Test | 48 | And I press "Save and return to course" 49 | And I should see "Topic 5" 50 | 51 | Scenario: Deleting the empty section in flexsections format 52 | When I open section "4" edit menu 53 | And I click on "Delete section" "link" in the "li#section-4" "css_element" 54 | Then I should not see "Topic 5" in the "region-main" "region" 55 | And I should see "Topic 4" in the "region-main" "region" 56 | And I should see "Fourth module" in the "region-main" "region" 57 | 58 | Scenario: Deleting the last section in flexsections format 59 | When I open section "5" edit menu 60 | And I click on "Delete section" "link" in the "li#section-5" "css_element" 61 | And I click on "Yes" "button" in the "Confirm" "dialogue" 62 | Then I should not see "Topic 5" in the "region-main" "region" 63 | And I should see "Topic 4" in the "region-main" "region" 64 | And I should not see "Fourth module" in the "region-main" "region" 65 | 66 | Scenario: Deleting the subsection in flexsections format 67 | When I open section "2" edit menu 68 | And I click on "Delete section" "link" in the "li#section-2" "css_element" 69 | And I click on "Yes" "button" in the "Confirm" "dialogue" 70 | Then I should not see "Topic 5" in the "region-main" "region" 71 | And I should see "Topic 4" in the "region-main" "region" 72 | And I should not see "Second module" in the "region-main" "region" 73 | And I should see "Fourth module" in the "region-main" "region" 74 | 75 | Scenario: Deleting the section with subsections in flexsections format 76 | When I open section "1" edit menu 77 | When I click on "Delete section" "link" in the "li#section-1" "css_element" 78 | And I click on "Yes" "button" in the "Confirm" "dialogue" 79 | Then I should not see "Topic 5" in the "region-main" "region" 80 | And I should not see "Topic 4" in the "region-main" "region" 81 | And I should not see "Topic 3" in the "region-main" "region" 82 | And I should see "Topic 2" in the "region-main" "region" 83 | And I should not see "First module" in the "region-main" "region" 84 | And I should not see "Second module" in the "region-main" "region" 85 | And I should not see "Third module" in the "region-main" "region" 86 | And I should see "Fourth module" in the "region-main" "region" 87 | -------------------------------------------------------------------------------- /templates/local/content/movesection_one.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/content/movesection_one 19 | 20 | Displays the target for section moving. 21 | 22 | Example context (json): 23 | { 24 | "title": "General", 25 | "id": 42, 26 | "number": 1, 27 | "sectionurl": "#", 28 | "indexcollapsed": 0, 29 | "testingonly-make-mustache-lint-happy": 1 30 | } 31 | 32 | }} 33 | {{#testingonly-make-mustache-lint-happy}} 34 | 109 | {{/testingonly-make-mustache-lint-happy}} 110 | -------------------------------------------------------------------------------- /lang/en/format_flexsections.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Strings for component Flexible sections course format. 19 | * 20 | * @package format_flexsections 21 | * @copyright 2022 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | $string['accordion'] = 'Accordion effect'; 26 | $string['accordiondesc'] = 'When one section is expanded, collapse all others.'; 27 | $string['addsection'] = 'Add section'; 28 | $string['addsections'] = 'Add section'; 29 | $string['addsubsection'] = 'Add subsection'; 30 | $string['backtocourse'] = 'Back to course \'{$a}\''; 31 | $string['backtosection'] = 'Back to \'{$a}\''; 32 | $string['cmbacklink'] = 'Display back link in activities'; 33 | $string['cmbacklinkdesc'] = 'Display link \'Back to ...\' allowing to return to the course section inside the section activities.'; 34 | $string['confirmdelete'] = 'Are you sure you want to delete this section? All activities and subsections will also be deleted'; 35 | $string['confirmmerge'] = 'Are you sure you want to merge this section content with the parent? All activities and subsections will be moved'; 36 | $string['courseindexdisplay'] = 'Display course index'; 37 | $string['courseindexdisplaydesc'] = 'Defines how to display the course index on the left side of the course page.'; 38 | $string['courseindexfull'] = 'Sections and activities'; 39 | $string['courseindexnone'] = 'Do not display'; 40 | $string['courseindexsections'] = 'Only sections'; 41 | $string['currentsection'] = 'This section'; 42 | $string['deletesection'] = 'Delete section'; 43 | $string['displaycontent'] = 'Display content'; 44 | $string['editsection'] = 'Edit section'; 45 | $string['editsectionname'] = 'Edit section name'; 46 | $string['errorsectiondepthexceeded'] = 'Subsection depth has exceeded configured value.'; 47 | $string['hidefromothers'] = 'Hide section'; 48 | $string['maxsectiondepth'] = 'Max subsection depth'; 49 | $string['maxsectiondepthdesc'] = 'Maximum number of subsection levels.'; 50 | $string['mergeup'] = 'Merge with parent'; 51 | $string['moveassubsection'] = 'As a subsection of \'{$a}\''; 52 | $string['movebeforecm'] = 'Before activity \'{$a}\''; 53 | $string['movebeforesection'] = 'Before \'{$a}\''; 54 | $string['movecmendofsection'] = 'To the end of section \'{$a}\''; 55 | $string['movecmsection'] = 'To the section \'{$a}\''; 56 | $string['moveendofsection'] = 'As the last subsection of \'{$a}\''; 57 | $string['movesectiontotheend'] = 'To the end'; 58 | $string['newsectionname'] = 'New name for section {$a}'; 59 | $string['page-course-view-flexsections'] = 'Any course main page in Flexible sections format'; 60 | $string['page-course-view-flexsections-x'] = 'Any course page in Flexible sections format'; 61 | $string['plugin_description'] = 'The course is divided into sections, each section can be displayed on the course page or as a link. Sections can have subsections.'; 62 | $string['pluginname'] = 'Flexible sections format'; 63 | $string['privacy:metadata'] = 'The Flexible sections format plugin does not store any personal data.'; 64 | $string['section0name'] = 'General'; 65 | $string['sectionname'] = 'Topic'; 66 | $string['showcollapsed'] = 'Display as a link'; 67 | $string['showexpanded'] = 'Display on the same page'; 68 | $string['showfromothers'] = 'Show section'; 69 | $string['showsection0title'] = 'Show top section title'; 70 | $string['showsection0title_help'] = 'When enabled, the general section will have a title and will be collapsible, same as it behaves in the Topics format.'; 71 | $string['showsection0titledefault'] = 'Show top section title by default'; 72 | $string['showsection0titledefaultdesc'] = 'This defines default setting that will be used for new and existing courses, it can be changed for individual courses in their settings.'; 73 | 74 | // Deprecated since 4.1. 75 | $string['addsubsectionfor'] = 'Add subsection for \'{$a}\''; 76 | $string['cancelmoving'] = 'Cancel moving \'{$a}\''; 77 | $string['removemarker'] = 'Do not mark as current'; 78 | $string['setmarker'] = 'Mark as current'; 79 | -------------------------------------------------------------------------------- /templates/local/content/section.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/content/section 19 | 20 | Displays a course section. 21 | 22 | Note: This template is a wrapper around the section/content template to allow course formats and theme designers to 23 | modify parts of the wrapper without having to copy/paste the entire template. 24 | 25 | Example context (json): 26 | { 27 | "num": 3, 28 | "id": 35, 29 | "controlmenu": "[tools menu]", 30 | "header": { 31 | "name": "Section title", 32 | "title": "Section title", 33 | "url": "#", 34 | "ishidden": true 35 | }, 36 | "cmlist": { 37 | "cms": [ 38 | { 39 | "cmitem": { 40 | "cmformat": { 41 | "cmname": {"displayvalue": "Forum example"}, 42 | "hasname": "true" 43 | }, 44 | "cmid": 3, 45 | "module": "forum", 46 | "anchor": "activity-3", 47 | "extraclasses": "newmessages" 48 | } 49 | }, 50 | { 51 | "cmitem": { 52 | "cmformat": { 53 | "cmname": {"displayvalue": "Another forum"}, 54 | "hasname": "true" 55 | }, 56 | "cmid": 4, 57 | "anchor": "activity-4", 58 | "module": "assign", 59 | "extraclasses": "" 60 | } 61 | } 62 | ], 63 | "hascms": true 64 | }, 65 | "ishidden": false, 66 | "iscurrent": true, 67 | "currentlink": "This topic", 68 | "availability": { 69 | "info": "Hidden from students", 70 | "hasavailability": true 71 | }, 72 | "summary": { 73 | "summarytext": "Summary text!" 74 | }, 75 | "controlmenu": { 76 | "menu": "Edit", 77 | "hasmenu": true 78 | }, 79 | "cmcontrols": "[Add an activity or resource]", 80 | "iscoursedisplaymultipage": true, 81 | "sectionreturnid": 0, 82 | "contentcollapsed": false, 83 | "insertafter": true, 84 | "numsections": 42, 85 | "sitehome": false, 86 | "highlightedlabel" : "Highlighted", 87 | "testingonly-make-mustache-lint-happy": 1 88 | } 89 | }} 90 | {{#testingonly-make-mustache-lint-happy}} 91 |
    92 | {{/testingonly-make-mustache-lint-happy}} 93 | 94 | 120 | 121 | {{#testingonly-make-mustache-lint-happy}} 122 |
123 | {{/testingonly-make-mustache-lint-happy}} 124 | -------------------------------------------------------------------------------- /tests/behat/indentation.feature: -------------------------------------------------------------------------------- 1 | @format @format_flexsections @javascript 2 | Feature: Course module indentation in Flexsections course format 3 | In order to reset indentation in course modules 4 | As a admin 5 | I want change indent value for all the modules of a course format courses in one go 6 | 7 | Background: 8 | Given the following "courses" exist: 9 | | fullname | shortname | format | 10 | | Flexsections Course 1 | T1 | flexsections | 11 | | Flexsections Course 2 | T2 | flexsections | 12 | And the following "activities" exist: 13 | | activity | name | intro | course | idnumber | 14 | | forum | Flexsections forum name | Flexsections forum description | T1 | forum1 | 15 | | data | Flexsections database name | Flexsections database description | T1 | data1 | 16 | | wiki | Flexsections wiki name | Flexsections wiki description | T2 | wiki1 | 17 | And I log in as "admin" 18 | And I am on "Flexsections Course 1" course homepage with editing mode on 19 | And I open "Flexsections forum name" actions menu 20 | And I click on "Move right" "link" in the "Flexsections forum name" activity 21 | And I open "Flexsections forum name" actions menu 22 | And "Move right" "link" in the "Flexsections forum name" "activity" should not be visible 23 | And "Move left" "link" in the "Flexsections forum name" "activity" should be visible 24 | And I press the escape key 25 | And I open "Flexsections database name" actions menu 26 | And "Move right" "link" in the "Flexsections database name" "activity" should be visible 27 | And "Move left" "link" in the "Flexsections database name" "activity" should not be visible 28 | And I am on "Flexsections Course 2" course homepage with editing mode on 29 | And I open "Flexsections wiki name" actions menu 30 | And I click on "Move right" "link" in the "Flexsections wiki name" activity 31 | And I open "Flexsections wiki name" actions menu 32 | And "Move right" "link" in the "Flexsections wiki name" "activity" should not be visible 33 | And "Move left" "link" in the "Flexsections wiki name" "activity" should be visible 34 | 35 | Scenario: Apply course indentation reset for Flexsections format 36 | Given I navigate to "Plugins > Course formats > Flexible sections format" in site administration 37 | And I wait "5" seconds 38 | And "Reset indentation" "link" should exist 39 | When I click on "Reset indentation" "link" 40 | And "Reset indentation" "button" should exist 41 | And I click on "Reset indentation" "button" 42 | Then I should see "Indentation reset" 43 | And I am on "Flexsections Course 1" course homepage with editing mode on 44 | And I open "Flexsections forum name" actions menu 45 | And "Move right" "link" in the "Flexsections forum name" "activity" should be visible 46 | And "Move left" "link" in the "Flexsections forum name" "activity" should not be visible 47 | And I press the escape key 48 | And I open "Flexsections database name" actions menu 49 | And "Move right" "link" in the "Flexsections database name" "activity" should be visible 50 | And "Move left" "link" in the "Flexsections database name" "activity" should not be visible 51 | And I am on "Flexsections Course 2" course homepage with editing mode on 52 | And I open "Flexsections wiki name" actions menu 53 | And "Move right" "link" in the "Flexsections wiki name" "activity" should be visible 54 | And "Move left" "link" in the "Flexsections wiki name" "activity" should not be visible 55 | 56 | Scenario: Cancel course indentation reset for Flexsections format 57 | Given I navigate to "Plugins > Course formats > Flexible sections format" in site administration 58 | And "Reset indentation" "link" should exist 59 | When I click on "Reset indentation" "link" 60 | And "Reset indentation" "button" should exist 61 | And "Cancel" "button" should exist 62 | And I click on "Cancel" "button" 63 | Then I should not see "Indentation reset" 64 | And I am on "Flexsections Course 1" course homepage with editing mode on 65 | And I open "Flexsections forum name" actions menu 66 | And "Move right" "link" in the "Flexsections forum name" "activity" should not be visible 67 | And "Move left" "link" in the "Flexsections forum name" "activity" should be visible 68 | And I press the escape key 69 | And I open "Flexsections database name" actions menu 70 | And "Move right" "link" in the "Flexsections database name" "activity" should be visible 71 | And "Move left" "link" in the "Flexsections database name" "activity" should not be visible 72 | And I am on "Flexsections Course 2" course homepage with editing mode on 73 | And I open "Flexsections wiki name" actions menu 74 | And "Move right" "link" in the "Flexsections wiki name" "activity" should not be visible 75 | And "Move left" "link" in the "Flexsections wiki name" "activity" should be visible 76 | -------------------------------------------------------------------------------- /templates/local/content.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/content 19 | 20 | Displays the complete course format. 21 | 22 | Example context (json): 23 | { 24 | "title": "Course", 25 | "initialsection": { 26 | "num": 0, 27 | "id": 34, 28 | "cmlist": { 29 | "cms": [ 30 | { 31 | "cmitem": { 32 | "cmformat": { 33 | "cmname": {"displayvalue": "Forum example"}, 34 | "hasname": "true" 35 | }, 36 | "cmid": 3, 37 | "anchor": "module-3", 38 | "module": "forum", 39 | "extraclasses": "newmessages" 40 | } 41 | } 42 | ], 43 | "hascms": true 44 | }, 45 | "iscurrent": true, 46 | "summary": { 47 | "summarytext": "Summary text!" 48 | } 49 | }, 50 | "sections": [ 51 | { 52 | "num": 1, 53 | "id": 35, 54 | "header": { 55 | "title": "Section title", 56 | "url": "#" 57 | }, 58 | "cmlist": { 59 | "cms": [ 60 | { 61 | "cmitem": { 62 | "cmformat": { 63 | "cmname": {"displayvalue": "Another forum"}, 64 | "hasname": "true" 65 | }, 66 | "cmid": 4, 67 | "anchor": "module-4", 68 | "module": "forum", 69 | "extraclasses": "newmessages" 70 | } 71 | } 72 | ], 73 | "hascms": true 74 | }, 75 | "iscurrent": true, 76 | "summary": { 77 | "summarytext": "Summary text!" 78 | } 79 | } 80 | ], 81 | "format": "flexsections", 82 | "sectionreturn": 1 83 | } 84 | }} 85 | 86 | {{> format_flexsections/local/navigate_back_to }} 87 | 88 |
90 |

{{{title}}}

91 | {{{completionhelp}}} 92 |
    93 | {{#initialsection}} 94 | {{> format_flexsections/local/content/section }} 95 | {{/initialsection}} 96 | {{#sections}} 97 | {{> format_flexsections/local/content/section }} 98 | {{/sections}} 99 |
100 | {{#hasnavigation}} 101 |
102 | {{#sectionnavigation}} 103 | {{> format_flexsections/local/content/section }} 104 | {{/sectionnavigation}} 105 |
    106 | {{#singlesection}} 107 | {{> format_flexsections/local/content/section }} 108 | {{/singlesection}} 109 |
110 | {{#sectionselector}} 111 | {{> format_flexsections/local/content/section }} 112 | {{/sectionselector}} 113 |
114 | {{/hasnavigation}} 115 | {{#bulkedittools}} 116 | {{$ core_courseformat/local/content/bulkedittools}} 117 | {{> core_courseformat/local/content/bulkedittools}} 118 | {{/ core_courseformat/local/content/bulkedittools}} 119 | {{/bulkedittools}} 120 |
121 | {{#js}} 122 | require(['format_flexsections/local/content'], function(component) { 123 | component.init('{{uniqid}}-course-format', {}, {{sectionreturn}}); 124 | }); 125 | {{/js}} 126 | -------------------------------------------------------------------------------- /templates/local/content/movecm_one.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/content/movecm_one 19 | 20 | Displays the course index. 21 | 22 | Example context (json): 23 | { 24 | "title": "General", 25 | "id": 42, 26 | "number": 1, 27 | "sectionurl": "#", 28 | "hascms": true, 29 | "cms": [ 30 | { 31 | "name": "Glossary of characters", 32 | "id": "10", 33 | "url": "#" 34 | }, 35 | { 36 | "name": "World Cinema forum", 37 | "id": "11", 38 | "url": "#" 39 | }, 40 | { 41 | "name": "Announcements", 42 | "id": "12", 43 | "url": "#" 44 | } 45 | ], 46 | "testingonly-make-mustache-lint-happy": 1 47 | } 48 | }} 49 | {{#testingonly-make-mustache-lint-happy}} 50 |
51 | {{/testingonly-make-mustache-lint-happy}} 52 | 53 | 119 | 120 | {{#testingonly-make-mustache-lint-happy}} 121 |
122 | {{/testingonly-make-mustache-lint-happy}} 123 | -------------------------------------------------------------------------------- /.github/workflows/moodle.yml: -------------------------------------------------------------------------------- 1 | name: Moodle Plugin CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-22.04 8 | 9 | services: 10 | postgres: 11 | image: postgres:15 12 | env: 13 | POSTGRES_USER: 'postgres' 14 | POSTGRES_HOST_AUTH_METHOD: 'trust' 15 | ports: 16 | - 5432:5432 17 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 18 | 19 | mariadb: 20 | image: mariadb:10 21 | env: 22 | MYSQL_USER: 'root' 23 | MYSQL_ALLOW_EMPTY_PASSWORD: "true" 24 | MYSQL_CHARACTER_SET_SERVER: "utf8mb4" 25 | MYSQL_COLLATION_SERVER: "utf8mb4_unicode_ci" 26 | ports: 27 | - 3306:3306 28 | options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | include: 34 | - php: '8.4' 35 | moodle-branch: 'MOODLE_500_STABLE' 36 | database: 'mariadb' 37 | - php: '8.2' 38 | moodle-branch: 'MOODLE_500_STABLE' 39 | database: 'pgsql' 40 | - php: '8.4' 41 | # Main job. Run all checks that do not require setup and only need to be run once. 42 | runchecks: 'all' 43 | moodle-branch: 'MOODLE_501_STABLE' 44 | database: 'pgsql' 45 | - php: '8.2' 46 | moodle-branch: 'MOODLE_501_STABLE' 47 | database: 'mariadb' 48 | 49 | steps: 50 | - name: Check out repository code 51 | uses: actions/checkout@v4 52 | with: 53 | path: plugin 54 | 55 | - name: Setup PHP ${{ matrix.php }} 56 | uses: shivammathur/setup-php@v2 57 | with: 58 | php-version: ${{ matrix.php }} 59 | extensions: ${{ matrix.extensions }} 60 | ini-values: max_input_vars=5000 61 | # If you are not using code coverage, keep "none". Otherwise, use "pcov" (Moodle 3.10 and up) or "xdebug". 62 | # If you try to use code coverage with "none", it will fallback to phpdbg (which has known problems). 63 | coverage: none 64 | 65 | - name: Initialise moodle-plugin-ci 66 | run: | 67 | composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4 68 | echo $(cd ci/bin; pwd) >> $GITHUB_PATH 69 | echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH 70 | sudo locale-gen en_AU.UTF-8 71 | echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV 72 | 73 | - name: Install moodle-plugin-ci 74 | run: moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1 75 | env: 76 | DB: ${{ matrix.database }} 77 | MOODLE_BRANCH: ${{ matrix.moodle-branch }} 78 | # Uncomment this to run Behat tests using the Moodle App. 79 | # MOODLE_APP: 'true' 80 | 81 | - name: PHP Lint 82 | if: ${{ !cancelled() && matrix.runchecks == 'all' }} 83 | run: moodle-plugin-ci phplint 84 | 85 | - name: PHP Mess Detector 86 | continue-on-error: true # This step will show errors but will not fail 87 | if: ${{ !cancelled() && matrix.runchecks == 'all' }} 88 | run: moodle-plugin-ci phpmd 89 | 90 | - name: Moodle Code Checker 91 | if: ${{ !cancelled() && matrix.runchecks == 'all' }} 92 | run: moodle-plugin-ci phpcs --max-warnings 0 93 | 94 | - name: Moodle PHPDoc Checker 95 | if: ${{ !cancelled() && matrix.runchecks == 'all' }} 96 | run: moodle-plugin-ci phpdoc --max-warnings 0 97 | 98 | - name: Validating 99 | if: ${{ !cancelled() }} 100 | run: moodle-plugin-ci validate 101 | 102 | - name: Check upgrade savepoints 103 | if: ${{ !cancelled() && matrix.runchecks == 'all' }} 104 | run: moodle-plugin-ci savepoints 105 | 106 | - name: Mustache Lint 107 | if: ${{ !cancelled() && matrix.runchecks == 'all' }} 108 | run: moodle-plugin-ci mustache 109 | 110 | - name: Grunt 111 | if: ${{ !cancelled() && matrix.runchecks == 'all' }} 112 | run: moodle-plugin-ci grunt --max-lint-warnings 0 113 | 114 | - name: PHPUnit tests 115 | if: ${{ !cancelled() }} 116 | run: moodle-plugin-ci phpunit --fail-on-warning 117 | 118 | - name: Behat features 119 | id: behat 120 | if: ${{ !cancelled() }} 121 | run: moodle-plugin-ci behat --profile chrome 122 | 123 | - name: Upload Behat Faildump 124 | if: ${{ failure() && steps.behat.outcome == 'failure' }} 125 | uses: actions/upload-artifact@v4 126 | with: 127 | name: Behat Faildump (${{ join(matrix.*, ', ') }}) 128 | path: ${{ github.workspace }}/moodledata/behat_dump 129 | retention-days: 7 130 | if-no-files-found: ignore 131 | 132 | - name: Mark cancelled jobs as failed. 133 | if: ${{ cancelled() }} 134 | run: exit 1 135 | -------------------------------------------------------------------------------- /classes/output/courseformat/content.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\output\courseformat; 18 | 19 | use core_courseformat\external\get_state; 20 | use course_modinfo; 21 | use stdClass; 22 | 23 | /** 24 | * Render a course content. 25 | * 26 | * @package format_flexsections 27 | * @copyright 2022 Marina Glancy 28 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 29 | */ 30 | class content extends \core_courseformat\output\local\content { 31 | 32 | /** @var \format_flexsections the course format class */ 33 | protected $format; 34 | 35 | /** @var bool Flexsections format has add section. */ 36 | protected $hasaddsection = true; 37 | 38 | /** 39 | * Template name for this exporter 40 | * 41 | * @param \renderer_base $renderer 42 | * @return string 43 | */ 44 | public function get_template_name(\renderer_base $renderer): string { 45 | // Mdlcode uses: template 'format_flexsections/local/content'. 46 | return 'format_flexsections/local/content'; 47 | } 48 | 49 | /** 50 | * Export this data so it can be used as the context for a mustache template (core/inplace_editable). 51 | * 52 | * @param \renderer_base $output typically, the renderer that's calling this function 53 | * @return \stdClass data context for a mustache template 54 | */ 55 | public function export_for_template(\renderer_base $output) { 56 | $data = parent::export_for_template($output); 57 | 58 | // If we are on course view page for particular section. 59 | if ($this->format->get_viewed_section()) { 60 | // Do not display the "General" section when on a page of another section. 61 | $data->initialsection = null; 62 | 63 | // Add 'back to parent' control. 64 | $section = $this->format->get_section($this->format->get_viewed_section()); 65 | if ($section->parent) { 66 | $sr = $this->format->find_collapsed_parent($section->parent); 67 | $url = $this->format->get_view_url($section->section, ['sr' => $sr]); 68 | $data->backtosection = [ 69 | 'url' => $url->out(false), 70 | 'sectionname' => $this->format->get_section_name($section->parent), 71 | ]; 72 | } else { 73 | $sr = 0; 74 | $url = $this->format->get_view_url($section->section, ['sr' => $sr]); 75 | $context = \context_course::instance($this->format->get_courseid()); 76 | $data->backtocourse = [ 77 | 'url' => $url->out(false), 78 | 'coursename' => format_string($this->format->get_course()->fullname, true, ['context' => $context]), 79 | ]; 80 | } 81 | 82 | // Hide add section link below page content. 83 | $data->numsections = false; 84 | } 85 | $data->accordion = $this->format->get_accordion_setting() ? 1 : ''; 86 | $data->mainsection = $this->format->get_viewed_section(); 87 | 88 | return $data; 89 | } 90 | 91 | /** 92 | * Return an array of sections to display. 93 | * 94 | * This method is used to differentiate between display a specific section 95 | * or a list of them. 96 | * 97 | * @param course_modinfo $modinfo the current course modinfo object 98 | * @return \section_info[] an array of section_info to display 99 | */ 100 | protected function get_sections_to_display(course_modinfo $modinfo): array { 101 | $singlesectionid = $this->format->get_sectionid(); 102 | if ($singlesectionid) { 103 | return [ 104 | $modinfo->get_section_info_by_id($singlesectionid), 105 | ]; 106 | } 107 | 108 | $viewedsection = $this->format->get_viewed_section(); 109 | return array_values(array_filter($modinfo->get_section_info_all(), function($s) use ($viewedsection) { 110 | if ($s->is_delegated()) { 111 | return false; 112 | } 113 | return (!$s->section) || 114 | (!$viewedsection && !$s->parent && $this->format->is_section_visible($s)) || 115 | ($viewedsection && $s->section == $viewedsection); 116 | })); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [5.0.1] - 2025-10-07 5 | ### Changed 6 | - Added tests to ensure compatibility with Moodle 5.1 7 | 8 | ## [5.0] - 2025-05-10 9 | ### Added 10 | - Support for Moodle 5.0 11 | 12 | ## [4.1.4] - 2025-05-10 13 | ### Added 14 | - Bulk edit of activity modules (except for "Move" aciton) #103 #67 15 | ### Fixed 16 | - (4.4+) Item 'Permalink' in the course section edit menu displays the copy-to-clipboard popup 17 | and allows to copy the link to the section #95 18 | - Section 'Permalink' contain section ids rather than section numbers (persistent after 19 | moving sections around) 20 | - (4.4+) Removed the 'View' item from the course section edit menu added by core, it conflicts the 21 | functionality of the format_flexsections plugin "Display as a link". 22 | - (4.5+) Do not display a link to add subsection (mod_subsection) as it is confusing with the 23 | flexible sections subsections. 24 | - (4.5+) If the subsections (mod_subsection) already exist in the course (i.e. they were added 25 | before the course format was changed to flexible sections), display them correctly and 26 | allow to delete them. 27 | - Use lock when deleting sections to avoid course corruption #82 28 | 29 | ## [4.1.3] - 2024-12-08 30 | ### Fixed 31 | - Fixed exception: Call to undefined function format_flexsections_add_back_link_to_cm #99, #101 32 | 33 | ## [4.1.2] - 2024-10-06 34 | ### Fixed 35 | - When a section used to be displayed on a course page and is now displayed as a link, 36 | the old student preferences about collapsed state affect the visibility of 37 | the section summary #68 38 | - Coding error when trying to collapse a large number of sections #88 39 | (workaround for the core bug MDL-78073) 40 | ### Added 41 | - Support for Moodle 4.5 42 | 43 | ## [4.1.1] - 2024-10-02 44 | ### Fixed 45 | - When section has availability restriction and the restriction is displayed, all subsections 46 | and activities in the subsections should not be available. 47 | This also fixes the exception that made course index disappear for students #89 48 | - Coding style fixes for upcoming 4.5 release 49 | 50 | ## [4.1.0] - 2024-05-22 51 | ### Added 52 | - Support for Moodle 4.4, #83 53 | - Ability to duplicate sections, #69 54 | ### Fixed 55 | - Fixed fatal PHP error on Moodle 4.3 introduced by changes in MDL-81610 56 | 57 | ## [4.0.6] - 2023-12-23 58 | ### Added 59 | - Adjusted automatic tests for Moodle 4.3 60 | ### Fixed 61 | - #71 - 'Back to' link does not display ampersand (&) correcty 62 | 63 | ## [4.0.5] - 2023-08-08 64 | ### Fixed 65 | - When scrolling the page, the course index now correctly highlights subsections. 66 | Thanks to [luttje](https://github.com/luttje) for the patch submitted in 67 | https://github.com/marinaglancy/moodle-format_flexsections/issues/47 . 68 | - Fixed unexpected scrolling when expanding a section in accordion mode. 69 | ### Added 70 | - Course level settings for 'accordion' effect, 'Back to...' link and how to 71 | show the course index. (In addition to site-level settings added in v4.0.4) 72 | - In accordion mode clicking on the section in the course index will expand 73 | it in the course contents. 74 | 75 | ## [4.0.4] - 2023-07-02 76 | ### Added 77 | - Setting to show header for the General section and make it collapsible 78 | - Link 'Add section' between sections (only for the top-level sections on the 79 | current page) 80 | - Setting how to show the course index (sections and activities, only sections, 81 | do not display) 82 | - Setting to enable 'accordion' effect - when one section is expanded, all others 83 | are collapsed 84 | - Setting to display 'Back to...' link inside the activities allowing to return 85 | back to the course section 86 | - Setting 'maxsections' now only affects the number of top-level sections. Number 87 | of subsections is unlimited (there is also a setting about the maximum depth). 88 | ### Fixed 89 | - Fixed a bug when drag&drop of activities was not possible if the target 90 | section is empty 91 | 92 | Release contributor: **Hogeschool Inholland, the Netherlands**. 93 | 94 | ## [4.0.3] - 2023-05-06 95 | ### Added 96 | - Allow to indent activities on the course page 97 | - Added automated tests on Moodle 4.2 98 | - Subsection depth limiting course format setting. Setting maximum number of 99 | subsection levels will restrict ability of user to create sections at levels 100 | deeper than configured. The setting does not affect existing course subsections 101 | layout. 102 | ### Fixed 103 | - Fixed a bug causing section not to be moved to the correct location in some cases. 104 | See https://github.com/marinaglancy/moodle-format_flexsections/issues/37 105 | - Trigger event when section is deleted 106 | 107 | ## [4.0.2] - 2023-04-17 108 | ### Changed 109 | - Course index always shows all sections and activities regardless of the current page. More details 110 | https://github.com/marinaglancy/moodle-format_flexsections/issues/39 111 | - Added automated tests on Moodle 4.1 112 | 113 | ## [4.0.1] - 2022-06-19 114 | ### Added 115 | - Course format "Flexible sections" now has UI in-line with the Moodle LMS 4.0 course formats. It supports AJAX editing of activities and sections and course index. 116 | -------------------------------------------------------------------------------- /templates/local/courseindex/section.mustache: -------------------------------------------------------------------------------- 1 | {{! 2 | This file is part of Moodle - http://moodle.org/ 3 | 4 | Moodle is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Moodle is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Moodle. If not, see . 16 | }} 17 | {{! 18 | @template format_flexsections/local/courseindex/section 19 | 20 | Displays a course index section entry. 21 | 22 | Example context (json): 23 | { 24 | "title": "General", 25 | "id": 23, 26 | "uniqid": "0", 27 | "number": 1, 28 | "sectionurl": "#", 29 | "indexcollapsed": 0, 30 | "current": 1, 31 | "visible": 1, 32 | "hasrestrictions": 0, 33 | "cms": [ 34 | { 35 | "id": 10, 36 | "name": "Glossary of characters", 37 | "url": "#", 38 | "visible": 1, 39 | "isactive": 0 40 | }, 41 | { 42 | "id": 11, 43 | "name": "World Cinema forum", 44 | "url": "#", 45 | "visible": 1, 46 | "isactive": 0 47 | }, 48 | { 49 | "id": 12, 50 | "name": "Announcements", 51 | "url": "#", 52 | "visible": 0, 53 | "isactive": 1 54 | } 55 | ], 56 | "testingonly-make-mustache-lint-happy": true 57 | } 58 | }} 59 | 60 | {{#testingonly-make-mustache-lint-happy}} 61 |
62 | {{/testingonly-make-mustache-lint-happy}} 63 | 64 |
73 | 115 |
119 | {{^hidecmsinindex}} 120 |
    121 | {{#cms}} 122 | {{> core_courseformat/local/courseindex/cm }} 123 | {{/cms}} 124 |
125 | {{/hidecmsinindex}} 126 |
127 | {{#children}} 128 | {{> format_flexsections/local/courseindex/section }} 129 | {{/children}} 130 |
131 |
132 |
133 | 134 | {{#testingonly-make-mustache-lint-happy}} 135 |
136 | {{/testingonly-make-mustache-lint-happy}} 137 | 138 | {{#js}} 139 | require(['format_flexsections/local/courseindex/section'], function(component) { 140 | component.init('{{uniqid}}-course-index-section-{{id}}'); 141 | }); 142 | {{/js}} 143 | -------------------------------------------------------------------------------- /classes/local/helpers/preferences.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\local\helpers; 18 | 19 | use cache; 20 | use core_text; 21 | 22 | /** 23 | * Helps to store and retrieve large user preferences 24 | * 25 | * @package format_flexsections 26 | * @copyright Marina Glancy 27 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 28 | */ 29 | trait preferences { 30 | 31 | /** 32 | * Name for the part of the preference 33 | * 34 | * @param string $name 35 | * @param int $cnt 36 | * @return string 37 | */ 38 | protected static function get_preference_name(string $name, int $cnt = 0): string { 39 | return $cnt ? "{$name}#{$cnt}" : $name; 40 | } 41 | 42 | /** 43 | * Set a preference for the user (value can be longer than 1333) 44 | * 45 | * @param string $name 46 | * @param string|null $value 47 | * @return void 48 | */ 49 | public static function set_long_preference(string $name, ?string $value): void { 50 | $allpreferences = array_filter(get_user_preferences(), function($prefname) use ($name) { 51 | return $prefname === $name || (strpos($prefname, "{$name}#") === 0); 52 | }, ARRAY_FILTER_USE_KEY); 53 | $len = ceil(core_text::strlen((string)$value) / 1300); 54 | for ($cnt = 0; $cnt < $len; $cnt++) { 55 | $pref = self::get_preference_name($name, $cnt); 56 | set_user_preference($pref, core_text::substr($value, $cnt * 1300, 1300)); 57 | unset($allpreferences[$pref]); 58 | } 59 | foreach (array_keys($allpreferences) as $pref) { 60 | unset_user_preference($pref); 61 | } 62 | } 63 | 64 | /** 65 | * Get a preference for the user (value can be longer than 1333) 66 | * 67 | * @param string $name 68 | * @return string 69 | */ 70 | public static function get_long_preference(string $name): string { 71 | $allpreferences = array_filter(get_user_preferences(), function($prefname) use ($name) { 72 | return $prefname === $name || (strpos($prefname, "{$name}#") === 0); 73 | }, ARRAY_FILTER_USE_KEY); 74 | $value = ''; 75 | for ($cnt = 0; $cnt < count($allpreferences); $cnt++) { 76 | $value .= $allpreferences[self::get_preference_name($name, $cnt)] ?? ''; 77 | } 78 | return $value; 79 | } 80 | 81 | /** 82 | * Return the format section preferences. 83 | * 84 | * @return array of preferences indexed by preference name 85 | */ 86 | public function get_sections_preferences_by_preference(): array { 87 | $course = $this->get_course(); 88 | try { 89 | $sectionpreferences = json_decode( 90 | self::get_long_preference("coursesectionspreferences_{$course->id}"), 91 | true, 92 | ) ?: []; 93 | } catch (\Throwable $e) { 94 | $sectionpreferences = []; 95 | } 96 | return $sectionpreferences; 97 | } 98 | 99 | /** 100 | * Return the format section preferences. 101 | * 102 | * @param string $preferencename preference name 103 | * @param int[] $sectionids affected section ids 104 | * 105 | */ 106 | public function set_sections_preference(string $preferencename, array $sectionids) { 107 | $sectionpreferences = $this->get_sections_preferences_by_preference(); 108 | $sectionpreferences[$preferencename] = $sectionids; 109 | $this->persist_to_user_preference($sectionpreferences); 110 | } 111 | 112 | /** 113 | * Add section preference ids. 114 | * 115 | * @param string $preferencename preference name 116 | * @param array $sectionids affected section ids 117 | */ 118 | public function add_section_preference_ids(string $preferencename, array $sectionids): void { 119 | $sectionpreferences = $this->get_sections_preferences_by_preference(); 120 | if (!isset($sectionpreferences[$preferencename])) { 121 | $sectionpreferences[$preferencename] = []; 122 | } 123 | foreach ($sectionids as $sectionid) { 124 | if (!in_array($sectionid, $sectionpreferences[$preferencename])) { 125 | $sectionpreferences[$preferencename][] = $sectionid; 126 | } 127 | } 128 | $this->persist_to_user_preference($sectionpreferences); 129 | } 130 | 131 | /** 132 | * Remove section preference ids. 133 | * 134 | * @param string $preferencename preference name 135 | * @param array $sectionids affected section ids 136 | */ 137 | public function remove_section_preference_ids(string $preferencename, array $sectionids): void { 138 | $sectionpreferences = $this->get_sections_preferences_by_preference(); 139 | if (!isset($sectionpreferences[$preferencename])) { 140 | $sectionpreferences[$preferencename] = []; 141 | } 142 | foreach ($sectionids as $sectionid) { 143 | if (($key = array_search($sectionid, $sectionpreferences[$preferencename])) !== false) { 144 | unset($sectionpreferences[$preferencename][$key]); 145 | } 146 | } 147 | $this->persist_to_user_preference($sectionpreferences); 148 | } 149 | 150 | /** 151 | * Persist the section preferences to the user preferences. 152 | * 153 | * @param array $sectionpreferences the section preferences 154 | */ 155 | private function persist_to_user_preference(array $sectionpreferences): void { 156 | $course = $this->get_course(); 157 | self::set_long_preference('coursesectionspreferences_' . $course->id, json_encode($sectionpreferences)); 158 | // Invalidate section preferences cache. 159 | $coursesectionscache = cache::make('core', 'coursesectionspreferences'); 160 | $coursesectionscache->delete($course->id); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /tests/behat/add_sections.feature: -------------------------------------------------------------------------------- 1 | @format @format_flexsections @javascript 2 | Feature: Adding sections in flexsections format 3 | 4 | Background: 5 | Given the following "courses" exist: 6 | | fullname | shortname | format | numsections | 7 | | Course 1 | C1 | flexsections | 0 | 8 | And the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | student | Sam | Student | student@example.com | 11 | | teacher | Tom | Teacher | teacher@example.com | 12 | And the following "course enrolments" exist: 13 | | user | course | role | 14 | | student | C1 | student | 15 | | teacher | C1 | editingteacher | 16 | And the following "format_flexsections > sections" exist: 17 | | name | course | parent | collapsed | 18 | | t100 | C1 | | 0 | 19 | | t200 | C1 | | 0 | 20 | | t300 | C1 | | 1 | 21 | | t110 | C1 | t100 | 0 | 22 | | t120 | C1 | t100 | 0 | 23 | | t121 | C1 | t120 | 0 | 24 | | t210 | C1 | t200 | 0 | 25 | | t211 | C1 | t210 | 0 | 26 | | t111 | C1 | t110 | 0 | 27 | | t220 | C1 | t200 | 0 | 28 | | t310 | C1 | t300 | 0 | 29 | | t320 | C1 | t300 | 0 | 30 | | t311 | C1 | t310 | 0 | 31 | | t312 | C1 | t310 | 0 | 32 | | t321 | C1 | t320 | 0 | 33 | And the following "activities" exist: 34 | | activity | course | idnumber | name | section | 35 | | page | C1 | p1 | Page in General section | 0 | 36 | | page | C1 | p2 | Page in first section | 1 | 37 | | page | C1 | p3 | Page in t300 section | 10 | 38 | | page | C1 | p4 | Page in t310 section | 11 | 39 | 40 | Scenario: Adding section after the general section on the main course page 41 | When I log in as "teacher" 42 | And I am on the "C1" course page 43 | And I turn editing mode on 44 | And I click on "Add section" "link" in the "li#section-0" "css_element" 45 | Then "Topic 1" "section" should appear after "Page in General section" "text" in the "region-main" "region" 46 | And "t100" "section" should appear after "Topic 1" "section" in the "region-main" "region" 47 | And I click on "Collapse all" "link" in the "region-main" "region" 48 | And I click on "Add section" "link" in the "Topic 1" "section" 49 | And "Topic 2" "section" should appear after "Topic 1" "section" 50 | And "t100" "section" should appear after "Topic 2" "section" 51 | 52 | Scenario: Adding section between the sections on the main course page 53 | When I log in as "teacher" 54 | And I am on the "C1" course page 55 | And I turn editing mode on 56 | And I click on "Add section" "link" in the "t100" "section" 57 | Then "Topic 6" "section" should appear after "t100" "section" 58 | And "Topic 6" "section" should appear after "t110" "section" 59 | And "t200" "section" should appear after "Topic 6" "section" 60 | And I click on "Collapse all" "link" in the "region-main" "region" 61 | And I click on "Add section" "link" in the "Topic 6" "section" 62 | And "Topic 7" "section" should appear after "Topic 6" "section" 63 | And "t200" "section" should appear after "Topic 7" "section" 64 | 65 | Scenario: Adding section as the first subsection 66 | When I log in as "teacher" 67 | And I am on the "C1" course page 68 | And I turn editing mode on 69 | And I click on "t300" "link" in the "region-main" "region" 70 | And I click on "Add section" "link" in the "li#section-10" "css_element" 71 | Then "Topic 11" "text" should appear after "Page in t300 section" "text" in the "region-main" "region" 72 | And "t310" "text" should appear after "Topic 11" "text" in the "region-main" "region" 73 | And I click on "Collapse all" "link" in the "region-main" "region" 74 | And I click on "Add section" "link" in the "li#section-11" "css_element" 75 | And "Topic 12" "text" should appear after "Topic 11" "text" 76 | And "t310" "text" should appear after "Topic 12" "text" 77 | 78 | Scenario: Adding section between the sections on the subsection page 79 | When I log in as "teacher" 80 | And I am on the "C1" course page 81 | And I turn editing mode on 82 | And I click on "t300" "link" in the "region-main" "region" 83 | And I click on "Add section" "link" in the "li#section-11" "css_element" 84 | Then "Topic 14" "text" should appear after "t312" "text" 85 | And "t320" "section" should appear after "Topic 14" "text" 86 | And I click on "Collapse all" "link" in the "region-main" "region" 87 | And I click on "Add section" "link" in the "li#section-14" "css_element" 88 | And "Topic 15" "text" should appear after "Topic 14" "text" 89 | And "t320" "text" should appear after "Topic 15" "text" 90 | 91 | Scenario: Respecting maxsections when adding sections 92 | Given the following config values are set as admin: 93 | | maxsections | 4 | moodlecourse | 94 | When I log in as "teacher" 95 | And I am on the "C1" course page 96 | And I turn editing mode on 97 | And I click on "Add section" "link" in the "li#section-10" "css_element" 98 | And "Topic 16" "text" should appear after "t300" "text" in the "region-main" "region" 99 | # Adding another section would show a warning but we can't test it in behat yet 100 | # We can add subsections though 101 | And I open section "1" edit menu 102 | And I click on "Add subsection" "link" in the "li#section-1" "css_element" 103 | And "Topic 6" "text" should appear after "t121" "text" in the "region-main" "region" 104 | And "Topic 6" "text" should appear before "t200" "text" in the "region-main" "region" 105 | And I am on the "C1" course page 106 | And I click on "t300" "link" in the "region-main" "region" 107 | # Any amount of subsections can be added here 108 | And I follow "Add section" 109 | And "Topic 12" "text" should appear after "Page in t300 section" "text" in the "region-main" "region" 110 | And "t310" "text" should appear after "Topic 12" "text" in the "region-main" "region" 111 | And I click on "Add section" "link" in the "li#section-13" "css_element" 112 | And "Topic 16" "text" should appear after "t312" "text" in the "region-main" "region" 113 | And "t320" "text" should appear after "Topic 16" "text" in the "region-main" "region" 114 | -------------------------------------------------------------------------------- /classes/output/courseformat/content/section.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace format_flexsections\output\courseformat\content; 18 | 19 | use stdClass; 20 | 21 | /** 22 | * Contains the section controls output class. 23 | * 24 | * @package format_flexsections 25 | * @copyright 2022 Marina Glancy 26 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 | */ 28 | class section extends \core_courseformat\output\local\content\section { 29 | 30 | /** @var \format_flexsections the course format */ 31 | protected $format; 32 | 33 | /** @var int subsection level */ 34 | protected $level = 1; 35 | 36 | /** 37 | * Template name 38 | * 39 | * @param \renderer_base $renderer 40 | * @return string 41 | */ 42 | public function get_template_name(\renderer_base $renderer): string { 43 | return 'format_flexsections/local/content/section'; 44 | } 45 | 46 | /** 47 | * Data exporter 48 | * 49 | * @param \renderer_base $output 50 | * @return stdClass 51 | */ 52 | public function export_for_template(\renderer_base $output): stdClass { 53 | $format = $this->format; 54 | $course = $format->get_course(); 55 | 56 | $data = parent::export_for_template($output); 57 | 58 | // For sections that are displayed as a link do not print list of cms or controls. 59 | $showaslink = $this->section->collapsed == FORMAT_FLEXSECTIONS_COLLAPSED 60 | && $this->format->get_viewed_section() != $this->section->section; 61 | 62 | $data->showaslink = $showaslink; 63 | if ($showaslink) { 64 | $data->cmlist = []; 65 | $data->cmcontrols = ''; 66 | } 67 | 68 | // Add subsections. 69 | if (!$showaslink) { 70 | $data->subsections = $this->section->section ? $this->get_subsections($output) : []; 71 | $data->level = $this->level; 72 | } 73 | 74 | if ((!$course->showsection0title && $this->section->section === 0) || 75 | ($this->section->section !== 0 && $this->section->section === $this->format->get_viewed_section())) { 76 | // Never collapse content of top section in single section view or 77 | // when showing title of the top section is not shown. 78 | $data->contentcollapsed = false; 79 | } 80 | 81 | if ($this->section->section === 0 || $this->section->section === $this->format->get_viewed_section()) { 82 | // Show collapse/expand all menu at top section header. 83 | $data->collapsemenu = true; 84 | } else { 85 | $data->collapsemenu = false; 86 | } 87 | 88 | $data->addsectionafter = false; 89 | $data->insertsubsection = false; 90 | if ($this->format->should_display_add_sub_section_link($this->section->parent) 91 | && ($this->section->section != $this->format->get_viewed_section() || $this->section->section === 0)) { 92 | // Display 'Add section' button after to insert after this section. 93 | $data->addsectionafter = $this->export_add_section($output); 94 | } 95 | if ($this->section->section && $this->format->should_display_add_sub_section_link($this->section->section)) { 96 | // Display 'Add section' button to insert a section as a first direct child of this section. 97 | $data->insertsubsection = $this->export_add_section($output, $this->section->id); 98 | } 99 | 100 | return $data; 101 | } 102 | 103 | /** 104 | * Exporter for the 'Add section' link 105 | * 106 | * @param \renderer_base $output 107 | * @param int $parentid 108 | * @return stdClass 109 | */ 110 | protected function export_add_section(\renderer_base $output, int $parentid = 0): stdClass { 111 | $addsectionclass = $this->format->get_output_classname('content\\addsection'); 112 | /** @var \core_courseformat\output\local\content\addsection $addsection */ 113 | $addsection = new $addsectionclass($this->format); 114 | $data = $addsection->export_for_template($output); 115 | $data->insertparentid = $parentid; 116 | return $data; 117 | } 118 | 119 | /** 120 | * Subsections (recursive) 121 | * 122 | * @param \renderer_base $output 123 | * @return array 124 | */ 125 | protected function get_subsections(\renderer_base $output): array { 126 | $modinfo = $this->format->get_modinfo(); 127 | $data = []; 128 | foreach ($modinfo->get_section_info_all() as $section) { 129 | if ($section->parent == $this->section->section) { 130 | if ($this->format->is_section_visible($section)) { 131 | $instance = new static($this->format, $section); 132 | $instance->level++; 133 | $d = (array)($instance->export_for_template($output)) + 134 | $this->default_section_properties(); 135 | $data[] = (object)$d; 136 | } 137 | } 138 | } 139 | return $data; 140 | } 141 | 142 | /** 143 | * Since we display sections nested the values from the parent can propagate in templates 144 | * 145 | * @return array 146 | */ 147 | protected function default_section_properties(): array { 148 | return [ 149 | 'collapsemenu' => false, 'summary' => [], 150 | 'insertafter' => false, 'numsections' => false, 151 | 'availability' => [], 'restrictionlock' => false, 'hasavailability' => false, 152 | 'isstealth' => false, 'ishidden' => false, 'notavailable' => false, 'hiddenfromstudents' => false, 153 | 'controlmenu' => [], 'cmcontrols' => '', 154 | 'singleheader' => [], 'header' => [], 155 | 'cmsummary' => [], 'onlysummary' => false, 'cmlist' => [], 156 | ]; 157 | } 158 | } 159 | --------------------------------------------------------------------------------