From 18c6b9c0da22e9c67190290ed560027976774416 Mon Sep 17 00:00:00 2001 From: ferran Date: Thu, 24 Apr 2025 11:16:10 +0200 Subject: [PATCH 1/6] MDL-84291 course: remove max section limits on user actions --- .upgradenotes/MDL-84291-2025051308320419.yml | 8 +++++ course/changenumsections.php | 13 -------- .../local/service/content_item_service.php | 5 +-- .../output/local/content/addsection.php | 33 +++++++++---------- course/format/classes/stateactions.php | 9 ----- course/modedit.php | 9 ----- 6 files changed, 24 insertions(+), 53 deletions(-) create mode 100644 .upgradenotes/MDL-84291-2025051308320419.yml diff --git a/.upgradenotes/MDL-84291-2025051308320419.yml b/.upgradenotes/MDL-84291-2025051308320419.yml new file mode 100644 index 00000000000..aaad732bbbc --- /dev/null +++ b/.upgradenotes/MDL-84291-2025051308320419.yml @@ -0,0 +1,8 @@ +issueNumber: MDL-84291 +notes: + core_courseformat: + - message: >- + The param $maxsections of get_num_sections_data in addsection output is + not used anymore. If your format overrides this method, you should add + a default value 0 to be consistent with the new implementation. + type: changed diff --git a/course/changenumsections.php b/course/changenumsections.php index 8f6ea284ae2..5209523fca4 100644 --- a/course/changenumsections.php +++ b/course/changenumsections.php @@ -48,7 +48,6 @@ require_sesskey(); $desirednumsections = 0; $courseformat = course_get_format($course); $lastsectionnumber = $courseformat->get_last_section_number(); -$maxsections = $courseformat->get_max_sections(); if (isset($courseformatoptions['numsections']) && $increase !== null) { $desirednumsections = $courseformatoptions['numsections'] + 1; @@ -57,18 +56,6 @@ if (isset($courseformatoptions['numsections']) && $increase !== null) { $desirednumsections = $lastsectionnumber + $numsections; } -if ($desirednumsections > $maxsections) { - // Increase in number of sections is not allowed. - \core\notification::warning(get_string('maxsectionslimit', 'moodle', $maxsections)); - $increase = null; - $insertsection = null; - $numsections = 0; - - if (!$returnurl) { - $returnurl = course_get_url($course); - } -} - if (isset($courseformatoptions['numsections']) && $increase !== null) { if ($increase) { // Add an additional section. diff --git a/course/classes/local/service/content_item_service.php b/course/classes/local/service/content_item_service.php index e828b2cbc80..0e16419ec64 100644 --- a/course/classes/local/service/content_item_service.php +++ b/course/classes/local/service/content_item_service.php @@ -277,11 +277,8 @@ class content_item_service { return course_allowed_module($course, explode('_', $parents[$contentitem->get_component_name()])[1], $user); }); - $format = course_get_format($course); - $maxsectionsreached = ($format->get_last_section_number() >= $format->get_max_sections()); - // Now, check there is no delegated section into a delegated section. - if (is_null($sectioninfo) || $sectioninfo->is_delegated() || $maxsectionsreached) { + if (is_null($sectioninfo) || $sectioninfo->is_delegated()) { $availablecontentitems = array_filter($availablecontentitems, function($contentitem){ return !sectiondelegate::has_delegate_class($contentitem->get_component_name()); }); diff --git a/course/format/classes/output/local/content/addsection.php b/course/format/classes/output/local/content/addsection.php index 342a3ca302b..1b3c0527fff 100644 --- a/course/format/classes/output/local/content/addsection.php +++ b/course/format/classes/output/local/content/addsection.php @@ -72,16 +72,15 @@ class addsection implements named_templatable, renderable { $options = $format->get_format_options(); $lastsection = $format->get_last_section_number(); - $maxsections = $format->get_max_sections(); // Component based formats handle add section button in the frontend. - $show = ($lastsection < $maxsections) || $format->supports_components(); + $show = $format->supports_components(); $supportsnumsections = array_key_exists('numsections', $options); if ($supportsnumsections) { - $data = $this->get_num_sections_data($output, $lastsection, $maxsections); + $data = $this->get_num_sections_data($output, $lastsection); } else if (course_get_format($course)->uses_sections() && $show) { - $data = $this->get_add_section_data($output, $lastsection, $maxsections); + $data = $this->get_add_section_data($output, $lastsection); } if (count((array)$data)) { @@ -99,22 +98,20 @@ class addsection implements named_templatable, renderable { * * @param \renderer_base $output typically, the renderer that's calling this function * @param int $lastsection the last section number - * @param int $maxsections the maximum number of sections + * @param int $maxsections unused (max sections is not needed anymore) * @return stdClass data context for a mustache template */ - protected function get_num_sections_data(\renderer_base $output, int $lastsection, int $maxsections): stdClass { + protected function get_num_sections_data(\renderer_base $output, int $lastsection, int $maxsections = 0): stdClass { $format = $this->format; $course = $format->get_course(); $data = new stdClass(); - if ($lastsection < $maxsections) { - $data->increase = (object) [ - 'url' => new moodle_url( - '/course/changenumsections.php', - ['courseid' => $course->id, 'increase' => true, 'sesskey' => sesskey()] - ), - ]; - } + $data->increase = (object) [ + 'url' => new moodle_url( + '/course/changenumsections.php', + ['courseid' => $course->id, 'increase' => true, 'sesskey' => sesskey()] + ), + ]; if ($course->numsections > 0) { $data->decrease = (object) [ @@ -137,10 +134,10 @@ class addsection implements named_templatable, renderable { * * @param \renderer_base $output typically, the renderer that's calling this function * @param int $lastsection the last section number - * @param int $maxsections the maximum number of sections + * @param int $maxsections unused (max sections is not needed anymore) * @return stdClass data context for a mustache template */ - protected function get_add_section_data(\renderer_base $output, int $lastsection, int $maxsections): stdClass { + protected function get_add_section_data(\renderer_base $output, int $lastsection, int $maxsections = 0): stdClass { $format = $this->format; $course = $format->get_course(); $data = new stdClass(); @@ -156,8 +153,8 @@ class addsection implements named_templatable, renderable { $data->addsections = (object) [ 'url' => new moodle_url('/course/changenumsections.php', $params), 'title' => $addstring, - 'newsection' => $maxsections - $lastsection, - 'canaddsection' => $lastsection < $maxsections, + 'newsection' => $lastsection + 1, + 'canaddsection' => true, ]; return $data; } diff --git a/course/format/classes/stateactions.php b/course/format/classes/stateactions.php index f9af39264f3..c2e4ac14c71 100644 --- a/course/format/classes/stateactions.php +++ b/course/format/classes/stateactions.php @@ -267,15 +267,6 @@ class stateactions { $coursecontext = context_course::instance($course->id); require_capability('moodle/course:update', $coursecontext); - // Get course format settings. - $format = course_get_format($course->id); - $lastsectionnumber = $format->get_last_section_number(); - $maxsections = $format->get_max_sections(); - - if ($lastsectionnumber >= $maxsections) { - throw new moodle_exception('maxsectionslimit', 'moodle', '', $maxsections); - } - $modinfo = get_fast_modinfo($course); // Get target section. diff --git a/course/modedit.php b/course/modedit.php index f81fa565d3f..9b8758f7b11 100644 --- a/course/modedit.php +++ b/course/modedit.php @@ -72,15 +72,6 @@ if (!empty($add)) { // will be the closest match we have. navigation_node::override_active_url(course_get_url($course, $section)); - // MDL-69431 Validate that $section (url param) does not exceed the maximum for this course / format. - // If too high (e.g. section *id* not number) non-sequential sections inserted in course_sections table. - // Then on import, backup fills 'gap' with empty sections (see restore_rebuild_course_cache). Avoid this. - $courseformat = course_get_format($course); - $maxsections = $courseformat->get_max_sections(); - if ($section > $maxsections) { - throw new \moodle_exception('maxsectionslimit', 'moodle', '', $maxsections); - } - list($module, $context, $cw, $cm, $data) = prepare_new_moduleinfo_data($course, $add, $section); $data->return = 0; if (!is_null($sectionreturn)) { From 4d9e0aa137ab29a94fd03d3f6e77560f5640d0a5 Mon Sep 17 00:00:00 2001 From: ferran Date: Thu, 24 Apr 2025 11:19:01 +0200 Subject: [PATCH 2/6] MDL-84291 course: remove max sections UI elements --- .upgradenotes/MDL-84291-2025042303303788.yml | 8 +++ .upgradenotes/MDL-84291-2025060310563124.yml | 7 ++ .../amd/build/local/content/actions.min.js | 4 +- .../build/local/content/actions.min.js.map | 2 +- .../format/amd/src/local/content/actions.js | 72 +------------------ course/format/classes/base.php | 10 +++ .../classes/output/local/state/course.php | 1 - .../local/content/addsection.mustache | 8 --- lang/en/courseformat.php | 4 +- lang/en/deprecated.txt | 2 + lang/en/moodle.php | 8 ++- mod/subsection/classes/permission.php | 3 - mod/subsection/tests/permission_test.php | 21 +----- 13 files changed, 40 insertions(+), 110 deletions(-) create mode 100644 .upgradenotes/MDL-84291-2025042303303788.yml create mode 100644 .upgradenotes/MDL-84291-2025060310563124.yml diff --git a/.upgradenotes/MDL-84291-2025042303303788.yml b/.upgradenotes/MDL-84291-2025042303303788.yml new file mode 100644 index 00000000000..622a4807509 --- /dev/null +++ b/.upgradenotes/MDL-84291-2025042303303788.yml @@ -0,0 +1,8 @@ +issueNumber: MDL-84291 +notes: + core_courseformat: + - message: >- + The maxsections setting is now considered deprecated and will be removed + in Moodle 6.0. Consider implementing your own setting in your format + plugin if needed. + type: deprecated diff --git a/.upgradenotes/MDL-84291-2025060310563124.yml b/.upgradenotes/MDL-84291-2025060310563124.yml new file mode 100644 index 00000000000..dd21cdb1993 --- /dev/null +++ b/.upgradenotes/MDL-84291-2025060310563124.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-84291 +notes: + core_courseformat: + - message: >- + The format base method get_max_sections has been deprecated, as the + maxsections setting is also deprecated and no longer in use. + type: deprecated diff --git a/course/format/amd/build/local/content/actions.min.js b/course/format/amd/build/local/content/actions.min.js index 141a4acc28e..344b3607e0c 100644 --- a/course/format/amd/build/local/content/actions.min.js +++ b/course/format/amd/build/local/content/actions.min.js @@ -1,4 +1,4 @@ -define("core_courseformat/local/content/actions",["exports","core/reactive","core/local/inplace_editable/events","theme_boost/bootstrap/collapse","core/log","core/modal","core/modal_save_cancel","core/modal_delete_cancel","core/modal_copy_to_clipboard","core/modal_events","core/templates","core/prefetch","core/str","core/normalise","core_courseformat/local/content/actions/bulkselection","core_course/events","core/pending","core_courseformat/local/courseeditor/contenttree","core/notification"],(function(_exports,_reactive,_events,_collapse,_log,_modal,_modal_save_cancel,_modal_delete_cancel,_modal_copy_to_clipboard,_modal_events,_templates,_prefetch,_str,_normalise,_bulkselection,CourseEvents,_pending,_contenttree,_notification){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("core_courseformat/local/content/actions",["exports","core/reactive","core/local/inplace_editable/events","theme_boost/bootstrap/collapse","core/log","core/modal","core/modal_save_cancel","core/modal_delete_cancel","core/modal_copy_to_clipboard","core/modal_events","core/templates","core/prefetch","core/str","core/normalise","core_courseformat/local/content/actions/bulkselection","core/pending","core_courseformat/local/courseeditor/contenttree"],(function(_exports,_reactive,_events,_collapse,_log,_modal,_modal_save_cancel,_modal_delete_cancel,_modal_copy_to_clipboard,_modal_events,_templates,_prefetch,_str,_normalise,_bulkselection,_pending,_contenttree){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Course state actions dispatcher. * @@ -9,6 +9,6 @@ define("core_courseformat/local/content/actions",["exports","core/reactive","cor * @class core_courseformat/local/content/actions * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_collapse=_interopRequireDefault(_collapse),_log=_interopRequireDefault(_log),_modal=_interopRequireDefault(_modal),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_modal_delete_cancel=_interopRequireDefault(_modal_delete_cancel),_modal_copy_to_clipboard=_interopRequireDefault(_modal_copy_to_clipboard),_modal_events=_interopRequireDefault(_modal_events),_templates=_interopRequireDefault(_templates),CourseEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CourseEvents),_pending=_interopRequireDefault(_pending),_contenttree=_interopRequireDefault(_contenttree),_notification=_interopRequireDefault(_notification),(0,_prefetch.prefetchStrings)("core",["movecoursesection","movecoursemodule","confirm","delete"]);const directMutations={sectionHide:"sectionHide",sectionShow:"sectionShow",cmHide:"cmHide",cmShow:"cmShow",cmStealth:"cmStealth",cmMoveRight:"cmMoveRight",cmMoveLeft:"cmMoveLeft",cmNoGroups:"cmNoGroups",cmSeparateGroups:"cmSeparateGroups",cmVisibleGroups:"cmVisibleGroups"};class _default extends _reactive.BaseComponent{create(){this.name="content_actions",this.selectors={ACTIONLINK:"[data-action]",SECTIONLINK:"[data-for='section']",CMLINK:"[data-for='cm']",SECTIONNODE:"[data-for='sectionnode']",MODALTOGGLER:"[data-bs-toggle='collapse']",ADDSECTION:"[data-action='addSection']",CONTENTTREE:"#destination-selector",ACTIONMENU:".action-menu",ACTIONMENUTOGGLER:'[data-bs-toggle="dropdown"]',OPTIONSRADIO:"[type='radio']",COURSEADDSECTION:"#course-addsection",MAXSECTIONSWARNING:"[data-region='max-sections-warning']",ADDSECTIONREGION:"[data-region='section-addsection']"},this.classes={DISABLED:"disabled",ITALIC:"fst-italic",DISPLAYNONE:"d-none"}}static addActions(actions){for(const[action,mutationReference]of Object.entries(actions)){if("function"!=typeof mutationReference&&"string"!=typeof mutationReference)throw new Error("".concat(action," action must be a mutation name or a function"));directMutations[action]=mutationReference}}stateReady(state){this.addEventListener(this.element,"click",this._dispatchClick),this._checkSectionlist({state:state}),this.addEventListener(this.element,CourseEvents.sectionRefreshed,(()=>this._checkSectionlist({state:state}))),this.addEventListener(this.element,_events.eventTypes.elementUpdated,this._inplaceEditableHandler)}getWatchers(){return[{watch:"course.sectionlist:updated",handler:this._checkSectionlist}]}_dispatchClick(event){const target=event.target.closest(this.selectors.ACTIONLINK);if(!target)return;if(target.classList.contains(this.classes.DISABLED))return void event.preventDefault();const actionName=target.dataset.action,methodName=this._actionMethodName(actionName);if(void 0===this[methodName])return void 0!==directMutations[actionName]?"function"==typeof directMutations[actionName]?void directMutations[actionName](target,event):void this._requestMutationAction(target,event,directMutations[actionName]):void 0;this[methodName](target,event)}_actionMethodName(name){const requestName=name.charAt(0).toUpperCase()+name.slice(1);return"_request".concat(requestName)}_checkSectionlist(_ref){let{state:state}=_ref;this._setAddSectionLocked(state.course.sectionlist.length>state.course.maxsections)}_inplaceEditableHandler(event){var _event$detail,_event$detail$ajaxret,_event$detail2,_event$detail2$ajaxre;const itemtype=null===(_event$detail=event.detail)||void 0===_event$detail||null===(_event$detail$ajaxret=_event$detail.ajaxreturn)||void 0===_event$detail$ajaxret?void 0:_event$detail$ajaxret.itemtype,itemid=parseInt(null===(_event$detail2=event.detail)||void 0===_event$detail2||null===(_event$detail2$ajaxre=_event$detail2.ajaxreturn)||void 0===_event$detail2$ajaxre?void 0:_event$detail2$ajaxre.itemid);Number.isFinite(itemid)&&itemtype&&("activityname"!==itemtype?"sectionname"!==itemtype&&"sectionnamenl"!==itemtype||this.reactive.dispatch("sectionState",[itemid]):this.reactive.dispatch("cmState",[itemid]))}_getTargetIds(target){var _target$dataset,_target$dataset2;let ids=[];null!=target&&null!==(_target$dataset=target.dataset)&&void 0!==_target$dataset&&_target$dataset.id&&ids.push(target.dataset.id);const bulkType=null==target||null===(_target$dataset2=target.dataset)||void 0===_target$dataset2?void 0:_target$dataset2.bulk;if(!bulkType)return ids;const bulk=this.reactive.get("bulk");return bulk.enabled&&bulk.selectedType===bulkType&&(ids=[...ids,...bulk.selection]),ids}async _requestMoveSection(target,event){const sectionIds=this._getTargetIds(target);if(0==sectionIds.length)return;event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveSectionModal"),editTools=this._getClosestActionMenuToogler(target),data=this.reactive.getExporter().course(this.reactive.state);let titleText=null,sectionInfo=null;1==sectionIds.length?(sectionInfo=this.reactive.get("section",sectionIds[0]),data.sectionid=sectionInfo.id,data.sectiontitle=sectionInfo.title,data.information=await this.reactive.getFormatString("sectionmove_info",data.sectiontitle),titleText=this.reactive.getFormatString("sectionmove_title")):(data.information=await this.reactive.getFormatString("sectionsmove_info",sectionIds.length),titleText=this.reactive.getFormatString("sectionsmove_title"));const modal=await this._modalBodyRenderedPromise(_modal.default,{title:titleText,body:_templates.default.render("core_courseformat/local/content/movesection",data)}),modalBody=(0,_normalise.getFirst)(modal.getBody());sectionIds.forEach((sectionId=>{const currentElement=modalBody.querySelector("".concat(this.selectors.SECTIONLINK,"[data-id='").concat(sectionId,"']"));this._disableLink(currentElement)})),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER},!0),modalBody.addEventListener("click",(event=>{const target=event.target;target.matches("a")&&"section"==target.dataset.for&&void 0!==target.dataset.id&&(target.getAttribute("aria-disabled")||(event.preventDefault(),this.reactive.dispatch("sectionMoveAfter",sectionIds,target.dataset.id),this._destroyModal(modal,editTools)))})),pendingModalReady.resolve()}async _requestMoveCm(target,event){const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveCmModal"),editTools=this._getClosestActionMenuToogler(target),exporter=this.reactive.getExporter(),data=exporter.course(this.reactive.state);let titleText=null;if(1==cmIds.length){const cmInfo=this.reactive.get("cm",cmIds[0]);data.cmid=cmInfo.id,data.cmname=cmInfo.name,data.information=await this.reactive.getFormatString("cmmove_info",data.cmname),titleText=cmInfo.hasdelegatedsection?this.reactive.getFormatString("cmmove_subsectiontitle"):this.reactive.getFormatString("cmmove_title")}else data.information=await this.reactive.getFormatString("cmsmove_info",cmIds.length),titleText=this.reactive.getFormatString("cmsmove_title");const modal=await this._modalBodyRenderedPromise(_modal.default,{title:titleText,body:_templates.default.render("core_courseformat/local/content/movecm",data)}),modalBody=(0,_normalise.getFirst)(modal.getBody());cmIds.forEach((cmId=>{const currentElement=modalBody.querySelector("".concat(this.selectors.CMLINK,"[data-id='").concat(cmId,"']"));this._disableLink(currentElement)})),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER,ENTER:this.selectors.SECTIONLINK}),cmIds.forEach((cmId=>{const cmInfo=this.reactive.get("cm",cmId);let selector;selector=cmInfo.hasdelegatedsection?"".concat(this.selectors.SECTIONLINK,"[data-id='").concat(cmInfo.sectionid,"']"):"".concat(this.selectors.CMLINK,"[data-id='").concat(cmId,"']");const currentElement=modalBody.querySelector(selector);this._expandCmMoveModalParentSections(modalBody,currentElement)})),modalBody.addEventListener("click",(event=>{const target=event.target;if(!target.matches("a")||void 0===target.dataset.for||void 0===target.dataset.id)return;if(target.getAttribute("aria-disabled"))return;let targetSectionId,targetCmId;event.preventDefault();let droppedCmIds=[...cmIds];if("cm"==target.dataset.for){const dropData=exporter.cmDraggableData(this.reactive.state,target.dataset.id);targetSectionId=dropData.sectionid,targetCmId=dropData.nextcmid}else{const section=this.reactive.get("section",target.dataset.id);targetSectionId=target.dataset.id,targetCmId=null==section?void 0:section.cmlist[0]}this.reactive.get("section",targetSectionId).component&&(droppedCmIds=droppedCmIds.filter((cmId=>!this.reactive.get("cm",cmId).hasdelegatedsection))),0!==droppedCmIds.length&&(this.reactive.dispatch("cmMove",droppedCmIds,targetSectionId,targetCmId),this._destroyModal(modal,editTools))})),pendingModalReady.resolve()}_expandCmMoveModalParentSections(modalBody,element){var _toggler$dataset$targ;const sectionnode=element.closest(this.selectors.SECTIONNODE);if(!sectionnode)return;const toggler=sectionnode.querySelector(this.selectors.MODALTOGGLER);let collapsibleId=null!==(_toggler$dataset$targ=toggler.dataset.target)&&void 0!==_toggler$dataset$targ?_toggler$dataset$targ:toggler.getAttribute("href");if(collapsibleId){collapsibleId=collapsibleId.replace("#","");const expandNode=modalBody.querySelector("#".concat(collapsibleId));new _collapse.default(expandNode,{toggle:!1}).show()}this._expandCmMoveModalParentSections(modalBody,sectionnode.parentElement)}async _requestAddSection(target,event){var _target$dataset$id;event.preventDefault(),this.reactive.dispatch("addSection",null!==(_target$dataset$id=target.dataset.id)&&void 0!==_target$dataset$id?_target$dataset$id:0)}async _requestAddModule(target,event){_log.default.debug("AddModule action is deprecated. Use newModule instead"),event.preventDefault(),this.reactive.dispatch("addModule",target.dataset.modname,target.dataset.sectionnum,target.dataset.beforemod)}async _requestNewModule(target,event){event.preventDefault(),this.reactive.dispatch("newModule",target.dataset.modname,target.dataset.sectionid,target.dataset.beforemod)}async _requestDeleteSection(target,event){const sectionIds=this._getTargetIds(target);if(0==sectionIds.length)return;if(event.preventDefault(),!sectionIds.some((sectionId=>{var _sectionInfo$cmlist;const sectionInfo=this.reactive.get("section",sectionId);return(null!==(_sectionInfo$cmlist=sectionInfo.cmlist)&&void 0!==_sectionInfo$cmlist?_sectionInfo$cmlist:[]).length||sectionInfo.hassummary||sectionInfo.rawtitle})))return void this._dispatchSectionDelete(sectionIds,target);let bodyText=null,titleText=null;if(1==sectionIds.length){titleText=this.reactive.getFormatString("sectiondelete_title");const sectionInfo=this.reactive.get("section",sectionIds[0]);bodyText=this.reactive.getFormatString("sectiondelete_info",{name:sectionInfo.title})}else titleText=this.reactive.getFormatString("sectionsdelete_title"),bodyText=this.reactive.getFormatString("sectionsdelete_info",{count:sectionIds.length});const modal=await this._modalBodyRenderedPromise(_modal_delete_cancel.default,{title:titleText,body:bodyText});modal.getRoot().on(_modal_events.default.delete,(e=>{e.preventDefault(),modal.destroy(),this._dispatchSectionDelete(sectionIds,target)}))}async _dispatchSectionDelete(sectionIds,target){await this.reactive.dispatch("sectionDelete",sectionIds),target.baseURI.includes("section.php")&&(window.location.href=this.reactive.get("course").baseurl)}async _requestToggleSelectionCm(target,event){(0,_bulkselection.toggleBulkSelectionAction)(this.reactive,target,event,"cm")}async _requestToggleSelectionSection(target,event){(0,_bulkselection.toggleBulkSelectionAction)(this.reactive,target,event,"section")}async _requestMutationAction(target,event,mutationName){(target.dataset.id||"bulkaction"===target.dataset.for)&&(event.preventDefault(),"bulkaction"===target.dataset.for?this.reactive.dispatch(mutationName,this.reactive.get("bulk").selection):this.reactive.dispatch(mutationName,[target.dataset.id]))}_requestPermalink(target,event){event.preventDefault(),_modal_copy_to_clipboard.default.create({text:target.getAttribute("href")},(0,_str.getString)("sectionlink","course"))}async _requestCmDuplicate(target,event){var _target$dataset$secti;const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;const sectionId=null!==(_target$dataset$secti=target.dataset.sectionid)&&void 0!==_target$dataset$secti?_target$dataset$secti:null;event.preventDefault(),this.reactive.dispatch("cmDuplicate",cmIds,sectionId)}async _requestCmDelete(target,event){const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;event.preventDefault();let bodyText=null,titleText=null,delegatedsection=null;if(1==cmIds.length){const cmInfo=this.reactive.get("cm",cmIds[0]);cmInfo.hasdelegatedsection?(delegatedsection=cmInfo.delegatesectionid,titleText=this.reactive.getFormatString("cmdelete_subsectiontitle"),bodyText=(0,_str.getString)("sectiondelete_info","core_courseformat",{type:cmInfo.modname,name:cmInfo.name})):(titleText=this.reactive.getFormatString("cmdelete_title"),bodyText=(0,_str.getString)("cmdelete_info","core_courseformat",{type:cmInfo.modname,name:cmInfo.name}))}else titleText=(0,_str.getString)("cmsdelete_title","core_courseformat"),bodyText=(0,_str.getString)("cmsdelete_info","core_courseformat",{count:cmIds.length});const modal=await this._modalBodyRenderedPromise(_modal_delete_cancel.default,{title:titleText,body:bodyText});modal.getRoot().on(_modal_events.default.delete,(e=>{if(e.preventDefault(),modal.destroy(),this.reactive.dispatch("cmDelete",cmIds),1==cmIds.length&&delegatedsection&&target.baseURI.includes("section.php")){let parameters=new URLSearchParams(window.location.search);parameters.has("id")&¶meters.get("id")==delegatedsection&&this._dispatchSectionDelete([delegatedsection],target)}}))}async _requestCmAvailability(target){const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;const data={allowstealth:this.reactive.getExporter().canUseStealth(this.reactive.state,cmIds)},modal=await this._modalBodyRenderedPromise(_modal_save_cancel.default,{title:(0,_str.getString)("availability","core"),body:_templates.default.render("core_courseformat/local/content/cm/availabilitymodal",data),saveButtonText:(0,_str.getString)("apply","core")});this._setupMutationRadioButtonModal(modal,cmIds)}async _requestSectionAvailability(target){const sectionIds=this._getTargetIds(target);if(0==sectionIds.length)return;const title=1==sectionIds.length?"sectionavailability_title":"sectionsavailability_title",modal=await this._modalBodyRenderedPromise(_modal_save_cancel.default,{title:this.reactive.getFormatString(title),body:_templates.default.render("core_courseformat/local/content/section/availabilitymodal",[]),saveButtonText:(0,_str.getString)("apply","core")});this._setupMutationRadioButtonModal(modal,sectionIds)}_setupMutationRadioButtonModal(modal,ids){modal.setButtonDisabled("save",!0);const submitFunction=radio=>{const mutation=null==radio?void 0:radio.value;return!!mutation&&(this.reactive.dispatch(mutation,ids),!0)},modalBody=(0,_normalise.getFirst)(modal.getBody());modalBody.querySelectorAll(this.selectors.OPTIONSRADIO).forEach((radio=>{radio.addEventListener("change",(()=>{modal.setButtonDisabled("save",!1)})),radio.parentNode.addEventListener("click",(()=>{radio.checked=!0,modal.setButtonDisabled("save",!1)})),radio.parentNode.addEventListener("dblclick",(dbClickEvent=>{submitFunction(radio)&&(dbClickEvent.preventDefault(),modal.destroy())}))})),modal.getRoot().on(_modal_events.default.save,(()=>{const radio=modalBody.querySelector("".concat(this.selectors.OPTIONSRADIO,":checked"));submitFunction(radio)}))}_setAddSectionLocked(locked){this.getElements(this.selectors.ADDSECTIONREGION).forEach((element=>{element.classList.toggle(this.classes.DISABLED,locked);const addSectionElement=element.querySelector(this.selectors.ADDSECTION);addSectionElement.classList.toggle(this.classes.DISABLED,locked),this.setElementLocked(addSectionElement,locked),locked?((0,_str.getString)("sectionaddmax","core_courseformat").then((text=>addSectionElement.setAttribute("title",text))).catch(_notification.default.exception),addSectionElement.style.pointerEvents=null,addSectionElement.style.userSelect=null):addSectionElement.setAttribute("title",addSectionElement.dataset.addSections)}));const courseAddSection=this.getElement(this.selectors.COURSEADDSECTION);if(courseAddSection){courseAddSection.querySelector(this.selectors.ADDSECTION).classList.toggle(this.classes.DISPLAYNONE,locked);courseAddSection.querySelector(this.selectors.MAXSECTIONSWARNING).classList.toggle(this.classes.DISPLAYNONE,!locked)}}_disableLink(element){element&&(element.style.pointerEvents="none",element.style.userSelect="none",element.classList.add(this.classes.DISABLED),element.classList.add(this.classes.ITALIC),element.setAttribute("aria-disabled",!0),element.addEventListener("click",(event=>event.preventDefault())))}_modalBodyRenderedPromise(ModalClass,modalParams){return new Promise(((resolve,reject)=>{ModalClass.create(modalParams).then((modal=>{modal.setRemoveOnClose(!0),modal.getRoot().on(_modal_events.default.bodyRendered,(()=>{resolve(modal)})),void 0!==modalParams.saveButtonText&&modal.setSaveButtonText(modalParams.saveButtonText),void 0!==modalParams.deleteButtonText&&modal.setDeleteButtonText(modalParams.saveButtonText),modal.show()})).catch((()=>{reject("Cannot load modal content")}))}))}_destroyModal(modal,element){modal.hide();const pendingDestroy=new _pending.default("courseformat/actions:destroyModal");element&&element.focus(),setTimeout((()=>{modal.destroy(),pendingDestroy.resolve()}),500)}_getClosestActionMenuToogler(element){const actionMenu=element.closest(this.selectors.ACTIONMENU);if(actionMenu)return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER)}}return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_collapse=_interopRequireDefault(_collapse),_log=_interopRequireDefault(_log),_modal=_interopRequireDefault(_modal),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_modal_delete_cancel=_interopRequireDefault(_modal_delete_cancel),_modal_copy_to_clipboard=_interopRequireDefault(_modal_copy_to_clipboard),_modal_events=_interopRequireDefault(_modal_events),_templates=_interopRequireDefault(_templates),_pending=_interopRequireDefault(_pending),_contenttree=_interopRequireDefault(_contenttree),(0,_prefetch.prefetchStrings)("core",["movecoursesection","movecoursemodule","confirm","delete"]);const directMutations={sectionHide:"sectionHide",sectionShow:"sectionShow",cmHide:"cmHide",cmShow:"cmShow",cmStealth:"cmStealth",cmMoveRight:"cmMoveRight",cmMoveLeft:"cmMoveLeft",cmNoGroups:"cmNoGroups",cmSeparateGroups:"cmSeparateGroups",cmVisibleGroups:"cmVisibleGroups"};class _default extends _reactive.BaseComponent{create(){this.name="content_actions",this.selectors={ACTIONLINK:"[data-action]",SECTIONLINK:"[data-for='section']",CMLINK:"[data-for='cm']",SECTIONNODE:"[data-for='sectionnode']",MODALTOGGLER:"[data-bs-toggle='collapse']",ADDSECTION:"[data-action='addSection']",CONTENTTREE:"#destination-selector",ACTIONMENU:".action-menu",ACTIONMENUTOGGLER:'[data-bs-toggle="dropdown"]',OPTIONSRADIO:"[type='radio']",COURSEADDSECTION:"#course-addsection",ADDSECTIONREGION:"[data-region='section-addsection']"},this.classes={DISABLED:"disabled",ITALIC:"fst-italic",DISPLAYNONE:"d-none"}}static addActions(actions){for(const[action,mutationReference]of Object.entries(actions)){if("function"!=typeof mutationReference&&"string"!=typeof mutationReference)throw new Error("".concat(action," action must be a mutation name or a function"));directMutations[action]=mutationReference}}stateReady(){this.addEventListener(this.element,"click",this._dispatchClick),this.addEventListener(this.element,_events.eventTypes.elementUpdated,this._inplaceEditableHandler)}_dispatchClick(event){const target=event.target.closest(this.selectors.ACTIONLINK);if(!target)return;if(target.classList.contains(this.classes.DISABLED))return void event.preventDefault();const actionName=target.dataset.action,methodName=this._actionMethodName(actionName);if(void 0===this[methodName])return void 0!==directMutations[actionName]?"function"==typeof directMutations[actionName]?void directMutations[actionName](target,event):void this._requestMutationAction(target,event,directMutations[actionName]):void 0;this[methodName](target,event)}_actionMethodName(name){const requestName=name.charAt(0).toUpperCase()+name.slice(1);return"_request".concat(requestName)}_inplaceEditableHandler(event){var _event$detail,_event$detail$ajaxret,_event$detail2,_event$detail2$ajaxre;const itemtype=null===(_event$detail=event.detail)||void 0===_event$detail||null===(_event$detail$ajaxret=_event$detail.ajaxreturn)||void 0===_event$detail$ajaxret?void 0:_event$detail$ajaxret.itemtype,itemid=parseInt(null===(_event$detail2=event.detail)||void 0===_event$detail2||null===(_event$detail2$ajaxre=_event$detail2.ajaxreturn)||void 0===_event$detail2$ajaxre?void 0:_event$detail2$ajaxre.itemid);Number.isFinite(itemid)&&itemtype&&("activityname"!==itemtype?"sectionname"!==itemtype&&"sectionnamenl"!==itemtype||this.reactive.dispatch("sectionState",[itemid]):this.reactive.dispatch("cmState",[itemid]))}_getTargetIds(target){var _target$dataset,_target$dataset2;let ids=[];null!=target&&null!==(_target$dataset=target.dataset)&&void 0!==_target$dataset&&_target$dataset.id&&ids.push(target.dataset.id);const bulkType=null==target||null===(_target$dataset2=target.dataset)||void 0===_target$dataset2?void 0:_target$dataset2.bulk;if(!bulkType)return ids;const bulk=this.reactive.get("bulk");return bulk.enabled&&bulk.selectedType===bulkType&&(ids=[...ids,...bulk.selection]),ids}async _requestMoveSection(target,event){const sectionIds=this._getTargetIds(target);if(0==sectionIds.length)return;event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveSectionModal"),editTools=this._getClosestActionMenuToogler(target),data=this.reactive.getExporter().course(this.reactive.state);let titleText=null,sectionInfo=null;1==sectionIds.length?(sectionInfo=this.reactive.get("section",sectionIds[0]),data.sectionid=sectionInfo.id,data.sectiontitle=sectionInfo.title,data.information=await this.reactive.getFormatString("sectionmove_info",data.sectiontitle),titleText=this.reactive.getFormatString("sectionmove_title")):(data.information=await this.reactive.getFormatString("sectionsmove_info",sectionIds.length),titleText=this.reactive.getFormatString("sectionsmove_title"));const modal=await this._modalBodyRenderedPromise(_modal.default,{title:titleText,body:_templates.default.render("core_courseformat/local/content/movesection",data)}),modalBody=(0,_normalise.getFirst)(modal.getBody());sectionIds.forEach((sectionId=>{const currentElement=modalBody.querySelector("".concat(this.selectors.SECTIONLINK,"[data-id='").concat(sectionId,"']"));this._disableLink(currentElement)})),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER},!0),modalBody.addEventListener("click",(event=>{const target=event.target;target.matches("a")&&"section"==target.dataset.for&&void 0!==target.dataset.id&&(target.getAttribute("aria-disabled")||(event.preventDefault(),this.reactive.dispatch("sectionMoveAfter",sectionIds,target.dataset.id),this._destroyModal(modal,editTools)))})),pendingModalReady.resolve()}async _requestMoveCm(target,event){const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveCmModal"),editTools=this._getClosestActionMenuToogler(target),exporter=this.reactive.getExporter(),data=exporter.course(this.reactive.state);let titleText=null;if(1==cmIds.length){const cmInfo=this.reactive.get("cm",cmIds[0]);data.cmid=cmInfo.id,data.cmname=cmInfo.name,data.information=await this.reactive.getFormatString("cmmove_info",data.cmname),titleText=cmInfo.hasdelegatedsection?this.reactive.getFormatString("cmmove_subsectiontitle"):this.reactive.getFormatString("cmmove_title")}else data.information=await this.reactive.getFormatString("cmsmove_info",cmIds.length),titleText=this.reactive.getFormatString("cmsmove_title");const modal=await this._modalBodyRenderedPromise(_modal.default,{title:titleText,body:_templates.default.render("core_courseformat/local/content/movecm",data)}),modalBody=(0,_normalise.getFirst)(modal.getBody());cmIds.forEach((cmId=>{const currentElement=modalBody.querySelector("".concat(this.selectors.CMLINK,"[data-id='").concat(cmId,"']"));this._disableLink(currentElement)})),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER,ENTER:this.selectors.SECTIONLINK}),cmIds.forEach((cmId=>{const cmInfo=this.reactive.get("cm",cmId);let selector;selector=cmInfo.hasdelegatedsection?"".concat(this.selectors.SECTIONLINK,"[data-id='").concat(cmInfo.sectionid,"']"):"".concat(this.selectors.CMLINK,"[data-id='").concat(cmId,"']");const currentElement=modalBody.querySelector(selector);this._expandCmMoveModalParentSections(modalBody,currentElement)})),modalBody.addEventListener("click",(event=>{const target=event.target;if(!target.matches("a")||void 0===target.dataset.for||void 0===target.dataset.id)return;if(target.getAttribute("aria-disabled"))return;let targetSectionId,targetCmId;event.preventDefault();let droppedCmIds=[...cmIds];if("cm"==target.dataset.for){const dropData=exporter.cmDraggableData(this.reactive.state,target.dataset.id);targetSectionId=dropData.sectionid,targetCmId=dropData.nextcmid}else{const section=this.reactive.get("section",target.dataset.id);targetSectionId=target.dataset.id,targetCmId=null==section?void 0:section.cmlist[0]}this.reactive.get("section",targetSectionId).component&&(droppedCmIds=droppedCmIds.filter((cmId=>!this.reactive.get("cm",cmId).hasdelegatedsection))),0!==droppedCmIds.length&&(this.reactive.dispatch("cmMove",droppedCmIds,targetSectionId,targetCmId),this._destroyModal(modal,editTools))})),pendingModalReady.resolve()}_expandCmMoveModalParentSections(modalBody,element){var _toggler$dataset$targ;const sectionnode=element.closest(this.selectors.SECTIONNODE);if(!sectionnode)return;const toggler=sectionnode.querySelector(this.selectors.MODALTOGGLER);let collapsibleId=null!==(_toggler$dataset$targ=toggler.dataset.target)&&void 0!==_toggler$dataset$targ?_toggler$dataset$targ:toggler.getAttribute("href");if(collapsibleId){collapsibleId=collapsibleId.replace("#","");const expandNode=modalBody.querySelector("#".concat(collapsibleId));new _collapse.default(expandNode,{toggle:!1}).show()}this._expandCmMoveModalParentSections(modalBody,sectionnode.parentElement)}async _requestAddSection(target,event){var _target$dataset$id;event.preventDefault(),this.reactive.dispatch("addSection",null!==(_target$dataset$id=target.dataset.id)&&void 0!==_target$dataset$id?_target$dataset$id:0)}async _requestAddModule(target,event){_log.default.debug("AddModule action is deprecated. Use newModule instead"),event.preventDefault(),this.reactive.dispatch("addModule",target.dataset.modname,target.dataset.sectionnum,target.dataset.beforemod)}async _requestNewModule(target,event){event.preventDefault(),this.reactive.dispatch("newModule",target.dataset.modname,target.dataset.sectionid,target.dataset.beforemod)}async _requestDeleteSection(target,event){const sectionIds=this._getTargetIds(target);if(0==sectionIds.length)return;if(event.preventDefault(),!sectionIds.some((sectionId=>{var _sectionInfo$cmlist;const sectionInfo=this.reactive.get("section",sectionId);return(null!==(_sectionInfo$cmlist=sectionInfo.cmlist)&&void 0!==_sectionInfo$cmlist?_sectionInfo$cmlist:[]).length||sectionInfo.hassummary||sectionInfo.rawtitle})))return void this._dispatchSectionDelete(sectionIds,target);let bodyText=null,titleText=null;if(1==sectionIds.length){titleText=this.reactive.getFormatString("sectiondelete_title");const sectionInfo=this.reactive.get("section",sectionIds[0]);bodyText=this.reactive.getFormatString("sectiondelete_info",{name:sectionInfo.title})}else titleText=this.reactive.getFormatString("sectionsdelete_title"),bodyText=this.reactive.getFormatString("sectionsdelete_info",{count:sectionIds.length});const modal=await this._modalBodyRenderedPromise(_modal_delete_cancel.default,{title:titleText,body:bodyText});modal.getRoot().on(_modal_events.default.delete,(e=>{e.preventDefault(),modal.destroy(),this._dispatchSectionDelete(sectionIds,target)}))}async _dispatchSectionDelete(sectionIds,target){await this.reactive.dispatch("sectionDelete",sectionIds),target.baseURI.includes("section.php")&&(window.location.href=this.reactive.get("course").baseurl)}async _requestToggleSelectionCm(target,event){(0,_bulkselection.toggleBulkSelectionAction)(this.reactive,target,event,"cm")}async _requestToggleSelectionSection(target,event){(0,_bulkselection.toggleBulkSelectionAction)(this.reactive,target,event,"section")}async _requestMutationAction(target,event,mutationName){(target.dataset.id||"bulkaction"===target.dataset.for)&&(event.preventDefault(),"bulkaction"===target.dataset.for?this.reactive.dispatch(mutationName,this.reactive.get("bulk").selection):this.reactive.dispatch(mutationName,[target.dataset.id]))}_requestPermalink(target,event){event.preventDefault(),_modal_copy_to_clipboard.default.create({text:target.getAttribute("href")},(0,_str.getString)("sectionlink","course"))}async _requestCmDuplicate(target,event){var _target$dataset$secti;const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;const sectionId=null!==(_target$dataset$secti=target.dataset.sectionid)&&void 0!==_target$dataset$secti?_target$dataset$secti:null;event.preventDefault(),this.reactive.dispatch("cmDuplicate",cmIds,sectionId)}async _requestCmDelete(target,event){const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;event.preventDefault();let bodyText=null,titleText=null,delegatedsection=null;if(1==cmIds.length){const cmInfo=this.reactive.get("cm",cmIds[0]);cmInfo.hasdelegatedsection?(delegatedsection=cmInfo.delegatesectionid,titleText=this.reactive.getFormatString("cmdelete_subsectiontitle"),bodyText=(0,_str.getString)("sectiondelete_info","core_courseformat",{type:cmInfo.modname,name:cmInfo.name})):(titleText=this.reactive.getFormatString("cmdelete_title"),bodyText=(0,_str.getString)("cmdelete_info","core_courseformat",{type:cmInfo.modname,name:cmInfo.name}))}else titleText=(0,_str.getString)("cmsdelete_title","core_courseformat"),bodyText=(0,_str.getString)("cmsdelete_info","core_courseformat",{count:cmIds.length});const modal=await this._modalBodyRenderedPromise(_modal_delete_cancel.default,{title:titleText,body:bodyText});modal.getRoot().on(_modal_events.default.delete,(e=>{if(e.preventDefault(),modal.destroy(),this.reactive.dispatch("cmDelete",cmIds),1==cmIds.length&&delegatedsection&&target.baseURI.includes("section.php")){let parameters=new URLSearchParams(window.location.search);parameters.has("id")&¶meters.get("id")==delegatedsection&&this._dispatchSectionDelete([delegatedsection],target)}}))}async _requestCmAvailability(target){const cmIds=this._getTargetIds(target);if(0==cmIds.length)return;const data={allowstealth:this.reactive.getExporter().canUseStealth(this.reactive.state,cmIds)},modal=await this._modalBodyRenderedPromise(_modal_save_cancel.default,{title:(0,_str.getString)("availability","core"),body:_templates.default.render("core_courseformat/local/content/cm/availabilitymodal",data),saveButtonText:(0,_str.getString)("apply","core")});this._setupMutationRadioButtonModal(modal,cmIds)}async _requestSectionAvailability(target){const sectionIds=this._getTargetIds(target);if(0==sectionIds.length)return;const title=1==sectionIds.length?"sectionavailability_title":"sectionsavailability_title",modal=await this._modalBodyRenderedPromise(_modal_save_cancel.default,{title:this.reactive.getFormatString(title),body:_templates.default.render("core_courseformat/local/content/section/availabilitymodal",[]),saveButtonText:(0,_str.getString)("apply","core")});this._setupMutationRadioButtonModal(modal,sectionIds)}_setupMutationRadioButtonModal(modal,ids){modal.setButtonDisabled("save",!0);const submitFunction=radio=>{const mutation=null==radio?void 0:radio.value;return!!mutation&&(this.reactive.dispatch(mutation,ids),!0)},modalBody=(0,_normalise.getFirst)(modal.getBody());modalBody.querySelectorAll(this.selectors.OPTIONSRADIO).forEach((radio=>{radio.addEventListener("change",(()=>{modal.setButtonDisabled("save",!1)})),radio.parentNode.addEventListener("click",(()=>{radio.checked=!0,modal.setButtonDisabled("save",!1)})),radio.parentNode.addEventListener("dblclick",(dbClickEvent=>{submitFunction(radio)&&(dbClickEvent.preventDefault(),modal.destroy())}))})),modal.getRoot().on(_modal_events.default.save,(()=>{const radio=modalBody.querySelector("".concat(this.selectors.OPTIONSRADIO,":checked"));submitFunction(radio)}))}_disableLink(element){element&&(element.style.pointerEvents="none",element.style.userSelect="none",element.classList.add(this.classes.DISABLED),element.classList.add(this.classes.ITALIC),element.setAttribute("aria-disabled",!0),element.addEventListener("click",(event=>event.preventDefault())))}_modalBodyRenderedPromise(ModalClass,modalParams){return new Promise(((resolve,reject)=>{ModalClass.create(modalParams).then((modal=>{modal.setRemoveOnClose(!0),modal.getRoot().on(_modal_events.default.bodyRendered,(()=>{resolve(modal)})),void 0!==modalParams.saveButtonText&&modal.setSaveButtonText(modalParams.saveButtonText),void 0!==modalParams.deleteButtonText&&modal.setDeleteButtonText(modalParams.saveButtonText),modal.show()})).catch((()=>{reject("Cannot load modal content")}))}))}_destroyModal(modal,element){modal.hide();const pendingDestroy=new _pending.default("courseformat/actions:destroyModal");element&&element.focus(),setTimeout((()=>{modal.destroy(),pendingDestroy.resolve()}),500)}_getClosestActionMenuToogler(element){const actionMenu=element.closest(this.selectors.ACTIONMENU);if(actionMenu)return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=actions.min.js.map \ No newline at end of file diff --git a/course/format/amd/build/local/content/actions.min.js.map b/course/format/amd/build/local/content/actions.min.js.map index 3d1cf7bebda..ec207d9ee13 100644 --- a/course/format/amd/build/local/content/actions.min.js.map +++ b/course/format/amd/build/local/content/actions.min.js.map @@ -1 +1 @@ -{"version":3,"file":"actions.min.js","sources":["../../../src/local/content/actions.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\n/**\n * Course state actions dispatcher.\n *\n * This module captures all data-dispatch links in the course content and dispatch the proper\n * state mutation, including any confirmation and modal required.\n *\n * @module core_courseformat/local/content/actions\n * @class core_courseformat/local/content/actions\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {eventTypes} from 'core/local/inplace_editable/events';\nimport Collapse from 'theme_boost/bootstrap/collapse';\nimport log from 'core/log';\nimport Modal from 'core/modal';\nimport ModalSaveCancel from 'core/modal_save_cancel';\nimport ModalDeleteCancel from 'core/modal_delete_cancel';\nimport ModalCopyToClipboard from 'core/modal_copy_to_clipboard';\nimport ModalEvents from 'core/modal_events';\nimport Templates from 'core/templates';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport {getFirst} from 'core/normalise';\nimport {toggleBulkSelectionAction} from 'core_courseformat/local/content/actions/bulkselection';\nimport * as CourseEvents from 'core_course/events';\nimport Pending from 'core/pending';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.\nimport Notification from \"core/notification\";\n\n// Load global strings.\nprefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']);\n\n// Mutations are dispatched by the course content actions.\n// Formats can use this module addActions static method to add custom actions.\n// Direct mutations can be simple strings (mutation) name or functions.\nconst directMutations = {\n sectionHide: 'sectionHide',\n sectionShow: 'sectionShow',\n cmHide: 'cmHide',\n cmShow: 'cmShow',\n cmStealth: 'cmStealth',\n cmMoveRight: 'cmMoveRight',\n cmMoveLeft: 'cmMoveLeft',\n cmNoGroups: 'cmNoGroups',\n cmSeparateGroups: 'cmSeparateGroups',\n cmVisibleGroups: 'cmVisibleGroups',\n};\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_actions';\n // Default query selectors.\n this.selectors = {\n ACTIONLINK: `[data-action]`,\n // Move modal selectors.\n SECTIONLINK: `[data-for='section']`,\n CMLINK: `[data-for='cm']`,\n SECTIONNODE: `[data-for='sectionnode']`,\n MODALTOGGLER: `[data-bs-toggle='collapse']`,\n ADDSECTION: `[data-action='addSection']`,\n CONTENTTREE: `#destination-selector`,\n ACTIONMENU: `.action-menu`,\n ACTIONMENUTOGGLER: `[data-bs-toggle=\"dropdown\"]`,\n // Availability modal selectors.\n OPTIONSRADIO: `[type='radio']`,\n COURSEADDSECTION: `#course-addsection`,\n MAXSECTIONSWARNING: `[data-region='max-sections-warning']`,\n ADDSECTIONREGION: `[data-region='section-addsection']`,\n };\n // Component css classes.\n this.classes = {\n DISABLED: `disabled`,\n ITALIC: `fst-italic`,\n DISPLAYNONE: `d-none`,\n };\n }\n\n /**\n * Add extra actions to the module.\n *\n * @param {array} actions array of methods to execute\n */\n static addActions(actions) {\n for (const [action, mutationReference] of Object.entries(actions)) {\n if (typeof mutationReference !== 'function' && typeof mutationReference !== 'string') {\n throw new Error(`${action} action must be a mutation name or a function`);\n }\n directMutations[action] = mutationReference;\n }\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data.\n *\n */\n stateReady(state) {\n // Delegate dispatch clicks.\n this.addEventListener(\n this.element,\n 'click',\n this._dispatchClick\n );\n // Check section limit.\n this._checkSectionlist({state});\n // Add an Event listener to recalculate limits it if a section HTML is altered.\n this.addEventListener(\n this.element,\n CourseEvents.sectionRefreshed,\n () => this._checkSectionlist({state})\n );\n // Any inplace editable update needs state refresh.\n this.addEventListener(\n this.element,\n eventTypes.elementUpdated,\n this._inplaceEditableHandler\n );\n }\n\n /**\n * Return the component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n // Check section limit.\n {watch: `course.sectionlist:updated`, handler: this._checkSectionlist},\n ];\n }\n\n _dispatchClick(event) {\n const target = event.target.closest(this.selectors.ACTIONLINK);\n if (!target) {\n return;\n }\n if (target.classList.contains(this.classes.DISABLED)) {\n event.preventDefault();\n return;\n }\n\n // Invoke proper method.\n const actionName = target.dataset.action;\n const methodName = this._actionMethodName(actionName);\n\n if (this[methodName] !== undefined) {\n this[methodName](target, event);\n return;\n }\n\n // Check direct mutations or mutations handlers.\n if (directMutations[actionName] !== undefined) {\n if (typeof directMutations[actionName] === 'function') {\n directMutations[actionName](target, event);\n return;\n }\n this._requestMutationAction(target, event, directMutations[actionName]);\n return;\n }\n }\n\n _actionMethodName(name) {\n const requestName = name.charAt(0).toUpperCase() + name.slice(1);\n return `_request${requestName}`;\n }\n\n /**\n * Check the section list and disable some options if needed.\n *\n * @param {Object} detail the update details.\n * @param {Object} detail.state the state object.\n */\n _checkSectionlist({state}) {\n // Disable \"add section\" actions if the course max sections has been exceeded.\n this._setAddSectionLocked(state.course.sectionlist.length > state.course.maxsections);\n }\n\n /**\n * Handle inplace editable updates.\n *\n * @param {Event} event the triggered event\n * @private\n */\n _inplaceEditableHandler(event) {\n const itemtype = event.detail?.ajaxreturn?.itemtype;\n const itemid = parseInt(event.detail?.ajaxreturn?.itemid);\n if (!Number.isFinite(itemid) || !itemtype) {\n return;\n }\n\n if (itemtype === 'activityname') {\n this.reactive.dispatch('cmState', [itemid]);\n return;\n }\n // Sections uses sectionname for normal sections and sectionnamenl for the no link sections.\n if (itemtype === 'sectionname' || itemtype === 'sectionnamenl') {\n this.reactive.dispatch('sectionState', [itemid]);\n return;\n }\n }\n\n /**\n * Return the ids represented by this element.\n *\n * Depending on the dataset attributes the action could represent a single id\n * or a bulk actions with all the current selected ids.\n *\n * @param {HTMLElement} target\n * @returns {Number[]} array of Ids\n */\n _getTargetIds(target) {\n let ids = [];\n if (target?.dataset?.id) {\n ids.push(target.dataset.id);\n }\n const bulkType = target?.dataset?.bulk;\n if (!bulkType) {\n return ids;\n }\n const bulk = this.reactive.get('bulk');\n if (bulk.enabled && bulk.selectedType === bulkType) {\n ids = [...ids, ...bulk.selection];\n }\n return ids;\n }\n\n /**\n * Handle a move section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveSection(target, event) {\n // Check we have an id.\n const sectionIds = this._getTargetIds(target);\n if (sectionIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveSectionModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n let titleText = null;\n\n // Add the target section id and title.\n let sectionInfo = null;\n if (sectionIds.length == 1) {\n sectionInfo = this.reactive.get('section', sectionIds[0]);\n data.sectionid = sectionInfo.id;\n data.sectiontitle = sectionInfo.title;\n data.information = await this.reactive.getFormatString('sectionmove_info', data.sectiontitle);\n titleText = this.reactive.getFormatString('sectionmove_title');\n } else {\n data.information = await this.reactive.getFormatString('sectionsmove_info', sectionIds.length);\n titleText = this.reactive.getFormatString('sectionsmove_title');\n }\n\n\n // Create the modal.\n // Build the modal parameters from the event data.\n const modal = await this._modalBodyRenderedPromise(Modal, {\n title: titleText,\n body: Templates.render('core_courseformat/local/content/movesection', data),\n });\n\n const modalBody = getFirst(modal.getBody());\n\n // Disable current selected section ids.\n sectionIds.forEach(sectionId => {\n const currentElement = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-id='${sectionId}']`);\n this._disableLink(currentElement);\n });\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n },\n true\n );\n\n // Capture click.\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for != 'section' || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n this.reactive.dispatch('sectionMoveAfter', sectionIds, target.dataset.id);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Handle a move cm request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveCm(target, event) {\n // Check we have an id.\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveCmModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n\n let titleText = null;\n if (cmIds.length == 1) {\n const cmInfo = this.reactive.get('cm', cmIds[0]);\n data.cmid = cmInfo.id;\n data.cmname = cmInfo.name;\n data.information = await this.reactive.getFormatString('cmmove_info', data.cmname);\n if (cmInfo.hasdelegatedsection) {\n titleText = this.reactive.getFormatString('cmmove_subsectiontitle');\n } else {\n titleText = this.reactive.getFormatString('cmmove_title');\n }\n } else {\n data.information = await this.reactive.getFormatString('cmsmove_info', cmIds.length);\n titleText = this.reactive.getFormatString('cmsmove_title');\n }\n\n // Create the modal.\n // Build the modal parameters from the event data.\n const modal = await this._modalBodyRenderedPromise(Modal, {\n title: titleText,\n body: Templates.render('core_courseformat/local/content/movecm', data),\n });\n\n const modalBody = getFirst(modal.getBody());\n\n // Disable current selected section ids.\n cmIds.forEach(cmId => {\n const currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);\n this._disableLink(currentElement);\n });\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n ENTER: this.selectors.SECTIONLINK,\n }\n );\n\n cmIds.forEach(cmId => {\n const cmInfo = this.reactive.get('cm', cmId);\n let selector;\n if (!cmInfo.hasdelegatedsection) {\n selector = `${this.selectors.CMLINK}[data-id='${cmId}']`;\n } else {\n selector = `${this.selectors.SECTIONLINK}[data-id='${cmInfo.sectionid}']`;\n }\n const currentElement = modalBody.querySelector(selector);\n this._expandCmMoveModalParentSections(modalBody, currentElement);\n });\n\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for === undefined || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n\n let targetSectionId;\n let targetCmId;\n let droppedCmIds = [...cmIds];\n if (target.dataset.for == 'cm') {\n const dropData = exporter.cmDraggableData(this.reactive.state, target.dataset.id);\n targetSectionId = dropData.sectionid;\n targetCmId = dropData.nextcmid;\n } else {\n const section = this.reactive.get('section', target.dataset.id);\n targetSectionId = target.dataset.id;\n targetCmId = section?.cmlist[0];\n }\n const section = this.reactive.get('section', targetSectionId);\n if (section.component) {\n // Remove cmIds which are not allowed to be moved to this delegated section (mostly\n // all other delegated cm).\n droppedCmIds = droppedCmIds.filter(cmId => {\n const cmInfo = this.reactive.get('cm', cmId);\n return !cmInfo.hasdelegatedsection;\n });\n }\n if (droppedCmIds.length === 0) {\n return; // No cm to move.\n }\n this.reactive.dispatch('cmMove', droppedCmIds, targetSectionId, targetCmId);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Expand all the modal tree branches that contains the element.\n *\n * @private\n * @param {HTMLElement} modalBody the modal body element\n * @param {HTMLElement} element the element to display\n */\n _expandCmMoveModalParentSections(modalBody, element) {\n const sectionnode = element.closest(this.selectors.SECTIONNODE);\n if (!sectionnode) {\n return;\n }\n\n const toggler = sectionnode.querySelector(this.selectors.MODALTOGGLER);\n let collapsibleId = toggler.dataset.target ?? toggler.getAttribute('href');\n if (collapsibleId) {\n // We cannot be sure we have # in the id element name.\n collapsibleId = collapsibleId.replace('#', '');\n const expandNode = modalBody.querySelector(`#${collapsibleId}`);\n new Collapse(expandNode, {toggle: false}).show();\n }\n\n // Section are a tree structure, we need to expand all the parents.\n this._expandCmMoveModalParentSections(modalBody, sectionnode.parentElement);\n }\n\n /**\n * Handle a create section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestAddSection(target, event) {\n event.preventDefault();\n this.reactive.dispatch('addSection', target.dataset.id ?? 0);\n }\n\n /**\n * Handle a create subsection request.\n *\n * @deprecated since Moodle 5.0 MDL-83469.\n * @todo MDL-83851 This will be deleted in Moodle 6.0.\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestAddModule(target, event) {\n log.debug('AddModule action is deprecated. Use newModule instead');\n event.preventDefault();\n this.reactive.dispatch('addModule', target.dataset.modname, target.dataset.sectionnum, target.dataset.beforemod);\n }\n\n /**\n * Handle a new create subsection request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestNewModule(target, event) {\n event.preventDefault();\n this.reactive.dispatch('newModule', target.dataset.modname, target.dataset.sectionid, target.dataset.beforemod);\n }\n\n /**\n * Handle a delete section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestDeleteSection(target, event) {\n const sectionIds = this._getTargetIds(target);\n if (sectionIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n // We don't need confirmation to delete empty sections.\n let needsConfirmation = sectionIds.some(sectionId => {\n const sectionInfo = this.reactive.get('section', sectionId);\n const cmList = sectionInfo.cmlist ?? [];\n return (cmList.length || sectionInfo.hassummary || sectionInfo.rawtitle);\n });\n if (!needsConfirmation) {\n this._dispatchSectionDelete(sectionIds, target);\n return;\n }\n\n let bodyText = null;\n let titleText = null;\n if (sectionIds.length == 1) {\n titleText = this.reactive.getFormatString('sectiondelete_title');\n const sectionInfo = this.reactive.get('section', sectionIds[0]);\n bodyText = this.reactive.getFormatString('sectiondelete_info', {name: sectionInfo.title});\n } else {\n titleText = this.reactive.getFormatString('sectionsdelete_title');\n bodyText = this.reactive.getFormatString('sectionsdelete_info', {count: sectionIds.length});\n }\n\n const modal = await this._modalBodyRenderedPromise(ModalDeleteCancel, {\n title: titleText,\n body: bodyText,\n });\n\n modal.getRoot().on(\n ModalEvents.delete,\n e => {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n modal.destroy();\n this._dispatchSectionDelete(sectionIds, target);\n }\n );\n }\n\n /**\n * Dispatch the section delete action and handle the redirection if necessary.\n *\n * @param {Array} sectionIds the IDs of the sections to delete.\n * @param {Element} target the dispatch action element\n */\n async _dispatchSectionDelete(sectionIds, target) {\n await this.reactive.dispatch('sectionDelete', sectionIds);\n if (target.baseURI.includes('section.php')) {\n // Redirect to the course main page if the section is the current page.\n window.location.href = this.reactive.get('course').baseurl;\n }\n }\n\n /**\n * Handle a toggle cm selection.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestToggleSelectionCm(target, event) {\n toggleBulkSelectionAction(this.reactive, target, event, 'cm');\n }\n\n /**\n * Handle a toggle section selection.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestToggleSelectionSection(target, event) {\n toggleBulkSelectionAction(this.reactive, target, event, 'section');\n }\n\n /**\n * Basic mutation action helper.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n * @param {string} mutationName the mutation name\n */\n async _requestMutationAction(target, event, mutationName) {\n if (!target.dataset.id && target.dataset.for !== 'bulkaction') {\n return;\n }\n event.preventDefault();\n if (target.dataset.for === 'bulkaction') {\n // If the mutation is a bulk action we use the current selection.\n this.reactive.dispatch(mutationName, this.reactive.get('bulk').selection);\n } else {\n this.reactive.dispatch(mutationName, [target.dataset.id]);\n }\n }\n\n /**\n * Handle a course permalink modal request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n _requestPermalink(target, event) {\n event.preventDefault();\n ModalCopyToClipboard.create(\n {\n text: target.getAttribute('href'),\n },\n getString('sectionlink', 'course')\n );\n return;\n }\n\n /**\n * Handle a course module duplicate request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestCmDuplicate(target, event) {\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n const sectionId = target.dataset.sectionid ?? null;\n event.preventDefault();\n this.reactive.dispatch('cmDuplicate', cmIds, sectionId);\n }\n\n /**\n * Handle a delete cm request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestCmDelete(target, event) {\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n let bodyText = null;\n let titleText = null;\n let delegatedsection = null;\n if (cmIds.length == 1) {\n const cmInfo = this.reactive.get('cm', cmIds[0]);\n if (cmInfo.hasdelegatedsection) {\n delegatedsection = cmInfo.delegatesectionid;\n titleText = this.reactive.getFormatString('cmdelete_subsectiontitle');\n bodyText = getString(\n 'sectiondelete_info',\n 'core_courseformat',\n {\n type: cmInfo.modname,\n name: cmInfo.name,\n }\n );\n } else {\n titleText = this.reactive.getFormatString('cmdelete_title');\n bodyText = getString(\n 'cmdelete_info',\n 'core_courseformat',\n {\n type: cmInfo.modname,\n name: cmInfo.name,\n }\n );\n }\n } else {\n titleText = getString('cmsdelete_title', 'core_courseformat');\n bodyText = getString(\n 'cmsdelete_info',\n 'core_courseformat',\n {count: cmIds.length}\n );\n }\n\n const modal = await this._modalBodyRenderedPromise(ModalDeleteCancel, {\n title: titleText,\n body: bodyText,\n });\n\n modal.getRoot().on(\n ModalEvents.delete,\n e => {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n modal.destroy();\n this.reactive.dispatch('cmDelete', cmIds);\n if (cmIds.length == 1 && delegatedsection && target.baseURI.includes('section.php')) {\n // Redirect to the course main page if the subsection is the current page.\n let parameters = new URLSearchParams(window.location.search);\n if (parameters.has('id') && parameters.get('id') == delegatedsection) {\n this._dispatchSectionDelete([delegatedsection], target);\n }\n }\n }\n );\n }\n\n /**\n * Handle a cm availability change request.\n *\n * @param {Element} target the dispatch action element\n */\n async _requestCmAvailability(target) {\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n // Show the availability modal to decide which action to trigger.\n const exporter = this.reactive.getExporter();\n const data = {\n allowstealth: exporter.canUseStealth(this.reactive.state, cmIds),\n };\n const modal = await this._modalBodyRenderedPromise(ModalSaveCancel, {\n title: getString('availability', 'core'),\n body: Templates.render('core_courseformat/local/content/cm/availabilitymodal', data),\n saveButtonText: getString('apply', 'core'),\n });\n\n this._setupMutationRadioButtonModal(modal, cmIds);\n }\n\n /**\n * Handle a section availability change request.\n *\n * @param {Element} target the dispatch action element\n */\n async _requestSectionAvailability(target) {\n const sectionIds = this._getTargetIds(target);\n if (sectionIds.length == 0) {\n return;\n }\n const title = (sectionIds.length == 1) ? 'sectionavailability_title' : 'sectionsavailability_title';\n // Show the availability modal to decide which action to trigger.\n const modal = await this._modalBodyRenderedPromise(ModalSaveCancel, {\n title: this.reactive.getFormatString(title),\n body: Templates.render('core_courseformat/local/content/section/availabilitymodal', []),\n saveButtonText: getString('apply', 'core'),\n });\n\n this._setupMutationRadioButtonModal(modal, sectionIds);\n }\n\n /**\n * Add events to a mutation selector radio buttons modal.\n * @param {Modal} modal\n * @param {Number[]} ids the section or cm ids to apply the mutation\n */\n _setupMutationRadioButtonModal(modal, ids) {\n // The save button is not enabled until the user selects an option.\n modal.setButtonDisabled('save', true);\n\n const submitFunction = (radio) => {\n const mutation = radio?.value;\n if (!mutation) {\n return false;\n }\n this.reactive.dispatch(mutation, ids);\n return true;\n };\n\n const modalBody = getFirst(modal.getBody());\n const radioOptions = modalBody.querySelectorAll(this.selectors.OPTIONSRADIO);\n radioOptions.forEach(radio => {\n radio.addEventListener('change', () => {\n modal.setButtonDisabled('save', false);\n });\n radio.parentNode.addEventListener('click', () => {\n radio.checked = true;\n modal.setButtonDisabled('save', false);\n });\n radio.parentNode.addEventListener('dblclick', dbClickEvent => {\n if (submitFunction(radio)) {\n dbClickEvent.preventDefault();\n modal.destroy();\n }\n });\n });\n\n modal.getRoot().on(\n ModalEvents.save,\n () => {\n const radio = modalBody.querySelector(`${this.selectors.OPTIONSRADIO}:checked`);\n submitFunction(radio);\n }\n );\n }\n\n /**\n * Disable all add sections actions.\n *\n * @param {boolean} locked the new locked value.\n */\n _setAddSectionLocked(locked) {\n const targets = this.getElements(this.selectors.ADDSECTIONREGION);\n targets.forEach(element => {\n element.classList.toggle(this.classes.DISABLED, locked);\n const addSectionElement = element.querySelector(this.selectors.ADDSECTION);\n addSectionElement.classList.toggle(this.classes.DISABLED, locked);\n this.setElementLocked(addSectionElement, locked);\n // We tweak the element to show a tooltip as a title attribute.\n if (locked) {\n getString('sectionaddmax', 'core_courseformat')\n .then((text) => addSectionElement.setAttribute('title', text))\n .catch(Notification.exception);\n addSectionElement.style.pointerEvents = null; // Unlocks the pointer events.\n addSectionElement.style.userSelect = null; // Unlocks the pointer events.\n } else {\n addSectionElement.setAttribute('title', addSectionElement.dataset.addSections);\n }\n });\n const courseAddSection = this.getElement(this.selectors.COURSEADDSECTION);\n if (courseAddSection) {\n const addSection = courseAddSection.querySelector(this.selectors.ADDSECTION);\n addSection.classList.toggle(this.classes.DISPLAYNONE, locked);\n const noMoreSections = courseAddSection.querySelector(this.selectors.MAXSECTIONSWARNING);\n noMoreSections.classList.toggle(this.classes.DISPLAYNONE, !locked);\n }\n }\n\n /**\n * Replace an element with a copy with a different tag name.\n *\n * @param {Element} element the original element\n */\n _disableLink(element) {\n if (element) {\n element.style.pointerEvents = 'none';\n element.style.userSelect = 'none';\n element.classList.add(this.classes.DISABLED);\n element.classList.add(this.classes.ITALIC);\n element.setAttribute('aria-disabled', true);\n element.addEventListener('click', event => event.preventDefault());\n }\n }\n\n /**\n * Render a modal and return a body ready promise.\n *\n * @param {Modal} ModalClass the modal class\n * @param {object} modalParams the modal params\n * @return {Promise} the modal body ready promise\n */\n _modalBodyRenderedPromise(ModalClass, modalParams) {\n return new Promise((resolve, reject) => {\n ModalClass.create(modalParams).then((modal) => {\n modal.setRemoveOnClose(true);\n // Handle body loading event.\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n resolve(modal);\n });\n // Configure some extra modal params.\n if (modalParams.saveButtonText !== undefined) {\n modal.setSaveButtonText(modalParams.saveButtonText);\n }\n if (modalParams.deleteButtonText !== undefined) {\n modal.setDeleteButtonText(modalParams.saveButtonText);\n }\n modal.show();\n return;\n }).catch(() => {\n reject(`Cannot load modal content`);\n });\n });\n }\n\n /**\n * Hide and later destroy a modal.\n *\n * Behat will fail if we remove the modal while some boostrap collapse is executing.\n *\n * @param {Modal} modal\n * @param {HTMLElement} element the dom element to focus on.\n */\n _destroyModal(modal, element) {\n modal.hide();\n const pendingDestroy = new Pending(`courseformat/actions:destroyModal`);\n if (element) {\n element.focus();\n }\n setTimeout(() =>{\n modal.destroy();\n pendingDestroy.resolve();\n }, 500);\n }\n\n /**\n * Get the closest actions menu toggler to an action element.\n *\n * @param {HTMLElement} element the action link element\n * @returns {HTMLElement|undefined}\n */\n _getClosestActionMenuToogler(element) {\n const actionMenu = element.closest(this.selectors.ACTIONMENU);\n if (!actionMenu) {\n return undefined;\n }\n return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER);\n }\n}\n"],"names":["directMutations","sectionHide","sectionShow","cmHide","cmShow","cmStealth","cmMoveRight","cmMoveLeft","cmNoGroups","cmSeparateGroups","cmVisibleGroups","BaseComponent","create","name","selectors","ACTIONLINK","SECTIONLINK","CMLINK","SECTIONNODE","MODALTOGGLER","ADDSECTION","CONTENTTREE","ACTIONMENU","ACTIONMENUTOGGLER","OPTIONSRADIO","COURSEADDSECTION","MAXSECTIONSWARNING","ADDSECTIONREGION","classes","DISABLED","ITALIC","DISPLAYNONE","actions","action","mutationReference","Object","entries","Error","stateReady","state","addEventListener","this","element","_dispatchClick","_checkSectionlist","CourseEvents","sectionRefreshed","eventTypes","elementUpdated","_inplaceEditableHandler","getWatchers","watch","handler","event","target","closest","classList","contains","preventDefault","actionName","dataset","methodName","_actionMethodName","undefined","_requestMutationAction","requestName","charAt","toUpperCase","slice","_setAddSectionLocked","course","sectionlist","length","maxsections","itemtype","detail","_event$detail","ajaxreturn","_event$detail$ajaxret","itemid","parseInt","_event$detail2","_event$detail2$ajaxre","Number","isFinite","reactive","dispatch","_getTargetIds","ids","_target$dataset","id","push","bulkType","_target$dataset2","bulk","get","enabled","selectedType","selection","sectionIds","pendingModalReady","Pending","editTools","_getClosestActionMenuToogler","data","getExporter","titleText","sectionInfo","sectionid","sectiontitle","title","information","getFormatString","modal","_modalBodyRenderedPromise","Modal","body","Templates","render","modalBody","getBody","forEach","sectionId","currentElement","querySelector","_disableLink","ContentTree","SECTION","TOGGLER","COLLAPSE","matches","for","getAttribute","_destroyModal","resolve","cmIds","exporter","cmInfo","cmid","cmname","hasdelegatedsection","cmId","ENTER","selector","_expandCmMoveModalParentSections","targetSectionId","targetCmId","droppedCmIds","dropData","cmDraggableData","nextcmid","section","cmlist","component","filter","sectionnode","toggler","collapsibleId","replace","expandNode","Collapse","toggle","show","parentElement","debug","modname","sectionnum","beforemod","some","hassummary","rawtitle","_dispatchSectionDelete","bodyText","count","ModalDeleteCancel","getRoot","on","ModalEvents","delete","e","destroy","baseURI","includes","window","location","href","baseurl","mutationName","_requestPermalink","text","delegatedsection","delegatesectionid","type","parameters","URLSearchParams","search","has","allowstealth","canUseStealth","ModalSaveCancel","saveButtonText","_setupMutationRadioButtonModal","setButtonDisabled","submitFunction","radio","mutation","value","querySelectorAll","parentNode","checked","dbClickEvent","save","locked","getElements","addSectionElement","setElementLocked","then","setAttribute","catch","Notification","exception","style","pointerEvents","userSelect","addSections","courseAddSection","getElement","add","ModalClass","modalParams","Promise","reject","setRemoveOnClose","bodyRendered","setSaveButtonText","deleteButtonText","setDeleteButtonText","hide","pendingDestroy","focus","setTimeout","actionMenu"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"actions.min.js","sources":["../../../src/local/content/actions.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\n/**\n * Course state actions dispatcher.\n *\n * This module captures all data-dispatch links in the course content and dispatch the proper\n * state mutation, including any confirmation and modal required.\n *\n * @module core_courseformat/local/content/actions\n * @class core_courseformat/local/content/actions\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {eventTypes} from 'core/local/inplace_editable/events';\nimport Collapse from 'theme_boost/bootstrap/collapse';\nimport log from 'core/log';\nimport Modal from 'core/modal';\nimport ModalSaveCancel from 'core/modal_save_cancel';\nimport ModalDeleteCancel from 'core/modal_delete_cancel';\nimport ModalCopyToClipboard from 'core/modal_copy_to_clipboard';\nimport ModalEvents from 'core/modal_events';\nimport Templates from 'core/templates';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport {getFirst} from 'core/normalise';\nimport {toggleBulkSelectionAction} from 'core_courseformat/local/content/actions/bulkselection';\nimport Pending from 'core/pending';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n\n// Load global strings.\nprefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']);\n\n// Mutations are dispatched by the course content actions.\n// Formats can use this module addActions static method to add custom actions.\n// Direct mutations can be simple strings (mutation) name or functions.\nconst directMutations = {\n sectionHide: 'sectionHide',\n sectionShow: 'sectionShow',\n cmHide: 'cmHide',\n cmShow: 'cmShow',\n cmStealth: 'cmStealth',\n cmMoveRight: 'cmMoveRight',\n cmMoveLeft: 'cmMoveLeft',\n cmNoGroups: 'cmNoGroups',\n cmSeparateGroups: 'cmSeparateGroups',\n cmVisibleGroups: 'cmVisibleGroups',\n};\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_actions';\n // Default query selectors.\n this.selectors = {\n ACTIONLINK: `[data-action]`,\n // Move modal selectors.\n SECTIONLINK: `[data-for='section']`,\n CMLINK: `[data-for='cm']`,\n SECTIONNODE: `[data-for='sectionnode']`,\n MODALTOGGLER: `[data-bs-toggle='collapse']`,\n ADDSECTION: `[data-action='addSection']`,\n CONTENTTREE: `#destination-selector`,\n ACTIONMENU: `.action-menu`,\n ACTIONMENUTOGGLER: `[data-bs-toggle=\"dropdown\"]`,\n // Availability modal selectors.\n OPTIONSRADIO: `[type='radio']`,\n COURSEADDSECTION: `#course-addsection`,\n ADDSECTIONREGION: `[data-region='section-addsection']`,\n };\n // Component css classes.\n this.classes = {\n DISABLED: `disabled`,\n ITALIC: `fst-italic`,\n DISPLAYNONE: `d-none`,\n };\n }\n\n /**\n * Add extra actions to the module.\n *\n * @param {array} actions array of methods to execute\n */\n static addActions(actions) {\n for (const [action, mutationReference] of Object.entries(actions)) {\n if (typeof mutationReference !== 'function' && typeof mutationReference !== 'string') {\n throw new Error(`${action} action must be a mutation name or a function`);\n }\n directMutations[action] = mutationReference;\n }\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n // Delegate dispatch clicks.\n this.addEventListener(\n this.element,\n 'click',\n this._dispatchClick\n );\n // Any inplace editable update needs state refresh.\n this.addEventListener(\n this.element,\n eventTypes.elementUpdated,\n this._inplaceEditableHandler\n );\n }\n\n _dispatchClick(event) {\n const target = event.target.closest(this.selectors.ACTIONLINK);\n if (!target) {\n return;\n }\n if (target.classList.contains(this.classes.DISABLED)) {\n event.preventDefault();\n return;\n }\n\n // Invoke proper method.\n const actionName = target.dataset.action;\n const methodName = this._actionMethodName(actionName);\n\n if (this[methodName] !== undefined) {\n this[methodName](target, event);\n return;\n }\n\n // Check direct mutations or mutations handlers.\n if (directMutations[actionName] !== undefined) {\n if (typeof directMutations[actionName] === 'function') {\n directMutations[actionName](target, event);\n return;\n }\n this._requestMutationAction(target, event, directMutations[actionName]);\n return;\n }\n }\n\n _actionMethodName(name) {\n const requestName = name.charAt(0).toUpperCase() + name.slice(1);\n return `_request${requestName}`;\n }\n\n /**\n * Handle inplace editable updates.\n *\n * @param {Event} event the triggered event\n * @private\n */\n _inplaceEditableHandler(event) {\n const itemtype = event.detail?.ajaxreturn?.itemtype;\n const itemid = parseInt(event.detail?.ajaxreturn?.itemid);\n if (!Number.isFinite(itemid) || !itemtype) {\n return;\n }\n\n if (itemtype === 'activityname') {\n this.reactive.dispatch('cmState', [itemid]);\n return;\n }\n // Sections uses sectionname for normal sections and sectionnamenl for the no link sections.\n if (itemtype === 'sectionname' || itemtype === 'sectionnamenl') {\n this.reactive.dispatch('sectionState', [itemid]);\n return;\n }\n }\n\n /**\n * Return the ids represented by this element.\n *\n * Depending on the dataset attributes the action could represent a single id\n * or a bulk actions with all the current selected ids.\n *\n * @param {HTMLElement} target\n * @returns {Number[]} array of Ids\n */\n _getTargetIds(target) {\n let ids = [];\n if (target?.dataset?.id) {\n ids.push(target.dataset.id);\n }\n const bulkType = target?.dataset?.bulk;\n if (!bulkType) {\n return ids;\n }\n const bulk = this.reactive.get('bulk');\n if (bulk.enabled && bulk.selectedType === bulkType) {\n ids = [...ids, ...bulk.selection];\n }\n return ids;\n }\n\n /**\n * Handle a move section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveSection(target, event) {\n // Check we have an id.\n const sectionIds = this._getTargetIds(target);\n if (sectionIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveSectionModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n let titleText = null;\n\n // Add the target section id and title.\n let sectionInfo = null;\n if (sectionIds.length == 1) {\n sectionInfo = this.reactive.get('section', sectionIds[0]);\n data.sectionid = sectionInfo.id;\n data.sectiontitle = sectionInfo.title;\n data.information = await this.reactive.getFormatString('sectionmove_info', data.sectiontitle);\n titleText = this.reactive.getFormatString('sectionmove_title');\n } else {\n data.information = await this.reactive.getFormatString('sectionsmove_info', sectionIds.length);\n titleText = this.reactive.getFormatString('sectionsmove_title');\n }\n\n\n // Create the modal.\n // Build the modal parameters from the event data.\n const modal = await this._modalBodyRenderedPromise(Modal, {\n title: titleText,\n body: Templates.render('core_courseformat/local/content/movesection', data),\n });\n\n const modalBody = getFirst(modal.getBody());\n\n // Disable current selected section ids.\n sectionIds.forEach(sectionId => {\n const currentElement = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-id='${sectionId}']`);\n this._disableLink(currentElement);\n });\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n },\n true\n );\n\n // Capture click.\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for != 'section' || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n this.reactive.dispatch('sectionMoveAfter', sectionIds, target.dataset.id);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Handle a move cm request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveCm(target, event) {\n // Check we have an id.\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveCmModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n\n let titleText = null;\n if (cmIds.length == 1) {\n const cmInfo = this.reactive.get('cm', cmIds[0]);\n data.cmid = cmInfo.id;\n data.cmname = cmInfo.name;\n data.information = await this.reactive.getFormatString('cmmove_info', data.cmname);\n if (cmInfo.hasdelegatedsection) {\n titleText = this.reactive.getFormatString('cmmove_subsectiontitle');\n } else {\n titleText = this.reactive.getFormatString('cmmove_title');\n }\n } else {\n data.information = await this.reactive.getFormatString('cmsmove_info', cmIds.length);\n titleText = this.reactive.getFormatString('cmsmove_title');\n }\n\n // Create the modal.\n // Build the modal parameters from the event data.\n const modal = await this._modalBodyRenderedPromise(Modal, {\n title: titleText,\n body: Templates.render('core_courseformat/local/content/movecm', data),\n });\n\n const modalBody = getFirst(modal.getBody());\n\n // Disable current selected section ids.\n cmIds.forEach(cmId => {\n const currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);\n this._disableLink(currentElement);\n });\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n ENTER: this.selectors.SECTIONLINK,\n }\n );\n\n cmIds.forEach(cmId => {\n const cmInfo = this.reactive.get('cm', cmId);\n let selector;\n if (!cmInfo.hasdelegatedsection) {\n selector = `${this.selectors.CMLINK}[data-id='${cmId}']`;\n } else {\n selector = `${this.selectors.SECTIONLINK}[data-id='${cmInfo.sectionid}']`;\n }\n const currentElement = modalBody.querySelector(selector);\n this._expandCmMoveModalParentSections(modalBody, currentElement);\n });\n\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for === undefined || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n\n let targetSectionId;\n let targetCmId;\n let droppedCmIds = [...cmIds];\n if (target.dataset.for == 'cm') {\n const dropData = exporter.cmDraggableData(this.reactive.state, target.dataset.id);\n targetSectionId = dropData.sectionid;\n targetCmId = dropData.nextcmid;\n } else {\n const section = this.reactive.get('section', target.dataset.id);\n targetSectionId = target.dataset.id;\n targetCmId = section?.cmlist[0];\n }\n const section = this.reactive.get('section', targetSectionId);\n if (section.component) {\n // Remove cmIds which are not allowed to be moved to this delegated section (mostly\n // all other delegated cm).\n droppedCmIds = droppedCmIds.filter(cmId => {\n const cmInfo = this.reactive.get('cm', cmId);\n return !cmInfo.hasdelegatedsection;\n });\n }\n if (droppedCmIds.length === 0) {\n return; // No cm to move.\n }\n this.reactive.dispatch('cmMove', droppedCmIds, targetSectionId, targetCmId);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Expand all the modal tree branches that contains the element.\n *\n * @private\n * @param {HTMLElement} modalBody the modal body element\n * @param {HTMLElement} element the element to display\n */\n _expandCmMoveModalParentSections(modalBody, element) {\n const sectionnode = element.closest(this.selectors.SECTIONNODE);\n if (!sectionnode) {\n return;\n }\n\n const toggler = sectionnode.querySelector(this.selectors.MODALTOGGLER);\n let collapsibleId = toggler.dataset.target ?? toggler.getAttribute('href');\n if (collapsibleId) {\n // We cannot be sure we have # in the id element name.\n collapsibleId = collapsibleId.replace('#', '');\n const expandNode = modalBody.querySelector(`#${collapsibleId}`);\n new Collapse(expandNode, {toggle: false}).show();\n }\n\n // Section are a tree structure, we need to expand all the parents.\n this._expandCmMoveModalParentSections(modalBody, sectionnode.parentElement);\n }\n\n /**\n * Handle a create section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestAddSection(target, event) {\n event.preventDefault();\n this.reactive.dispatch('addSection', target.dataset.id ?? 0);\n }\n\n /**\n * Handle a create subsection request.\n *\n * @deprecated since Moodle 5.0 MDL-83469.\n * @todo MDL-83851 This will be deleted in Moodle 6.0.\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestAddModule(target, event) {\n log.debug('AddModule action is deprecated. Use newModule instead');\n event.preventDefault();\n this.reactive.dispatch('addModule', target.dataset.modname, target.dataset.sectionnum, target.dataset.beforemod);\n }\n\n /**\n * Handle a new create subsection request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestNewModule(target, event) {\n event.preventDefault();\n this.reactive.dispatch('newModule', target.dataset.modname, target.dataset.sectionid, target.dataset.beforemod);\n }\n\n /**\n * Handle a delete section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestDeleteSection(target, event) {\n const sectionIds = this._getTargetIds(target);\n if (sectionIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n // We don't need confirmation to delete empty sections.\n let needsConfirmation = sectionIds.some(sectionId => {\n const sectionInfo = this.reactive.get('section', sectionId);\n const cmList = sectionInfo.cmlist ?? [];\n return (cmList.length || sectionInfo.hassummary || sectionInfo.rawtitle);\n });\n if (!needsConfirmation) {\n this._dispatchSectionDelete(sectionIds, target);\n return;\n }\n\n let bodyText = null;\n let titleText = null;\n if (sectionIds.length == 1) {\n titleText = this.reactive.getFormatString('sectiondelete_title');\n const sectionInfo = this.reactive.get('section', sectionIds[0]);\n bodyText = this.reactive.getFormatString('sectiondelete_info', {name: sectionInfo.title});\n } else {\n titleText = this.reactive.getFormatString('sectionsdelete_title');\n bodyText = this.reactive.getFormatString('sectionsdelete_info', {count: sectionIds.length});\n }\n\n const modal = await this._modalBodyRenderedPromise(ModalDeleteCancel, {\n title: titleText,\n body: bodyText,\n });\n\n modal.getRoot().on(\n ModalEvents.delete,\n e => {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n modal.destroy();\n this._dispatchSectionDelete(sectionIds, target);\n }\n );\n }\n\n /**\n * Dispatch the section delete action and handle the redirection if necessary.\n *\n * @param {Array} sectionIds the IDs of the sections to delete.\n * @param {Element} target the dispatch action element\n */\n async _dispatchSectionDelete(sectionIds, target) {\n await this.reactive.dispatch('sectionDelete', sectionIds);\n if (target.baseURI.includes('section.php')) {\n // Redirect to the course main page if the section is the current page.\n window.location.href = this.reactive.get('course').baseurl;\n }\n }\n\n /**\n * Handle a toggle cm selection.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestToggleSelectionCm(target, event) {\n toggleBulkSelectionAction(this.reactive, target, event, 'cm');\n }\n\n /**\n * Handle a toggle section selection.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestToggleSelectionSection(target, event) {\n toggleBulkSelectionAction(this.reactive, target, event, 'section');\n }\n\n /**\n * Basic mutation action helper.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n * @param {string} mutationName the mutation name\n */\n async _requestMutationAction(target, event, mutationName) {\n if (!target.dataset.id && target.dataset.for !== 'bulkaction') {\n return;\n }\n event.preventDefault();\n if (target.dataset.for === 'bulkaction') {\n // If the mutation is a bulk action we use the current selection.\n this.reactive.dispatch(mutationName, this.reactive.get('bulk').selection);\n } else {\n this.reactive.dispatch(mutationName, [target.dataset.id]);\n }\n }\n\n /**\n * Handle a course permalink modal request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n _requestPermalink(target, event) {\n event.preventDefault();\n ModalCopyToClipboard.create(\n {\n text: target.getAttribute('href'),\n },\n getString('sectionlink', 'course')\n );\n return;\n }\n\n /**\n * Handle a course module duplicate request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestCmDuplicate(target, event) {\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n const sectionId = target.dataset.sectionid ?? null;\n event.preventDefault();\n this.reactive.dispatch('cmDuplicate', cmIds, sectionId);\n }\n\n /**\n * Handle a delete cm request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestCmDelete(target, event) {\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n\n event.preventDefault();\n\n let bodyText = null;\n let titleText = null;\n let delegatedsection = null;\n if (cmIds.length == 1) {\n const cmInfo = this.reactive.get('cm', cmIds[0]);\n if (cmInfo.hasdelegatedsection) {\n delegatedsection = cmInfo.delegatesectionid;\n titleText = this.reactive.getFormatString('cmdelete_subsectiontitle');\n bodyText = getString(\n 'sectiondelete_info',\n 'core_courseformat',\n {\n type: cmInfo.modname,\n name: cmInfo.name,\n }\n );\n } else {\n titleText = this.reactive.getFormatString('cmdelete_title');\n bodyText = getString(\n 'cmdelete_info',\n 'core_courseformat',\n {\n type: cmInfo.modname,\n name: cmInfo.name,\n }\n );\n }\n } else {\n titleText = getString('cmsdelete_title', 'core_courseformat');\n bodyText = getString(\n 'cmsdelete_info',\n 'core_courseformat',\n {count: cmIds.length}\n );\n }\n\n const modal = await this._modalBodyRenderedPromise(ModalDeleteCancel, {\n title: titleText,\n body: bodyText,\n });\n\n modal.getRoot().on(\n ModalEvents.delete,\n e => {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n modal.destroy();\n this.reactive.dispatch('cmDelete', cmIds);\n if (cmIds.length == 1 && delegatedsection && target.baseURI.includes('section.php')) {\n // Redirect to the course main page if the subsection is the current page.\n let parameters = new URLSearchParams(window.location.search);\n if (parameters.has('id') && parameters.get('id') == delegatedsection) {\n this._dispatchSectionDelete([delegatedsection], target);\n }\n }\n }\n );\n }\n\n /**\n * Handle a cm availability change request.\n *\n * @param {Element} target the dispatch action element\n */\n async _requestCmAvailability(target) {\n const cmIds = this._getTargetIds(target);\n if (cmIds.length == 0) {\n return;\n }\n // Show the availability modal to decide which action to trigger.\n const exporter = this.reactive.getExporter();\n const data = {\n allowstealth: exporter.canUseStealth(this.reactive.state, cmIds),\n };\n const modal = await this._modalBodyRenderedPromise(ModalSaveCancel, {\n title: getString('availability', 'core'),\n body: Templates.render('core_courseformat/local/content/cm/availabilitymodal', data),\n saveButtonText: getString('apply', 'core'),\n });\n\n this._setupMutationRadioButtonModal(modal, cmIds);\n }\n\n /**\n * Handle a section availability change request.\n *\n * @param {Element} target the dispatch action element\n */\n async _requestSectionAvailability(target) {\n const sectionIds = this._getTargetIds(target);\n if (sectionIds.length == 0) {\n return;\n }\n const title = (sectionIds.length == 1) ? 'sectionavailability_title' : 'sectionsavailability_title';\n // Show the availability modal to decide which action to trigger.\n const modal = await this._modalBodyRenderedPromise(ModalSaveCancel, {\n title: this.reactive.getFormatString(title),\n body: Templates.render('core_courseformat/local/content/section/availabilitymodal', []),\n saveButtonText: getString('apply', 'core'),\n });\n\n this._setupMutationRadioButtonModal(modal, sectionIds);\n }\n\n /**\n * Add events to a mutation selector radio buttons modal.\n * @param {Modal} modal\n * @param {Number[]} ids the section or cm ids to apply the mutation\n */\n _setupMutationRadioButtonModal(modal, ids) {\n // The save button is not enabled until the user selects an option.\n modal.setButtonDisabled('save', true);\n\n const submitFunction = (radio) => {\n const mutation = radio?.value;\n if (!mutation) {\n return false;\n }\n this.reactive.dispatch(mutation, ids);\n return true;\n };\n\n const modalBody = getFirst(modal.getBody());\n const radioOptions = modalBody.querySelectorAll(this.selectors.OPTIONSRADIO);\n radioOptions.forEach(radio => {\n radio.addEventListener('change', () => {\n modal.setButtonDisabled('save', false);\n });\n radio.parentNode.addEventListener('click', () => {\n radio.checked = true;\n modal.setButtonDisabled('save', false);\n });\n radio.parentNode.addEventListener('dblclick', dbClickEvent => {\n if (submitFunction(radio)) {\n dbClickEvent.preventDefault();\n modal.destroy();\n }\n });\n });\n\n modal.getRoot().on(\n ModalEvents.save,\n () => {\n const radio = modalBody.querySelector(`${this.selectors.OPTIONSRADIO}:checked`);\n submitFunction(radio);\n }\n );\n }\n\n /**\n * Replace an element with a copy with a different tag name.\n *\n * @param {Element} element the original element\n */\n _disableLink(element) {\n if (element) {\n element.style.pointerEvents = 'none';\n element.style.userSelect = 'none';\n element.classList.add(this.classes.DISABLED);\n element.classList.add(this.classes.ITALIC);\n element.setAttribute('aria-disabled', true);\n element.addEventListener('click', event => event.preventDefault());\n }\n }\n\n /**\n * Render a modal and return a body ready promise.\n *\n * @param {Modal} ModalClass the modal class\n * @param {object} modalParams the modal params\n * @return {Promise} the modal body ready promise\n */\n _modalBodyRenderedPromise(ModalClass, modalParams) {\n return new Promise((resolve, reject) => {\n ModalClass.create(modalParams).then((modal) => {\n modal.setRemoveOnClose(true);\n // Handle body loading event.\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n resolve(modal);\n });\n // Configure some extra modal params.\n if (modalParams.saveButtonText !== undefined) {\n modal.setSaveButtonText(modalParams.saveButtonText);\n }\n if (modalParams.deleteButtonText !== undefined) {\n modal.setDeleteButtonText(modalParams.saveButtonText);\n }\n modal.show();\n return;\n }).catch(() => {\n reject(`Cannot load modal content`);\n });\n });\n }\n\n /**\n * Hide and later destroy a modal.\n *\n * Behat will fail if we remove the modal while some boostrap collapse is executing.\n *\n * @param {Modal} modal\n * @param {HTMLElement} element the dom element to focus on.\n */\n _destroyModal(modal, element) {\n modal.hide();\n const pendingDestroy = new Pending(`courseformat/actions:destroyModal`);\n if (element) {\n element.focus();\n }\n setTimeout(() =>{\n modal.destroy();\n pendingDestroy.resolve();\n }, 500);\n }\n\n /**\n * Get the closest actions menu toggler to an action element.\n *\n * @param {HTMLElement} element the action link element\n * @returns {HTMLElement|undefined}\n */\n _getClosestActionMenuToogler(element) {\n const actionMenu = element.closest(this.selectors.ACTIONMENU);\n if (!actionMenu) {\n return undefined;\n }\n return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER);\n }\n}\n"],"names":["directMutations","sectionHide","sectionShow","cmHide","cmShow","cmStealth","cmMoveRight","cmMoveLeft","cmNoGroups","cmSeparateGroups","cmVisibleGroups","BaseComponent","create","name","selectors","ACTIONLINK","SECTIONLINK","CMLINK","SECTIONNODE","MODALTOGGLER","ADDSECTION","CONTENTTREE","ACTIONMENU","ACTIONMENUTOGGLER","OPTIONSRADIO","COURSEADDSECTION","ADDSECTIONREGION","classes","DISABLED","ITALIC","DISPLAYNONE","actions","action","mutationReference","Object","entries","Error","stateReady","addEventListener","this","element","_dispatchClick","eventTypes","elementUpdated","_inplaceEditableHandler","event","target","closest","classList","contains","preventDefault","actionName","dataset","methodName","_actionMethodName","undefined","_requestMutationAction","requestName","charAt","toUpperCase","slice","itemtype","detail","_event$detail","ajaxreturn","_event$detail$ajaxret","itemid","parseInt","_event$detail2","_event$detail2$ajaxre","Number","isFinite","reactive","dispatch","_getTargetIds","ids","_target$dataset","id","push","bulkType","_target$dataset2","bulk","get","enabled","selectedType","selection","sectionIds","length","pendingModalReady","Pending","editTools","_getClosestActionMenuToogler","data","getExporter","course","state","titleText","sectionInfo","sectionid","sectiontitle","title","information","getFormatString","modal","_modalBodyRenderedPromise","Modal","body","Templates","render","modalBody","getBody","forEach","sectionId","currentElement","querySelector","_disableLink","ContentTree","SECTION","TOGGLER","COLLAPSE","matches","for","getAttribute","_destroyModal","resolve","cmIds","exporter","cmInfo","cmid","cmname","hasdelegatedsection","cmId","ENTER","selector","_expandCmMoveModalParentSections","targetSectionId","targetCmId","droppedCmIds","dropData","cmDraggableData","nextcmid","section","cmlist","component","filter","sectionnode","toggler","collapsibleId","replace","expandNode","Collapse","toggle","show","parentElement","debug","modname","sectionnum","beforemod","some","hassummary","rawtitle","_dispatchSectionDelete","bodyText","count","ModalDeleteCancel","getRoot","on","ModalEvents","delete","e","destroy","baseURI","includes","window","location","href","baseurl","mutationName","_requestPermalink","text","delegatedsection","delegatesectionid","type","parameters","URLSearchParams","search","has","allowstealth","canUseStealth","ModalSaveCancel","saveButtonText","_setupMutationRadioButtonModal","setButtonDisabled","submitFunction","radio","mutation","value","querySelectorAll","parentNode","checked","dbClickEvent","save","style","pointerEvents","userSelect","add","setAttribute","ModalClass","modalParams","Promise","reject","then","setRemoveOnClose","bodyRendered","setSaveButtonText","deleteButtonText","setDeleteButtonText","catch","hide","pendingDestroy","focus","setTimeout","actionMenu"],"mappings":"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"} \ No newline at end of file diff --git a/course/format/amd/src/local/content/actions.js b/course/format/amd/src/local/content/actions.js index 8fc375ffaec..1a138aaf493 100644 --- a/course/format/amd/src/local/content/actions.js +++ b/course/format/amd/src/local/content/actions.js @@ -39,11 +39,8 @@ import {prefetchStrings} from 'core/prefetch'; import {getString} from 'core/str'; import {getFirst} from 'core/normalise'; import {toggleBulkSelectionAction} from 'core_courseformat/local/content/actions/bulkselection'; -import * as CourseEvents from 'core_course/events'; import Pending from 'core/pending'; import ContentTree from 'core_courseformat/local/courseeditor/contenttree'; -// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated. -import Notification from "core/notification"; // Load global strings. prefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']); @@ -87,7 +84,6 @@ export default class extends BaseComponent { // Availability modal selectors. OPTIONSRADIO: `[type='radio']`, COURSEADDSECTION: `#course-addsection`, - MAXSECTIONSWARNING: `[data-region='max-sections-warning']`, ADDSECTIONREGION: `[data-region='section-addsection']`, }; // Component css classes. @@ -114,25 +110,14 @@ export default class extends BaseComponent { /** * Initial state ready method. - * - * @param {Object} state the state data. - * */ - stateReady(state) { + stateReady() { // Delegate dispatch clicks. this.addEventListener( this.element, 'click', this._dispatchClick ); - // Check section limit. - this._checkSectionlist({state}); - // Add an Event listener to recalculate limits it if a section HTML is altered. - this.addEventListener( - this.element, - CourseEvents.sectionRefreshed, - () => this._checkSectionlist({state}) - ); // Any inplace editable update needs state refresh. this.addEventListener( this.element, @@ -141,18 +126,6 @@ export default class extends BaseComponent { ); } - /** - * Return the component watchers. - * - * @returns {Array} of watchers - */ - getWatchers() { - return [ - // Check section limit. - {watch: `course.sectionlist:updated`, handler: this._checkSectionlist}, - ]; - } - _dispatchClick(event) { const target = event.target.closest(this.selectors.ACTIONLINK); if (!target) { @@ -188,17 +161,6 @@ export default class extends BaseComponent { return `_request${requestName}`; } - /** - * Check the section list and disable some options if needed. - * - * @param {Object} detail the update details. - * @param {Object} detail.state the state object. - */ - _checkSectionlist({state}) { - // Disable "add section" actions if the course max sections has been exceeded. - this._setAddSectionLocked(state.course.sectionlist.length > state.course.maxsections); - } - /** * Handle inplace editable updates. * @@ -813,38 +775,6 @@ export default class extends BaseComponent { ); } - /** - * Disable all add sections actions. - * - * @param {boolean} locked the new locked value. - */ - _setAddSectionLocked(locked) { - const targets = this.getElements(this.selectors.ADDSECTIONREGION); - targets.forEach(element => { - element.classList.toggle(this.classes.DISABLED, locked); - const addSectionElement = element.querySelector(this.selectors.ADDSECTION); - addSectionElement.classList.toggle(this.classes.DISABLED, locked); - this.setElementLocked(addSectionElement, locked); - // We tweak the element to show a tooltip as a title attribute. - if (locked) { - getString('sectionaddmax', 'core_courseformat') - .then((text) => addSectionElement.setAttribute('title', text)) - .catch(Notification.exception); - addSectionElement.style.pointerEvents = null; // Unlocks the pointer events. - addSectionElement.style.userSelect = null; // Unlocks the pointer events. - } else { - addSectionElement.setAttribute('title', addSectionElement.dataset.addSections); - } - }); - const courseAddSection = this.getElement(this.selectors.COURSEADDSECTION); - if (courseAddSection) { - const addSection = courseAddSection.querySelector(this.selectors.ADDSECTION); - addSection.classList.toggle(this.classes.DISPLAYNONE, locked); - const noMoreSections = courseAddSection.querySelector(this.selectors.MAXSECTIONSWARNING); - noMoreSections.classList.toggle(this.classes.DISPLAYNONE, !locked); - } - } - /** * Replace an element with a copy with a different tag name. * diff --git a/course/format/classes/base.php b/course/format/classes/base.php index c5f04b9afb8..2ead7de7a00 100644 --- a/course/format/classes/base.php +++ b/course/format/classes/base.php @@ -448,9 +448,19 @@ abstract class base { /** * Method used to get the maximum number of sections for this course format. + * + * @deprecated Since 5.1 the setting is removed. + * @todo Remove this method in Moodle 6.0 (MDL-85272). * @return int */ + #[\core\attribute\deprecated( + replacement: null, + reason: 'The maxsection setting is removed.', + since: '5.1', + mdl: 'MDL-84291', + )] public function get_max_sections() { + \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]); $maxsections = get_config('moodlecourse', 'maxsections'); if (!isset($maxsections) || !is_numeric($maxsections)) { $maxsections = 52; diff --git a/course/format/classes/output/local/state/course.php b/course/format/classes/output/local/state/course.php index cd2059d3fe7..53a427cc6e8 100644 --- a/course/format/classes/output/local/state/course.php +++ b/course/format/classes/output/local/state/course.php @@ -67,7 +67,6 @@ class course implements renderable { 'sectionlist' => [], 'editmode' => $format->show_editor(), 'highlighted' => $format->get_section_highlighted_name(), - 'maxsections' => $format->get_max_sections(), 'baseurl' => $url->out(), 'statekey' => course_format::session_cache($course), 'maxbytes' => $maxbytes, diff --git a/course/format/templates/local/content/addsection.mustache b/course/format/templates/local/content/addsection.mustache index eed967b0403..d63db96f499 100644 --- a/course/format/templates/local/content/addsection.mustache +++ b/course/format/templates/local/content/addsection.mustache @@ -61,14 +61,6 @@ {{#pix}} t/add, core {{/pix}} {{title}} -
-
- {{#pix}}t/block, moodle{{/pix}} -
-
- {{#str}}maxsectionaddmessage, core_courseformat{{/str}} -
-
{{/addsections}} {{/showaddsection}} diff --git a/lang/en/courseformat.php b/lang/en/courseformat.php index dd761553f90..0f868ae1aad 100644 --- a/lang/en/courseformat.php +++ b/lang/en/courseformat.php @@ -69,7 +69,6 @@ $string['cmsmove_title'] = 'Move selected activities'; $string['cmsmove_info'] = 'Move {$a} activities after'; $string['courseindex'] = 'Course index'; $string['courseindexoptions'] = 'Course index options'; -$string['maxsectionaddmessage'] = 'You have reached the maximum number of sections allowed for a course.'; $string['nobulkaction'] = 'No bulk actions available'; $string['orphansectionwarning'] = 'This section and its content are not part of the course structure and are not visible to students. To use any of this content, move it to a different section.'; $string['preference:coursesectionspreferences'] = 'Section user preferences for course {$a}'; @@ -96,3 +95,6 @@ $string['sectionsmove_info'] = 'Move {$a} sections after'; $string['sectionsmove_title'] = 'Move selected sections'; $string['selectcm'] = 'Select activity {$a}'; $string['selectsection'] = 'Select section {$a}'; + +// Deprecated since Moodle 5.1. +$string['maxsectionaddmessage'] = 'You have reached the maximum number of sections allowed for a course.'; diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index 7cc37d8cece..66bd6723046 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -82,3 +82,5 @@ lockverbose,core_grades showverbose,core_grades unlockverbose,core_grades aiusagestats,core_hub +maxsectionslimit,core +maxsectionaddmessage,core_courseformat diff --git a/lang/en/moodle.php b/lang/en/moodle.php index a8e2c53490c..a84ec7301ee 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1288,7 +1288,6 @@ $string['match'] = 'Match'; $string['matchingsearchandrole'] = 'Matching \'{$a->search}\' and {$a->role}'; $string['matchofthefollowing'] = 'of the following:'; $string['maxareabytesreached'] = 'The file (or the total size of several files) is larger than the space remaining in this area.'; -$string['maxsectionslimit'] = 'Cannot create new section as it would exceed the maximum number of sections allowed for this course ({$a}).'; $string['maxfilesize'] = 'Maximum size for new files: {$a}'; $string['maxfilesreached'] = 'You are allowed to attach a maximum of {$a} file(s) to this item'; $string['maximumchars'] = 'Maximum of {$a} characters'; @@ -1297,8 +1296,8 @@ $string['maximumgradex'] = 'Maximum grade: {$a}'; $string['maximumshort'] = 'Max'; $string['maximumupload'] = 'Maximum upload size'; $string['maximumupload_help'] = 'The maximum file size allowed for student uploads to the course. Additionally, you can further restrict the maximum upload size for each activity.'; -$string['maxnumberweeks'] = 'Maximum number of sections'; -$string['maxnumberweeks_desc'] = 'The maximum value in the number of sections drop-down menu (applies to certain course formats only).'; +$string['maxnumberweeks'] = 'Maximum number of sections (deprecated)'; +$string['maxnumberweeks_desc'] = 'The maximum value in the number of sections drop-down menu (applies to certain course formats only). This setting is deprecated and will be removed in a future version of Moodle. Please use the course format settings instead.'; $string['maxnumcoursesincombo'] = 'Browse {$a->numberofcourses} courses.'; $string['maxsize'] = 'Max size: {$a}'; $string['maxsizeandareasize'] = 'Maximum size for new files: {$a->size}, overall limit: {$a->areasize}'; @@ -2488,3 +2487,6 @@ $string['registrationcontactyes'] = 'Yes, provide a form for other Moodlers to c // Deprecated since Moodle 5.0. $string['failedtaskcontexturlname'] = 'Status report'; + +// Deprecated since Moodle 5.1. +$string['maxsectionslimit'] = 'Cannot create new section as it would exceed the maximum number of sections allowed for this course ({$a}).'; diff --git a/mod/subsection/classes/permission.php b/mod/subsection/classes/permission.php index 1ed0a6db481..b8f14735b2f 100644 --- a/mod/subsection/classes/permission.php +++ b/mod/subsection/classes/permission.php @@ -49,9 +49,6 @@ class permission { return false; } $format = course_get_format($section->course); - if ($format->get_last_section_number() >= $format->get_max_sections()) { - return false; - } if (!$format->supports_components()) { return false; } diff --git a/mod/subsection/tests/permission_test.php b/mod/subsection/tests/permission_test.php index a0acc371cb5..9171665ede8 100644 --- a/mod/subsection/tests/permission_test.php +++ b/mod/subsection/tests/permission_test.php @@ -37,7 +37,6 @@ final class permission_test extends advanced_testcase { * @param bool $ismoddisabled * @param bool $missingcapability * @param bool $isdelegated - * @param bool $maxsectionsreached * @param string $format * @param bool $expected * @@ -47,7 +46,6 @@ final class permission_test extends advanced_testcase { bool $ismoddisabled, bool $missingcapability, bool $isdelegated, - bool $maxsectionsreached, string $format, bool $expected ): void { @@ -68,10 +66,6 @@ final class permission_test extends advanced_testcase { assign_capability('mod/subsection:addinstance', CAP_PROHIBIT, $userrole, context_course::instance($course->id)); } - if ($maxsectionsreached) { - set_config('maxsections', 5, 'moodlecourse'); - } - if ($isdelegated) { $this->getDataGenerator()->create_module('subsection', ['course' => $course->id, 'section' => 1]); $targetsection = $courseformat->get_modinfo()->get_section_info(6); @@ -92,7 +86,6 @@ final class permission_test extends advanced_testcase { 'ismoddisabled' => true, 'missingcapability' => false, 'isdelegated' => false, - 'maxsectionsreached' => false, 'format' => 'topics', 'expected' => false, ], @@ -100,15 +93,6 @@ final class permission_test extends advanced_testcase { 'ismoddisabled' => false, 'missingcapability' => true, 'isdelegated' => false, - 'maxsectionsreached' => false, - 'format' => 'topics', - 'expected' => false, - ], - 'Max sections reached' => [ - 'ismoddisabled' => false, - 'missingcapability' => false, - 'isdelegated' => false, - 'maxsectionsreached' => true, 'format' => 'topics', 'expected' => false, ], @@ -116,7 +100,6 @@ final class permission_test extends advanced_testcase { 'ismoddisabled' => false, 'missingcapability' => false, 'isdelegated' => true, - 'maxsectionsreached' => false, 'format' => 'topics', 'expected' => false, ], @@ -124,15 +107,13 @@ final class permission_test extends advanced_testcase { 'ismoddisabled' => false, 'missingcapability' => false, 'isdelegated' => false, - 'maxsectionsreached' => false, 'format' => 'singleactivity', 'expected' => false, ], - 'Plugin enabled, with capability, max sections not reached, not inside a delegated section' => [ + 'Plugin enabled, with capability, not inside a delegated section' => [ 'ismoddisabled' => false, 'missingcapability' => false, 'isdelegated' => false, - 'maxsectionsreached' => false, 'format' => 'topics', 'expected' => true, ], From 3bc7ca55134a0a79efc5defba4f2c6484837c595 Mon Sep 17 00:00:00 2001 From: ferran Date: Thu, 24 Apr 2025 11:19:26 +0200 Subject: [PATCH 3/6] MDL-84291 course: remove max section behat tests --- .../tests/behat/course_courseindex.feature | 14 ------ .../tests/behat/max_number_sections.feature | 50 ------------------- .../tests/behat/subsection_limit.feature | 29 ----------- 3 files changed, 93 deletions(-) delete mode 100644 course/tests/behat/max_number_sections.feature delete mode 100644 mod/subsection/tests/behat/subsection_limit.feature diff --git a/course/format/tests/behat/course_courseindex.feature b/course/format/tests/behat/course_courseindex.feature index 93333bfb262..2106f571fc4 100644 --- a/course/format/tests/behat/course_courseindex.feature +++ b/course/format/tests/behat/course_courseindex.feature @@ -381,17 +381,3 @@ Feature: Course index depending on role And I turn editing mode on When I set the field "Edit section name" in the "page-header" "region" to "Custom section name" Then I should see "Custom section name" in the "courseindex-content" "region" - - @javascript - Scenario: We cannot add a section when the number of section reaches maxsections but as soon as we reach under the limit we can add a section again. - Given the following config values are set as admin: - | maxsections | 4 | moodlecourse| - And I log in as "teacher1" - And I am on "Course 1" course homepage with editing mode on - Then I should see "Section 1" in the "courseindex-content" "region" - And ".disabled" "css_element" should exist in the "[data-action='addSection']" "css_element" - And I should see "You have reached the maximum number of sections allowed for a course." - And I delete section "4" - And I click on "Delete" "button" in the ".modal" "css_element" - And ".disabled" "css_element" should not exist in the "[data-action='addSection']" "css_element" - And I should not see "You have reached the maximum number of sections allowed for a course." diff --git a/course/tests/behat/max_number_sections.feature b/course/tests/behat/max_number_sections.feature deleted file mode 100644 index 9c64b7fa992..00000000000 --- a/course/tests/behat/max_number_sections.feature +++ /dev/null @@ -1,50 +0,0 @@ -@core @core_course -Feature: The maximum number of weeks/topics in a course can be configured - In order to set boundaries to courses size - As a manager - I need to limit the number of weeks/topics a course can have - - Background: - Given the following "users" exist: - | username | firstname | lastname | email | - | manager1 | Manager | 1 | manager1@example.com | - And the following "system role assigns" exist: - | user | course | role | - | manager1 | Acceptance test site | manager | - And I log in as "admin" - And I navigate to "Courses > Default settings > Course default settings" in site administration - - @javascript - Scenario: The number of sections can be increased and the limits are applied to courses - Given I set the field "Maximum number of sections" to "100" - When I press "Save changes" - And the field "Maximum number of sections" matches value "100" - And the "Number of sections" select box should contain "100" - And I log out - And I log in as "manager1" - And the following "course" exists: - | fullname | New course fullname | - | shortname | New course shortname | - | format | topics | - | numsections | 90 | - | initsections | 1 | - And I am on the "New course fullname" course page - Then I should see "Section 90" - - @javascript - Scenario: The number of sections can be reduced to 0 and the limits are applied to courses - Given I set the field "Maximum number of sections" to "0" - When I press "Save changes" - And the field "Maximum number of sections" matches value "0" - And the "Number of sections" select box should contain "0" - And the "Number of sections" select box should not contain "52" - And I log out - And I log in as "manager1" - And the following "course" exists: - | fullname | New course fullname | - | shortname | New course shortname | - | format | topics | - | numsections | 0 | - | initsections | 1 | - And I am on the "New course fullname" course page - Then I should not see "Section 1" diff --git a/mod/subsection/tests/behat/subsection_limit.feature b/mod/subsection/tests/behat/subsection_limit.feature deleted file mode 100644 index de53f65d1e3..00000000000 --- a/mod/subsection/tests/behat/subsection_limit.feature +++ /dev/null @@ -1,29 +0,0 @@ -@mod @mod_subsection -Feature: Teacher can only add subsection when certain conditions are met - In order to limit subsections - As an teacher - I need to create subsections only when possible - - Background: - Given the following "users" exist: - | username | firstname | lastname | email | - | teacher1 | Teacher | 1 | teacher1@example.com | - And the following "courses" exist: - | fullname | shortname | category | numsections | initsections | - | Course 1 | C1 | 0 | 5 | 1 | - And the following "course enrolments" exist: - | user | course | role | - | teacher1 | C1 | editingteacher | - - @javascript - Scenario: We cannot add subsections when maxsections is reached - Given the following config values are set as admin: - | maxsections | 10 | moodlecourse | - And I log in as "teacher1" - And I am on "Course 1" course homepage with editing mode on - And I click on "Add content" "button" in the "Section 1" "section" - And I click on "Subsection" "link" in the ".dropdown-menu.show" "css_element" - When the following config values are set as admin: - | maxsections | 4 | moodlecourse | - And I am on "Course 1" course homepage - And I should see "You have reached the maximum number of sections allowed for a course." From 56b0e93d7cbfa1bb7409588119b182cc8f28d242 Mon Sep 17 00:00:00 2001 From: ferran Date: Tue, 29 Apr 2025 11:27:40 +0200 Subject: [PATCH 4/6] MDL-84291 course: deprecate maxsections setting --- admin/presets/classes/manager.php | 1 + admin/settings/courses.php | 10 ++++++++-- lib/adminlib.php | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/admin/presets/classes/manager.php b/admin/presets/classes/manager.php index 6719ab77d2c..d7d0a265b48 100644 --- a/admin/presets/classes/manager.php +++ b/admin/presets/classes/manager.php @@ -93,6 +93,7 @@ class manager { 'adminpresets_admin_settings_country_select' => 'adminpresets_admin_setting_configmultiselect_with_loader', 'adminpresets_admin_settings_coursecat_select' => 'adminpresets_admin_setting_configmultiselect_with_loader', 'adminpresets_admin_settings_h5plib_handler_select' => 'adminpresets_admin_setting_configselect', + // TODO: remove adminpresets_admin_settings_num_course_sections in Moodle 6.0 (MDL-85272). 'adminpresets_admin_settings_num_course_sections' => 'adminpresets_admin_setting_configmultiselect_with_loader', 'adminpresets_admin_settings_sitepolicy_handler_select' => 'adminpresets_admin_setting_configselect', 'adminpresets_antivirus_clamav_pathtounixsocket_setting' => 'adminpresets_admin_setting_configtext', diff --git a/admin/settings/courses.php b/admin/settings/courses.php index 723650e9a04..2c62ce83be7 100644 --- a/admin/settings/courses.php +++ b/admin/settings/courses.php @@ -157,11 +157,17 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { $temp->add(new admin_setting_configselect('moodlecourse/format', new lang_string('format'), new lang_string('coursehelpformat'), 'topics', $formcourseformats)); + // TODO: remove this setting in Moodle 6.0 (MDL-85272). $temp->add(new admin_setting_configtext('moodlecourse/maxsections', new lang_string('maxnumberweeks'), new lang_string('maxnumberweeks_desc'), 52)); - $temp->add(new admin_settings_num_course_sections('moodlecourse/numsections', new lang_string('numberweeks'), - new lang_string('coursehelpnumberweeks'), 4)); + $temp->add(new admin_setting_configtext( + name: 'moodlecourse/numsections', + visiblename: new lang_string('numberweeks'), + description: new lang_string('coursehelpnumberweeks'), + defaultsetting: 4, + paramtype: PARAM_INT, + )); $choices = array(); $choices['0'] = new lang_string('hiddensectionscollapsed'); diff --git a/lib/adminlib.php b/lib/adminlib.php index 85dd2c4fabc..df690c4a225 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -5414,11 +5414,29 @@ class admin_settings_country_select extends admin_setting_configselect { * admin_setting_configselect for the default number of sections in a course, * simply so we can lazy-load the choices. * + * @deprecated since Moodle 5.2. + * @todo Remove this class in Moodle 6.0 (MDL-85272). * @copyright 2011 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class admin_settings_num_course_sections extends admin_setting_configselect { + /** + * Constructor. + * + * @param string $name The name of the setting + * @param string $visiblename The visible name of the setting + * @param string $description The description of the setting + * @param int $defaultsetting The default setting value + * @deprecated since Moodle 5.2 + * @todo Final deprecation in Moodle 6.0 (MDL-84291) + */ + #[\core\attribute\deprecated( + replacement: 'admin_setting_configtext', + since: '5.1', + mdl: 'MDL-84291', + )] public function __construct($name, $visiblename, $description, $defaultsetting) { + \core\deprecation::emit_deprecation_if_present(__FUNCTION__); parent::__construct($name, $visiblename, $description, $defaultsetting, array()); } From b3cb1eb18a42b8f44b732ba46e4341d408b4fc3d Mon Sep 17 00:00:00 2001 From: ferran Date: Tue, 29 Apr 2025 11:29:21 +0200 Subject: [PATCH 5/6] MDL-84291 format_topics: create courses with default number of sections --- .upgradenotes/MDL-84291-2025042902174752.yml | 8 ++++++ course/format/topics/lib.php | 12 +++------ .../tests/behat/default_sections.feature | 25 +++++++++++++++++++ 3 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 .upgradenotes/MDL-84291-2025042902174752.yml create mode 100644 course/format/topics/tests/behat/default_sections.feature diff --git a/.upgradenotes/MDL-84291-2025042902174752.yml b/.upgradenotes/MDL-84291-2025042902174752.yml new file mode 100644 index 00000000000..a0b2a2efbe7 --- /dev/null +++ b/.upgradenotes/MDL-84291-2025042902174752.yml @@ -0,0 +1,8 @@ +issueNumber: MDL-84291 +notes: + format_topics: + - message: >- + Now the custom sections format won't ask for initial sections on the + creation form. Instead it will use the system number of sections + settings directly. + type: improved diff --git a/course/format/topics/lib.php b/course/format/topics/lib.php index 575280d85ce..8d0d5923156 100644 --- a/course/format/topics/lib.php +++ b/course/format/topics/lib.php @@ -285,17 +285,11 @@ class format_topics extends core_courseformat\base { $elements = parent::create_edit_form_elements($mform, $forsection); if (!$forsection && (empty($COURSE->id) || $COURSE->id == SITEID)) { - // Add "numsections" element to the create course form - it will force new course to be prepopulated - // with empty sections. - // The "Number of sections" option is no longer available when editing course, instead teachers should - // delete and add sections when needed. + // Custom sections are always created with the default number of sections. $courseconfig = get_config('moodlecourse'); - $max = (int)$courseconfig->maxsections; - $element = $mform->addElement('select', 'numsections', get_string('numberweeks'), range(0, $max ?: 52)); + $element = $mform->addElement('hidden', 'numsections'); $mform->setType('numsections', PARAM_INT); - if (is_null($mform->getElementValue('numsections'))) { - $mform->setDefault('numsections', $courseconfig->numsections); - } + $mform->setDefault('numsections', $courseconfig->numsections); array_unshift($elements, $element); } diff --git a/course/format/topics/tests/behat/default_sections.feature b/course/format/topics/tests/behat/default_sections.feature new file mode 100644 index 00000000000..1924e1ea091 --- /dev/null +++ b/course/format/topics/tests/behat/default_sections.feature @@ -0,0 +1,25 @@ +@format @format_topics +Feature: Custom sections are created with the system default number of sections + In order to create courses + As a course creator + I need my courses to be created as the system default number of sections + + @javascript + Scenario: Default number of sections in course creation + Given the following config values are set as admin: + | numsections | 5 | moodlecourse | + When I log in as "admin" + And I navigate to "Courses > Manage courses and categories" in site administration + And I click on "Create new course" "link" + And I expand all fieldsets + And I set the field "Course full name" to "Course 1" + And I set the field "Course short name" to "C1" + And I set the field "Format" to "Custom sections" + Then I should not see "Number of sections" + And I click on "Save and display" "button" + And "[data-for='section'][data-number='1']" "css_element" should exist + And "[data-for='section'][data-number='2']" "css_element" should exist + And "[data-for='section'][data-number='3']" "css_element" should exist + And "[data-for='section'][data-number='4']" "css_element" should exist + And "[data-for='section'][data-number='5']" "css_element" should exist + And "[data-for='section'][data-number='6']" "css_element" should not exist From c960a9c5b8886041c7a865085d3d205ded52da33 Mon Sep 17 00:00:00 2001 From: ferran Date: Tue, 29 Apr 2025 11:29:45 +0200 Subject: [PATCH 6/6] MDL-84291 format_weeks: max initial sections setting --- .upgradenotes/MDL-84291-2025042902162982.yml | 8 ++ course/format/weeks/db/upgrade.php | 14 ++++ course/format/weeks/lang/en/format_weeks.php | 2 + course/format/weeks/lib.php | 3 +- course/format/weeks/settings.php | 8 ++ .../tests/behat/default_sections.feature | 74 +++++++++++++++++++ course/format/weeks/version.php | 2 +- 7 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 .upgradenotes/MDL-84291-2025042902162982.yml create mode 100644 course/format/weeks/tests/behat/default_sections.feature diff --git a/.upgradenotes/MDL-84291-2025042902162982.yml b/.upgradenotes/MDL-84291-2025042902162982.yml new file mode 100644 index 00000000000..d82580f7175 --- /dev/null +++ b/.upgradenotes/MDL-84291-2025042902162982.yml @@ -0,0 +1,8 @@ +issueNumber: MDL-84291 +notes: + format_weeks: + - message: >- + The weekly sections format now has a system setting called Maximum + initial number of weeks that replaced the old "Max sections" when + creating a new course + type: improved diff --git a/course/format/weeks/db/upgrade.php b/course/format/weeks/db/upgrade.php index 9ed64d17b8f..65965ffaa18 100644 --- a/course/format/weeks/db/upgrade.php +++ b/course/format/weeks/db/upgrade.php @@ -46,5 +46,19 @@ function xmldb_format_weeks_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + if ($oldversion < 2025052600) { + $config = get_config('format_weeks'); + // Crerate the default maxinitialsections setting if is not set. + if (!isset($config->maxinitialsections)) { + // The system may have some maxsections defined. We will keep the same value. + $courseconfig = get_config('moodlecourse'); + $max = (int) $courseconfig->maxsections; + $config->maxinitialsections = $max ?: 52; + set_config('maxinitialsections', $config->maxinitialsections, 'format_weeks'); + } + + upgrade_plugin_savepoint(true, 2025052600, 'format', 'weeks'); + } + return true; } diff --git a/course/format/weeks/lang/en/format_weeks.php b/course/format/weeks/lang/en/format_weeks.php index 58f009fafe3..5730599a48b 100644 --- a/course/format/weeks/lang/en/format_weeks.php +++ b/course/format/weeks/lang/en/format_weeks.php @@ -27,6 +27,8 @@ $string['automaticenddate'] = 'Calculate the end date from the number of section $string['automaticenddate_help'] = 'If enabled, the end date for the course will be automatically calculated from the number of sections and the course start date.'; $string['currentsection'] = 'Current week'; $string['hidefromothers'] = 'Hide'; +$string['maxinitialsections'] = 'Maximum number of weeks at course creation'; +$string['maxinitialsections_help'] = 'Sets the maximum number of weeks that can be assigned to a course at the time of creation. This limit helps prevent accidental creation of overly long courses. It does not restrict teachers from adding more weeks later during course editing.'; $string['page-course-view-weeks'] = 'Any course main page in weekly sections format'; $string['page-course-view-weeks-x'] = 'Any course page in weekly sections format'; $string['pluginname'] = 'Weekly sections'; diff --git a/course/format/weeks/lib.php b/course/format/weeks/lib.php index 5b65792f3e4..9c3c76942af 100644 --- a/course/format/weeks/lib.php +++ b/course/format/weeks/lib.php @@ -319,7 +319,8 @@ class format_weeks extends core_courseformat\base { // The "Number of sections" option is no longer available when editing course, instead teachers should // delete and add sections when needed. $courseconfig = get_config('moodlecourse'); - $max = (int)$courseconfig->maxsections; + $weeksconfig = get_config('format_weeks'); + $max = (int)$weeksconfig->maxinitialsections; $element = $mform->addElement('select', 'numsections', get_string('numberweeks'), range(0, $max ?: 52)); $mform->setType('numsections', PARAM_INT); if (is_null($mform->getElementValue('numsections'))) { diff --git a/course/format/weeks/settings.php b/course/format/weeks/settings.php index 036e5aad869..bbbbd22d7df 100644 --- a/course/format/weeks/settings.php +++ b/course/format/weeks/settings.php @@ -33,4 +33,12 @@ if ($ADMIN->fulltree) { new lang_string('indentation_help', 'format_weeks').'
'.$link, 1 )); + + $settings->add(new admin_setting_configtext( + name: 'format_weeks/maxinitialsections', + visiblename: new lang_string('maxinitialsections', 'format_weeks'), + description: new lang_string('maxinitialsections_help', 'format_weeks'), + defaultsetting: 52, + paramtype: PARAM_INT, + )); } diff --git a/course/format/weeks/tests/behat/default_sections.feature b/course/format/weeks/tests/behat/default_sections.feature new file mode 100644 index 00000000000..c7bf393fdcc --- /dev/null +++ b/course/format/weeks/tests/behat/default_sections.feature @@ -0,0 +1,74 @@ +@format @format_weeks +Feature: Weeks format courses are created with the system default number of sections + In order to create courses + As a course creator + I need my week courses to be created as the system default number of sections + + @javascript + Scenario: Weeks formats cannot be created with more sections than the format max + Given the following config values are set as admin: + | maxinitialsections | 5 | format_weeks | + | numsections | 40 | moodlecourse | + When I log in as "admin" + And I navigate to "Courses > Manage courses and categories" in site administration + And I click on "Create new course" "link" + And I expand all fieldsets + And I set the field "Course full name" to "Course 1" + And I set the field "Course short name" to "C1" + And I set the field "Format" to "Weekly sections" + Then the "Number of sections" select box should contain "5" + And the "Number of sections" select box should not contain "6" + And I expand all fieldsets + And I set the field "Number of sections" to "5" + And I click on "Save and display" "button" + And "[data-for='section'][data-number='1']" "css_element" should exist + And "[data-for='section'][data-number='2']" "css_element" should exist + And "[data-for='section'][data-number='3']" "css_element" should exist + And "[data-for='section'][data-number='4']" "css_element" should exist + And "[data-for='section'][data-number='5']" "css_element" should exist + And "[data-for='section'][data-number='6']" "css_element" should not exist + + @javascript + Scenario: Weeks formats will be created with the system default + Given the following config values are set as admin: + | numsections | 5 | moodlecourse | + When I log in as "admin" + And I navigate to "Courses > Manage courses and categories" in site administration + And I click on "Create new course" "link" + And I expand all fieldsets + And I set the field "Course full name" to "Course 1" + And I set the field "Course short name" to "C1" + And I set the field "Format" to "Weekly sections" + Then the "Number of sections" select box should contain "52" + And the "Number of sections" select box should not contain "53" + And I click on "Save and display" "button" + And "[data-for='section'][data-number='1']" "css_element" should exist + And "[data-for='section'][data-number='2']" "css_element" should exist + And "[data-for='section'][data-number='3']" "css_element" should exist + And "[data-for='section'][data-number='4']" "css_element" should exist + And "[data-for='section'][data-number='5']" "css_element" should exist + And "[data-for='section'][data-number='6']" "css_element" should not exist + + @javascript + Scenario: Weeks formats can be created with a specific number of sections + Given the following config values are set as admin: + | numsections | 4 | moodlecourse | + | maxinitialsections | 10 | format_weeks | + When I log in as "admin" + And I navigate to "Courses > Manage courses and categories" in site administration + And I click on "Create new course" "link" + And I expand all fieldsets + And I set the field "Course full name" to "Course 1" + And I set the field "Course short name" to "C1" + And I set the field "Format" to "Weekly sections" + Then the "Number of sections" select box should contain "10" + And the "Number of sections" select box should not contain "11" + And I expand all fieldsets + And I set the field "Number of sections" to "5" + And I click on "Save and display" "button" + And "[data-for='section'][data-number='1']" "css_element" should exist + And "[data-for='section'][data-number='2']" "css_element" should exist + And "[data-for='section'][data-number='3']" "css_element" should exist + And "[data-for='section'][data-number='4']" "css_element" should exist + And "[data-for='section'][data-number='5']" "css_element" should exist + And "[data-for='section'][data-number='6']" "css_element" should not exist diff --git a/course/format/weeks/version.php b/course/format/weeks/version.php index 4899784a3b5..c19c7a69437 100644 --- a/course/format/weeks/version.php +++ b/course/format/weeks/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2025052600; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'format_weeks'; // Full name of the plugin (used for diagnostics).