From 390a5af3da34177d2e69fdadd25078d7579761c3 Mon Sep 17 00:00:00 2001 From: james-cnz <5689414+james-cnz@users.noreply.github.com> Date: Wed, 2 Jul 2025 12:25:33 +1200 Subject: [PATCH 001/553] MDL-85917 course: Relocate section title update code --- .../format/amd/build/local/content.min.js | 2 +- .../format/amd/build/local/content.min.js.map | 2 +- public/course/format/amd/src/local/content.js | 42 ++++++++++--------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/public/course/format/amd/build/local/content.min.js b/public/course/format/amd/build/local/content.min.js index f30e6a8ac4c9e..517d25a2e1bbc 100644 --- a/public/course/format/amd/build/local/content.min.js +++ b/public/course/format/amd/build/local/content.min.js @@ -6,6 +6,6 @@ define("core_courseformat/local/content",["exports","core/reactive","theme_boost * @class core_courseformat/local/content * @copyright 2020 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),_config=_interopRequireDefault(_config),_inplace_editable=_interopRequireDefault(_inplace_editable),_section=_interopRequireDefault(_section),_cmitem=_interopRequireDefault(_cmitem),_fragment=_interopRequireDefault(_fragment),_templates=_interopRequireDefault(_templates),_actions=_interopRequireDefault(_actions),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),_log=_interopRequireDefault(_log);class Component extends _reactive.BaseComponent{create(descriptor){var _descriptor$sectionRe,_descriptor$pageSecti;this.name="course_format",this.selectors={SECTION:"[data-for='section']",SECTION_ITEM:"[data-for='section_title']",SECTION_CMLIST:"[data-for='cmlist']",COURSE_SECTIONLIST:"[data-for='course_sectionlist']",CM:"[data-for='cmitem']",TOGGLER:'[data-action="togglecoursecontentsection"]',COLLAPSE:'[data-bs-toggle="collapse"]',TOGGLEALL:'[data-toggle="toggleall"]',ACTIVITYTAG:"li",SECTIONTAG:"li"},this.selectorGenerators={cmNameFor:id=>"[data-cm-name-for='".concat(id,"']"),sectionNameFor:id=>"[data-section-name-for='".concat(id,"']")},this.classes={COLLAPSED:"collapsed",ACTIVITY:"activity",STATEDREADY:"stateready",SECTION:"section"},this.dettachedCms={},this.dettachedSections={},this.sections={},this.cms={},this.sectionReturn=null!==(_descriptor$sectionRe=null==descriptor?void 0:descriptor.sectionReturn)&&void 0!==_descriptor$sectionRe?_descriptor$sectionRe:null,this.pageSectionId=null!==(_descriptor$pageSecti=null==descriptor?void 0:descriptor.pageSectionId)&&void 0!==_descriptor$pageSecti?_descriptor$pageSecti:null,this.debouncedReloads=new Map}static init(target,selectors,sectionReturn,pageSectionId){let element=document.querySelector(target);return element||(_log.default.debug("Init component with id is deprecated, use a query selector instead."),element=document.getElementById(target)),new Component({element:element,reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors,sectionReturn:sectionReturn,pageSectionId:pageSectionId})}stateReady(state){this._indexContents(),this.addEventListener(this.element,"click",this._sectionTogglers);const toogleAll=this.getElement(this.selectors.TOGGLEALL);if(toogleAll){const collapseElementIds=[...this.getElements(this.selectors.COLLAPSE)].map((element=>element.id));toogleAll.setAttribute("aria-controls",collapseElementIds.join(" ")),this.addEventListener(toogleAll,"click",this._allSectionToggler),this.addEventListener(toogleAll,"keydown",(e=>{" "===e.key&&this._allSectionToggler(e)})),this._refreshAllSectionsToggler(state)}this.reactive.supportComponents&&(this.reactive.isEditing&&new _actions.default(this),this.element.classList.add(this.classes.STATEDREADY)),this.addEventListener(this.element,CourseEvents.manualCompletionToggled,this._completionHandler),this.addEventListener(document,"scroll",(0,_utils.throttle)(this._scrollHandler.bind(this),50))}_sectionTogglers(event){const sectionlink=event.target.closest(this.selectors.TOGGLER),closestCollapse=event.target.closest(this.selectors.COLLAPSE),isChevron=null==closestCollapse?void 0:closestCollapse.closest(this.selectors.SECTION_ITEM);if(sectionlink||isChevron){var _toggler$classList$co;const section=event.target.closest(this.selectors.SECTION),toggler=section.querySelector(this.selectors.COLLAPSE);let isCollapsed=null!==(_toggler$classList$co=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co&&_toggler$classList$co;isChevron&&(isCollapsed=!isCollapsed);const sectionId=section.getAttribute("data-id");this.reactive.dispatch("sectionContentCollapsed",[sectionId],!isCollapsed)}}_allSectionToggler(event){var _course$sectionlist;event.preventDefault();const isAllCollapsed=event.target.closest(this.selectors.TOGGLEALL).classList.contains(this.classes.COLLAPSED),course=this.reactive.get("course");this.reactive.dispatch("sectionContentCollapsed",null!==(_course$sectionlist=course.sectionlist)&&void 0!==_course$sectionlist?_course$sectionlist:[],!isAllCollapsed)}getWatchers(){var _this$sectionReturn,_this$pageSectionId;return this.reactive.sectionReturn=null!==(_this$sectionReturn=null==this?void 0:this.sectionReturn)&&void 0!==_this$sectionReturn?_this$sectionReturn:null,this.reactive.pageSectionId=null!==(_this$pageSectionId=null==this?void 0:this.pageSectionId)&&void 0!==_this$pageSectionId?_this$pageSectionId:null,this.reactive.supportComponents?[{watch:"cm.visible:updated",handler:this._reloadCm},{watch:"cm.stealth:updated",handler:this._reloadCm},{watch:"cm.sectionid:updated",handler:this._reloadCm},{watch:"cm.indent:updated",handler:this._reloadCm},{watch:"cm.groupmode:updated",handler:this._reloadCm},{watch:"cm.name:updated",handler:this._refreshCmName},{watch:"section.number:updated",handler:this._refreshSectionNumber},{watch:"section.title:updated",handler:this._refreshSectionTitle},{watch:"section.contentcollapsed:updated",handler:this._refreshSectionCollapsed},{watch:"transaction:start",handler:this._startProcessing},{watch:"course.sectionlist:updated",handler:this._refreshCourseSectionlist},{watch:"section.cmlist:updated",handler:this._refreshSectionCmlist},{watch:"section.visible:updated",handler:this._reloadSection},{watch:"state:updated",handler:this._indexContents}]:[]}_refreshCmName(_ref){let{element:element}=_ref;this.getElements(this.selectorGenerators.cmNameFor(element.id)).forEach((cmNameFor=>{cmNameFor.textContent=element.name}))}_refreshSectionCollapsed(_ref2){var _toggler$classList$co2;let{state:state,element:element}=_ref2;const target=this.getElement(this.selectors.SECTION,element.id);if(!target)throw new Error("Unknown section with ID ".concat(element.id));const toggler=target.querySelector(this.selectors.COLLAPSE),isCollapsed=null!==(_toggler$classList$co2=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co2&&_toggler$classList$co2;if(element.contentcollapsed!==isCollapsed){var _toggler$dataset$targ;let collapsibleId=null!==(_toggler$dataset$targ=toggler.dataset.target)&&void 0!==_toggler$dataset$targ?_toggler$dataset$targ:toggler.getAttribute("href");if(!collapsibleId)return;collapsibleId=collapsibleId.replace("#","");const collapsible=document.getElementById(collapsibleId);if(!collapsible)return;element.contentcollapsed?_collapse.default.getOrCreateInstance(collapsible,{toggle:!1}).hide():_collapse.default.getOrCreateInstance(collapsible,{toggle:!1}).show()}this._refreshAllSectionsToggler(state)}_refreshAllSectionsToggler(state){const target=this.getElement(this.selectors.TOGGLEALL);if(!target)return;const sectionIsCollapsible=this._getCollapsibleSections();let allcollapsed=!0,allexpanded=!0;state.section.forEach((section=>{sectionIsCollapsible[section.id]&&(allcollapsed=allcollapsed&§ion.contentcollapsed,allexpanded=allexpanded&&!section.contentcollapsed)})),allcollapsed&&(target.classList.add(this.classes.COLLAPSED),target.setAttribute("aria-expanded",!1)),allexpanded&&(target.classList.remove(this.classes.COLLAPSED),target.setAttribute("aria-expanded",!0))}_getCollapsibleSections(){let sectionIsCollapsible={};const togglerDoms=this.element.querySelectorAll(this.selectors.COLLAPSE);for(let togglerDom of togglerDoms){const headerDom=togglerDom.closest(this.selectors.SECTION_ITEM);headerDom&&(sectionIsCollapsible[headerDom.dataset.id]=!0)}return sectionIsCollapsible}_startProcessing(){this.dettachedCms={},this.dettachedSections={}}_completionHandler(_ref3){let{detail:detail}=_ref3;void 0!==detail&&this.reactive.dispatch("cmCompletion",[detail.cmid],detail.completed)}_scrollHandler(){const pageOffset=window.scrollY,items=this.reactive.getExporter().allItemsArray(this.reactive.state);let pageItem=null;items.every((item=>{const index="section"===item.type?this.sections:this.cms;if(void 0===index[item.id])return!0;const element=index[item.id].element;return pageItem=item,pageOffset>=element.offsetTop})),pageItem&&this.reactive.dispatch("setPageItem",pageItem.type,pageItem.id)}_refreshSectionNumber(_ref4){let{element:element}=_ref4;const target=this.getElement(this.selectors.SECTION,element.id);if(!target)return;target.id="section-".concat(element.number),target.dataset.sectionid=element.number,target.dataset.number=element.number;const inplace=_inplace_editable.default.getInplaceEditable(target.querySelector(this.selectors.SECTION_ITEM));if(inplace){const currentvalue=inplace.getValue(),currentitemid=inplace.getItemId();""===inplace.getValue()&&(currentitemid!=element.id||currentvalue==element.rawtitle&&""!=element.rawtitle||inplace.setValue(element.rawtitle))}}_refreshSectionTitle(_ref5){let{element:element}=_ref5;document.querySelectorAll(this.selectorGenerators.sectionNameFor(element.id)).forEach((sectionNameFor=>{sectionNameFor.textContent=element.title}))}_refreshSectionCmlist(_ref6){var _element$cmlist;let{state:state,element:element}=_ref6;const cmlist=null!==(_element$cmlist=element.cmlist)&&void 0!==_element$cmlist?_element$cmlist:[],section=this.getElement(this.selectors.SECTION,element.id),listparent=null==section?void 0:section.querySelector(this.selectors.SECTION_CMLIST),createCm=this._createCmItem.bind(this);listparent&&this._fixOrder(listparent,cmlist,this.selectors.CM,this.dettachedCms,createCm),this._refreshAllSectionsToggler(state)}_refreshCourseSectionlist(_ref7){var _this$reactive$sectio,_this$reactive,_this$reactive2;let{state:state}=_ref7;if(null!==(null!==(_this$reactive$sectio=null===(_this$reactive=this.reactive)||void 0===_this$reactive?void 0:_this$reactive.sectionReturn)&&void 0!==_this$reactive$sectio?_this$reactive$sectio:null===(_this$reactive2=this.reactive)||void 0===_this$reactive2?void 0:_this$reactive2.pageSectionId))return;const sectionlist=this.reactive.getExporter().listedSectionIds(state),listparent=this.getElement(this.selectors.COURSE_SECTIONLIST),createSection=this._createSectionItem.bind(this);listparent&&this._fixOrder(listparent,sectionlist,this.selectors.SECTION,this.dettachedSections,createSection),this._refreshAllSectionsToggler(state)}_indexContents(){this._scanIndex(this.selectors.SECTION,this.sections,(item=>new _section.default(item))),this._scanIndex(this.selectors.CM,this.cms,(item=>new _cmitem.default(item)))}_scanIndex(selector,index,creationhandler){this.getElements("".concat(selector,":not([data-indexed])")).forEach((item=>{var _item$dataset;null!=item&&null!==(_item$dataset=item.dataset)&&void 0!==_item$dataset&&_item$dataset.id&&(void 0!==index[item.dataset.id]&&index[item.dataset.id].unregister(),index[item.dataset.id]=creationhandler({...this,element:item}),item.dataset.indexed=!0)}))}_reloadCm(_ref8){let{element:element}=_ref8;if(!this.getElement(this.selectors.CM,element.id))return;this._getDebouncedReloadCm(element.id)()}_getDebouncedReloadCm(cmId){const pendingKey="courseformat/content:reloadCm_".concat(cmId);let debouncedReload=this.debouncedReloads.get(pendingKey);if(debouncedReload)return debouncedReload;return debouncedReload=(0,_utils.debounce)((()=>{var _this$reactive$sectio2,_this$reactive3,_this$reactive$pageSe,_this$reactive4;const pendingReload=new _pending.default(pendingKey);this.debouncedReloads.delete(pendingKey);const cmitem=this.getElement(this.selectors.CM,cmId);if(!cmitem)return pendingReload.resolve();return _fragment.default.loadFragment("core_courseformat","cmitem",_config.default.courseContextId,{id:cmId,courseid:_config.default.courseId,sr:null!==(_this$reactive$sectio2=null===(_this$reactive3=this.reactive)||void 0===_this$reactive3?void 0:_this$reactive3.sectionReturn)&&void 0!==_this$reactive$sectio2?_this$reactive$sectio2:null,pagesectionid:null!==(_this$reactive$pageSe=null===(_this$reactive4=this.reactive)||void 0===_this$reactive4?void 0:_this$reactive4.pageSectionId)&&void 0!==_this$reactive$pageSe?_this$reactive$pageSe:null}).then(((html,js)=>document.contains(cmitem)?(_templates.default.replaceNode(cmitem,html,js),this._indexContents(),pendingReload.resolve(),!0):(pendingReload.resolve(),!1))).catch((()=>{pendingReload.resolve()})),pendingReload}),200,{cancel:!0,pending:!0}),this.debouncedReloads.set(pendingKey,debouncedReload),debouncedReload}_cancelDebouncedReloadCm(cmId){const pendingKey="courseformat/content:reloadCm_".concat(cmId),debouncedReload=this.debouncedReloads.get(pendingKey);debouncedReload&&(debouncedReload.cancel(),this.debouncedReloads.delete(pendingKey))}_reloadSection(_ref9){let{element:element}=_ref9;const pendingReload=new _pending.default("courseformat/content:reloadSection_".concat(element.id)),sectionitem=this.getElement(this.selectors.SECTION,element.id);if(sectionitem){var _this$reactive$sectio3,_this$reactive5,_this$reactive$pageSe2,_this$reactive6;for(const cmId of element.cmlist)this._cancelDebouncedReloadCm(cmId);_fragment.default.loadFragment("core_courseformat","section",_config.default.courseContextId,{id:element.id,courseid:_config.default.courseId,sr:null!==(_this$reactive$sectio3=null===(_this$reactive5=this.reactive)||void 0===_this$reactive5?void 0:_this$reactive5.sectionReturn)&&void 0!==_this$reactive$sectio3?_this$reactive$sectio3:null,pagesectionid:null!==(_this$reactive$pageSe2=null===(_this$reactive6=this.reactive)||void 0===_this$reactive6?void 0:_this$reactive6.pageSectionId)&&void 0!==_this$reactive$pageSe2?_this$reactive$pageSe2:null}).then(((html,js)=>{_templates.default.replaceNode(sectionitem,html,js),this._indexContents(),pendingReload.resolve()})).catch((()=>{pendingReload.resolve()}))}}_createCmItem(container,cmid){const newItem=document.createElement(this.selectors.ACTIVITYTAG);return newItem.dataset.for="cmitem",newItem.dataset.id=cmid,newItem.id="module-".concat(cmid),newItem.classList.add(this.classes.ACTIVITY),container.append(newItem),this._reloadCm({element:this.reactive.get("cm",cmid)}),newItem}_createSectionItem(container,sectionid){const section=this.reactive.get("section",sectionid),newItem=document.createElement(this.selectors.SECTIONTAG);return newItem.dataset.for="section",newItem.dataset.id=sectionid,newItem.dataset.number=section.number,newItem.id="section-".concat(sectionid),newItem.classList.add(this.classes.SECTION),container.append(newItem),this._reloadSection({element:section}),newItem}async _fixOrder(container,neworder,selector,dettachedelements,createMethod){if(void 0===container)return;if(!neworder.length)return container.classList.add("hidden"),void(container.innerHTML="");container.classList.remove("hidden"),neworder.forEach(((itemid,index)=>{var _ref10,_this$getElement;let item=null!==(_ref10=null!==(_this$getElement=this.getElement(selector,itemid))&&void 0!==_this$getElement?_this$getElement:dettachedelements[itemid])&&void 0!==_ref10?_ref10:createMethod(container,itemid);if(void 0===item)return;const currentitem=container.children[index];void 0!==currentitem?currentitem!==item&&container.insertBefore(item,currentitem):container.append(item)}));const orphanElements=[];for(;container.children.length>neworder.length;){var _lastchild$classList,_lastchild$dataset;const lastchild=container.lastChild;var _lastchild$dataset$id,_lastchild$dataset2;if(null!=lastchild&&null!==(_lastchild$classList=lastchild.classList)&&void 0!==_lastchild$classList&&_lastchild$classList.contains("dndupload-preview")||null!==(_lastchild$dataset=lastchild.dataset)&&void 0!==_lastchild$dataset&&_lastchild$dataset.orphan)orphanElements.push(lastchild);else dettachedelements[null!==(_lastchild$dataset$id=null==lastchild||null===(_lastchild$dataset2=lastchild.dataset)||void 0===_lastchild$dataset2?void 0:_lastchild$dataset2.id)&&void 0!==_lastchild$dataset$id?_lastchild$dataset$id:0]=lastchild;container.removeChild(lastchild)}orphanElements.forEach((element=>{container.append(element)}))}}return _exports.default=Component,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_collapse=_interopRequireDefault(_collapse),_config=_interopRequireDefault(_config),_inplace_editable=_interopRequireDefault(_inplace_editable),_section=_interopRequireDefault(_section),_cmitem=_interopRequireDefault(_cmitem),_fragment=_interopRequireDefault(_fragment),_templates=_interopRequireDefault(_templates),_actions=_interopRequireDefault(_actions),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),_log=_interopRequireDefault(_log);class Component extends _reactive.BaseComponent{create(descriptor){var _descriptor$sectionRe,_descriptor$pageSecti;this.name="course_format",this.selectors={SECTION:"[data-for='section']",SECTION_ITEM:"[data-for='section_title']",SECTION_CMLIST:"[data-for='cmlist']",COURSE_SECTIONLIST:"[data-for='course_sectionlist']",CM:"[data-for='cmitem']",TOGGLER:'[data-action="togglecoursecontentsection"]',COLLAPSE:'[data-bs-toggle="collapse"]',TOGGLEALL:'[data-toggle="toggleall"]',ACTIVITYTAG:"li",SECTIONTAG:"li"},this.selectorGenerators={cmNameFor:id=>"[data-cm-name-for='".concat(id,"']"),sectionNameFor:id=>"[data-section-name-for='".concat(id,"']")},this.classes={COLLAPSED:"collapsed",ACTIVITY:"activity",STATEDREADY:"stateready",SECTION:"section"},this.dettachedCms={},this.dettachedSections={},this.sections={},this.cms={},this.sectionReturn=null!==(_descriptor$sectionRe=null==descriptor?void 0:descriptor.sectionReturn)&&void 0!==_descriptor$sectionRe?_descriptor$sectionRe:null,this.pageSectionId=null!==(_descriptor$pageSecti=null==descriptor?void 0:descriptor.pageSectionId)&&void 0!==_descriptor$pageSecti?_descriptor$pageSecti:null,this.debouncedReloads=new Map}static init(target,selectors,sectionReturn,pageSectionId){let element=document.querySelector(target);return element||(_log.default.debug("Init component with id is deprecated, use a query selector instead."),element=document.getElementById(target)),new Component({element:element,reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors,sectionReturn:sectionReturn,pageSectionId:pageSectionId})}stateReady(state){this._indexContents(),this.addEventListener(this.element,"click",this._sectionTogglers);const toogleAll=this.getElement(this.selectors.TOGGLEALL);if(toogleAll){const collapseElementIds=[...this.getElements(this.selectors.COLLAPSE)].map((element=>element.id));toogleAll.setAttribute("aria-controls",collapseElementIds.join(" ")),this.addEventListener(toogleAll,"click",this._allSectionToggler),this.addEventListener(toogleAll,"keydown",(e=>{" "===e.key&&this._allSectionToggler(e)})),this._refreshAllSectionsToggler(state)}this.reactive.supportComponents&&(this.reactive.isEditing&&new _actions.default(this),this.element.classList.add(this.classes.STATEDREADY)),this.addEventListener(this.element,CourseEvents.manualCompletionToggled,this._completionHandler),this.addEventListener(document,"scroll",(0,_utils.throttle)(this._scrollHandler.bind(this),50))}_sectionTogglers(event){const sectionlink=event.target.closest(this.selectors.TOGGLER),closestCollapse=event.target.closest(this.selectors.COLLAPSE),isChevron=null==closestCollapse?void 0:closestCollapse.closest(this.selectors.SECTION_ITEM);if(sectionlink||isChevron){var _toggler$classList$co;const section=event.target.closest(this.selectors.SECTION),toggler=section.querySelector(this.selectors.COLLAPSE);let isCollapsed=null!==(_toggler$classList$co=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co&&_toggler$classList$co;isChevron&&(isCollapsed=!isCollapsed);const sectionId=section.getAttribute("data-id");this.reactive.dispatch("sectionContentCollapsed",[sectionId],!isCollapsed)}}_allSectionToggler(event){var _course$sectionlist;event.preventDefault();const isAllCollapsed=event.target.closest(this.selectors.TOGGLEALL).classList.contains(this.classes.COLLAPSED),course=this.reactive.get("course");this.reactive.dispatch("sectionContentCollapsed",null!==(_course$sectionlist=course.sectionlist)&&void 0!==_course$sectionlist?_course$sectionlist:[],!isAllCollapsed)}getWatchers(){var _this$sectionReturn,_this$pageSectionId;return this.reactive.sectionReturn=null!==(_this$sectionReturn=null==this?void 0:this.sectionReturn)&&void 0!==_this$sectionReturn?_this$sectionReturn:null,this.reactive.pageSectionId=null!==(_this$pageSectionId=null==this?void 0:this.pageSectionId)&&void 0!==_this$pageSectionId?_this$pageSectionId:null,this.reactive.supportComponents?[{watch:"cm.visible:updated",handler:this._reloadCm},{watch:"cm.stealth:updated",handler:this._reloadCm},{watch:"cm.sectionid:updated",handler:this._reloadCm},{watch:"cm.indent:updated",handler:this._reloadCm},{watch:"cm.groupmode:updated",handler:this._reloadCm},{watch:"cm.name:updated",handler:this._refreshCmName},{watch:"section.number:updated",handler:this._refreshSectionNumber},{watch:"section.title:updated",handler:this._refreshSectionTitle},{watch:"section.contentcollapsed:updated",handler:this._refreshSectionCollapsed},{watch:"transaction:start",handler:this._startProcessing},{watch:"course.sectionlist:updated",handler:this._refreshCourseSectionlist},{watch:"section.cmlist:updated",handler:this._refreshSectionCmlist},{watch:"section.visible:updated",handler:this._reloadSection},{watch:"state:updated",handler:this._indexContents}]:[]}_refreshCmName(_ref){let{element:element}=_ref;this.getElements(this.selectorGenerators.cmNameFor(element.id)).forEach((cmNameFor=>{cmNameFor.textContent=element.name}))}_refreshSectionCollapsed(_ref2){var _toggler$classList$co2;let{state:state,element:element}=_ref2;const target=this.getElement(this.selectors.SECTION,element.id);if(!target)throw new Error("Unknown section with ID ".concat(element.id));const toggler=target.querySelector(this.selectors.COLLAPSE),isCollapsed=null!==(_toggler$classList$co2=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co2&&_toggler$classList$co2;if(element.contentcollapsed!==isCollapsed){var _toggler$dataset$targ;let collapsibleId=null!==(_toggler$dataset$targ=toggler.dataset.target)&&void 0!==_toggler$dataset$targ?_toggler$dataset$targ:toggler.getAttribute("href");if(!collapsibleId)return;collapsibleId=collapsibleId.replace("#","");const collapsible=document.getElementById(collapsibleId);if(!collapsible)return;element.contentcollapsed?_collapse.default.getOrCreateInstance(collapsible,{toggle:!1}).hide():_collapse.default.getOrCreateInstance(collapsible,{toggle:!1}).show()}this._refreshAllSectionsToggler(state)}_refreshAllSectionsToggler(state){const target=this.getElement(this.selectors.TOGGLEALL);if(!target)return;const sectionIsCollapsible=this._getCollapsibleSections();let allcollapsed=!0,allexpanded=!0;state.section.forEach((section=>{sectionIsCollapsible[section.id]&&(allcollapsed=allcollapsed&§ion.contentcollapsed,allexpanded=allexpanded&&!section.contentcollapsed)})),allcollapsed&&(target.classList.add(this.classes.COLLAPSED),target.setAttribute("aria-expanded",!1)),allexpanded&&(target.classList.remove(this.classes.COLLAPSED),target.setAttribute("aria-expanded",!0))}_getCollapsibleSections(){let sectionIsCollapsible={};const togglerDoms=this.element.querySelectorAll(this.selectors.COLLAPSE);for(let togglerDom of togglerDoms){const headerDom=togglerDom.closest(this.selectors.SECTION_ITEM);headerDom&&(sectionIsCollapsible[headerDom.dataset.id]=!0)}return sectionIsCollapsible}_startProcessing(){this.dettachedCms={},this.dettachedSections={}}_completionHandler(_ref3){let{detail:detail}=_ref3;void 0!==detail&&this.reactive.dispatch("cmCompletion",[detail.cmid],detail.completed)}_scrollHandler(){const pageOffset=window.scrollY,items=this.reactive.getExporter().allItemsArray(this.reactive.state);let pageItem=null;items.every((item=>{const index="section"===item.type?this.sections:this.cms;if(void 0===index[item.id])return!0;const element=index[item.id].element;return pageItem=item,pageOffset>=element.offsetTop})),pageItem&&this.reactive.dispatch("setPageItem",pageItem.type,pageItem.id)}_refreshSectionNumber(_ref4){let{element:element}=_ref4;const target=this.getElement(this.selectors.SECTION,element.id);target&&(target.id="section-".concat(element.number),target.dataset.sectionid=element.number,target.dataset.number=element.number)}_refreshSectionTitle(_ref5){let{element:element}=_ref5;document.querySelectorAll(this.selectorGenerators.sectionNameFor(element.id)).forEach((sectionNameFor=>{sectionNameFor.textContent=element.title}));const target=this.getElement(this.selectors.SECTION,element.id);if(!target)return;const inplace=_inplace_editable.default.getInplaceEditable(target.querySelector(this.selectors.SECTION_ITEM));if(inplace){inplace.getItemId()==element.id&&inplace.setValue(element.rawtitle)}}_refreshSectionCmlist(_ref6){var _element$cmlist;let{state:state,element:element}=_ref6;const cmlist=null!==(_element$cmlist=element.cmlist)&&void 0!==_element$cmlist?_element$cmlist:[],section=this.getElement(this.selectors.SECTION,element.id),listparent=null==section?void 0:section.querySelector(this.selectors.SECTION_CMLIST),createCm=this._createCmItem.bind(this);listparent&&this._fixOrder(listparent,cmlist,this.selectors.CM,this.dettachedCms,createCm),this._refreshAllSectionsToggler(state)}_refreshCourseSectionlist(_ref7){var _this$reactive$sectio,_this$reactive,_this$reactive2;let{state:state}=_ref7;if(null!==(null!==(_this$reactive$sectio=null===(_this$reactive=this.reactive)||void 0===_this$reactive?void 0:_this$reactive.sectionReturn)&&void 0!==_this$reactive$sectio?_this$reactive$sectio:null===(_this$reactive2=this.reactive)||void 0===_this$reactive2?void 0:_this$reactive2.pageSectionId))return;const sectionlist=this.reactive.getExporter().listedSectionIds(state),listparent=this.getElement(this.selectors.COURSE_SECTIONLIST),createSection=this._createSectionItem.bind(this);listparent&&this._fixOrder(listparent,sectionlist,this.selectors.SECTION,this.dettachedSections,createSection),this._refreshAllSectionsToggler(state)}_indexContents(){this._scanIndex(this.selectors.SECTION,this.sections,(item=>new _section.default(item))),this._scanIndex(this.selectors.CM,this.cms,(item=>new _cmitem.default(item)))}_scanIndex(selector,index,creationhandler){this.getElements("".concat(selector,":not([data-indexed])")).forEach((item=>{var _item$dataset;null!=item&&null!==(_item$dataset=item.dataset)&&void 0!==_item$dataset&&_item$dataset.id&&(void 0!==index[item.dataset.id]&&index[item.dataset.id].unregister(),index[item.dataset.id]=creationhandler({...this,element:item}),item.dataset.indexed=!0)}))}_reloadCm(_ref8){let{element:element}=_ref8;if(!this.getElement(this.selectors.CM,element.id))return;this._getDebouncedReloadCm(element.id)()}_getDebouncedReloadCm(cmId){const pendingKey="courseformat/content:reloadCm_".concat(cmId);let debouncedReload=this.debouncedReloads.get(pendingKey);if(debouncedReload)return debouncedReload;return debouncedReload=(0,_utils.debounce)((()=>{var _this$reactive$sectio2,_this$reactive3,_this$reactive$pageSe,_this$reactive4;const pendingReload=new _pending.default(pendingKey);this.debouncedReloads.delete(pendingKey);const cmitem=this.getElement(this.selectors.CM,cmId);if(!cmitem)return pendingReload.resolve();return _fragment.default.loadFragment("core_courseformat","cmitem",_config.default.courseContextId,{id:cmId,courseid:_config.default.courseId,sr:null!==(_this$reactive$sectio2=null===(_this$reactive3=this.reactive)||void 0===_this$reactive3?void 0:_this$reactive3.sectionReturn)&&void 0!==_this$reactive$sectio2?_this$reactive$sectio2:null,pagesectionid:null!==(_this$reactive$pageSe=null===(_this$reactive4=this.reactive)||void 0===_this$reactive4?void 0:_this$reactive4.pageSectionId)&&void 0!==_this$reactive$pageSe?_this$reactive$pageSe:null}).then(((html,js)=>document.contains(cmitem)?(_templates.default.replaceNode(cmitem,html,js),this._indexContents(),pendingReload.resolve(),!0):(pendingReload.resolve(),!1))).catch((()=>{pendingReload.resolve()})),pendingReload}),200,{cancel:!0,pending:!0}),this.debouncedReloads.set(pendingKey,debouncedReload),debouncedReload}_cancelDebouncedReloadCm(cmId){const pendingKey="courseformat/content:reloadCm_".concat(cmId),debouncedReload=this.debouncedReloads.get(pendingKey);debouncedReload&&(debouncedReload.cancel(),this.debouncedReloads.delete(pendingKey))}_reloadSection(_ref9){let{element:element}=_ref9;const pendingReload=new _pending.default("courseformat/content:reloadSection_".concat(element.id)),sectionitem=this.getElement(this.selectors.SECTION,element.id);if(sectionitem){var _this$reactive$sectio3,_this$reactive5,_this$reactive$pageSe2,_this$reactive6;for(const cmId of element.cmlist)this._cancelDebouncedReloadCm(cmId);_fragment.default.loadFragment("core_courseformat","section",_config.default.courseContextId,{id:element.id,courseid:_config.default.courseId,sr:null!==(_this$reactive$sectio3=null===(_this$reactive5=this.reactive)||void 0===_this$reactive5?void 0:_this$reactive5.sectionReturn)&&void 0!==_this$reactive$sectio3?_this$reactive$sectio3:null,pagesectionid:null!==(_this$reactive$pageSe2=null===(_this$reactive6=this.reactive)||void 0===_this$reactive6?void 0:_this$reactive6.pageSectionId)&&void 0!==_this$reactive$pageSe2?_this$reactive$pageSe2:null}).then(((html,js)=>{_templates.default.replaceNode(sectionitem,html,js),this._indexContents(),pendingReload.resolve()})).catch((()=>{pendingReload.resolve()}))}}_createCmItem(container,cmid){const newItem=document.createElement(this.selectors.ACTIVITYTAG);return newItem.dataset.for="cmitem",newItem.dataset.id=cmid,newItem.id="module-".concat(cmid),newItem.classList.add(this.classes.ACTIVITY),container.append(newItem),this._reloadCm({element:this.reactive.get("cm",cmid)}),newItem}_createSectionItem(container,sectionid){const section=this.reactive.get("section",sectionid),newItem=document.createElement(this.selectors.SECTIONTAG);return newItem.dataset.for="section",newItem.dataset.id=sectionid,newItem.dataset.number=section.number,newItem.id="section-".concat(sectionid),newItem.classList.add(this.classes.SECTION),container.append(newItem),this._reloadSection({element:section}),newItem}async _fixOrder(container,neworder,selector,dettachedelements,createMethod){if(void 0===container)return;if(!neworder.length)return container.classList.add("hidden"),void(container.innerHTML="");container.classList.remove("hidden"),neworder.forEach(((itemid,index)=>{var _ref10,_this$getElement;let item=null!==(_ref10=null!==(_this$getElement=this.getElement(selector,itemid))&&void 0!==_this$getElement?_this$getElement:dettachedelements[itemid])&&void 0!==_ref10?_ref10:createMethod(container,itemid);if(void 0===item)return;const currentitem=container.children[index];void 0!==currentitem?currentitem!==item&&container.insertBefore(item,currentitem):container.append(item)}));const orphanElements=[];for(;container.children.length>neworder.length;){var _lastchild$classList,_lastchild$dataset;const lastchild=container.lastChild;var _lastchild$dataset$id,_lastchild$dataset2;if(null!=lastchild&&null!==(_lastchild$classList=lastchild.classList)&&void 0!==_lastchild$classList&&_lastchild$classList.contains("dndupload-preview")||null!==(_lastchild$dataset=lastchild.dataset)&&void 0!==_lastchild$dataset&&_lastchild$dataset.orphan)orphanElements.push(lastchild);else dettachedelements[null!==(_lastchild$dataset$id=null==lastchild||null===(_lastchild$dataset2=lastchild.dataset)||void 0===_lastchild$dataset2?void 0:_lastchild$dataset2.id)&&void 0!==_lastchild$dataset$id?_lastchild$dataset$id:0]=lastchild;container.removeChild(lastchild)}orphanElements.forEach((element=>{container.append(element)}))}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=content.min.js.map \ No newline at end of file diff --git a/public/course/format/amd/build/local/content.min.js.map b/public/course/format/amd/build/local/content.min.js.map index b915774db1429..da91976a084b0 100644 --- a/public/course/format/amd/build/local/content.min.js.map +++ b/public/course/format/amd/build/local/content.min.js.map @@ -1 +1 @@ -{"version":3,"file":"content.min.js","sources":["../../src/local/content.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 index main component.\n *\n * @module core_courseformat/local/content\n * @class core_courseformat/local/content\n * @copyright 2020 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport Collapse from 'theme_boost/bootstrap/collapse';\nimport {throttle, debounce} from 'core/utils';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport Config from 'core/config';\nimport inplaceeditable from 'core/inplace_editable';\nimport Section from 'core_courseformat/local/content/section';\nimport CmItem from 'core_courseformat/local/content/section/cmitem';\nimport Fragment from 'core/fragment';\nimport Templates from 'core/templates';\nimport DispatchActions from 'core_courseformat/local/content/actions';\nimport * as CourseEvents from 'core_course/events';\nimport Pending from 'core/pending';\nimport log from \"core/log\";\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n *\n * @param {Object} descriptor the component descriptor\n */\n create(descriptor) {\n // Optional component name for debugging.\n this.name = 'course_format';\n // Default query selectors.\n this.selectors = {\n SECTION: `[data-for='section']`,\n SECTION_ITEM: `[data-for='section_title']`,\n SECTION_CMLIST: `[data-for='cmlist']`,\n COURSE_SECTIONLIST: `[data-for='course_sectionlist']`,\n CM: `[data-for='cmitem']`,\n TOGGLER: `[data-action=\"togglecoursecontentsection\"]`,\n COLLAPSE: `[data-bs-toggle=\"collapse\"]`,\n TOGGLEALL: `[data-toggle=\"toggleall\"]`,\n // Formats can override the activity tag but a default one is needed to create new elements.\n ACTIVITYTAG: 'li',\n SECTIONTAG: 'li',\n };\n this.selectorGenerators = {\n cmNameFor: (id) => `[data-cm-name-for='${id}']`,\n sectionNameFor: (id) => `[data-section-name-for='${id}']`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n COLLAPSED: `collapsed`,\n // Course content classes.\n ACTIVITY: `activity`,\n STATEDREADY: `stateready`,\n SECTION: `section`,\n };\n // Array to save dettached elements during element resorting.\n this.dettachedCms = {};\n this.dettachedSections = {};\n // Index of sections and cms components.\n this.sections = {};\n this.cms = {};\n // The section number and ID of the displayed page.\n this.sectionReturn = descriptor?.sectionReturn ?? null;\n this.pageSectionId = descriptor?.pageSectionId ?? null;\n this.debouncedReloads = new Map();\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @param {number} sectionReturn the section number of the displayed page\n * @param {number} pageSectionId the section ID of the displayed page\n * @return {Component}\n */\n static init(target, selectors, sectionReturn, pageSectionId) {\n let element = document.querySelector(target);\n // TODO Remove this if condition as part of MDL-83851.\n if (!element) {\n log.debug('Init component with id is deprecated, use a query selector instead.');\n element = document.getElementById(target);\n }\n return new Component({\n element,\n reactive: getCurrentCourseEditor(),\n selectors,\n sectionReturn,\n pageSectionId,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data\n */\n stateReady(state) {\n this._indexContents();\n // Activate section togglers.\n this.addEventListener(this.element, 'click', this._sectionTogglers);\n\n // Collapse/Expand all sections button.\n const toogleAll = this.getElement(this.selectors.TOGGLEALL);\n if (toogleAll) {\n\n // Ensure collapse menu button adds aria-controls attribute referring to each collapsible element.\n const collapseElements = this.getElements(this.selectors.COLLAPSE);\n const collapseElementIds = [...collapseElements].map(element => element.id);\n toogleAll.setAttribute('aria-controls', collapseElementIds.join(' '));\n\n this.addEventListener(toogleAll, 'click', this._allSectionToggler);\n this.addEventListener(toogleAll, 'keydown', e => {\n // Collapse/expand all sections when Space key is pressed on the toggle button.\n if (e.key === ' ') {\n this._allSectionToggler(e);\n }\n });\n this._refreshAllSectionsToggler(state);\n }\n\n if (this.reactive.supportComponents) {\n // Actions are only available in edit mode.\n if (this.reactive.isEditing) {\n new DispatchActions(this);\n }\n\n // Mark content as state ready.\n this.element.classList.add(this.classes.STATEDREADY);\n }\n\n // Capture completion events.\n this.addEventListener(\n this.element,\n CourseEvents.manualCompletionToggled,\n this._completionHandler\n );\n\n // Capture page scroll to update page item.\n this.addEventListener(\n document,\n \"scroll\",\n throttle(this._scrollHandler.bind(this), 50)\n );\n }\n\n /**\n * Setup sections toggler.\n *\n * Toggler click is delegated to the main course content element because new sections can\n * appear at any moment and this way we prevent accidental double bindings.\n *\n * @param {Event} event the triggered event\n */\n _sectionTogglers(event) {\n const sectionlink = event.target.closest(this.selectors.TOGGLER);\n const closestCollapse = event.target.closest(this.selectors.COLLAPSE);\n // Assume that chevron is the only collapse toggler in a section heading;\n // I think this is the most efficient way to verify at the moment.\n const isChevron = closestCollapse?.closest(this.selectors.SECTION_ITEM);\n\n if (sectionlink || isChevron) {\n\n const section = event.target.closest(this.selectors.SECTION);\n const toggler = section.querySelector(this.selectors.COLLAPSE);\n let isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n // If the click was on the chevron, Bootstrap already toggled the section before this event.\n if (isChevron) {\n isCollapsed = !isCollapsed;\n }\n\n const sectionId = section.getAttribute('data-id');\n this.reactive.dispatch(\n 'sectionContentCollapsed',\n [sectionId],\n !isCollapsed,\n );\n }\n }\n\n /**\n * Handle the collapse/expand all sections button.\n *\n * Toggler click is delegated to the main course content element because new sections can\n * appear at any moment and this way we prevent accidental double bindings.\n *\n * @param {Event} event the triggered event\n */\n _allSectionToggler(event) {\n event.preventDefault();\n\n const target = event.target.closest(this.selectors.TOGGLEALL);\n const isAllCollapsed = target.classList.contains(this.classes.COLLAPSED);\n\n const course = this.reactive.get('course');\n this.reactive.dispatch(\n 'sectionContentCollapsed',\n course.sectionlist ?? [],\n !isAllCollapsed\n );\n }\n\n /**\n * Return the component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n // Section return is a global page variable but most formats define it just before start printing\n // the course content. This is the reason why we define this page setting here.\n this.reactive.sectionReturn = this?.sectionReturn ?? null;\n this.reactive.pageSectionId = this?.pageSectionId ?? null;\n\n // Check if the course format is compatible with reactive components.\n if (!this.reactive.supportComponents) {\n return [];\n }\n return [\n // State changes that require to reload some course modules.\n {watch: `cm.visible:updated`, handler: this._reloadCm},\n {watch: `cm.stealth:updated`, handler: this._reloadCm},\n {watch: `cm.sectionid:updated`, handler: this._reloadCm},\n {watch: `cm.indent:updated`, handler: this._reloadCm},\n {watch: `cm.groupmode:updated`, handler: this._reloadCm},\n {watch: `cm.name:updated`, handler: this._refreshCmName},\n // Update section number and title.\n {watch: `section.number:updated`, handler: this._refreshSectionNumber},\n {watch: `section.title:updated`, handler: this._refreshSectionTitle},\n // Collapse and expand sections.\n {watch: `section.contentcollapsed:updated`, handler: this._refreshSectionCollapsed},\n // Sections and cm sorting.\n {watch: `transaction:start`, handler: this._startProcessing},\n {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},\n {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},\n // Section visibility.\n {watch: `section.visible:updated`, handler: this._reloadSection},\n // Reindex sections and cms.\n {watch: `state:updated`, handler: this._indexContents},\n ];\n }\n\n /**\n * Update a course module name on the whole page.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshCmName({element}) {\n // Update classes.\n // Replace the text content of the cm name.\n const allCmNamesFor = this.getElements(\n this.selectorGenerators.cmNameFor(element.id)\n );\n allCmNamesFor.forEach((cmNameFor) => {\n cmNameFor.textContent = element.name;\n });\n }\n\n /**\n * Update section collapsed state via bootstrap if necessary.\n *\n * Formats that do not use bootstrap must override this method in order to keep the section\n * toggling working.\n *\n * @param {object} args\n * @param {Object} args.state The state data\n * @param {Object} args.element The element to update\n */\n _refreshSectionCollapsed({state, element}) {\n const target = this.getElement(this.selectors.SECTION, element.id);\n if (!target) {\n throw new Error(`Unknown section with ID ${element.id}`);\n }\n // Check if it is already done.\n const toggler = target.querySelector(this.selectors.COLLAPSE);\n const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n\n if (element.contentcollapsed !== isCollapsed) {\n let collapsibleId = toggler.dataset.target ?? toggler.getAttribute(\"href\");\n if (!collapsibleId) {\n return;\n }\n collapsibleId = collapsibleId.replace('#', '');\n const collapsible = document.getElementById(collapsibleId);\n if (!collapsible) {\n return;\n }\n if (element.contentcollapsed) {\n Collapse.getOrCreateInstance(collapsible, {toggle: false}).hide();\n } else {\n Collapse.getOrCreateInstance(collapsible, {toggle: false}).show();\n }\n }\n\n this._refreshAllSectionsToggler(state);\n }\n\n /**\n * Refresh the collapse/expand all sections element.\n *\n * @param {Object} state The state data\n */\n _refreshAllSectionsToggler(state) {\n const target = this.getElement(this.selectors.TOGGLEALL);\n if (!target) {\n return;\n }\n\n const sectionIsCollapsible = this._getCollapsibleSections();\n\n // Check if we have all sections collapsed/expanded.\n let allcollapsed = true;\n let allexpanded = true;\n state.section.forEach(\n section => {\n if (sectionIsCollapsible[section.id]) {\n allcollapsed = allcollapsed && section.contentcollapsed;\n allexpanded = allexpanded && !section.contentcollapsed;\n }\n }\n );\n if (allcollapsed) {\n target.classList.add(this.classes.COLLAPSED);\n target.setAttribute('aria-expanded', false);\n }\n if (allexpanded) {\n target.classList.remove(this.classes.COLLAPSED);\n target.setAttribute('aria-expanded', true);\n }\n }\n\n /**\n * Find collapsible sections.\n */\n _getCollapsibleSections() {\n let sectionIsCollapsible = {};\n const togglerDoms = this.element.querySelectorAll(this.selectors.COLLAPSE);\n for (let togglerDom of togglerDoms) {\n const headerDom = togglerDom.closest(this.selectors.SECTION_ITEM);\n if (headerDom) {\n sectionIsCollapsible[headerDom.dataset.id] = true;\n }\n }\n return sectionIsCollapsible;\n }\n\n /**\n * Setup the component to start a transaction.\n *\n * Some of the course actions replaces the current DOM element with a new one before updating the\n * course state. This means the component cannot preload any index properly until the transaction starts.\n *\n */\n _startProcessing() {\n // During a section or cm sorting, some elements could be dettached from the DOM and we\n // need to store somewhare in case they are needed later.\n this.dettachedCms = {};\n this.dettachedSections = {};\n }\n\n /**\n * Activity manual completion listener.\n *\n * @param {Event} event the custom ecent\n */\n _completionHandler({detail}) {\n if (detail === undefined) {\n return;\n }\n this.reactive.dispatch('cmCompletion', [detail.cmid], detail.completed);\n }\n\n /**\n * Check the current page scroll and update the active element if necessary.\n */\n _scrollHandler() {\n const pageOffset = window.scrollY;\n const items = this.reactive.getExporter().allItemsArray(this.reactive.state);\n // Check what is the active element now.\n let pageItem = null;\n items.every(item => {\n const index = (item.type === 'section') ? this.sections : this.cms;\n if (index[item.id] === undefined) {\n return true;\n }\n\n const element = index[item.id].element;\n pageItem = item;\n return pageOffset >= element.offsetTop;\n });\n if (pageItem) {\n this.reactive.dispatch('setPageItem', pageItem.type, pageItem.id);\n }\n }\n\n /**\n * Update a course section when the section number changes.\n *\n * The courseActions module used for most course section tools still depends on css classes and\n * section numbers (not id). To prevent inconsistencies when a section is moved, we need to refresh\n * the\n *\n * Course formats can override the section title rendering so the frontend depends heavily on backend\n * rendering. Luckily in edit mode we can trigger a title update using the inplace_editable module.\n *\n * @param {Object} param\n * @param {Object} param.element details the update details.\n */\n _refreshSectionNumber({element}) {\n // Find the element.\n const target = this.getElement(this.selectors.SECTION, element.id);\n if (!target) {\n // Job done. Nothing to refresh.\n return;\n }\n // Update section numbers in all data, css and YUI attributes.\n target.id = `section-${element.number}`;\n // YUI uses section number as section id in data-sectionid, in principle if a format use components\n // don't need this sectionid attribute anymore, but we keep the compatibility in case some plugin\n // use it for legacy purposes.\n target.dataset.sectionid = element.number;\n // The data-number is the attribute used by components to store the section number.\n target.dataset.number = element.number;\n\n // Update title and title inplace editable, if any.\n const inplace = inplaceeditable.getInplaceEditable(target.querySelector(this.selectors.SECTION_ITEM));\n if (inplace) {\n // The course content HTML can be modified at any moment, so the function need to do some checkings\n // to make sure the inplace editable still represents the same itemid.\n const currentvalue = inplace.getValue();\n const currentitemid = inplace.getItemId();\n // Unnamed sections must be recalculated.\n if (inplace.getValue() === '') {\n // The value to send can be an empty value if it is a default name.\n if (currentitemid == element.id && (currentvalue != element.rawtitle || element.rawtitle == '')) {\n inplace.setValue(element.rawtitle);\n }\n }\n }\n }\n\n /**\n * Update a course section name on the whole page.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshSectionTitle({element}) {\n // Replace the text content of the section name in the whole page.\n const allSectionNamesFor = document.querySelectorAll(\n this.selectorGenerators.sectionNameFor(element.id)\n );\n allSectionNamesFor.forEach((sectionNameFor) => {\n sectionNameFor.textContent = element.title;\n });\n }\n\n /**\n * Refresh a section cm list.\n *\n * @param {Object} param\n * @param {Object} param.state the full state object.\n * @param {Object} param.element details the update details.\n */\n _refreshSectionCmlist({state, element}) {\n const cmlist = element.cmlist ?? [];\n const section = this.getElement(this.selectors.SECTION, element.id);\n const listparent = section?.querySelector(this.selectors.SECTION_CMLIST);\n // A method to create a fake element to be replaced when the item is ready.\n const createCm = this._createCmItem.bind(this);\n if (listparent) {\n this._fixOrder(listparent, cmlist, this.selectors.CM, this.dettachedCms, createCm);\n }\n this._refreshAllSectionsToggler(state);\n }\n\n /**\n * Refresh the section list.\n *\n * @param {Object} param\n * @param {Object} param.state the full state object.\n */\n _refreshCourseSectionlist({state}) {\n // If we have a section return means we only show a single section so no need to fix order.\n if ((this.reactive?.sectionReturn ?? this.reactive?.pageSectionId) !== null) {\n return;\n }\n const sectionlist = this.reactive.getExporter().listedSectionIds(state);\n const listparent = this.getElement(this.selectors.COURSE_SECTIONLIST);\n // For now section cannot be created at a frontend level.\n const createSection = this._createSectionItem.bind(this);\n if (listparent) {\n this._fixOrder(listparent, sectionlist, this.selectors.SECTION, this.dettachedSections, createSection);\n }\n this._refreshAllSectionsToggler(state);\n }\n\n /**\n * Regenerate content indexes.\n *\n * This method is used when a legacy action refresh some content element.\n */\n _indexContents() {\n // Find unindexed sections.\n this._scanIndex(\n this.selectors.SECTION,\n this.sections,\n (item) => {\n return new Section(item);\n }\n );\n\n // Find unindexed cms.\n this._scanIndex(\n this.selectors.CM,\n this.cms,\n (item) => {\n return new CmItem(item);\n }\n );\n }\n\n /**\n * Reindex a content (section or cm) of the course content.\n *\n * This method is used internally by _indexContents.\n *\n * @param {string} selector the DOM selector to scan\n * @param {*} index the index attribute to update\n * @param {*} creationhandler method to create a new indexed element\n */\n _scanIndex(selector, index, creationhandler) {\n const items = this.getElements(`${selector}:not([data-indexed])`);\n items.forEach((item) => {\n if (!item?.dataset?.id) {\n return;\n }\n // Delete previous item component.\n if (index[item.dataset.id] !== undefined) {\n index[item.dataset.id].unregister();\n }\n // Create the new component.\n index[item.dataset.id] = creationhandler({\n ...this,\n element: item,\n });\n // Mark as indexed.\n item.dataset.indexed = true;\n });\n }\n\n /**\n * Reload a course module contents.\n *\n * Most course module HTML is still strongly backend dependant.\n * Some changes require to get a new version of the module.\n *\n * @param {object} param0 the watcher details\n * @param {object} param0.element the state object\n */\n _reloadCm({element}) {\n if (!this.getElement(this.selectors.CM, element.id)) {\n return;\n }\n const debouncedReload = this._getDebouncedReloadCm(element.id);\n debouncedReload();\n }\n\n /**\n * Generate or get a reload CM debounced function.\n * @param {Number} cmId\n * @returns {Function} the debounced reload function\n */\n _getDebouncedReloadCm(cmId) {\n const pendingKey = `courseformat/content:reloadCm_${cmId}`;\n let debouncedReload = this.debouncedReloads.get(pendingKey);\n if (debouncedReload) {\n return debouncedReload;\n }\n const reload = () => {\n const pendingReload = new Pending(pendingKey);\n this.debouncedReloads.delete(pendingKey);\n const cmitem = this.getElement(this.selectors.CM, cmId);\n if (!cmitem) {\n return pendingReload.resolve();\n }\n const promise = Fragment.loadFragment(\n 'core_courseformat',\n 'cmitem',\n Config.courseContextId,\n {\n id: cmId,\n courseid: Config.courseId,\n sr: this.reactive?.sectionReturn ?? null,\n pagesectionid: this.reactive?.pageSectionId ?? null,\n }\n );\n promise.then((html, js) => {\n // Other state change can reload the CM or the section before this one.\n if (!document.contains(cmitem)) {\n pendingReload.resolve();\n return false;\n }\n Templates.replaceNode(cmitem, html, js);\n this._indexContents();\n pendingReload.resolve();\n return true;\n }).catch(() => {\n pendingReload.resolve();\n });\n return pendingReload;\n };\n debouncedReload = debounce(\n reload,\n 200,\n {\n cancel: true, pending: true\n }\n );\n this.debouncedReloads.set(pendingKey, debouncedReload);\n return debouncedReload;\n }\n\n /**\n * Cancel the active reload CM debounced function, if any.\n * @param {Number} cmId\n */\n _cancelDebouncedReloadCm(cmId) {\n const pendingKey = `courseformat/content:reloadCm_${cmId}`;\n const debouncedReload = this.debouncedReloads.get(pendingKey);\n if (!debouncedReload) {\n return;\n }\n debouncedReload.cancel();\n this.debouncedReloads.delete(pendingKey);\n }\n\n /**\n * Reload a course section contents.\n *\n * Section HTML is still strongly backend dependant.\n * Some changes require to get a new version of the section.\n *\n * @param {details} param0 the watcher details\n * @param {object} param0.element the state object\n */\n _reloadSection({element}) {\n const pendingReload = new Pending(`courseformat/content:reloadSection_${element.id}`);\n const sectionitem = this.getElement(this.selectors.SECTION, element.id);\n if (sectionitem) {\n // Cancel any pending reload because the section will reload cms too.\n for (const cmId of element.cmlist) {\n this._cancelDebouncedReloadCm(cmId);\n }\n const promise = Fragment.loadFragment(\n 'core_courseformat',\n 'section',\n Config.courseContextId,\n {\n id: element.id,\n courseid: Config.courseId,\n sr: this.reactive?.sectionReturn ?? null,\n pagesectionid: this.reactive?.pageSectionId ?? null,\n }\n );\n promise.then((html, js) => {\n Templates.replaceNode(sectionitem, html, js);\n this._indexContents();\n pendingReload.resolve();\n }).catch(() => {\n pendingReload.resolve();\n });\n }\n }\n\n /**\n * Create a new course module item in a section.\n *\n * Thos method will append a fake item in the container and trigger an ajax request to\n * replace the fake element by the real content.\n *\n * @param {Element} container the container element (section)\n * @param {Number} cmid the course-module ID\n * @returns {Element} the created element\n */\n _createCmItem(container, cmid) {\n const newItem = document.createElement(this.selectors.ACTIVITYTAG);\n newItem.dataset.for = 'cmitem';\n newItem.dataset.id = cmid;\n // The legacy actions.js requires a specific ID and class to refresh the CM.\n newItem.id = `module-${cmid}`;\n newItem.classList.add(this.classes.ACTIVITY);\n container.append(newItem);\n this._reloadCm({\n element: this.reactive.get('cm', cmid),\n });\n return newItem;\n }\n\n /**\n * Create a new section item.\n *\n * This method will append a fake item in the container and trigger an ajax request to\n * replace the fake element by the real content.\n *\n * @param {Element} container the container element (section)\n * @param {Number} sectionid the course-module ID\n * @returns {Element} the created element\n */\n _createSectionItem(container, sectionid) {\n const section = this.reactive.get('section', sectionid);\n const newItem = document.createElement(this.selectors.SECTIONTAG);\n newItem.dataset.for = 'section';\n newItem.dataset.id = sectionid;\n newItem.dataset.number = section.number;\n // The legacy actions.js requires a specific ID and class to refresh the section.\n newItem.id = `section-${sectionid}`;\n newItem.classList.add(this.classes.SECTION);\n container.append(newItem);\n this._reloadSection({\n element: section,\n });\n return newItem;\n }\n\n /**\n * Fix/reorder the section or cms order.\n *\n * @param {Element} container the HTML element to reorder.\n * @param {Array} neworder an array with the ids order\n * @param {string} selector the element selector\n * @param {Object} dettachedelements a list of dettached elements\n * @param {function} createMethod method to create missing elements\n */\n async _fixOrder(container, neworder, selector, dettachedelements, createMethod) {\n if (container === undefined) {\n return;\n }\n\n // Empty lists should not be visible.\n if (!neworder.length) {\n container.classList.add('hidden');\n container.innerHTML = '';\n return;\n }\n\n // Grant the list is visible (in case it was empty).\n container.classList.remove('hidden');\n\n // Move the elements in order at the beginning of the list.\n neworder.forEach((itemid, index) => {\n let item = this.getElement(selector, itemid) ?? dettachedelements[itemid] ?? createMethod(container, itemid);\n if (item === undefined) {\n // Missing elements cannot be sorted.\n return;\n }\n // Get the current elemnt at that position.\n const currentitem = container.children[index];\n if (currentitem === undefined) {\n container.append(item);\n return;\n }\n if (currentitem !== item) {\n container.insertBefore(item, currentitem);\n }\n });\n\n // Remove the remaining elements.\n const orphanElements = [];\n while (container.children.length > neworder.length) {\n const lastchild = container.lastChild;\n // Any orphan element is always displayed after the listed elements.\n // Also, some third-party plugins can use a fake dndupload-preview indicator.\n if (lastchild?.classList?.contains('dndupload-preview') || lastchild.dataset?.orphan) {\n orphanElements.push(lastchild);\n } else {\n dettachedelements[lastchild?.dataset?.id ?? 0] = lastchild;\n }\n container.removeChild(lastchild);\n }\n // Restore orphan elements.\n orphanElements.forEach((element) => {\n container.append(element);\n });\n }\n}\n"],"names":["Component","BaseComponent","create","descriptor","name","selectors","SECTION","SECTION_ITEM","SECTION_CMLIST","COURSE_SECTIONLIST","CM","TOGGLER","COLLAPSE","TOGGLEALL","ACTIVITYTAG","SECTIONTAG","selectorGenerators","cmNameFor","id","sectionNameFor","classes","COLLAPSED","ACTIVITY","STATEDREADY","dettachedCms","dettachedSections","sections","cms","sectionReturn","pageSectionId","debouncedReloads","Map","target","element","document","querySelector","debug","getElementById","reactive","stateReady","state","_indexContents","addEventListener","this","_sectionTogglers","toogleAll","getElement","collapseElementIds","getElements","map","setAttribute","join","_allSectionToggler","e","key","_refreshAllSectionsToggler","supportComponents","isEditing","DispatchActions","classList","add","CourseEvents","manualCompletionToggled","_completionHandler","_scrollHandler","bind","event","sectionlink","closest","closestCollapse","isChevron","section","toggler","isCollapsed","contains","sectionId","getAttribute","dispatch","preventDefault","isAllCollapsed","course","get","sectionlist","getWatchers","watch","handler","_reloadCm","_refreshCmName","_refreshSectionNumber","_refreshSectionTitle","_refreshSectionCollapsed","_startProcessing","_refreshCourseSectionlist","_refreshSectionCmlist","_reloadSection","forEach","textContent","Error","contentcollapsed","collapsibleId","dataset","replace","collapsible","getOrCreateInstance","toggle","hide","show","sectionIsCollapsible","_getCollapsibleSections","allcollapsed","allexpanded","remove","togglerDoms","querySelectorAll","togglerDom","headerDom","detail","undefined","cmid","completed","pageOffset","window","scrollY","items","getExporter","allItemsArray","pageItem","every","item","index","type","offsetTop","number","sectionid","inplace","inplaceeditable","getInplaceEditable","currentvalue","getValue","currentitemid","getItemId","rawtitle","setValue","title","cmlist","listparent","createCm","_createCmItem","_fixOrder","_this$reactive","_this$reactive2","listedSectionIds","createSection","_createSectionItem","_scanIndex","Section","CmItem","selector","creationhandler","_item$dataset","unregister","indexed","_getDebouncedReloadCm","debouncedReload","cmId","pendingKey","pendingReload","Pending","delete","cmitem","resolve","Fragment","loadFragment","Config","courseContextId","courseid","courseId","sr","_this$reactive3","pagesectionid","_this$reactive4","then","html","js","replaceNode","catch","cancel","pending","set","_cancelDebouncedReloadCm","sectionitem","_this$reactive5","_this$reactive6","container","newItem","createElement","for","append","neworder","dettachedelements","createMethod","length","innerHTML","itemid","currentitem","children","insertBefore","orphanElements","lastchild","lastChild","_lastchild$dataset","orphan","push","_lastchild$dataset2","removeChild"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"content.min.js","sources":["../../src/local/content.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 index main component.\n *\n * @module core_courseformat/local/content\n * @class core_courseformat/local/content\n * @copyright 2020 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport Collapse from 'theme_boost/bootstrap/collapse';\nimport {throttle, debounce} from 'core/utils';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport Config from 'core/config';\nimport inplaceeditable from 'core/inplace_editable';\nimport Section from 'core_courseformat/local/content/section';\nimport CmItem from 'core_courseformat/local/content/section/cmitem';\nimport Fragment from 'core/fragment';\nimport Templates from 'core/templates';\nimport DispatchActions from 'core_courseformat/local/content/actions';\nimport * as CourseEvents from 'core_course/events';\nimport Pending from 'core/pending';\nimport log from \"core/log\";\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n *\n * @param {Object} descriptor the component descriptor\n */\n create(descriptor) {\n // Optional component name for debugging.\n this.name = 'course_format';\n // Default query selectors.\n this.selectors = {\n SECTION: `[data-for='section']`,\n SECTION_ITEM: `[data-for='section_title']`,\n SECTION_CMLIST: `[data-for='cmlist']`,\n COURSE_SECTIONLIST: `[data-for='course_sectionlist']`,\n CM: `[data-for='cmitem']`,\n TOGGLER: `[data-action=\"togglecoursecontentsection\"]`,\n COLLAPSE: `[data-bs-toggle=\"collapse\"]`,\n TOGGLEALL: `[data-toggle=\"toggleall\"]`,\n // Formats can override the activity tag but a default one is needed to create new elements.\n ACTIVITYTAG: 'li',\n SECTIONTAG: 'li',\n };\n this.selectorGenerators = {\n cmNameFor: (id) => `[data-cm-name-for='${id}']`,\n sectionNameFor: (id) => `[data-section-name-for='${id}']`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n COLLAPSED: `collapsed`,\n // Course content classes.\n ACTIVITY: `activity`,\n STATEDREADY: `stateready`,\n SECTION: `section`,\n };\n // Array to save dettached elements during element resorting.\n this.dettachedCms = {};\n this.dettachedSections = {};\n // Index of sections and cms components.\n this.sections = {};\n this.cms = {};\n // The section number and ID of the displayed page.\n this.sectionReturn = descriptor?.sectionReturn ?? null;\n this.pageSectionId = descriptor?.pageSectionId ?? null;\n this.debouncedReloads = new Map();\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @param {number} sectionReturn the section number of the displayed page\n * @param {number} pageSectionId the section ID of the displayed page\n * @return {Component}\n */\n static init(target, selectors, sectionReturn, pageSectionId) {\n let element = document.querySelector(target);\n // TODO Remove this if condition as part of MDL-83851.\n if (!element) {\n log.debug('Init component with id is deprecated, use a query selector instead.');\n element = document.getElementById(target);\n }\n return new Component({\n element,\n reactive: getCurrentCourseEditor(),\n selectors,\n sectionReturn,\n pageSectionId,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data\n */\n stateReady(state) {\n this._indexContents();\n // Activate section togglers.\n this.addEventListener(this.element, 'click', this._sectionTogglers);\n\n // Collapse/Expand all sections button.\n const toogleAll = this.getElement(this.selectors.TOGGLEALL);\n if (toogleAll) {\n\n // Ensure collapse menu button adds aria-controls attribute referring to each collapsible element.\n const collapseElements = this.getElements(this.selectors.COLLAPSE);\n const collapseElementIds = [...collapseElements].map(element => element.id);\n toogleAll.setAttribute('aria-controls', collapseElementIds.join(' '));\n\n this.addEventListener(toogleAll, 'click', this._allSectionToggler);\n this.addEventListener(toogleAll, 'keydown', e => {\n // Collapse/expand all sections when Space key is pressed on the toggle button.\n if (e.key === ' ') {\n this._allSectionToggler(e);\n }\n });\n this._refreshAllSectionsToggler(state);\n }\n\n if (this.reactive.supportComponents) {\n // Actions are only available in edit mode.\n if (this.reactive.isEditing) {\n new DispatchActions(this);\n }\n\n // Mark content as state ready.\n this.element.classList.add(this.classes.STATEDREADY);\n }\n\n // Capture completion events.\n this.addEventListener(\n this.element,\n CourseEvents.manualCompletionToggled,\n this._completionHandler\n );\n\n // Capture page scroll to update page item.\n this.addEventListener(\n document,\n \"scroll\",\n throttle(this._scrollHandler.bind(this), 50)\n );\n }\n\n /**\n * Setup sections toggler.\n *\n * Toggler click is delegated to the main course content element because new sections can\n * appear at any moment and this way we prevent accidental double bindings.\n *\n * @param {Event} event the triggered event\n */\n _sectionTogglers(event) {\n const sectionlink = event.target.closest(this.selectors.TOGGLER);\n const closestCollapse = event.target.closest(this.selectors.COLLAPSE);\n // Assume that chevron is the only collapse toggler in a section heading;\n // I think this is the most efficient way to verify at the moment.\n const isChevron = closestCollapse?.closest(this.selectors.SECTION_ITEM);\n\n if (sectionlink || isChevron) {\n\n const section = event.target.closest(this.selectors.SECTION);\n const toggler = section.querySelector(this.selectors.COLLAPSE);\n let isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n // If the click was on the chevron, Bootstrap already toggled the section before this event.\n if (isChevron) {\n isCollapsed = !isCollapsed;\n }\n\n const sectionId = section.getAttribute('data-id');\n this.reactive.dispatch(\n 'sectionContentCollapsed',\n [sectionId],\n !isCollapsed,\n );\n }\n }\n\n /**\n * Handle the collapse/expand all sections button.\n *\n * Toggler click is delegated to the main course content element because new sections can\n * appear at any moment and this way we prevent accidental double bindings.\n *\n * @param {Event} event the triggered event\n */\n _allSectionToggler(event) {\n event.preventDefault();\n\n const target = event.target.closest(this.selectors.TOGGLEALL);\n const isAllCollapsed = target.classList.contains(this.classes.COLLAPSED);\n\n const course = this.reactive.get('course');\n this.reactive.dispatch(\n 'sectionContentCollapsed',\n course.sectionlist ?? [],\n !isAllCollapsed\n );\n }\n\n /**\n * Return the component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n // Section return is a global page variable but most formats define it just before start printing\n // the course content. This is the reason why we define this page setting here.\n this.reactive.sectionReturn = this?.sectionReturn ?? null;\n this.reactive.pageSectionId = this?.pageSectionId ?? null;\n\n // Check if the course format is compatible with reactive components.\n if (!this.reactive.supportComponents) {\n return [];\n }\n return [\n // State changes that require to reload some course modules.\n {watch: `cm.visible:updated`, handler: this._reloadCm},\n {watch: `cm.stealth:updated`, handler: this._reloadCm},\n {watch: `cm.sectionid:updated`, handler: this._reloadCm},\n {watch: `cm.indent:updated`, handler: this._reloadCm},\n {watch: `cm.groupmode:updated`, handler: this._reloadCm},\n {watch: `cm.name:updated`, handler: this._refreshCmName},\n // Update section number and title.\n {watch: `section.number:updated`, handler: this._refreshSectionNumber},\n {watch: `section.title:updated`, handler: this._refreshSectionTitle},\n // Collapse and expand sections.\n {watch: `section.contentcollapsed:updated`, handler: this._refreshSectionCollapsed},\n // Sections and cm sorting.\n {watch: `transaction:start`, handler: this._startProcessing},\n {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},\n {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},\n // Section visibility.\n {watch: `section.visible:updated`, handler: this._reloadSection},\n // Reindex sections and cms.\n {watch: `state:updated`, handler: this._indexContents},\n ];\n }\n\n /**\n * Update a course module name on the whole page.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshCmName({element}) {\n // Update classes.\n // Replace the text content of the cm name.\n const allCmNamesFor = this.getElements(\n this.selectorGenerators.cmNameFor(element.id)\n );\n allCmNamesFor.forEach((cmNameFor) => {\n cmNameFor.textContent = element.name;\n });\n }\n\n /**\n * Update section collapsed state via bootstrap if necessary.\n *\n * Formats that do not use bootstrap must override this method in order to keep the section\n * toggling working.\n *\n * @param {object} args\n * @param {Object} args.state The state data\n * @param {Object} args.element The element to update\n */\n _refreshSectionCollapsed({state, element}) {\n const target = this.getElement(this.selectors.SECTION, element.id);\n if (!target) {\n throw new Error(`Unknown section with ID ${element.id}`);\n }\n // Check if it is already done.\n const toggler = target.querySelector(this.selectors.COLLAPSE);\n const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n\n if (element.contentcollapsed !== isCollapsed) {\n let collapsibleId = toggler.dataset.target ?? toggler.getAttribute(\"href\");\n if (!collapsibleId) {\n return;\n }\n collapsibleId = collapsibleId.replace('#', '');\n const collapsible = document.getElementById(collapsibleId);\n if (!collapsible) {\n return;\n }\n if (element.contentcollapsed) {\n Collapse.getOrCreateInstance(collapsible, {toggle: false}).hide();\n } else {\n Collapse.getOrCreateInstance(collapsible, {toggle: false}).show();\n }\n }\n\n this._refreshAllSectionsToggler(state);\n }\n\n /**\n * Refresh the collapse/expand all sections element.\n *\n * @param {Object} state The state data\n */\n _refreshAllSectionsToggler(state) {\n const target = this.getElement(this.selectors.TOGGLEALL);\n if (!target) {\n return;\n }\n\n const sectionIsCollapsible = this._getCollapsibleSections();\n\n // Check if we have all sections collapsed/expanded.\n let allcollapsed = true;\n let allexpanded = true;\n state.section.forEach(\n section => {\n if (sectionIsCollapsible[section.id]) {\n allcollapsed = allcollapsed && section.contentcollapsed;\n allexpanded = allexpanded && !section.contentcollapsed;\n }\n }\n );\n if (allcollapsed) {\n target.classList.add(this.classes.COLLAPSED);\n target.setAttribute('aria-expanded', false);\n }\n if (allexpanded) {\n target.classList.remove(this.classes.COLLAPSED);\n target.setAttribute('aria-expanded', true);\n }\n }\n\n /**\n * Find collapsible sections.\n */\n _getCollapsibleSections() {\n let sectionIsCollapsible = {};\n const togglerDoms = this.element.querySelectorAll(this.selectors.COLLAPSE);\n for (let togglerDom of togglerDoms) {\n const headerDom = togglerDom.closest(this.selectors.SECTION_ITEM);\n if (headerDom) {\n sectionIsCollapsible[headerDom.dataset.id] = true;\n }\n }\n return sectionIsCollapsible;\n }\n\n /**\n * Setup the component to start a transaction.\n *\n * Some of the course actions replaces the current DOM element with a new one before updating the\n * course state. This means the component cannot preload any index properly until the transaction starts.\n *\n */\n _startProcessing() {\n // During a section or cm sorting, some elements could be dettached from the DOM and we\n // need to store somewhare in case they are needed later.\n this.dettachedCms = {};\n this.dettachedSections = {};\n }\n\n /**\n * Activity manual completion listener.\n *\n * @param {Event} event the custom ecent\n */\n _completionHandler({detail}) {\n if (detail === undefined) {\n return;\n }\n this.reactive.dispatch('cmCompletion', [detail.cmid], detail.completed);\n }\n\n /**\n * Check the current page scroll and update the active element if necessary.\n */\n _scrollHandler() {\n const pageOffset = window.scrollY;\n const items = this.reactive.getExporter().allItemsArray(this.reactive.state);\n // Check what is the active element now.\n let pageItem = null;\n items.every(item => {\n const index = (item.type === 'section') ? this.sections : this.cms;\n if (index[item.id] === undefined) {\n return true;\n }\n\n const element = index[item.id].element;\n pageItem = item;\n return pageOffset >= element.offsetTop;\n });\n if (pageItem) {\n this.reactive.dispatch('setPageItem', pageItem.type, pageItem.id);\n }\n }\n\n /**\n * Update a course section when the section number changes.\n *\n * The courseActions module used for most course section tools still depends on css classes and\n * section numbers (not id). To prevent inconsistencies when a section is moved, we need to refresh\n * the section number.\n *\n * @param {Object} param\n * @param {Object} param.element details the update details.\n */\n _refreshSectionNumber({element}) {\n // Find the element.\n const target = this.getElement(this.selectors.SECTION, element.id);\n if (!target) {\n // Job done. Nothing to refresh.\n return;\n }\n // Update section numbers in all data, css and YUI attributes.\n target.id = `section-${element.number}`;\n // YUI uses section number as section id in data-sectionid, in principle if a format use components\n // don't need this sectionid attribute anymore, but we keep the compatibility in case some plugin\n // use it for legacy purposes.\n target.dataset.sectionid = element.number;\n // The data-number is the attribute used by components to store the section number.\n target.dataset.number = element.number;\n }\n\n /**\n * Update a course section name on the whole page.\n *\n * Course formats can override the section title rendering so the frontend depends heavily on backend\n * rendering. Luckily in edit mode we can trigger a title update using the inplace_editable module.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshSectionTitle({element}) {\n // Replace the text content of the section name in the whole page.\n const allSectionNamesFor = document.querySelectorAll(\n this.selectorGenerators.sectionNameFor(element.id)\n );\n allSectionNamesFor.forEach((sectionNameFor) => {\n sectionNameFor.textContent = element.title;\n });\n\n // Find the element.\n const target = this.getElement(this.selectors.SECTION, element.id);\n if (!target) {\n // Job done. Nothing to refresh.\n return;\n }\n\n // Update title and title inplace editable, if any.\n const inplace = inplaceeditable.getInplaceEditable(target.querySelector(this.selectors.SECTION_ITEM));\n if (inplace) {\n // The course content HTML can be modified at any moment, so the function need to do some checkings\n // to make sure the inplace editable still represents the same itemid.\n const currentitemid = inplace.getItemId();\n if (currentitemid == element.id) {\n inplace.setValue(element.rawtitle);\n }\n }\n }\n\n /**\n * Refresh a section cm list.\n *\n * @param {Object} param\n * @param {Object} param.state the full state object.\n * @param {Object} param.element details the update details.\n */\n _refreshSectionCmlist({state, element}) {\n const cmlist = element.cmlist ?? [];\n const section = this.getElement(this.selectors.SECTION, element.id);\n const listparent = section?.querySelector(this.selectors.SECTION_CMLIST);\n // A method to create a fake element to be replaced when the item is ready.\n const createCm = this._createCmItem.bind(this);\n if (listparent) {\n this._fixOrder(listparent, cmlist, this.selectors.CM, this.dettachedCms, createCm);\n }\n this._refreshAllSectionsToggler(state);\n }\n\n /**\n * Refresh the section list.\n *\n * @param {Object} param\n * @param {Object} param.state the full state object.\n */\n _refreshCourseSectionlist({state}) {\n // If we have a section return means we only show a single section so no need to fix order.\n if ((this.reactive?.sectionReturn ?? this.reactive?.pageSectionId) !== null) {\n return;\n }\n const sectionlist = this.reactive.getExporter().listedSectionIds(state);\n const listparent = this.getElement(this.selectors.COURSE_SECTIONLIST);\n // For now section cannot be created at a frontend level.\n const createSection = this._createSectionItem.bind(this);\n if (listparent) {\n this._fixOrder(listparent, sectionlist, this.selectors.SECTION, this.dettachedSections, createSection);\n }\n this._refreshAllSectionsToggler(state);\n }\n\n /**\n * Regenerate content indexes.\n *\n * This method is used when a legacy action refresh some content element.\n */\n _indexContents() {\n // Find unindexed sections.\n this._scanIndex(\n this.selectors.SECTION,\n this.sections,\n (item) => {\n return new Section(item);\n }\n );\n\n // Find unindexed cms.\n this._scanIndex(\n this.selectors.CM,\n this.cms,\n (item) => {\n return new CmItem(item);\n }\n );\n }\n\n /**\n * Reindex a content (section or cm) of the course content.\n *\n * This method is used internally by _indexContents.\n *\n * @param {string} selector the DOM selector to scan\n * @param {*} index the index attribute to update\n * @param {*} creationhandler method to create a new indexed element\n */\n _scanIndex(selector, index, creationhandler) {\n const items = this.getElements(`${selector}:not([data-indexed])`);\n items.forEach((item) => {\n if (!item?.dataset?.id) {\n return;\n }\n // Delete previous item component.\n if (index[item.dataset.id] !== undefined) {\n index[item.dataset.id].unregister();\n }\n // Create the new component.\n index[item.dataset.id] = creationhandler({\n ...this,\n element: item,\n });\n // Mark as indexed.\n item.dataset.indexed = true;\n });\n }\n\n /**\n * Reload a course module contents.\n *\n * Most course module HTML is still strongly backend dependant.\n * Some changes require to get a new version of the module.\n *\n * @param {object} param0 the watcher details\n * @param {object} param0.element the state object\n */\n _reloadCm({element}) {\n if (!this.getElement(this.selectors.CM, element.id)) {\n return;\n }\n const debouncedReload = this._getDebouncedReloadCm(element.id);\n debouncedReload();\n }\n\n /**\n * Generate or get a reload CM debounced function.\n * @param {Number} cmId\n * @returns {Function} the debounced reload function\n */\n _getDebouncedReloadCm(cmId) {\n const pendingKey = `courseformat/content:reloadCm_${cmId}`;\n let debouncedReload = this.debouncedReloads.get(pendingKey);\n if (debouncedReload) {\n return debouncedReload;\n }\n const reload = () => {\n const pendingReload = new Pending(pendingKey);\n this.debouncedReloads.delete(pendingKey);\n const cmitem = this.getElement(this.selectors.CM, cmId);\n if (!cmitem) {\n return pendingReload.resolve();\n }\n const promise = Fragment.loadFragment(\n 'core_courseformat',\n 'cmitem',\n Config.courseContextId,\n {\n id: cmId,\n courseid: Config.courseId,\n sr: this.reactive?.sectionReturn ?? null,\n pagesectionid: this.reactive?.pageSectionId ?? null,\n }\n );\n promise.then((html, js) => {\n // Other state change can reload the CM or the section before this one.\n if (!document.contains(cmitem)) {\n pendingReload.resolve();\n return false;\n }\n Templates.replaceNode(cmitem, html, js);\n this._indexContents();\n pendingReload.resolve();\n return true;\n }).catch(() => {\n pendingReload.resolve();\n });\n return pendingReload;\n };\n debouncedReload = debounce(\n reload,\n 200,\n {\n cancel: true, pending: true\n }\n );\n this.debouncedReloads.set(pendingKey, debouncedReload);\n return debouncedReload;\n }\n\n /**\n * Cancel the active reload CM debounced function, if any.\n * @param {Number} cmId\n */\n _cancelDebouncedReloadCm(cmId) {\n const pendingKey = `courseformat/content:reloadCm_${cmId}`;\n const debouncedReload = this.debouncedReloads.get(pendingKey);\n if (!debouncedReload) {\n return;\n }\n debouncedReload.cancel();\n this.debouncedReloads.delete(pendingKey);\n }\n\n /**\n * Reload a course section contents.\n *\n * Section HTML is still strongly backend dependant.\n * Some changes require to get a new version of the section.\n *\n * @param {details} param0 the watcher details\n * @param {object} param0.element the state object\n */\n _reloadSection({element}) {\n const pendingReload = new Pending(`courseformat/content:reloadSection_${element.id}`);\n const sectionitem = this.getElement(this.selectors.SECTION, element.id);\n if (sectionitem) {\n // Cancel any pending reload because the section will reload cms too.\n for (const cmId of element.cmlist) {\n this._cancelDebouncedReloadCm(cmId);\n }\n const promise = Fragment.loadFragment(\n 'core_courseformat',\n 'section',\n Config.courseContextId,\n {\n id: element.id,\n courseid: Config.courseId,\n sr: this.reactive?.sectionReturn ?? null,\n pagesectionid: this.reactive?.pageSectionId ?? null,\n }\n );\n promise.then((html, js) => {\n Templates.replaceNode(sectionitem, html, js);\n this._indexContents();\n pendingReload.resolve();\n }).catch(() => {\n pendingReload.resolve();\n });\n }\n }\n\n /**\n * Create a new course module item in a section.\n *\n * Thos method will append a fake item in the container and trigger an ajax request to\n * replace the fake element by the real content.\n *\n * @param {Element} container the container element (section)\n * @param {Number} cmid the course-module ID\n * @returns {Element} the created element\n */\n _createCmItem(container, cmid) {\n const newItem = document.createElement(this.selectors.ACTIVITYTAG);\n newItem.dataset.for = 'cmitem';\n newItem.dataset.id = cmid;\n // The legacy actions.js requires a specific ID and class to refresh the CM.\n newItem.id = `module-${cmid}`;\n newItem.classList.add(this.classes.ACTIVITY);\n container.append(newItem);\n this._reloadCm({\n element: this.reactive.get('cm', cmid),\n });\n return newItem;\n }\n\n /**\n * Create a new section item.\n *\n * This method will append a fake item in the container and trigger an ajax request to\n * replace the fake element by the real content.\n *\n * @param {Element} container the container element (section)\n * @param {Number} sectionid the course-module ID\n * @returns {Element} the created element\n */\n _createSectionItem(container, sectionid) {\n const section = this.reactive.get('section', sectionid);\n const newItem = document.createElement(this.selectors.SECTIONTAG);\n newItem.dataset.for = 'section';\n newItem.dataset.id = sectionid;\n newItem.dataset.number = section.number;\n // The legacy actions.js requires a specific ID and class to refresh the section.\n newItem.id = `section-${sectionid}`;\n newItem.classList.add(this.classes.SECTION);\n container.append(newItem);\n this._reloadSection({\n element: section,\n });\n return newItem;\n }\n\n /**\n * Fix/reorder the section or cms order.\n *\n * @param {Element} container the HTML element to reorder.\n * @param {Array} neworder an array with the ids order\n * @param {string} selector the element selector\n * @param {Object} dettachedelements a list of dettached elements\n * @param {function} createMethod method to create missing elements\n */\n async _fixOrder(container, neworder, selector, dettachedelements, createMethod) {\n if (container === undefined) {\n return;\n }\n\n // Empty lists should not be visible.\n if (!neworder.length) {\n container.classList.add('hidden');\n container.innerHTML = '';\n return;\n }\n\n // Grant the list is visible (in case it was empty).\n container.classList.remove('hidden');\n\n // Move the elements in order at the beginning of the list.\n neworder.forEach((itemid, index) => {\n let item = this.getElement(selector, itemid) ?? dettachedelements[itemid] ?? createMethod(container, itemid);\n if (item === undefined) {\n // Missing elements cannot be sorted.\n return;\n }\n // Get the current elemnt at that position.\n const currentitem = container.children[index];\n if (currentitem === undefined) {\n container.append(item);\n return;\n }\n if (currentitem !== item) {\n container.insertBefore(item, currentitem);\n }\n });\n\n // Remove the remaining elements.\n const orphanElements = [];\n while (container.children.length > neworder.length) {\n const lastchild = container.lastChild;\n // Any orphan element is always displayed after the listed elements.\n // Also, some third-party plugins can use a fake dndupload-preview indicator.\n if (lastchild?.classList?.contains('dndupload-preview') || lastchild.dataset?.orphan) {\n orphanElements.push(lastchild);\n } else {\n dettachedelements[lastchild?.dataset?.id ?? 0] = lastchild;\n }\n container.removeChild(lastchild);\n }\n // Restore orphan elements.\n orphanElements.forEach((element) => {\n container.append(element);\n });\n }\n}\n"],"names":["Component","BaseComponent","create","descriptor","name","selectors","SECTION","SECTION_ITEM","SECTION_CMLIST","COURSE_SECTIONLIST","CM","TOGGLER","COLLAPSE","TOGGLEALL","ACTIVITYTAG","SECTIONTAG","selectorGenerators","cmNameFor","id","sectionNameFor","classes","COLLAPSED","ACTIVITY","STATEDREADY","dettachedCms","dettachedSections","sections","cms","sectionReturn","pageSectionId","debouncedReloads","Map","target","element","document","querySelector","debug","getElementById","reactive","stateReady","state","_indexContents","addEventListener","this","_sectionTogglers","toogleAll","getElement","collapseElementIds","getElements","map","setAttribute","join","_allSectionToggler","e","key","_refreshAllSectionsToggler","supportComponents","isEditing","DispatchActions","classList","add","CourseEvents","manualCompletionToggled","_completionHandler","_scrollHandler","bind","event","sectionlink","closest","closestCollapse","isChevron","section","toggler","isCollapsed","contains","sectionId","getAttribute","dispatch","preventDefault","isAllCollapsed","course","get","sectionlist","getWatchers","watch","handler","_reloadCm","_refreshCmName","_refreshSectionNumber","_refreshSectionTitle","_refreshSectionCollapsed","_startProcessing","_refreshCourseSectionlist","_refreshSectionCmlist","_reloadSection","forEach","textContent","Error","contentcollapsed","collapsibleId","dataset","replace","collapsible","getOrCreateInstance","toggle","hide","show","sectionIsCollapsible","_getCollapsibleSections","allcollapsed","allexpanded","remove","togglerDoms","querySelectorAll","togglerDom","headerDom","detail","undefined","cmid","completed","pageOffset","window","scrollY","items","getExporter","allItemsArray","pageItem","every","item","index","type","offsetTop","number","sectionid","title","inplace","inplaceeditable","getInplaceEditable","getItemId","setValue","rawtitle","cmlist","listparent","createCm","_createCmItem","_fixOrder","_this$reactive","_this$reactive2","listedSectionIds","createSection","_createSectionItem","_scanIndex","Section","CmItem","selector","creationhandler","_item$dataset","unregister","indexed","_getDebouncedReloadCm","debouncedReload","cmId","pendingKey","pendingReload","Pending","delete","cmitem","resolve","Fragment","loadFragment","Config","courseContextId","courseid","courseId","sr","_this$reactive3","pagesectionid","_this$reactive4","then","html","js","replaceNode","catch","cancel","pending","set","_cancelDebouncedReloadCm","sectionitem","_this$reactive5","_this$reactive6","container","newItem","createElement","for","append","neworder","dettachedelements","createMethod","length","innerHTML","itemid","currentitem","children","insertBefore","orphanElements","lastchild","lastChild","_lastchild$dataset","orphan","push","_lastchild$dataset2","removeChild"],"mappings":"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"} \ No newline at end of file diff --git a/public/course/format/amd/src/local/content.js b/public/course/format/amd/src/local/content.js index da8d80e4a31cf..b4436f4faeeaa 100644 --- a/public/course/format/amd/src/local/content.js +++ b/public/course/format/amd/src/local/content.js @@ -418,10 +418,7 @@ export default class Component extends BaseComponent { * * The courseActions module used for most course section tools still depends on css classes and * section numbers (not id). To prevent inconsistencies when a section is moved, we need to refresh - * the - * - * Course formats can override the section title rendering so the frontend depends heavily on backend - * rendering. Luckily in edit mode we can trigger a title update using the inplace_editable module. + * the section number. * * @param {Object} param * @param {Object} param.element details the update details. @@ -441,27 +438,14 @@ export default class Component extends BaseComponent { target.dataset.sectionid = element.number; // The data-number is the attribute used by components to store the section number. target.dataset.number = element.number; - - // Update title and title inplace editable, if any. - const inplace = inplaceeditable.getInplaceEditable(target.querySelector(this.selectors.SECTION_ITEM)); - if (inplace) { - // The course content HTML can be modified at any moment, so the function need to do some checkings - // to make sure the inplace editable still represents the same itemid. - const currentvalue = inplace.getValue(); - const currentitemid = inplace.getItemId(); - // Unnamed sections must be recalculated. - if (inplace.getValue() === '') { - // The value to send can be an empty value if it is a default name. - if (currentitemid == element.id && (currentvalue != element.rawtitle || element.rawtitle == '')) { - inplace.setValue(element.rawtitle); - } - } - } } /** * Update a course section name on the whole page. * + * Course formats can override the section title rendering so the frontend depends heavily on backend + * rendering. Luckily in edit mode we can trigger a title update using the inplace_editable module. + * * @param {object} param * @param {Object} param.element details the update details. */ @@ -473,6 +457,24 @@ export default class Component extends BaseComponent { allSectionNamesFor.forEach((sectionNameFor) => { sectionNameFor.textContent = element.title; }); + + // Find the element. + const target = this.getElement(this.selectors.SECTION, element.id); + if (!target) { + // Job done. Nothing to refresh. + return; + } + + // Update title and title inplace editable, if any. + const inplace = inplaceeditable.getInplaceEditable(target.querySelector(this.selectors.SECTION_ITEM)); + if (inplace) { + // The course content HTML can be modified at any moment, so the function need to do some checkings + // to make sure the inplace editable still represents the same itemid. + const currentitemid = inplace.getItemId(); + if (currentitemid == element.id) { + inplace.setValue(element.rawtitle); + } + } } /** From 35d753c4e2b5d12776665fe18368560d9935d100 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Tue, 16 Sep 2025 18:22:27 +0800 Subject: [PATCH 002/553] MDL-86654 customfield: Show drag handles only when necessary * Show category drag handles when there are 2 or more categories * Show custom field drag handles when there are a total of 2 or more fields on the page. --- public/customfield/classes/output/management.php | 10 +++++++++- public/customfield/externallib.php | 2 ++ public/customfield/templates/list.mustache | 13 +++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/public/customfield/classes/output/management.php b/public/customfield/classes/output/management.php index 49be1e7b4a43f..b5e86d3df1a42 100644 --- a/public/customfield/classes/output/management.php +++ b/public/customfield/classes/output/management.php @@ -85,7 +85,8 @@ public function export_for_template(\renderer_base $output) { ]); $categoriesarray = array(); - + $movablefieldscount = 0; + $movablecategoriescount = 0; foreach ($categories as $category) { $canedit = $data->component === $category->get('component') && $data->area === $category->get('area'); @@ -133,6 +134,9 @@ public function export_for_template(\renderer_base $output) { $categoryarray['canedit'] = $canedit; $categoryarray['fields'][] = $fieldarray; + if ($canedit) { + $movablefieldscount++; + } } if ($canedit) { @@ -152,6 +156,7 @@ public function export_for_template(\renderer_base $output) { $menu->attributes['class'] .= ' float-start me-1'; $categoryarray['addfieldmenu'] = $output->render($menu); + $movablecategoriescount++; } else { $categoryarray['addfieldmenu'] = ''; } @@ -160,6 +165,9 @@ public function export_for_template(\renderer_base $output) { } $data->categories = $categoriesarray; + $data->canmovecategories = $movablecategoriescount > 1; + // Can move fields if there are more than one field or if there are multiple categories. + $data->canmovefields = $movablefieldscount > 1 || $data->canmovecategories; if (empty($data->categories)) { $data->nocategories = get_string('nocategories', 'core_customfield'); diff --git a/public/customfield/externallib.php b/public/customfield/externallib.php index 4da6e08d6b5d6..57d2ab2201408 100644 --- a/public/customfield/externallib.php +++ b/public/customfield/externallib.php @@ -135,6 +135,8 @@ public static function reload_template_returns() { ) ) ), + 'canmovefields' => new external_value(PARAM_BOOL, 'Whether fields can be moved', VALUE_DEFAULT, false), + 'canmovecategories' => new external_value(PARAM_BOOL, 'Whether categories can be moved', VALUE_DEFAULT, false), ) ); } diff --git a/public/customfield/templates/list.mustache b/public/customfield/templates/list.mustache index be601b2ddca3e..e0689043cff41 100644 --- a/public/customfield/templates/list.mustache +++ b/public/customfield/templates/list.mustache @@ -38,6 +38,8 @@ "area": "course", "itemid": 0, "usescategories": 1, + "canmovecategories": 1, + "canmovefields": 1, "categories": [ { "id": "0", "nameeditable": "Other fields", @@ -80,7 +82,9 @@
{{#usescategories}}

- {{#canedit}}{{> core/drag_handle}}{{/canedit}} + {{#canmovecategories}} + {{#canedit}}{{> core/drag_handle}}{{/canedit}} + {{/canmovecategories}} {{{nameeditable}}} {{#canedit}} @@ -112,7 +116,12 @@ {{#fields}} - {{#canedit}}{{> core/drag_handle}}{{/canedit}}{{{name}}} + + {{#canmovefields}} + {{#canedit}}{{> core/drag_handle}}{{/canedit}} + {{/canmovefields}} + {{{name}}} + {{{shortname}}} {{{type}}} From 4d058c5c616111cabbc73a53f4bace9a5029299f Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Mon, 6 Oct 2025 15:32:27 +0800 Subject: [PATCH 003/553] MDL-86654 customfield: Ensure we can test the display of move category * And simplify checking for the move buttons --- .../tests/behat/shared_custom_fields.feature | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/public/customfield/tests/behat/shared_custom_fields.feature b/public/customfield/tests/behat/shared_custom_fields.feature index e503b885fa654..18fa059ec269b 100644 --- a/public/customfield/tests/behat/shared_custom_fields.feature +++ b/public/customfield/tests/behat/shared_custom_fields.feature @@ -47,12 +47,19 @@ Feature: Create shared categories and fields # Check that the inplaceeditable exists for course categories but not for shared categories. And "//div[contains(@class,'categoryinstance') and contains(.,'My course category') and .//span[contains(@class,'inplaceeditable')]]" "xpath_element" should exist And "//div[contains(@class,'categoryinstance') and contains(.,'My shared category') and .//span[contains(@class,'inplaceeditable')]]" "xpath_element" should not exist - # Check that the move category option exists for course categories but not for shared categories. - And "//span[contains(@class,'movecategory')][.//span[@title='Move \"My course category\"']]" "xpath_element" should exist - And "//span[contains(@class,'movecategory')][.//span[@title='Move \"My shared category\"']]" "xpath_element" should not exist - # Check that the move field option exists for course fields but not for shared fields. - And "//tr[@data-field-name='Course field 1']//span[@title='Move \"Course field 1\"']" "xpath_element" should exist - And "//tr[@data-field-name='Shared field 1']//span[@title='Move \"Shared field 1\"']" "xpath_element" should not exist + # There should be no move button for lone custom field categories. + And "Move \"My course category\"" "button" should not exist + # There should be no move button for lone custom fields within a single custom field category. + And "Move \"Course field 1\"" "button" should not exist + And I press "Add a new category" + # There should be no move button for shared categories and custom fields. + And "Move \"My shared category\"" "button" should not exist + And "Move \"Shared field 1\"" "button" should not exist + # TODO. We should not need to reload the page, but behat fails to find the move buttons otherwise. + And I reload the page + # With more than one category there should be move buttons for course categories and fields. + And "Move \"My course category\"" "button" should exist + And "Move \"Course field 1\"" "button" should exist Scenario: Select which shared custom fields categories are used in the course entity Given the following "custom field categories" exist: From 3649adaf2b1e310ef37c2ea9d6863f1804d314e9 Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Tue, 26 Aug 2025 16:32:49 -0700 Subject: [PATCH 004/553] MDL-86450 Forum: adds missing IDs to advanced search form elements. --- public/mod/forum/templates/big_search_form.mustache | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/mod/forum/templates/big_search_form.mustache b/public/mod/forum/templates/big_search_form.mustache index acb1072da89de..7dc96f7171202 100644 --- a/public/mod/forum/templates/big_search_form.mustache +++ b/public/mod/forum/templates/big_search_form.mustache @@ -94,7 +94,7 @@
- + {{{datefromfields}}}
@@ -110,7 +110,7 @@
- + {{{datetofields}}}
From a5d6ec1c4ee1b7b8c248f53877f680fbff4c2863 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Mon, 15 Sep 2025 12:38:38 +0100 Subject: [PATCH 005/553] MDL-86069 blog: verify success state of RSS file before processing. Avoids subsequent errors in the SimplePie library when trying to process invalid feed content, triggering numerous PHP deprecation notices. See: https://github.com/simplepie/simplepie/issues/810 --- public/blog/external_blog_edit_form.php | 2 +- public/blog/lib.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/blog/external_blog_edit_form.php b/public/blog/external_blog_edit_form.php index 1cf979b3aa601..68ca8ed7c56e9 100644 --- a/public/blog/external_blog_edit_form.php +++ b/public/blog/external_blog_edit_form.php @@ -82,7 +82,7 @@ public function validation($data, $files) { $rssfile = $rss->registry->create('File', array($data['url'])); $filetest = $rss->registry->create('Locator', array($rssfile)); - if (!$filetest->is_feed($rssfile)) { + if (empty($rssfile->success) || !$filetest->is_feed($rssfile)) { $errors['url'] = get_string('feedisinvalid', 'blog'); } else { $rss->set_feed_url($data['url']); diff --git a/public/blog/lib.php b/public/blog/lib.php index 47566276e7680..078c203811d43 100644 --- a/public/blog/lib.php +++ b/public/blog/lib.php @@ -153,7 +153,7 @@ function blog_sync_external_entries($externalblog) { $rssfile = $rss->registry->create('File', array($externalblog->url)); $filetest = $rss->registry->create('Locator', array($rssfile)); - if (!$filetest->is_feed($rssfile)) { + if (empty($rssfile->success) || !$filetest->is_feed($rssfile)) { $externalblog->failedlastsync = 1; $DB->update_record('blog_external', $externalblog); return false; From c191f7be0f7d02de7d092a57985d684c77e22823 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 19 Aug 2025 23:04:01 +0100 Subject: [PATCH 006/553] MDL-86332 completion: remove criteria only from current course on save. --- .upgradenotes/MDL-86332-2025081922154697.yml | 8 +++ .../criteria/completion_criteria_course.php | 2 +- public/course/completion.php | 2 +- public/lib/completionlib.php | 20 ++++-- public/lib/tests/completionlib_test.php | 66 ++++++++++++++++++- 5 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 .upgradenotes/MDL-86332-2025081922154697.yml diff --git a/.upgradenotes/MDL-86332-2025081922154697.yml b/.upgradenotes/MDL-86332-2025081922154697.yml new file mode 100644 index 0000000000000..e580c779d36ab --- /dev/null +++ b/.upgradenotes/MDL-86332-2025081922154697.yml @@ -0,0 +1,8 @@ +issueNumber: MDL-86332 +notes: + core_completion: + - message: >- + The `completion_info::clear_criteria` method takes an optional + `$removetypecriteria` to determine whether to remove course type + criteria from other courses that refer to the current course + type: changed diff --git a/public/completion/criteria/completion_criteria_course.php b/public/completion/criteria/completion_criteria_course.php index c5eb9cb151c66..4cd64f595671c 100644 --- a/public/completion/criteria/completion_criteria_course.php +++ b/public/completion/criteria/completion_criteria_course.php @@ -74,7 +74,7 @@ public function config_form_display(&$mform, $data = null) { /** * Update the criteria information stored in the database * - * @param array $data Form data + * @param stdClass $data Form data */ public function update_config(&$data) { diff --git a/public/course/completion.php b/public/course/completion.php index f83f8576db002..a251d22cfa864 100644 --- a/public/course/completion.php +++ b/public/course/completion.php @@ -95,7 +95,7 @@ } // Delete old criteria. - $completion->clear_criteria(); + $completion->clear_criteria(false); // Loop through each criteria type and run its update_config() method. global $COMPLETION_CRITERIA_TYPES; diff --git a/public/lib/completionlib.php b/public/lib/completionlib.php index f32eddea022ce..ccafce43337ab 100644 --- a/public/lib/completionlib.php +++ b/public/lib/completionlib.php @@ -478,17 +478,23 @@ public function get_aggregation_method($criteriatype = null) { /** * Clear old course completion criteria + * + * @param bool $removetypecriteria Also remove course type criteria from other courses that refer to the current course */ - public function clear_criteria() { + public function clear_criteria(bool $removetypecriteria = true): void { global $DB; + $select = 'course = :course'; + $params = ['course' => $this->course->id]; + // Remove completion criteria records for the course itself, and any records that refer to the course. - $select = 'course = :course OR (criteriatype = :type AND courseinstance = :courseinstance)'; - $params = [ - 'course' => $this->course_id, - 'type' => COMPLETION_CRITERIA_TYPE_COURSE, - 'courseinstance' => $this->course_id, - ]; + if ($removetypecriteria) { + $select .= ' OR (criteriatype = :type AND courseinstance = :courseinstance)'; + $params = array_merge($params, [ + 'type' => COMPLETION_CRITERIA_TYPE_COURSE, + 'courseinstance' => $this->course_id, + ]); + } $DB->delete_records_select('course_completion_criteria', $select, $params); $DB->delete_records('course_completion_aggr_methd', array('course' => $this->course_id)); diff --git a/public/lib/tests/completionlib_test.php b/public/lib/tests/completionlib_test.php index e2df63d814002..513e79d0da124 100644 --- a/public/lib/tests/completionlib_test.php +++ b/public/lib/tests/completionlib_test.php @@ -1466,9 +1466,70 @@ public function test_has_activities(): void { $this->assertFalse($c2->has_activities()); } + /** + * Data provider for {@see test_clear_criteria} + * + * @return bool[][] + */ + public static function clear_criteria_provider(): array { + return [ + [false], + [true], + ]; + } + + /** + * Test clearing criteria for current course + * + * @param bool $removetypecriteria + * + * @covers ::clear_criteria + * @dataProvider clear_criteria_provider + */ + public function test_clear_criteria(bool $removetypecriteria): void { + global $DB; + + $this->setup_data(); + + $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]); + + /** @var completion_criteria_self $criteria */ + $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_SELF]); + $criteriadata = (object) [ + 'id' => $courseprerequisite->id, + 'criteria_self' => 1, + ]; + $criteria->update_config($criteriadata); + + /** @var completion_criteria_course $criteria */ + $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]); + $criteriadata = (object) [ + 'id' => $this->course->id, + 'criteria_course' => [$courseprerequisite->id], + ]; + $criteria->update_config($criteriadata); + + // Sanity test. + $this->assertTrue($DB->record_exists('course_completion_criteria', ['course' => $courseprerequisite->id])); + + $completion = new completion_info($courseprerequisite); + $completion->clear_criteria($removetypecriteria); + + // There should be no criteria data for the course. + $this->assertFalse($DB->record_exists('course_completion_criteria', ['course' => $courseprerequisite->id])); + + // Course type criteria from other courses that refer to the course. + $this->assertEquals(!$removetypecriteria, $DB->record_exists('course_completion_criteria', [ + 'course' => $this->course->id, + 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE, + 'courseinstance' => $courseprerequisite->id, + ])); + } + /** * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses * + * @covers ::clear_criteria * @covers ::delete_course_completion_data * @covers ::delete_all_completion_data */ @@ -1479,13 +1540,12 @@ public function test_course_delete_prerequisite(): void { $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]); + /** @var completion_criteria_course $criteria */ + $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]); $criteriadata = (object) [ 'id' => $this->course->id, 'criteria_course' => [$courseprerequisite->id], ]; - - /** @var completion_criteria_course $criteria */ - $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]); $criteria->update_config($criteriadata); // Sanity test. From 0bb74501eab1cfbed7724c4bd5244860d64f0d86 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Sat, 4 Oct 2025 11:24:26 +0700 Subject: [PATCH 007/553] MDL-86828 upgrade: Add the 5.1.0 separation line to all upgrade scripts --- public/admin/tool/cohortroles/db/upgrade.php | 3 +++ public/admin/tool/customlang/db/upgrade.php | 3 +++ public/admin/tool/dataprivacy/db/upgrade.php | 3 +++ public/admin/tool/log/db/upgrade.php | 3 +++ public/admin/tool/log/store/database/db/upgrade.php | 3 +++ public/admin/tool/log/store/standard/db/upgrade.php | 3 +++ public/admin/tool/mfa/factor/auth/db/upgrade.php | 3 +++ public/admin/tool/mfa/factor/email/db/upgrade.php | 3 +++ public/admin/tool/mfa/factor/sms/db/upgrade.php | 3 +++ public/admin/tool/mfa/factor/totp/db/upgrade.php | 3 +++ public/admin/tool/mobile/db/upgrade.php | 3 +++ public/admin/tool/monitor/db/upgrade.php | 3 +++ public/admin/tool/moodlenet/db/upgrade.php | 3 +++ public/admin/tool/policy/db/upgrade.php | 3 +++ public/admin/tool/recyclebin/db/upgrade.php | 3 +++ public/admin/tool/usertours/db/upgrade.php | 3 +++ public/auth/db/db/upgrade.php | 3 +++ public/auth/email/db/upgrade.php | 3 +++ public/auth/ldap/db/upgrade.php | 3 +++ public/auth/lti/db/upgrade.php | 3 +++ public/auth/manual/db/upgrade.php | 3 +++ public/auth/none/db/upgrade.php | 3 +++ public/auth/oauth2/db/upgrade.php | 3 +++ public/auth/shibboleth/db/upgrade.php | 3 +++ public/blocks/badges/db/upgrade.php | 3 +++ public/blocks/calendar_month/db/upgrade.php | 3 +++ public/blocks/calendar_upcoming/db/upgrade.php | 3 +++ public/blocks/completionstatus/db/upgrade.php | 3 +++ public/blocks/course_summary/db/upgrade.php | 3 +++ public/blocks/feedback/db/upgrade.php | 3 +++ public/blocks/html/db/upgrade.php | 3 +++ public/blocks/myoverview/db/upgrade.php | 3 +++ public/blocks/navigation/db/upgrade.php | 3 +++ public/blocks/recent_activity/db/upgrade.php | 3 +++ public/blocks/recentlyaccesseditems/db/upgrade.php | 3 +++ public/blocks/rss_client/db/upgrade.php | 3 +++ public/blocks/selfcompletion/db/upgrade.php | 3 +++ public/blocks/settings/db/upgrade.php | 3 +++ public/blocks/tag_youtube/db/upgrade.php | 3 +++ public/blocks/timeline/db/upgrade.php | 3 +++ public/communication/provider/matrix/db/upgrade.php | 3 +++ public/course/format/topics/db/upgrade.php | 3 +++ public/course/format/weeks/db/upgrade.php | 3 +++ public/enrol/database/db/upgrade.php | 3 +++ public/enrol/fee/db/upgrade.php | 3 +++ public/enrol/flatfile/db/upgrade.php | 3 +++ public/enrol/guest/db/upgrade.php | 3 +++ public/enrol/imsenterprise/db/upgrade.php | 3 +++ public/enrol/lti/db/upgrade.php | 3 +++ public/enrol/manual/db/upgrade.php | 3 +++ public/enrol/paypal/db/upgrade.php | 3 +++ public/enrol/self/db/upgrade.php | 3 +++ public/filter/displayh5p/db/upgrade.php | 3 +++ public/filter/mathjaxloader/db/upgrade.php | 3 +++ public/filter/mediaplugin/db/upgrade.php | 3 +++ public/filter/tex/db/upgrade.php | 3 +++ public/grade/grading/form/guide/db/upgrade.php | 3 +++ public/grade/grading/form/rubric/db/upgrade.php | 3 +++ public/grade/report/grader/db/upgrade.php | 3 +++ public/grade/report/history/db/upgrade.php | 3 +++ public/grade/report/overview/db/upgrade.php | 3 +++ public/grade/report/user/db/upgrade.php | 3 +++ public/lib/antivirus/clamav/db/upgrade.php | 3 +++ public/lib/db/upgrade.php | 3 +++ public/lib/editor/tiny/plugins/premium/db/upgrade.php | 3 +++ public/lib/editor/tiny/plugins/recordrtc/db/upgrade.php | 3 +++ public/media/player/videojs/db/upgrade.php | 3 +++ public/message/output/email/db/upgrade.php | 3 +++ public/message/output/popup/db/upgrade.php | 3 +++ public/mod/assign/db/upgrade.php | 3 +++ public/mod/assign/feedback/comments/db/upgrade.php | 3 +++ public/mod/assign/feedback/editpdf/db/upgrade.php | 3 +++ public/mod/assign/feedback/file/db/upgrade.php | 3 +++ public/mod/assign/submission/comments/db/upgrade.php | 3 +++ public/mod/assign/submission/file/db/upgrade.php | 3 +++ public/mod/assign/submission/onlinetext/db/upgrade.php | 3 +++ public/mod/bigbluebuttonbn/db/upgrade.php | 3 +++ public/mod/book/db/upgrade.php | 3 +++ public/mod/choice/db/upgrade.php | 3 +++ public/mod/data/db/upgrade.php | 3 +++ public/mod/feedback/db/upgrade.php | 3 +++ public/mod/folder/db/upgrade.php | 3 +++ public/mod/forum/db/upgrade.php | 3 +++ public/mod/glossary/db/upgrade.php | 3 +++ public/mod/h5pactivity/db/upgrade.php | 3 +++ public/mod/imscp/db/upgrade.php | 3 +++ public/mod/label/db/upgrade.php | 3 +++ public/mod/lesson/db/upgrade.php | 3 +++ public/mod/lti/db/upgrade.php | 3 +++ public/mod/lti/service/gradebookservices/db/upgrade.php | 3 +++ public/mod/page/db/upgrade.php | 3 +++ public/mod/qbank/db/upgrade.php | 3 +++ public/mod/quiz/accessrule/seb/db/upgrade.php | 3 +++ public/mod/quiz/db/upgrade.php | 3 +++ public/mod/quiz/report/overview/db/upgrade.php | 3 +++ public/mod/quiz/report/statistics/db/upgrade.php | 3 +++ public/mod/resource/db/upgrade.php | 3 +++ public/mod/scorm/db/upgrade.php | 3 +++ public/mod/subsection/db/upgrade.php | 3 +++ public/mod/url/db/upgrade.php | 3 +++ public/mod/wiki/db/upgrade.php | 3 +++ public/mod/workshop/db/upgrade.php | 3 +++ public/mod/workshop/form/accumulative/db/upgrade.php | 3 +++ public/mod/workshop/form/comments/db/upgrade.php | 3 +++ public/mod/workshop/form/numerrors/db/upgrade.php | 3 +++ public/mod/workshop/form/rubric/db/upgrade.php | 3 +++ public/payment/gateway/paypal/db/upgrade.php | 3 +++ public/portfolio/googledocs/db/upgrade.php | 3 +++ public/question/bank/columnsortorder/db/upgrade.php | 3 +++ public/question/behaviour/manualgraded/db/upgrade.php | 3 +++ public/question/type/calculated/db/upgrade.php | 3 +++ public/question/type/calculatedmulti/db/upgrade.php | 3 +++ public/question/type/ddimageortext/db/upgrade.php | 3 +++ public/question/type/ddmarker/db/upgrade.php | 3 +++ public/question/type/essay/db/upgrade.php | 3 +++ public/question/type/match/db/upgrade.php | 3 +++ public/question/type/multianswer/db/upgrade.php | 3 +++ public/question/type/multichoice/db/upgrade.php | 3 +++ public/question/type/numerical/db/upgrade.php | 3 +++ public/question/type/ordering/db/upgrade.php | 3 +++ public/question/type/random/db/upgrade.php | 3 +++ public/question/type/randomsamatch/db/upgrade.php | 3 +++ public/question/type/shortanswer/db/upgrade.php | 3 +++ public/question/type/truefalse/db/upgrade.php | 3 +++ public/repository/dropbox/db/upgrade.php | 3 +++ public/repository/flickr/db/upgrade.php | 3 +++ public/repository/googledocs/db/upgrade.php | 3 +++ public/repository/onedrive/db/upgrade.php | 3 +++ public/search/engine/simpledb/db/upgrade.php | 3 +++ 129 files changed, 387 insertions(+) diff --git a/public/admin/tool/cohortroles/db/upgrade.php b/public/admin/tool/cohortroles/db/upgrade.php index a06e6855d93c8..93ab547e8a906 100644 --- a/public/admin/tool/cohortroles/db/upgrade.php +++ b/public/admin/tool/cohortroles/db/upgrade.php @@ -54,5 +54,8 @@ function xmldb_tool_cohortroles_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/customlang/db/upgrade.php b/public/admin/tool/customlang/db/upgrade.php index 44cf4ac15c048..465980a1bfd55 100644 --- a/public/admin/tool/customlang/db/upgrade.php +++ b/public/admin/tool/customlang/db/upgrade.php @@ -39,5 +39,8 @@ function xmldb_tool_customlang_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/dataprivacy/db/upgrade.php b/public/admin/tool/dataprivacy/db/upgrade.php index e92a5ebeb7c5d..83fb04f91cf76 100644 --- a/public/admin/tool/dataprivacy/db/upgrade.php +++ b/public/admin/tool/dataprivacy/db/upgrade.php @@ -109,5 +109,8 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/log/db/upgrade.php b/public/admin/tool/log/db/upgrade.php index 7a4ef369a5a78..c300988e9b854 100644 --- a/public/admin/tool/log/db/upgrade.php +++ b/public/admin/tool/log/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_tool_log_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/log/store/database/db/upgrade.php b/public/admin/tool/log/store/database/db/upgrade.php index 8490b8d87c7bd..5ab4f80d958a7 100644 --- a/public/admin/tool/log/store/database/db/upgrade.php +++ b/public/admin/tool/log/store/database/db/upgrade.php @@ -38,5 +38,8 @@ function xmldb_logstore_database_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/log/store/standard/db/upgrade.php b/public/admin/tool/log/store/standard/db/upgrade.php index ba4a84ba32617..32a2a2309d2a1 100644 --- a/public/admin/tool/log/store/standard/db/upgrade.php +++ b/public/admin/tool/log/store/standard/db/upgrade.php @@ -38,5 +38,8 @@ function xmldb_logstore_standard_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/mfa/factor/auth/db/upgrade.php b/public/admin/tool/mfa/factor/auth/db/upgrade.php index a18038506c830..4e443b4bf516e 100644 --- a/public/admin/tool/mfa/factor/auth/db/upgrade.php +++ b/public/admin/tool/mfa/factor/auth/db/upgrade.php @@ -57,5 +57,8 @@ function xmldb_factor_auth_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/mfa/factor/email/db/upgrade.php b/public/admin/tool/mfa/factor/email/db/upgrade.php index a54b0b7b3921f..72d32bd2ab789 100644 --- a/public/admin/tool/mfa/factor/email/db/upgrade.php +++ b/public/admin/tool/mfa/factor/email/db/upgrade.php @@ -56,5 +56,8 @@ function xmldb_factor_email_upgrade($oldversion): bool { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/mfa/factor/sms/db/upgrade.php b/public/admin/tool/mfa/factor/sms/db/upgrade.php index e391f53253942..5464499583553 100644 --- a/public/admin/tool/mfa/factor/sms/db/upgrade.php +++ b/public/admin/tool/mfa/factor/sms/db/upgrade.php @@ -99,5 +99,8 @@ classname: \smsgateway_aws\gateway::class, // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/mfa/factor/totp/db/upgrade.php b/public/admin/tool/mfa/factor/totp/db/upgrade.php index a3944548e5c5f..c3c5839650210 100644 --- a/public/admin/tool/mfa/factor/totp/db/upgrade.php +++ b/public/admin/tool/mfa/factor/totp/db/upgrade.php @@ -45,5 +45,8 @@ function xmldb_factor_totp_upgrade($oldversion): bool { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/mobile/db/upgrade.php b/public/admin/tool/mobile/db/upgrade.php index 17676cbb5ebdf..612e6866f6676 100644 --- a/public/admin/tool/mobile/db/upgrade.php +++ b/public/admin/tool/mobile/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_tool_mobile_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/monitor/db/upgrade.php b/public/admin/tool/monitor/db/upgrade.php index d5a9e8b2a7819..b90f7536df832 100644 --- a/public/admin/tool/monitor/db/upgrade.php +++ b/public/admin/tool/monitor/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_tool_monitor_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/moodlenet/db/upgrade.php b/public/admin/tool/moodlenet/db/upgrade.php index 795da5ff3e707..49f0d96dfb00d 100644 --- a/public/admin/tool/moodlenet/db/upgrade.php +++ b/public/admin/tool/moodlenet/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_tool_moodlenet_upgrade(int $oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/policy/db/upgrade.php b/public/admin/tool/policy/db/upgrade.php index 718efad43577b..a39d0d670db4c 100644 --- a/public/admin/tool/policy/db/upgrade.php +++ b/public/admin/tool/policy/db/upgrade.php @@ -45,5 +45,8 @@ function xmldb_tool_policy_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/recyclebin/db/upgrade.php b/public/admin/tool/recyclebin/db/upgrade.php index d834688535fbc..1e48547990419 100644 --- a/public/admin/tool/recyclebin/db/upgrade.php +++ b/public/admin/tool/recyclebin/db/upgrade.php @@ -76,5 +76,8 @@ function xmldb_tool_recyclebin_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2025041401, 'tool', 'recyclebin'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/usertours/db/upgrade.php b/public/admin/tool/usertours/db/upgrade.php index 2eda4e62a430c..cab38bf4e218d 100644 --- a/public/admin/tool/usertours/db/upgrade.php +++ b/public/admin/tool/usertours/db/upgrade.php @@ -55,5 +55,8 @@ function xmldb_tool_usertours_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/db/db/upgrade.php b/public/auth/db/db/upgrade.php index d33a501978208..2c72183457298 100644 --- a/public/auth/db/db/upgrade.php +++ b/public/auth/db/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_auth_db_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/email/db/upgrade.php b/public/auth/email/db/upgrade.php index c56ddfb089ddf..5a02dbb7678ff 100644 --- a/public/auth/email/db/upgrade.php +++ b/public/auth/email/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_auth_email_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/ldap/db/upgrade.php b/public/auth/ldap/db/upgrade.php index b13a72639af3f..389928bcebb20 100644 --- a/public/auth/ldap/db/upgrade.php +++ b/public/auth/ldap/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_auth_ldap_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/lti/db/upgrade.php b/public/auth/lti/db/upgrade.php index f00cbb1dd7926..43f1c0de93c6d 100644 --- a/public/auth/lti/db/upgrade.php +++ b/public/auth/lti/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_auth_lti_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/manual/db/upgrade.php b/public/auth/manual/db/upgrade.php index a8e350b3e8201..f5f56e53519d8 100644 --- a/public/auth/manual/db/upgrade.php +++ b/public/auth/manual/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_auth_manual_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/none/db/upgrade.php b/public/auth/none/db/upgrade.php index aa841ff89f65f..bee69aa11d70e 100644 --- a/public/auth/none/db/upgrade.php +++ b/public/auth/none/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_auth_none_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/oauth2/db/upgrade.php b/public/auth/oauth2/db/upgrade.php index 47dc0d9574337..e20c864a6f991 100644 --- a/public/auth/oauth2/db/upgrade.php +++ b/public/auth/oauth2/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_auth_oauth2_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/shibboleth/db/upgrade.php b/public/auth/shibboleth/db/upgrade.php index 3e6a7562ffa53..99b332cd35f9a 100644 --- a/public/auth/shibboleth/db/upgrade.php +++ b/public/auth/shibboleth/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_auth_shibboleth_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/badges/db/upgrade.php b/public/blocks/badges/db/upgrade.php index 964d4f150a2fe..1fbdc897e0a53 100644 --- a/public/blocks/badges/db/upgrade.php +++ b/public/blocks/badges/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_block_badges_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/calendar_month/db/upgrade.php b/public/blocks/calendar_month/db/upgrade.php index ff795d2ee0148..4f9aa43097c74 100644 --- a/public/blocks/calendar_month/db/upgrade.php +++ b/public/blocks/calendar_month/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_block_calendar_month_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/calendar_upcoming/db/upgrade.php b/public/blocks/calendar_upcoming/db/upgrade.php index a25b014ad903e..3c640bec21feb 100644 --- a/public/blocks/calendar_upcoming/db/upgrade.php +++ b/public/blocks/calendar_upcoming/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_block_calendar_upcoming_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/completionstatus/db/upgrade.php b/public/blocks/completionstatus/db/upgrade.php index d4a7ca0e00e73..aea2e0e2e70a0 100644 --- a/public/blocks/completionstatus/db/upgrade.php +++ b/public/blocks/completionstatus/db/upgrade.php @@ -59,5 +59,8 @@ function xmldb_block_completionstatus_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/course_summary/db/upgrade.php b/public/blocks/course_summary/db/upgrade.php index c87cdf70e77b9..2578d5dd39aa1 100644 --- a/public/blocks/course_summary/db/upgrade.php +++ b/public/blocks/course_summary/db/upgrade.php @@ -59,5 +59,8 @@ function xmldb_block_course_summary_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/feedback/db/upgrade.php b/public/blocks/feedback/db/upgrade.php index 6d54da15bb088..9d5f3a70aaca4 100644 --- a/public/blocks/feedback/db/upgrade.php +++ b/public/blocks/feedback/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_block_feedback_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/html/db/upgrade.php b/public/blocks/html/db/upgrade.php index cc7b9cb2f4f86..e22c811cd2669 100644 --- a/public/blocks/html/db/upgrade.php +++ b/public/blocks/html/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_block_html_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/myoverview/db/upgrade.php b/public/blocks/myoverview/db/upgrade.php index 04fbee064019e..6780e4bf44594 100644 --- a/public/blocks/myoverview/db/upgrade.php +++ b/public/blocks/myoverview/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_block_myoverview_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/navigation/db/upgrade.php b/public/blocks/navigation/db/upgrade.php index 2490b7f1d7757..7fc4d6064b3e2 100644 --- a/public/blocks/navigation/db/upgrade.php +++ b/public/blocks/navigation/db/upgrade.php @@ -66,5 +66,8 @@ function xmldb_block_navigation_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/recent_activity/db/upgrade.php b/public/blocks/recent_activity/db/upgrade.php index 9a79a3834211c..ca43768afe686 100644 --- a/public/blocks/recent_activity/db/upgrade.php +++ b/public/blocks/recent_activity/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_block_recent_activity_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/recentlyaccesseditems/db/upgrade.php b/public/blocks/recentlyaccesseditems/db/upgrade.php index 56a1a68bc9bc1..3f2e573cf2a91 100644 --- a/public/blocks/recentlyaccesseditems/db/upgrade.php +++ b/public/blocks/recentlyaccesseditems/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_block_recentlyaccesseditems_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/rss_client/db/upgrade.php b/public/blocks/rss_client/db/upgrade.php index 2c86bd7b0c754..ff972be953feb 100644 --- a/public/blocks/rss_client/db/upgrade.php +++ b/public/blocks/rss_client/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_block_rss_client_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/selfcompletion/db/upgrade.php b/public/blocks/selfcompletion/db/upgrade.php index b0865acab6a5f..a67b5fb31267d 100644 --- a/public/blocks/selfcompletion/db/upgrade.php +++ b/public/blocks/selfcompletion/db/upgrade.php @@ -59,5 +59,8 @@ function xmldb_block_selfcompletion_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/settings/db/upgrade.php b/public/blocks/settings/db/upgrade.php index f0a037b54aef0..9629ba4e0cf4a 100644 --- a/public/blocks/settings/db/upgrade.php +++ b/public/blocks/settings/db/upgrade.php @@ -66,5 +66,8 @@ function xmldb_block_settings_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/tag_youtube/db/upgrade.php b/public/blocks/tag_youtube/db/upgrade.php index 001cfe0220ceb..addcf93ca3fe4 100644 --- a/public/blocks/tag_youtube/db/upgrade.php +++ b/public/blocks/tag_youtube/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_block_tag_youtube_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/timeline/db/upgrade.php b/public/blocks/timeline/db/upgrade.php index cd5fcb91e37b9..60e8f030c709d 100644 --- a/public/blocks/timeline/db/upgrade.php +++ b/public/blocks/timeline/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_block_timeline_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/communication/provider/matrix/db/upgrade.php b/public/communication/provider/matrix/db/upgrade.php index 5f24f38741a36..6f281b181ff88 100644 --- a/public/communication/provider/matrix/db/upgrade.php +++ b/public/communication/provider/matrix/db/upgrade.php @@ -63,5 +63,8 @@ function xmldb_communication_matrix_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/course/format/topics/db/upgrade.php b/public/course/format/topics/db/upgrade.php index fc78203e59c71..777e6a0bbbcf4 100644 --- a/public/course/format/topics/db/upgrade.php +++ b/public/course/format/topics/db/upgrade.php @@ -64,5 +64,8 @@ function xmldb_format_topics_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/course/format/weeks/db/upgrade.php b/public/course/format/weeks/db/upgrade.php index 65965ffaa18fd..867992a45f22a 100644 --- a/public/course/format/weeks/db/upgrade.php +++ b/public/course/format/weeks/db/upgrade.php @@ -60,5 +60,8 @@ function xmldb_format_weeks_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2025052600, 'format', 'weeks'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/enrol/database/db/upgrade.php b/public/enrol/database/db/upgrade.php index b62d1990a8999..e6af6646b6bf1 100644 --- a/public/enrol/database/db/upgrade.php +++ b/public/enrol/database/db/upgrade.php @@ -99,5 +99,8 @@ function xmldb_enrol_database_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2025070501, 'enrol', 'database'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/enrol/fee/db/upgrade.php b/public/enrol/fee/db/upgrade.php index dc7c17e3ff656..f21dba6e0a62f 100644 --- a/public/enrol/fee/db/upgrade.php +++ b/public/enrol/fee/db/upgrade.php @@ -47,5 +47,8 @@ function xmldb_enrol_fee_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/enrol/flatfile/db/upgrade.php b/public/enrol/flatfile/db/upgrade.php index edcb868b6cb54..c8243fb7a3971 100644 --- a/public/enrol/flatfile/db/upgrade.php +++ b/public/enrol/flatfile/db/upgrade.php @@ -38,5 +38,8 @@ function xmldb_enrol_flatfile_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/enrol/guest/db/upgrade.php b/public/enrol/guest/db/upgrade.php index b0946bf78745e..3b99012ba929c 100644 --- a/public/enrol/guest/db/upgrade.php +++ b/public/enrol/guest/db/upgrade.php @@ -38,5 +38,8 @@ function xmldb_enrol_guest_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/enrol/imsenterprise/db/upgrade.php b/public/enrol/imsenterprise/db/upgrade.php index e8b2f4ca495fe..4f4912ca31e16 100644 --- a/public/enrol/imsenterprise/db/upgrade.php +++ b/public/enrol/imsenterprise/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_enrol_imsenterprise_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/enrol/lti/db/upgrade.php b/public/enrol/lti/db/upgrade.php index 3a51bc9d66909..624df46af96b7 100644 --- a/public/enrol/lti/db/upgrade.php +++ b/public/enrol/lti/db/upgrade.php @@ -50,5 +50,8 @@ function xmldb_enrol_lti_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/enrol/manual/db/upgrade.php b/public/enrol/manual/db/upgrade.php index a5aa074858592..cc91ced7f78b3 100644 --- a/public/enrol/manual/db/upgrade.php +++ b/public/enrol/manual/db/upgrade.php @@ -38,5 +38,8 @@ function xmldb_enrol_manual_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/enrol/paypal/db/upgrade.php b/public/enrol/paypal/db/upgrade.php index fa2203b118496..20a812e91e763 100644 --- a/public/enrol/paypal/db/upgrade.php +++ b/public/enrol/paypal/db/upgrade.php @@ -56,5 +56,8 @@ function xmldb_enrol_paypal_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/enrol/self/db/upgrade.php b/public/enrol/self/db/upgrade.php index 4b078784906df..8c2f004219beb 100644 --- a/public/enrol/self/db/upgrade.php +++ b/public/enrol/self/db/upgrade.php @@ -38,5 +38,8 @@ function xmldb_enrol_self_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/filter/displayh5p/db/upgrade.php b/public/filter/displayh5p/db/upgrade.php index 3741a902b90a4..6cef64b916e4f 100644 --- a/public/filter/displayh5p/db/upgrade.php +++ b/public/filter/displayh5p/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_filter_displayh5p_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/filter/mathjaxloader/db/upgrade.php b/public/filter/mathjaxloader/db/upgrade.php index f814cb3275c32..1a110b190e41b 100644 --- a/public/filter/mathjaxloader/db/upgrade.php +++ b/public/filter/mathjaxloader/db/upgrade.php @@ -54,5 +54,8 @@ function xmldb_filter_mathjaxloader_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/filter/mediaplugin/db/upgrade.php b/public/filter/mediaplugin/db/upgrade.php index a588666daaf1f..e1c4a54b66649 100644 --- a/public/filter/mediaplugin/db/upgrade.php +++ b/public/filter/mediaplugin/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_filter_mediaplugin_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/filter/tex/db/upgrade.php b/public/filter/tex/db/upgrade.php index a07e20c9adf42..c875ac8a62181 100644 --- a/public/filter/tex/db/upgrade.php +++ b/public/filter/tex/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_filter_tex_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/grade/grading/form/guide/db/upgrade.php b/public/grade/grading/form/guide/db/upgrade.php index 3c0aff498a46c..e3b8578e8b691 100644 --- a/public/grade/grading/form/guide/db/upgrade.php +++ b/public/grade/grading/form/guide/db/upgrade.php @@ -48,5 +48,8 @@ function xmldb_gradingform_guide_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/grade/grading/form/rubric/db/upgrade.php b/public/grade/grading/form/rubric/db/upgrade.php index 8f39148752207..188f0ffbd7e05 100644 --- a/public/grade/grading/form/rubric/db/upgrade.php +++ b/public/grade/grading/form/rubric/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_gradingform_rubric_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/grade/report/grader/db/upgrade.php b/public/grade/report/grader/db/upgrade.php index 001430d89656d..e8d598ec0b206 100644 --- a/public/grade/report/grader/db/upgrade.php +++ b/public/grade/report/grader/db/upgrade.php @@ -86,5 +86,8 @@ function xmldb_gradereport_grader_upgrade(int $oldversion): bool { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/grade/report/history/db/upgrade.php b/public/grade/report/history/db/upgrade.php index ea6de0ab81c92..861890a7feb4b 100644 --- a/public/grade/report/history/db/upgrade.php +++ b/public/grade/report/history/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_gradereport_history_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/grade/report/overview/db/upgrade.php b/public/grade/report/overview/db/upgrade.php index b18cc787d5c1c..ed756a5774137 100644 --- a/public/grade/report/overview/db/upgrade.php +++ b/public/grade/report/overview/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_gradereport_overview_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/grade/report/user/db/upgrade.php b/public/grade/report/user/db/upgrade.php index 5c98e6ed0741d..ea18705f9ae0c 100644 --- a/public/grade/report/user/db/upgrade.php +++ b/public/grade/report/user/db/upgrade.php @@ -42,5 +42,8 @@ function xmldb_gradereport_user_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/lib/antivirus/clamav/db/upgrade.php b/public/lib/antivirus/clamav/db/upgrade.php index f23c6f41f035f..2174f393dfdd8 100644 --- a/public/lib/antivirus/clamav/db/upgrade.php +++ b/public/lib/antivirus/clamav/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_antivirus_clamav_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/lib/db/upgrade.php b/public/lib/db/upgrade.php index 1514c14c56a54..92eda05655a4d 100644 --- a/public/lib/db/upgrade.php +++ b/public/lib/db/upgrade.php @@ -2301,5 +2301,8 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2025092200.00); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/lib/editor/tiny/plugins/premium/db/upgrade.php b/public/lib/editor/tiny/plugins/premium/db/upgrade.php index 9074c00b4f57e..4f74c6ba845a9 100644 --- a/public/lib/editor/tiny/plugins/premium/db/upgrade.php +++ b/public/lib/editor/tiny/plugins/premium/db/upgrade.php @@ -54,5 +54,8 @@ function xmldb_tiny_premium_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/lib/editor/tiny/plugins/recordrtc/db/upgrade.php b/public/lib/editor/tiny/plugins/recordrtc/db/upgrade.php index 4dd8829cbc649..d43a5a7cd78e8 100644 --- a/public/lib/editor/tiny/plugins/recordrtc/db/upgrade.php +++ b/public/lib/editor/tiny/plugins/recordrtc/db/upgrade.php @@ -68,5 +68,8 @@ function xmldb_tiny_recordrtc_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/media/player/videojs/db/upgrade.php b/public/media/player/videojs/db/upgrade.php index ed218286664b8..aef63c557837a 100644 --- a/public/media/player/videojs/db/upgrade.php +++ b/public/media/player/videojs/db/upgrade.php @@ -64,5 +64,8 @@ function xmldb_media_videojs_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/message/output/email/db/upgrade.php b/public/message/output/email/db/upgrade.php index f0750db3a21ef..23780dd9c7494 100644 --- a/public/message/output/email/db/upgrade.php +++ b/public/message/output/email/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_message_email_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/message/output/popup/db/upgrade.php b/public/message/output/popup/db/upgrade.php index f6fce3e41b937..a9a5351a23e31 100644 --- a/public/message/output/popup/db/upgrade.php +++ b/public/message/output/popup/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_message_popup_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/assign/db/upgrade.php b/public/mod/assign/db/upgrade.php index d9f6304192f16..61f5126a2a448 100644 --- a/public/mod/assign/db/upgrade.php +++ b/public/mod/assign/db/upgrade.php @@ -157,5 +157,8 @@ function xmldb_assign_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'assign'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/assign/feedback/comments/db/upgrade.php b/public/mod/assign/feedback/comments/db/upgrade.php index ebd66484263eb..badbd7fb64207 100644 --- a/public/mod/assign/feedback/comments/db/upgrade.php +++ b/public/mod/assign/feedback/comments/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_assignfeedback_comments_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/assign/feedback/editpdf/db/upgrade.php b/public/mod/assign/feedback/editpdf/db/upgrade.php index 9d66439ce628a..1cc5554f10d59 100644 --- a/public/mod/assign/feedback/editpdf/db/upgrade.php +++ b/public/mod/assign/feedback/editpdf/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_assignfeedback_editpdf_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/assign/feedback/file/db/upgrade.php b/public/mod/assign/feedback/file/db/upgrade.php index bac7763e9962a..b5bd28ad11852 100644 --- a/public/mod/assign/feedback/file/db/upgrade.php +++ b/public/mod/assign/feedback/file/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_assignfeedback_file_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/assign/submission/comments/db/upgrade.php b/public/mod/assign/submission/comments/db/upgrade.php index 99f72970b6e82..310d4788c1b6e 100644 --- a/public/mod/assign/submission/comments/db/upgrade.php +++ b/public/mod/assign/submission/comments/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_assignsubmission_comments_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/assign/submission/file/db/upgrade.php b/public/mod/assign/submission/file/db/upgrade.php index c9cd58cf5b715..e115c344050c4 100644 --- a/public/mod/assign/submission/file/db/upgrade.php +++ b/public/mod/assign/submission/file/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_assignsubmission_file_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/assign/submission/onlinetext/db/upgrade.php b/public/mod/assign/submission/onlinetext/db/upgrade.php index 426996846fec2..3864984d1e83c 100644 --- a/public/mod/assign/submission/onlinetext/db/upgrade.php +++ b/public/mod/assign/submission/onlinetext/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_assignsubmission_onlinetext_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/bigbluebuttonbn/db/upgrade.php b/public/mod/bigbluebuttonbn/db/upgrade.php index e177abc250c21..482bb4b375566 100644 --- a/public/mod/bigbluebuttonbn/db/upgrade.php +++ b/public/mod/bigbluebuttonbn/db/upgrade.php @@ -97,6 +97,9 @@ function xmldb_bigbluebuttonbn_upgrade($oldversion = 0) { upgrade_mod_savepoint(true, 2025041401, 'bigbluebuttonbn'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/book/db/upgrade.php b/public/mod/book/db/upgrade.php index 613c895102a3b..92b41ce88cc8c 100644 --- a/public/mod/book/db/upgrade.php +++ b/public/mod/book/db/upgrade.php @@ -66,5 +66,8 @@ function xmldb_book_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'book'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/choice/db/upgrade.php b/public/mod/choice/db/upgrade.php index 727b785216016..6d129917b8354 100644 --- a/public/mod/choice/db/upgrade.php +++ b/public/mod/choice/db/upgrade.php @@ -71,5 +71,8 @@ function xmldb_choice_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'choice'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/data/db/upgrade.php b/public/mod/data/db/upgrade.php index 881dd8c0f9421..a8e9f25717ead 100644 --- a/public/mod/data/db/upgrade.php +++ b/public/mod/data/db/upgrade.php @@ -87,5 +87,8 @@ function xmldb_data_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'data'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/feedback/db/upgrade.php b/public/mod/feedback/db/upgrade.php index 5a791232f22a9..e5ebfd54a2907 100644 --- a/public/mod/feedback/db/upgrade.php +++ b/public/mod/feedback/db/upgrade.php @@ -78,5 +78,8 @@ function xmldb_feedback_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'feedback'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/folder/db/upgrade.php b/public/mod/folder/db/upgrade.php index 6c7505e63c11f..b201c1a05cb17 100644 --- a/public/mod/folder/db/upgrade.php +++ b/public/mod/folder/db/upgrade.php @@ -74,5 +74,8 @@ function xmldb_folder_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'folder'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/forum/db/upgrade.php b/public/mod/forum/db/upgrade.php index 9fbbdfa85f61b..33e18af2f7c23 100644 --- a/public/mod/forum/db/upgrade.php +++ b/public/mod/forum/db/upgrade.php @@ -72,5 +72,8 @@ function xmldb_forum_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'forum'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/glossary/db/upgrade.php b/public/mod/glossary/db/upgrade.php index 0ce46cf44168c..70e5a896db60e 100644 --- a/public/mod/glossary/db/upgrade.php +++ b/public/mod/glossary/db/upgrade.php @@ -71,5 +71,8 @@ function xmldb_glossary_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'glossary'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/h5pactivity/db/upgrade.php b/public/mod/h5pactivity/db/upgrade.php index c92aba1bcfe16..4c4bb2977df99 100644 --- a/public/mod/h5pactivity/db/upgrade.php +++ b/public/mod/h5pactivity/db/upgrade.php @@ -90,5 +90,8 @@ function xmldb_h5pactivity_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'h5pactivity'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/imscp/db/upgrade.php b/public/mod/imscp/db/upgrade.php index 1adbff4cceba7..7a16dd8a58642 100644 --- a/public/mod/imscp/db/upgrade.php +++ b/public/mod/imscp/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_imscp_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'imscp'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/label/db/upgrade.php b/public/mod/label/db/upgrade.php index 7aeae7727aaeb..b3276102f5616 100644 --- a/public/mod/label/db/upgrade.php +++ b/public/mod/label/db/upgrade.php @@ -81,5 +81,8 @@ function xmldb_label_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025051301, 'label'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/lesson/db/upgrade.php b/public/mod/lesson/db/upgrade.php index ca1ff559f7e42..9fff5a4dc9c1f 100644 --- a/public/mod/lesson/db/upgrade.php +++ b/public/mod/lesson/db/upgrade.php @@ -79,5 +79,8 @@ function xmldb_lesson_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'lesson'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/lti/db/upgrade.php b/public/mod/lti/db/upgrade.php index 1b957317ef25a..6878ecbfd0713 100644 --- a/public/mod/lti/db/upgrade.php +++ b/public/mod/lti/db/upgrade.php @@ -138,5 +138,8 @@ function xmldb_lti_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'lti'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/lti/service/gradebookservices/db/upgrade.php b/public/mod/lti/service/gradebookservices/db/upgrade.php index 0c1736ba42a99..2f1f552d5890e 100644 --- a/public/mod/lti/service/gradebookservices/db/upgrade.php +++ b/public/mod/lti/service/gradebookservices/db/upgrade.php @@ -68,5 +68,8 @@ function xmldb_ltiservice_gradebookservices_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/page/db/upgrade.php b/public/mod/page/db/upgrade.php index ad35425acad5f..d1767c0744678 100644 --- a/public/mod/page/db/upgrade.php +++ b/public/mod/page/db/upgrade.php @@ -74,5 +74,8 @@ function xmldb_page_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'page'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/qbank/db/upgrade.php b/public/mod/qbank/db/upgrade.php index 2d7be720e1794..e21aaa7161049 100644 --- a/public/mod/qbank/db/upgrade.php +++ b/public/mod/qbank/db/upgrade.php @@ -51,5 +51,8 @@ function xmldb_qbank_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'qbank'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/quiz/accessrule/seb/db/upgrade.php b/public/mod/quiz/accessrule/seb/db/upgrade.php index d9994b649968c..ac4b7bb881b8a 100644 --- a/public/mod/quiz/accessrule/seb/db/upgrade.php +++ b/public/mod/quiz/accessrule/seb/db/upgrade.php @@ -75,5 +75,8 @@ function xmldb_quizaccess_seb_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/quiz/db/upgrade.php b/public/mod/quiz/db/upgrade.php index 784534bd0b6f8..fbd6d6a80fbac 100644 --- a/public/mod/quiz/db/upgrade.php +++ b/public/mod/quiz/db/upgrade.php @@ -149,5 +149,8 @@ function xmldb_quiz_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'quiz'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/quiz/report/overview/db/upgrade.php b/public/mod/quiz/report/overview/db/upgrade.php index 369ac2effe5fb..e51078aa0dba6 100644 --- a/public/mod/quiz/report/overview/db/upgrade.php +++ b/public/mod/quiz/report/overview/db/upgrade.php @@ -42,5 +42,8 @@ function xmldb_quiz_overview_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/quiz/report/statistics/db/upgrade.php b/public/mod/quiz/report/statistics/db/upgrade.php index 11f213de1bf42..77aa7c51f4f0e 100644 --- a/public/mod/quiz/report/statistics/db/upgrade.php +++ b/public/mod/quiz/report/statistics/db/upgrade.php @@ -41,5 +41,8 @@ function xmldb_quiz_statistics_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/resource/db/upgrade.php b/public/mod/resource/db/upgrade.php index ce26d0f71c22f..da36e96bfc126 100644 --- a/public/mod/resource/db/upgrade.php +++ b/public/mod/resource/db/upgrade.php @@ -74,5 +74,8 @@ function xmldb_resource_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'resource'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/scorm/db/upgrade.php b/public/mod/scorm/db/upgrade.php index 0513f25701dba..c5b1615c994f0 100644 --- a/public/mod/scorm/db/upgrade.php +++ b/public/mod/scorm/db/upgrade.php @@ -209,5 +209,8 @@ function xmldb_scorm_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'scorm'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/subsection/db/upgrade.php b/public/mod/subsection/db/upgrade.php index 7b5f5249d259e..67366d065e2ed 100644 --- a/public/mod/subsection/db/upgrade.php +++ b/public/mod/subsection/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_subsection_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'subsection'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/url/db/upgrade.php b/public/mod/url/db/upgrade.php index a4ae7abe54af6..306709cfe498b 100644 --- a/public/mod/url/db/upgrade.php +++ b/public/mod/url/db/upgrade.php @@ -84,5 +84,8 @@ function xmldb_url_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'url'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/wiki/db/upgrade.php b/public/mod/wiki/db/upgrade.php index 6f9f14de42fdb..2bf043e24e122 100644 --- a/public/mod/wiki/db/upgrade.php +++ b/public/mod/wiki/db/upgrade.php @@ -67,5 +67,8 @@ function xmldb_wiki_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'wiki'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/workshop/db/upgrade.php b/public/mod/workshop/db/upgrade.php index 0036d1a75dec7..a72a2d62162b7 100644 --- a/public/mod/workshop/db/upgrade.php +++ b/public/mod/workshop/db/upgrade.php @@ -64,5 +64,8 @@ function xmldb_workshop_upgrade($oldversion) { upgrade_mod_savepoint(true, 2025041401, 'workshop'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/workshop/form/accumulative/db/upgrade.php b/public/mod/workshop/form/accumulative/db/upgrade.php index 33ed386c154ac..66235d6d06558 100644 --- a/public/mod/workshop/form/accumulative/db/upgrade.php +++ b/public/mod/workshop/form/accumulative/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_workshopform_accumulative_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/workshop/form/comments/db/upgrade.php b/public/mod/workshop/form/comments/db/upgrade.php index 7ac4d6a4625fb..83cb8b17efec0 100644 --- a/public/mod/workshop/form/comments/db/upgrade.php +++ b/public/mod/workshop/form/comments/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_workshopform_comments_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/workshop/form/numerrors/db/upgrade.php b/public/mod/workshop/form/numerrors/db/upgrade.php index 500419562a41c..f75ecc9530c6b 100644 --- a/public/mod/workshop/form/numerrors/db/upgrade.php +++ b/public/mod/workshop/form/numerrors/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_workshopform_numerrors_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/mod/workshop/form/rubric/db/upgrade.php b/public/mod/workshop/form/rubric/db/upgrade.php index 5d96bfa654fde..b99b232723bd2 100644 --- a/public/mod/workshop/form/rubric/db/upgrade.php +++ b/public/mod/workshop/form/rubric/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_workshopform_rubric_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/payment/gateway/paypal/db/upgrade.php b/public/payment/gateway/paypal/db/upgrade.php index 2c333d19486fc..b4a7bbdd5d4af 100644 --- a/public/payment/gateway/paypal/db/upgrade.php +++ b/public/payment/gateway/paypal/db/upgrade.php @@ -60,5 +60,8 @@ function xmldb_paygw_paypal_upgrade(int $oldversion): bool { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/portfolio/googledocs/db/upgrade.php b/public/portfolio/googledocs/db/upgrade.php index bbf4564ca1649..f2a1673860c90 100644 --- a/public/portfolio/googledocs/db/upgrade.php +++ b/public/portfolio/googledocs/db/upgrade.php @@ -34,5 +34,8 @@ function xmldb_portfolio_googledocs_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/bank/columnsortorder/db/upgrade.php b/public/question/bank/columnsortorder/db/upgrade.php index ac4ee2e391169..3b1e1a62da557 100644 --- a/public/question/bank/columnsortorder/db/upgrade.php +++ b/public/question/bank/columnsortorder/db/upgrade.php @@ -95,5 +95,8 @@ function xmldb_qbank_columnsortorder_upgrade(int $oldversion): bool { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/behaviour/manualgraded/db/upgrade.php b/public/question/behaviour/manualgraded/db/upgrade.php index 7523c699e071b..2a8ce63c1655d 100644 --- a/public/question/behaviour/manualgraded/db/upgrade.php +++ b/public/question/behaviour/manualgraded/db/upgrade.php @@ -41,5 +41,8 @@ function xmldb_qbehaviour_manualgraded_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/calculated/db/upgrade.php b/public/question/type/calculated/db/upgrade.php index be85b1c9100f7..468f9ac630efd 100644 --- a/public/question/type/calculated/db/upgrade.php +++ b/public/question/type/calculated/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_qtype_calculated_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/calculatedmulti/db/upgrade.php b/public/question/type/calculatedmulti/db/upgrade.php index 50e8cbbea7050..fa0b2d6a7084e 100644 --- a/public/question/type/calculatedmulti/db/upgrade.php +++ b/public/question/type/calculatedmulti/db/upgrade.php @@ -60,5 +60,8 @@ function xmldb_qtype_calculatedmulti_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/ddimageortext/db/upgrade.php b/public/question/type/ddimageortext/db/upgrade.php index 392ad799dd310..96eb22762f3f6 100644 --- a/public/question/type/ddimageortext/db/upgrade.php +++ b/public/question/type/ddimageortext/db/upgrade.php @@ -49,5 +49,8 @@ function xmldb_qtype_ddimageortext_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/ddmarker/db/upgrade.php b/public/question/type/ddmarker/db/upgrade.php index b88e1c3ef76f7..98daa3f6c996a 100644 --- a/public/question/type/ddmarker/db/upgrade.php +++ b/public/question/type/ddmarker/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_qtype_ddmarker_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/essay/db/upgrade.php b/public/question/type/essay/db/upgrade.php index e5c45729c1266..6ed3478a48db1 100644 --- a/public/question/type/essay/db/upgrade.php +++ b/public/question/type/essay/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_qtype_essay_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/match/db/upgrade.php b/public/question/type/match/db/upgrade.php index 2fe5103f18b19..890134ae26e44 100644 --- a/public/question/type/match/db/upgrade.php +++ b/public/question/type/match/db/upgrade.php @@ -42,5 +42,8 @@ function xmldb_qtype_match_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/multianswer/db/upgrade.php b/public/question/type/multianswer/db/upgrade.php index d1db12e858c36..9b0d0ad6d534c 100644 --- a/public/question/type/multianswer/db/upgrade.php +++ b/public/question/type/multianswer/db/upgrade.php @@ -52,5 +52,8 @@ function xmldb_qtype_multianswer_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2025061000, 'qtype', 'multianswer'); } + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/multichoice/db/upgrade.php b/public/question/type/multichoice/db/upgrade.php index eb20d775102e2..4a3660f36ec2c 100644 --- a/public/question/type/multichoice/db/upgrade.php +++ b/public/question/type/multichoice/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_qtype_multichoice_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/numerical/db/upgrade.php b/public/question/type/numerical/db/upgrade.php index 72f86b76d3e1d..9c269765a893f 100644 --- a/public/question/type/numerical/db/upgrade.php +++ b/public/question/type/numerical/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_qtype_numerical_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/ordering/db/upgrade.php b/public/question/type/ordering/db/upgrade.php index 23eff0bd3879c..b2a9be14ab346 100644 --- a/public/question/type/ordering/db/upgrade.php +++ b/public/question/type/ordering/db/upgrade.php @@ -376,5 +376,8 @@ function xmldb_qtype_ordering_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/random/db/upgrade.php b/public/question/type/random/db/upgrade.php index 8fa964a7be777..80c3ca23f460d 100644 --- a/public/question/type/random/db/upgrade.php +++ b/public/question/type/random/db/upgrade.php @@ -42,5 +42,8 @@ function xmldb_qtype_random_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/randomsamatch/db/upgrade.php b/public/question/type/randomsamatch/db/upgrade.php index a841bd77c27ac..6a8ba85313845 100644 --- a/public/question/type/randomsamatch/db/upgrade.php +++ b/public/question/type/randomsamatch/db/upgrade.php @@ -42,5 +42,8 @@ function xmldb_qtype_randomsamatch_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/shortanswer/db/upgrade.php b/public/question/type/shortanswer/db/upgrade.php index 78798700f75db..bc16882fbf525 100644 --- a/public/question/type/shortanswer/db/upgrade.php +++ b/public/question/type/shortanswer/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_qtype_shortanswer_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/question/type/truefalse/db/upgrade.php b/public/question/type/truefalse/db/upgrade.php index f606b33225e08..59cebed431c6a 100644 --- a/public/question/type/truefalse/db/upgrade.php +++ b/public/question/type/truefalse/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_qtype_truefalse_upgrade(int $oldversion): bool { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/repository/dropbox/db/upgrade.php b/public/repository/dropbox/db/upgrade.php index 92ebc44941436..787a129541b79 100644 --- a/public/repository/dropbox/db/upgrade.php +++ b/public/repository/dropbox/db/upgrade.php @@ -34,5 +34,8 @@ function xmldb_repository_dropbox_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/repository/flickr/db/upgrade.php b/public/repository/flickr/db/upgrade.php index 6c52f1e201c12..7ce2dcc5cbe64 100644 --- a/public/repository/flickr/db/upgrade.php +++ b/public/repository/flickr/db/upgrade.php @@ -45,5 +45,8 @@ function xmldb_repository_flickr_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/repository/googledocs/db/upgrade.php b/public/repository/googledocs/db/upgrade.php index a346330d29d0e..179a3c76e5d74 100644 --- a/public/repository/googledocs/db/upgrade.php +++ b/public/repository/googledocs/db/upgrade.php @@ -34,5 +34,8 @@ function xmldb_repository_googledocs_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/repository/onedrive/db/upgrade.php b/public/repository/onedrive/db/upgrade.php index f955280187966..1a10c1b5094fe 100644 --- a/public/repository/onedrive/db/upgrade.php +++ b/public/repository/onedrive/db/upgrade.php @@ -37,5 +37,8 @@ function xmldb_repository_onedrive_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/search/engine/simpledb/db/upgrade.php b/public/search/engine/simpledb/db/upgrade.php index a130a83240618..d4658b1a97fc1 100644 --- a/public/search/engine/simpledb/db/upgrade.php +++ b/public/search/engine/simpledb/db/upgrade.php @@ -42,5 +42,8 @@ function xmldb_search_simpledb_upgrade($oldversion = 0) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } From 0ae5666e66bfeac6f86cdafe61a921e3da36c841 Mon Sep 17 00:00:00 2001 From: Stefan Hanauska Date: Mon, 25 Aug 2025 19:52:37 +0200 Subject: [PATCH 008/553] MDL-86300 backup: Save old id of top category Co-authored-by: Paola Maneggia --- public/backup/moodle2/restore_stepslib.php | 2 +- public/backup/moodle2/tests/moodle2_test.php | 66 +++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/public/backup/moodle2/restore_stepslib.php b/public/backup/moodle2/restore_stepslib.php index 09b2d036fbce2..c34d34904138f 100644 --- a/public/backup/moodle2/restore_stepslib.php +++ b/public/backup/moodle2/restore_stepslib.php @@ -5553,7 +5553,7 @@ protected function define_execution() { // From 3.5 onwards, all question categories should be a child of a special category called the "top" category. $info = backup_controller_dbops::decode_backup_temp_info($modulecat->info); if ($after35 && empty($info->parent)) { - $oldtopid = $modulecat->newitemid; + $oldtopid = $modulecat->itemid; $modulecat->newitemid = $top->id; } else { $cat = new stdClass(); diff --git a/public/backup/moodle2/tests/moodle2_test.php b/public/backup/moodle2/tests/moodle2_test.php index 6b37f92605872..a015c83e06a54 100644 --- a/public/backup/moodle2/tests/moodle2_test.php +++ b/public/backup/moodle2/tests/moodle2_test.php @@ -1088,14 +1088,78 @@ public function test_restore_question_category_34_35(): void { } } - // Make sure there is a single top level category in this context. + // Make sure there is a single top level category in this context and that the parents are set correctly. if ($cats) { $this->assertEquals(1, $topcategorycount[$context->id]); + $topcat = array_values($cats)[0]; + $this->assertEquals(0, $topcat->parent); + $othercat = array_values($cats)[1]; + $this->assertEquals($topcat->id, $othercat->parent); } } } } + /** + * Check that the backup/restore process correctly wires the question categories, see MDL-86300. + * @covers \restore_move_module_questions_categories::define_execution + */ + public function test_restore_question_categories_from_500(): void { + global $DB, $CFG, $USER; + + $this->resetAfterTest(true); + $this->setAdminUser(); + + // Create a course. + $generator = $this->getDataGenerator(); + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $course = $generator->create_course(); + + // Add a quiz with question categories. + $quiz = $generator->create_module('quiz', ['course' => $course->id]); + $quizcontext = \context_module::instance($quiz->cmid); + $questiongenerator->create_question_category(['contextid' => $quizcontext->id]); + $quizquestioncats = $DB->get_records('question_categories', ['contextid' => $quizcontext->id]); + $this->assertCount(3, $quizquestioncats); + + // Add a question bank with question categories. + $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id]); + $qbankcontext = \context_module::instance($qbank->cmid); + $questiongenerator->create_question_category(['contextid' => $qbankcontext->id]); + $qbankquestioncats = $DB->get_records('question_categories', ['contextid' => $qbankcontext->id]); + $this->assertCount(3, $qbankquestioncats); + + $targetcourseid = $this->backup_and_restore($course); + + // Check the quiz and qbank question categories in the target course, in particular the parent relationship. + $modinfo = get_fast_modinfo($targetcourseid); + + $targetquizzes = $modinfo->get_instances_of('quiz'); + $this->assertCount(1, $targetquizzes); + $targetquiz = reset($targetquizzes); + $targetquizcontext = \context_module::instance($targetquiz->id); + $targetquizcats = array_values( + $DB->get_records('question_categories', ['contextid' => $targetquizcontext->id], 'parent', 'id, name, parent') + ); + $this->assertCount(3, $targetquizcats); + $quiztop = $targetquizcats[0]; + $this->assertEquals(0, $quiztop->parent); + $quiznontop = $targetquizcats[1]; + $this->assertEquals($quiztop->id, $quiznontop->parent); + + $targetqbanks = $modinfo->get_instances_of('qbank'); + $this->assertCount(1, $targetqbanks); + $targetqbankcontext = \context_module::instance(reset($targetqbanks)->id); + $targetqbankcats = array_values( + $DB->get_records('question_categories', ['contextid' => $targetqbankcontext->id], 'parent', 'id, name, parent') + ); + $this->assertCount(3, $targetqbankcats); + $qbanktop = $targetqbankcats[0]; + $this->assertEquals(0, $qbanktop->parent); + $qbanknontop = $targetqbankcats[1]; + $this->assertEquals($qbanktop->id, $qbanknontop->parent); + } + /** * Test the content bank content through a backup and restore. */ From fe87f683a3176b0cc3683c8eab934e765f74442d Mon Sep 17 00:00:00 2001 From: Philipp Memmel Date: Wed, 20 Aug 2025 20:08:59 +0000 Subject: [PATCH 009/553] MDL-86382 qbank_bulkmove: Fix selecting placeholder in qbank selector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @Co-authored-by: Luca Bösch --- .../build/modal_question_bank_bulkmove.min.js | 2 +- .../modal_question_bank_bulkmove.min.js.map | 2 +- .../amd/src/modal_question_bank_bulkmove.js | 6 ++ .../bulkmove/tests/behat/bulk_move.feature | 55 +++++++++++++++++++ 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/public/question/bank/bulkmove/amd/build/modal_question_bank_bulkmove.min.js b/public/question/bank/bulkmove/amd/build/modal_question_bank_bulkmove.min.js index 848ff31809b34..bede2b6d5e5cc 100644 --- a/public/question/bank/bulkmove/amd/build/modal_question_bank_bulkmove.min.js +++ b/public/question/bank/bulkmove/amd/build/modal_question_bank_bulkmove.min.js @@ -1,3 +1,3 @@ -define("qbank_bulkmove/modal_question_bank_bulkmove",["exports","core/modal","core/fragment","core/str","core/form-autocomplete","core_question/repository","core/templates","core/notification","core/pending"],(function(_exports,_modal,Fragment,_str,_formAutocomplete,_repository,_templates,_notification,_pending){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=_interopRequireDefault(_modal),Fragment=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}(Fragment),_formAutocomplete=_interopRequireDefault(_formAutocomplete),_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);class ModalQuestionBankBulkmove extends _modal.default{static init(contextId,categoryId){document.addEventListener("click",(e=>{const trigger=e.target;trigger.classList.contains("dropdown-item")&&"move"===trigger.getAttribute("name")&&(e.preventDefault(),ModalQuestionBankBulkmove.create({contextId:contextId,title:(0,_str.getString)("bulkmoveheader","qbank_bulkmove"),show:!0,categoryId:categoryId}))}))}configure(modalConfig){this.contextId=modalConfig.contextId,this.targetBankContextId=modalConfig.contextId,this.initSelectedCategoryId(modalConfig.categoryId),modalConfig.removeOnClose=!0,super.configure(modalConfig)}initSelectedCategoryId(categoryId){const filter=new URLSearchParams(window.location.href).get("filter");if(filter){var _JSON$parse;const filteredCategoryId=null===(_JSON$parse=JSON.parse(filter))||void 0===_JSON$parse?void 0:_JSON$parse.category.values[0];return this.currentCategoryId=filteredCategoryId>0?filteredCategoryId:null,void(this.targetCategoryId=filteredCategoryId)}this.currentCategoryId=categoryId,this.targetCategoryId=categoryId}show(){return this.display(this.contextId,this.currentCategoryId),super.show()}async display(currentBankContextId,currentCategoryId){const displayPending=new _pending.default("qbank_bulkmove/bulk_move_modal");this.bodyPromise=await Fragment.loadFragment("qbank_bulkmove","bulk_move",currentBankContextId,{categoryid:currentCategoryId}),await this.setBody(this.bodyPromise),await this.enhanceSelects(),this.registerEnhancedEventListeners(),this.updateSaveButtonState(),displayPending.resolve()}registerEnhancedEventListeners(){document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY).addEventListener("change",(()=>{this.updateSaveButtonState()})),document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_BANK).addEventListener("change",(async e=>{await this.updateCategorySelector(e.currentTarget.value),this.updateSaveButtonState()})),this.getModal().on("click",ModalQuestionBankBulkmove.SELECTORS.SAVE_BUTTON,(e=>{e.preventDefault(),this.displayConfirmMove()}))}async displayConfirmMove(){this.setTitle((0,_str.getString)("confirm","core")),this.setBody((0,_str.getString)("confirmmove","qbank_bulkmove")),this.hasFooterContent()?this.showFooter():(this.setFooter(_templates.default.render("qbank_bulkmove/bulk_move_footer",{})),await this.getFooterPromise(),document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CONFIRM_BUTTON).addEventListener("click",(e=>{e.preventDefault(),this.moveQuestionsAfterConfirm(this.targetBankContextId,this.targetCategoryId)})),document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CANCEL_BUTTON).addEventListener("click",(e=>{e.preventDefault(),this.setTitle((0,_str.getString)("bulkmoveheader","qbank_bulkmove")),this.setBodyContent(_templates.default.renderForPromise("core/loading",{})),this.hideFooter(),this.display(this.targetBankContextId,this.targetCategoryId)})))}updateCategorySelector(selectedBankCmId){return selectedBankCmId?Fragment.loadFragment("core_question","category_selector",this.contextId,{bankcmid:selectedBankCmId}).then(((html,js)=>{const categorySelector=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.QUESTION_CATEGORY_SELECTOR);return _templates.default.replaceNode(categorySelector,html,js)})).then((()=>(document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_WARNING).classList.add("d-none"),this.enhanceSelects()))).catch(_notification.default.exception):(this.updateCategorySelectorState(!1),Promise.resolve())}updateCategorySelectorState(toEnable){const warning=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_WARNING),enhancedInput=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_ENHANCED_INPUT),suggestionButton=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_SUGGESTION),selection=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_SELECTION);toEnable?(warning.classList.add("d-none"),enhancedInput.removeAttribute("disabled"),suggestionButton.classList.remove("d-none")):(warning.classList.remove("d-none"),enhancedInput.setAttribute("disabled","disabled"),suggestionButton.classList.add("d-none"),selection.click())}updateSaveButtonState(){const saveButton=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SAVE_BUTTON),categorySelector=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY);[this.targetCategoryId,this.targetBankContextId]=categorySelector.value.split(","),this.targetCategoryId&&this.targetCategoryId!==this.currentCategoryId?saveButton.removeAttribute("disabled"):saveButton.setAttribute("disabled","disabled")}async moveQuestionsAfterConfirm(targetContextId,targetCategoryId){await this.setBody(_templates.default.render("core/loading",{}));const qelements=document.querySelectorAll(ModalQuestionBankBulkmove.SELECTORS.SELECTED_QUESTIONS),questionids=[];qelements.forEach((element=>{if(element.checked){const name=element.getAttribute("name");questionids.push(name.substr(1,name.length))}})),0===questionids.length&&await _notification.default.exception("No questions selected");try{window.location.href=await(0,_repository.moveQuestions)(targetContextId,targetCategoryId,questionids.join(),window.location.href)}catch(error){await _notification.default.exception(error)}}async enhanceSelects(){const placeholder=await(0,_str.getString)("searchbyname","mod_quiz");await _formAutocomplete.default.enhance(ModalQuestionBankBulkmove.SELECTORS.SEARCH_BANK,!1,"core_question/question_banks_datasource",placeholder,!1,!0,"",!0),await _formAutocomplete.default.enhance(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY,!1,null,placeholder,!1,!0,"",!0)}}return _exports.default=ModalQuestionBankBulkmove,_defineProperty(ModalQuestionBankBulkmove,"TYPE","qbank_bulkmove/bulkmove"),_defineProperty(ModalQuestionBankBulkmove,"SELECTORS",{SAVE_BUTTON:'[data-action="bulkmovesave"]',SELECTED_QUESTIONS:'table#categoryquestions input[id^="checkq"]',SEARCH_BANK:"#searchbanks",SEARCH_CATEGORY:".selectcategory",QUESTION_CATEGORY_SELECTOR:".question_category_selector",CATEGORY_OPTIONS:".selectcategory option",BANK_OPTIONS:"#searchbanks option",CATEGORY_ENHANCED_INPUT:".search-categories input",ORIGINAL_SELECTS:"select.bulk-move",CATEGORY_WARNING:"#searchcatwarning",CATEGORY_SUGGESTION:".search-categories span.form-autocomplete-downarrow",CATEGORY_SELECTION:'.search-categories span[role="option"][data-active-selection="true"]',CONFIRM_BUTTON:'.bulk-move-footer button[data-action="save"]',CANCEL_BUTTON:'.bulk-move-footer button[data-action="cancel"]'}),_exports.default})); +define("qbank_bulkmove/modal_question_bank_bulkmove",["exports","core/modal","core/fragment","core/str","core/form-autocomplete","core_question/repository","core/templates","core/notification","core/pending"],(function(_exports,_modal,Fragment,_str,_formAutocomplete,_repository,_templates,_notification,_pending){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=_interopRequireDefault(_modal),Fragment=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}(Fragment),_formAutocomplete=_interopRequireDefault(_formAutocomplete),_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);class ModalQuestionBankBulkmove extends _modal.default{static init(contextId,categoryId){document.addEventListener("click",(e=>{const trigger=e.target;trigger.classList.contains("dropdown-item")&&"move"===trigger.getAttribute("name")&&(e.preventDefault(),ModalQuestionBankBulkmove.create({contextId:contextId,title:(0,_str.getString)("bulkmoveheader","qbank_bulkmove"),show:!0,categoryId:categoryId}))}))}configure(modalConfig){this.contextId=modalConfig.contextId,this.targetBankContextId=modalConfig.contextId,this.initSelectedCategoryId(modalConfig.categoryId),modalConfig.removeOnClose=!0,super.configure(modalConfig)}initSelectedCategoryId(categoryId){const filter=new URLSearchParams(window.location.href).get("filter");if(filter){var _JSON$parse;const filteredCategoryId=null===(_JSON$parse=JSON.parse(filter))||void 0===_JSON$parse?void 0:_JSON$parse.category.values[0];return this.currentCategoryId=filteredCategoryId>0?filteredCategoryId:null,void(this.targetCategoryId=filteredCategoryId)}this.currentCategoryId=categoryId,this.targetCategoryId=categoryId}show(){return this.display(this.contextId,this.currentCategoryId),super.show()}async display(currentBankContextId,currentCategoryId){const displayPending=new _pending.default("qbank_bulkmove/bulk_move_modal");this.bodyPromise=await Fragment.loadFragment("qbank_bulkmove","bulk_move",currentBankContextId,{categoryid:currentCategoryId}),await this.setBody(this.bodyPromise),await this.enhanceSelects(),this.registerEnhancedEventListeners(),this.updateSaveButtonState(),displayPending.resolve()}registerEnhancedEventListeners(){document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY).addEventListener("change",(()=>{this.updateSaveButtonState()})),document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_BANK).addEventListener("change",(async e=>{0!==parseInt(e.target.value)?(await this.updateCategorySelector(e.currentTarget.value),this.updateSaveButtonState()):await this.updateCategorySelector(null)})),this.getModal().on("click",ModalQuestionBankBulkmove.SELECTORS.SAVE_BUTTON,(e=>{e.preventDefault(),this.displayConfirmMove()}))}async displayConfirmMove(){this.setTitle((0,_str.getString)("confirm","core")),this.setBody((0,_str.getString)("confirmmove","qbank_bulkmove")),this.hasFooterContent()?this.showFooter():(this.setFooter(_templates.default.render("qbank_bulkmove/bulk_move_footer",{})),await this.getFooterPromise(),document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CONFIRM_BUTTON).addEventListener("click",(e=>{e.preventDefault(),this.moveQuestionsAfterConfirm(this.targetBankContextId,this.targetCategoryId)})),document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CANCEL_BUTTON).addEventListener("click",(e=>{e.preventDefault(),this.setTitle((0,_str.getString)("bulkmoveheader","qbank_bulkmove")),this.setBodyContent(_templates.default.renderForPromise("core/loading",{})),this.hideFooter(),this.display(this.targetBankContextId,this.targetCategoryId)})))}updateCategorySelector(selectedBankCmId){return selectedBankCmId?Fragment.loadFragment("core_question","category_selector",this.contextId,{bankcmid:selectedBankCmId}).then(((html,js)=>{const categorySelector=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.QUESTION_CATEGORY_SELECTOR);return _templates.default.replaceNode(categorySelector,html,js)})).then((()=>(document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_WARNING).classList.add("d-none"),this.enhanceSelects()))).catch(_notification.default.exception):(this.updateCategorySelectorState(!1),Promise.resolve())}updateCategorySelectorState(toEnable){const warning=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_WARNING),enhancedInput=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_ENHANCED_INPUT),suggestionButton=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_SUGGESTION),selection=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_SELECTION);toEnable?(warning.classList.add("d-none"),enhancedInput.removeAttribute("disabled"),suggestionButton.classList.remove("d-none")):(warning.classList.remove("d-none"),enhancedInput.setAttribute("disabled","disabled"),suggestionButton.classList.add("d-none"),selection.click())}updateSaveButtonState(){const saveButton=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SAVE_BUTTON),categorySelector=document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY);[this.targetCategoryId,this.targetBankContextId]=categorySelector.value.split(","),this.targetCategoryId&&this.targetCategoryId!==this.currentCategoryId?saveButton.removeAttribute("disabled"):saveButton.setAttribute("disabled","disabled")}async moveQuestionsAfterConfirm(targetContextId,targetCategoryId){await this.setBody(_templates.default.render("core/loading",{}));const qelements=document.querySelectorAll(ModalQuestionBankBulkmove.SELECTORS.SELECTED_QUESTIONS),questionids=[];qelements.forEach((element=>{if(element.checked){const name=element.getAttribute("name");questionids.push(name.substr(1,name.length))}})),0===questionids.length&&await _notification.default.exception("No questions selected");try{window.location.href=await(0,_repository.moveQuestions)(targetContextId,targetCategoryId,questionids.join(),window.location.href)}catch(error){await _notification.default.exception(error)}}async enhanceSelects(){const placeholder=await(0,_str.getString)("searchbyname","mod_quiz");await _formAutocomplete.default.enhance(ModalQuestionBankBulkmove.SELECTORS.SEARCH_BANK,!1,"core_question/question_banks_datasource",placeholder,!1,!0,"",!0),await _formAutocomplete.default.enhance(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY,!1,null,placeholder,!1,!0,"",!0)}}return _exports.default=ModalQuestionBankBulkmove,_defineProperty(ModalQuestionBankBulkmove,"TYPE","qbank_bulkmove/bulkmove"),_defineProperty(ModalQuestionBankBulkmove,"SELECTORS",{SAVE_BUTTON:'[data-action="bulkmovesave"]',SELECTED_QUESTIONS:'table#categoryquestions input[id^="checkq"]',SEARCH_BANK:"#searchbanks",SEARCH_CATEGORY:".selectcategory",QUESTION_CATEGORY_SELECTOR:".question_category_selector",CATEGORY_OPTIONS:".selectcategory option",BANK_OPTIONS:"#searchbanks option",CATEGORY_ENHANCED_INPUT:".search-categories input",ORIGINAL_SELECTS:"select.bulk-move",CATEGORY_WARNING:"#searchcatwarning",CATEGORY_SUGGESTION:".search-categories span.form-autocomplete-downarrow",CATEGORY_SELECTION:'.search-categories span[role="option"][data-active-selection="true"]',CONFIRM_BUTTON:'.bulk-move-footer button[data-action="save"]',CANCEL_BUTTON:'.bulk-move-footer button[data-action="cancel"]'}),_exports.default})); //# sourceMappingURL=modal_question_bank_bulkmove.min.js.map \ No newline at end of file diff --git a/public/question/bank/bulkmove/amd/build/modal_question_bank_bulkmove.min.js.map b/public/question/bank/bulkmove/amd/build/modal_question_bank_bulkmove.min.js.map index e7e76da3dca6d..ff104492040dd 100644 --- a/public/question/bank/bulkmove/amd/build/modal_question_bank_bulkmove.min.js.map +++ b/public/question/bank/bulkmove/amd/build/modal_question_bank_bulkmove.min.js.map @@ -1 +1 @@ -{"version":3,"file":"modal_question_bank_bulkmove.min.js","sources":["../src/modal_question_bank_bulkmove.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 * Contain the logic for the bulkmove questions modal.\n *\n * @module qbank_bulkmove/modal_question_bank_bulkmove\n * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}\n * @author Simon Adams \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport * as Fragment from 'core/fragment';\nimport {getString} from 'core/str';\nimport AutoComplete from 'core/form-autocomplete';\nimport {moveQuestions} from 'core_question/repository';\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\n\n\nexport default class ModalQuestionBankBulkmove extends Modal {\n static TYPE = 'qbank_bulkmove/bulkmove';\n\n static SELECTORS = {\n SAVE_BUTTON: '[data-action=\"bulkmovesave\"]',\n SELECTED_QUESTIONS: 'table#categoryquestions input[id^=\"checkq\"]',\n SEARCH_BANK: '#searchbanks',\n SEARCH_CATEGORY: '.selectcategory',\n QUESTION_CATEGORY_SELECTOR: '.question_category_selector',\n CATEGORY_OPTIONS: '.selectcategory option',\n BANK_OPTIONS: '#searchbanks option',\n CATEGORY_ENHANCED_INPUT: '.search-categories input',\n ORIGINAL_SELECTS: 'select.bulk-move',\n CATEGORY_WARNING: '#searchcatwarning',\n CATEGORY_SUGGESTION: '.search-categories span.form-autocomplete-downarrow',\n CATEGORY_SELECTION: '.search-categories span[role=\"option\"][data-active-selection=\"true\"]',\n CONFIRM_BUTTON: '.bulk-move-footer button[data-action=\"save\"]',\n CANCEL_BUTTON: '.bulk-move-footer button[data-action=\"cancel\"]'\n };\n\n /**\n * @param {integer} contextId The current bank context id.\n * @param {integer} categoryId The current question category id.\n */\n static init(contextId, categoryId) {\n document.addEventListener('click', (e) => {\n const trigger = e.target;\n if (trigger.classList.contains('dropdown-item') && trigger.getAttribute('name') === 'move') {\n e.preventDefault();\n ModalQuestionBankBulkmove.create({\n contextId,\n title: getString('bulkmoveheader', 'qbank_bulkmove'),\n show: true,\n categoryId: categoryId,\n });\n }\n });\n }\n\n /**\n * Set the initialised config on the class.\n *\n * @param {Object} modalConfig\n */\n configure(modalConfig) {\n this.contextId = modalConfig.contextId;\n this.targetBankContextId = modalConfig.contextId;\n this.initSelectedCategoryId(modalConfig.categoryId);\n modalConfig.removeOnClose = true;\n super.configure(modalConfig);\n }\n\n /**\n * Initialise the category select based on the data passed to the JS or if a filter is applied in the url.\n * @param {integer} categoryId\n */\n initSelectedCategoryId(categoryId) {\n const filter = new URLSearchParams(window.location.href).get('filter');\n if (filter) {\n const filteredCategoryId = JSON.parse(filter)?.category.values[0];\n this.currentCategoryId = filteredCategoryId > 0 ? filteredCategoryId : null;\n this.targetCategoryId = filteredCategoryId;\n return;\n }\n this.currentCategoryId = categoryId;\n this.targetCategoryId = categoryId;\n }\n\n /**\n * Render the modal contents.\n * @return {Promise}\n */\n show() {\n void this.display(this.contextId, this.currentCategoryId);\n return super.show();\n }\n\n /**\n * Get the content to display and enhance the selects into auto complete fields.\n * @param {integer} currentBankContextId\n * @param {integer} currentCategoryId\n */\n async display(currentBankContextId, currentCategoryId) {\n const displayPending = new Pending('qbank_bulkmove/bulk_move_modal');\n this.bodyPromise = await Fragment.loadFragment(\n 'qbank_bulkmove',\n 'bulk_move',\n currentBankContextId,\n {\n 'categoryid': currentCategoryId,\n }\n );\n\n await this.setBody(this.bodyPromise);\n await this.enhanceSelects();\n this.registerEnhancedEventListeners();\n this.updateSaveButtonState();\n displayPending.resolve();\n }\n\n /**\n * Register event listeners on the enhanced selects. Must be done after they have been enhanced.\n */\n registerEnhancedEventListeners() {\n document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY).addEventListener(\"change\", () => {\n this.updateSaveButtonState();\n });\n\n document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_BANK).addEventListener(\"change\", async(e) => {\n await this.updateCategorySelector(e.currentTarget.value);\n this.updateSaveButtonState();\n });\n\n this.getModal().on(\"click\", ModalQuestionBankBulkmove.SELECTORS.SAVE_BUTTON, (e) => {\n e.preventDefault();\n void this.displayConfirmMove();\n });\n }\n\n /**\n * Update the body with a confirmation prompt and set confirm cancel buttons in the footer.\n * @return {Promise}\n */\n async displayConfirmMove() {\n this.setTitle(getString('confirm', 'core'));\n this.setBody(getString('confirmmove', 'qbank_bulkmove'));\n if (!this.hasFooterContent()) {\n // We don't have the footer yet so go grab it and register event listeners on the buttons.\n this.setFooter(Templates.render('qbank_bulkmove/bulk_move_footer', {}));\n await this.getFooterPromise();\n\n document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CONFIRM_BUTTON).addEventListener(\"click\", (e) => {\n e.preventDefault();\n this.moveQuestionsAfterConfirm(this.targetBankContextId, this.targetCategoryId);\n });\n\n document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CANCEL_BUTTON).addEventListener(\"click\", (e) => {\n e.preventDefault();\n this.setTitle(getString('bulkmoveheader', 'qbank_bulkmove'));\n this.setBodyContent(Templates.renderForPromise('core/loading', {}));\n this.hideFooter();\n this.display(this.targetBankContextId, this.targetCategoryId);\n });\n } else {\n // We already have a footer so just show it.\n this.showFooter();\n }\n }\n\n /**\n * Update the category selector based on the selected question bank.\n *\n * @param {Number} selectedBankCmId\n * @return {Promise} Resolved when the update is complete.\n */\n updateCategorySelector(selectedBankCmId) {\n if (!selectedBankCmId) {\n this.updateCategorySelectorState(false);\n return Promise.resolve();\n } else {\n return Fragment.loadFragment(\n 'core_question',\n 'category_selector',\n this.contextId,\n {\n 'bankcmid': selectedBankCmId,\n }\n )\n .then((html, js) => {\n const categorySelector = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.QUESTION_CATEGORY_SELECTOR);\n return Templates.replaceNode(categorySelector, html, js);\n })\n .then(() => {\n document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_WARNING).classList.add('d-none');\n return this.enhanceSelects();\n })\n .catch(Notification.exception);\n }\n }\n\n /**\n * Disable/enable the enhanced category selector field.\n * @param {boolean} toEnable True to enable, false to disable the field.\n */\n updateCategorySelectorState(toEnable) {\n const warning = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_WARNING);\n const enhancedInput = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_ENHANCED_INPUT);\n const suggestionButton = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_SUGGESTION);\n const selection = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_SELECTION);\n\n if (toEnable) {\n warning.classList.add('d-none');\n enhancedInput.removeAttribute('disabled');\n suggestionButton.classList.remove('d-none');\n } else {\n warning.classList.remove('d-none');\n enhancedInput.setAttribute('disabled', 'disabled');\n suggestionButton.classList.add('d-none');\n selection.click(); // Clear selected category.\n }\n }\n\n /**\n * Disable the button if the selected category is the same as the one the questions already belong to. Enable it otherwise.\n */\n updateSaveButtonState() {\n const saveButton = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SAVE_BUTTON);\n const categorySelector = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY);\n [this.targetCategoryId, this.targetBankContextId] = categorySelector.value.split(',');\n\n if (this.targetCategoryId && this.targetCategoryId !== this.currentCategoryId) {\n saveButton.removeAttribute('disabled');\n } else {\n saveButton.setAttribute('disabled', 'disabled');\n }\n }\n\n /**\n * Move the selected questions to their new target category.\n * @param {integer} targetContextId the target bank context id.\n * @param {integer} targetCategoryId the target question category id.\n * @return {Promise}\n */\n async moveQuestionsAfterConfirm(targetContextId, targetCategoryId) {\n await this.setBody(Templates.render('core/loading', {}));\n const qelements = document.querySelectorAll(ModalQuestionBankBulkmove.SELECTORS.SELECTED_QUESTIONS);\n const questionids = [];\n qelements.forEach((element) => {\n if (element.checked) {\n const name = element.getAttribute('name');\n questionids.push(name.substr(1, name.length));\n }\n });\n if (questionids.length === 0) {\n await Notification.exception('No questions selected');\n }\n\n try {\n window.location.href = await moveQuestions(\n targetContextId,\n targetCategoryId,\n questionids.join(),\n window.location.href\n );\n } catch (error) {\n await Notification.exception(error);\n }\n }\n\n /**\n * Take the provided select options and enhance them into auto-complete fields.\n *\n * @return {Promise}\n */\n async enhanceSelects() {\n const placeholder = await getString('searchbyname', 'mod_quiz');\n\n await AutoComplete.enhance(\n ModalQuestionBankBulkmove.SELECTORS.SEARCH_BANK,\n false,\n 'core_question/question_banks_datasource',\n placeholder,\n false,\n true,\n '',\n true,\n );\n\n await AutoComplete.enhance(\n ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY,\n false,\n null,\n placeholder,\n false,\n true,\n '',\n true,\n );\n }\n}\n"],"names":["ModalQuestionBankBulkmove","Modal","contextId","categoryId","document","addEventListener","e","trigger","target","classList","contains","getAttribute","preventDefault","create","title","show","configure","modalConfig","targetBankContextId","initSelectedCategoryId","removeOnClose","filter","URLSearchParams","window","location","href","get","filteredCategoryId","JSON","parse","_JSON$parse","category","values","currentCategoryId","targetCategoryId","this","display","super","currentBankContextId","displayPending","Pending","bodyPromise","Fragment","loadFragment","setBody","enhanceSelects","registerEnhancedEventListeners","updateSaveButtonState","resolve","querySelector","SELECTORS","SEARCH_CATEGORY","SEARCH_BANK","async","updateCategorySelector","currentTarget","value","getModal","on","SAVE_BUTTON","displayConfirmMove","setTitle","hasFooterContent","showFooter","setFooter","Templates","render","getFooterPromise","CONFIRM_BUTTON","moveQuestionsAfterConfirm","CANCEL_BUTTON","setBodyContent","renderForPromise","hideFooter","selectedBankCmId","then","html","js","categorySelector","QUESTION_CATEGORY_SELECTOR","replaceNode","CATEGORY_WARNING","add","catch","Notification","exception","updateCategorySelectorState","Promise","toEnable","warning","enhancedInput","CATEGORY_ENHANCED_INPUT","suggestionButton","CATEGORY_SUGGESTION","selection","CATEGORY_SELECTION","removeAttribute","remove","setAttribute","click","saveButton","split","targetContextId","qelements","querySelectorAll","SELECTED_QUESTIONS","questionids","forEach","element","checked","name","push","substr","length","join","error","placeholder","AutoComplete","enhance","CATEGORY_OPTIONS","BANK_OPTIONS","ORIGINAL_SELECTS"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"modal_question_bank_bulkmove.min.js","sources":["../src/modal_question_bank_bulkmove.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 * Contain the logic for the bulkmove questions modal.\n *\n * @module qbank_bulkmove/modal_question_bank_bulkmove\n * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}\n * @author Simon Adams \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport * as Fragment from 'core/fragment';\nimport {getString} from 'core/str';\nimport AutoComplete from 'core/form-autocomplete';\nimport {moveQuestions} from 'core_question/repository';\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\n\n\nexport default class ModalQuestionBankBulkmove extends Modal {\n static TYPE = 'qbank_bulkmove/bulkmove';\n\n static SELECTORS = {\n SAVE_BUTTON: '[data-action=\"bulkmovesave\"]',\n SELECTED_QUESTIONS: 'table#categoryquestions input[id^=\"checkq\"]',\n SEARCH_BANK: '#searchbanks',\n SEARCH_CATEGORY: '.selectcategory',\n QUESTION_CATEGORY_SELECTOR: '.question_category_selector',\n CATEGORY_OPTIONS: '.selectcategory option',\n BANK_OPTIONS: '#searchbanks option',\n CATEGORY_ENHANCED_INPUT: '.search-categories input',\n ORIGINAL_SELECTS: 'select.bulk-move',\n CATEGORY_WARNING: '#searchcatwarning',\n CATEGORY_SUGGESTION: '.search-categories span.form-autocomplete-downarrow',\n CATEGORY_SELECTION: '.search-categories span[role=\"option\"][data-active-selection=\"true\"]',\n CONFIRM_BUTTON: '.bulk-move-footer button[data-action=\"save\"]',\n CANCEL_BUTTON: '.bulk-move-footer button[data-action=\"cancel\"]'\n };\n\n /**\n * @param {integer} contextId The current bank context id.\n * @param {integer} categoryId The current question category id.\n */\n static init(contextId, categoryId) {\n document.addEventListener('click', (e) => {\n const trigger = e.target;\n if (trigger.classList.contains('dropdown-item') && trigger.getAttribute('name') === 'move') {\n e.preventDefault();\n ModalQuestionBankBulkmove.create({\n contextId,\n title: getString('bulkmoveheader', 'qbank_bulkmove'),\n show: true,\n categoryId: categoryId,\n });\n }\n });\n }\n\n /**\n * Set the initialised config on the class.\n *\n * @param {Object} modalConfig\n */\n configure(modalConfig) {\n this.contextId = modalConfig.contextId;\n this.targetBankContextId = modalConfig.contextId;\n this.initSelectedCategoryId(modalConfig.categoryId);\n modalConfig.removeOnClose = true;\n super.configure(modalConfig);\n }\n\n /**\n * Initialise the category select based on the data passed to the JS or if a filter is applied in the url.\n * @param {integer} categoryId\n */\n initSelectedCategoryId(categoryId) {\n const filter = new URLSearchParams(window.location.href).get('filter');\n if (filter) {\n const filteredCategoryId = JSON.parse(filter)?.category.values[0];\n this.currentCategoryId = filteredCategoryId > 0 ? filteredCategoryId : null;\n this.targetCategoryId = filteredCategoryId;\n return;\n }\n this.currentCategoryId = categoryId;\n this.targetCategoryId = categoryId;\n }\n\n /**\n * Render the modal contents.\n * @return {Promise}\n */\n show() {\n void this.display(this.contextId, this.currentCategoryId);\n return super.show();\n }\n\n /**\n * Get the content to display and enhance the selects into auto complete fields.\n * @param {integer} currentBankContextId\n * @param {integer} currentCategoryId\n */\n async display(currentBankContextId, currentCategoryId) {\n const displayPending = new Pending('qbank_bulkmove/bulk_move_modal');\n this.bodyPromise = await Fragment.loadFragment(\n 'qbank_bulkmove',\n 'bulk_move',\n currentBankContextId,\n {\n 'categoryid': currentCategoryId,\n }\n );\n\n await this.setBody(this.bodyPromise);\n await this.enhanceSelects();\n this.registerEnhancedEventListeners();\n this.updateSaveButtonState();\n displayPending.resolve();\n }\n\n /**\n * Register event listeners on the enhanced selects. Must be done after they have been enhanced.\n */\n registerEnhancedEventListeners() {\n document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY).addEventListener(\"change\", () => {\n this.updateSaveButtonState();\n });\n\n document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_BANK).addEventListener(\"change\", async(e) => {\n if (parseInt(e.target.value) === 0) {\n // The autocomplete contains a dummy option containing the text that the limit has been reached and the user\n // has to refine the search. Selection of this dummy option has to be handled separately.\n await this.updateCategorySelector(null);\n return;\n }\n await this.updateCategorySelector(e.currentTarget.value);\n this.updateSaveButtonState();\n });\n\n this.getModal().on(\"click\", ModalQuestionBankBulkmove.SELECTORS.SAVE_BUTTON, (e) => {\n e.preventDefault();\n void this.displayConfirmMove();\n });\n }\n\n /**\n * Update the body with a confirmation prompt and set confirm cancel buttons in the footer.\n * @return {Promise}\n */\n async displayConfirmMove() {\n this.setTitle(getString('confirm', 'core'));\n this.setBody(getString('confirmmove', 'qbank_bulkmove'));\n if (!this.hasFooterContent()) {\n // We don't have the footer yet so go grab it and register event listeners on the buttons.\n this.setFooter(Templates.render('qbank_bulkmove/bulk_move_footer', {}));\n await this.getFooterPromise();\n\n document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CONFIRM_BUTTON).addEventListener(\"click\", (e) => {\n e.preventDefault();\n this.moveQuestionsAfterConfirm(this.targetBankContextId, this.targetCategoryId);\n });\n\n document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CANCEL_BUTTON).addEventListener(\"click\", (e) => {\n e.preventDefault();\n this.setTitle(getString('bulkmoveheader', 'qbank_bulkmove'));\n this.setBodyContent(Templates.renderForPromise('core/loading', {}));\n this.hideFooter();\n this.display(this.targetBankContextId, this.targetCategoryId);\n });\n } else {\n // We already have a footer so just show it.\n this.showFooter();\n }\n }\n\n /**\n * Update the category selector based on the selected question bank.\n *\n * @param {Number} selectedBankCmId\n * @return {Promise} Resolved when the update is complete.\n */\n updateCategorySelector(selectedBankCmId) {\n if (!selectedBankCmId) {\n this.updateCategorySelectorState(false);\n return Promise.resolve();\n } else {\n return Fragment.loadFragment(\n 'core_question',\n 'category_selector',\n this.contextId,\n {\n 'bankcmid': selectedBankCmId,\n }\n )\n .then((html, js) => {\n const categorySelector = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.QUESTION_CATEGORY_SELECTOR);\n return Templates.replaceNode(categorySelector, html, js);\n })\n .then(() => {\n document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_WARNING).classList.add('d-none');\n return this.enhanceSelects();\n })\n .catch(Notification.exception);\n }\n }\n\n /**\n * Disable/enable the enhanced category selector field.\n * @param {boolean} toEnable True to enable, false to disable the field.\n */\n updateCategorySelectorState(toEnable) {\n const warning = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_WARNING);\n const enhancedInput = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_ENHANCED_INPUT);\n const suggestionButton = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_SUGGESTION);\n const selection = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_SELECTION);\n\n if (toEnable) {\n warning.classList.add('d-none');\n enhancedInput.removeAttribute('disabled');\n suggestionButton.classList.remove('d-none');\n } else {\n warning.classList.remove('d-none');\n enhancedInput.setAttribute('disabled', 'disabled');\n suggestionButton.classList.add('d-none');\n selection.click(); // Clear selected category.\n }\n }\n\n /**\n * Disable the button if the selected category is the same as the one the questions already belong to. Enable it otherwise.\n */\n updateSaveButtonState() {\n const saveButton = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SAVE_BUTTON);\n const categorySelector = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY);\n [this.targetCategoryId, this.targetBankContextId] = categorySelector.value.split(',');\n\n if (this.targetCategoryId && this.targetCategoryId !== this.currentCategoryId) {\n saveButton.removeAttribute('disabled');\n } else {\n saveButton.setAttribute('disabled', 'disabled');\n }\n }\n\n /**\n * Move the selected questions to their new target category.\n * @param {integer} targetContextId the target bank context id.\n * @param {integer} targetCategoryId the target question category id.\n * @return {Promise}\n */\n async moveQuestionsAfterConfirm(targetContextId, targetCategoryId) {\n await this.setBody(Templates.render('core/loading', {}));\n const qelements = document.querySelectorAll(ModalQuestionBankBulkmove.SELECTORS.SELECTED_QUESTIONS);\n const questionids = [];\n qelements.forEach((element) => {\n if (element.checked) {\n const name = element.getAttribute('name');\n questionids.push(name.substr(1, name.length));\n }\n });\n if (questionids.length === 0) {\n await Notification.exception('No questions selected');\n }\n\n try {\n window.location.href = await moveQuestions(\n targetContextId,\n targetCategoryId,\n questionids.join(),\n window.location.href\n );\n } catch (error) {\n await Notification.exception(error);\n }\n }\n\n /**\n * Take the provided select options and enhance them into auto-complete fields.\n *\n * @return {Promise}\n */\n async enhanceSelects() {\n const placeholder = await getString('searchbyname', 'mod_quiz');\n\n await AutoComplete.enhance(\n ModalQuestionBankBulkmove.SELECTORS.SEARCH_BANK,\n false,\n 'core_question/question_banks_datasource',\n placeholder,\n false,\n true,\n '',\n true,\n );\n\n await AutoComplete.enhance(\n ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY,\n false,\n null,\n placeholder,\n false,\n true,\n '',\n true,\n );\n }\n}\n"],"names":["ModalQuestionBankBulkmove","Modal","contextId","categoryId","document","addEventListener","e","trigger","target","classList","contains","getAttribute","preventDefault","create","title","show","configure","modalConfig","targetBankContextId","initSelectedCategoryId","removeOnClose","filter","URLSearchParams","window","location","href","get","filteredCategoryId","JSON","parse","_JSON$parse","category","values","currentCategoryId","targetCategoryId","this","display","super","currentBankContextId","displayPending","Pending","bodyPromise","Fragment","loadFragment","setBody","enhanceSelects","registerEnhancedEventListeners","updateSaveButtonState","resolve","querySelector","SELECTORS","SEARCH_CATEGORY","SEARCH_BANK","async","parseInt","value","updateCategorySelector","currentTarget","getModal","on","SAVE_BUTTON","displayConfirmMove","setTitle","hasFooterContent","showFooter","setFooter","Templates","render","getFooterPromise","CONFIRM_BUTTON","moveQuestionsAfterConfirm","CANCEL_BUTTON","setBodyContent","renderForPromise","hideFooter","selectedBankCmId","then","html","js","categorySelector","QUESTION_CATEGORY_SELECTOR","replaceNode","CATEGORY_WARNING","add","catch","Notification","exception","updateCategorySelectorState","Promise","toEnable","warning","enhancedInput","CATEGORY_ENHANCED_INPUT","suggestionButton","CATEGORY_SUGGESTION","selection","CATEGORY_SELECTION","removeAttribute","remove","setAttribute","click","saveButton","split","targetContextId","qelements","querySelectorAll","SELECTED_QUESTIONS","questionids","forEach","element","checked","name","push","substr","length","join","error","placeholder","AutoComplete","enhance","CATEGORY_OPTIONS","BANK_OPTIONS","ORIGINAL_SELECTS"],"mappings":"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"} \ No newline at end of file diff --git a/public/question/bank/bulkmove/amd/src/modal_question_bank_bulkmove.js b/public/question/bank/bulkmove/amd/src/modal_question_bank_bulkmove.js index 9d398bd823025..1252bb8ed0833 100644 --- a/public/question/bank/bulkmove/amd/src/modal_question_bank_bulkmove.js +++ b/public/question/bank/bulkmove/amd/src/modal_question_bank_bulkmove.js @@ -141,6 +141,12 @@ export default class ModalQuestionBankBulkmove extends Modal { }); document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_BANK).addEventListener("change", async(e) => { + if (parseInt(e.target.value) === 0) { + // The autocomplete contains a dummy option containing the text that the limit has been reached and the user + // has to refine the search. Selection of this dummy option has to be handled separately. + await this.updateCategorySelector(null); + return; + } await this.updateCategorySelector(e.currentTarget.value); this.updateSaveButtonState(); }); diff --git a/public/question/bank/bulkmove/tests/behat/bulk_move.feature b/public/question/bank/bulkmove/tests/behat/bulk_move.feature index fbd3d42a9d623..33da2440b14f2 100644 --- a/public/question/bank/bulkmove/tests/behat/bulk_move.feature +++ b/public/question/bank/bulkmove/tests/behat/bulk_move.feature @@ -221,3 +221,58 @@ Feature: Use the qbank plugin manager page for bulkmove And I click on "move" "button" And I open the autocomplete suggestions list in the ".search-banks" "css_element" Then "New question bank" "autocomplete_suggestions" should exist + + @javascript + Scenario: Clicking the dummy option for showing the reached limit in course does not throw an error + Given the following "activities" exist: + | activity | name | course | idnumber | + | qbank | Question bank Course 1 1 | C1 | qbankc101 | + | qbank | Question bank Course 1 2 | C1 | qbankc102 | + | qbank | Question bank Course 1 3 | C1 | qbankc103 | + | qbank | Question bank Course 1 4 | C1 | qbankc104 | + | qbank | Question bank Course 1 5 | C1 | qbankc105 | + | qbank | Question bank Course 1 6 | C1 | qbankc106 | + | qbank | Question bank Course 1 7 | C1 | qbankc107 | + | qbank | Question bank Course 1 8 | C1 | qbankc108 | + | qbank | Question bank Course 1 9 | C1 | qbankc109 | + | qbank | Question bank Course 1 10 | C1 | qbankc110 | + | qbank | Question bank Course 2 1 | C2 | qbankc201 | + | qbank | Question bank Course 2 2 | C2 | qbankc202 | + | qbank | Question bank Course 2 3 | C2 | qbankc203 | + | qbank | Question bank Course 2 4 | C2 | qbankc204 | + | qbank | Question bank Course 2 5 | C2 | qbankc205 | + | qbank | Question bank Course 2 6 | C2 | qbankc206 | + | qbank | Question bank Course 2 7 | C2 | qbankc207 | + | qbank | Question bank Course 2 8 | C2 | qbankc208 | + | qbank | Question bank Course 2 9 | C2 | qbankc209 | + | qbank | Question bank Course 2 10 | C2 | qbankc210 | + | qbank | Question bank Course 3 1 | C3 | qbankc301 | + | qbank | Question bank Course 3 2 | C3 | qbankc302 | + | qbank | Question bank Course 3 2 | C3 | qbankc303 | + | qbank | Question bank Course 3 4 | C3 | qbankc304 | + | qbank | Question bank Course 3 5 | C3 | qbankc305 | + | qbank | Question bank Course 3 6 | C3 | qbankc306 | + | qbank | Question bank Course 3 7 | C3 | qbankc307 | + | qbank | Question bank Course 3 8 | C3 | qbankc308 | + | qbank | Question bank Course 3 9 | C3 | qbankc309 | + | qbank | Question bank Course 3 10 | C3 | qbankc310 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C3 | editingteacher | + When I am on the "C1" "Course" page logged in as "teacher1" + And I navigate to "Question banks" in current page administration + And I follow "Question bank 1" + And I select "Categories" from the "Question bank tertiary navigation" singleselect + And I follow "Test questions 5" + And I click on "Fifth question" "checkbox" + And I click on "With selected" "button" + And I click on "move" "button" + And I wait until the page is ready + And "Question bank" "autocomplete_selection" should exist + And I press the tab key + And I press the tab key + And I press the tab key + And I type "Question bank Course" + Then I click on "More than 20 results. You need to refine your search." item in the autocomplete list + And "More than 20 results. You need to refine your search." "autocomplete_selection" should exist + And I should see "You must select a question bank before you can select a category." From 7f2b6002a95f65aac5c2a9dea9bf44d5c9aefa32 Mon Sep 17 00:00:00 2001 From: Mark Sharp Date: Mon, 29 Sep 2025 16:24:32 +0100 Subject: [PATCH 010/553] MDL-78495 Badges: Exclude badges from recycle bin - co-authored by: Sara Arjona Add badges to recycle bin test --- .../tool/recyclebin/classes/course_bin.php | 6 ++++ .../tests/behat/basic_functionality.feature | 29 ++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/public/admin/tool/recyclebin/classes/course_bin.php b/public/admin/tool/recyclebin/classes/course_bin.php index e68fb1fbb2993..0fdddc30dca2d 100644 --- a/public/admin/tool/recyclebin/classes/course_bin.php +++ b/public/admin/tool/recyclebin/classes/course_bin.php @@ -145,6 +145,12 @@ public function store_item($cm) { return; } + // We never need badge information here. + if ($plan->setting_exists('badges')) { + $badges = $plan->get_setting('badges'); + $badges->set_value(false); + } + $controller->execute_plan(); // We don't need the forced setting anymore, hence restore previous settings. diff --git a/public/admin/tool/recyclebin/tests/behat/basic_functionality.feature b/public/admin/tool/recyclebin/tests/behat/basic_functionality.feature index ca888ef106981..307b328e49e84 100644 --- a/public/admin/tool/recyclebin/tests/behat/basic_functionality.feature +++ b/public/admin/tool/recyclebin/tests/behat/basic_functionality.feature @@ -39,11 +39,19 @@ Feature: Basic recycle bin functionality | student2 | G1 | | student2 | G2 | And the following config values are set as admin: - | coursebinenable | 1 | tool_recyclebin | - | categorybinenable | 1 | tool_recyclebin | - | coursebinexpiry | 604800 | tool_recyclebin | + | coursebinenable | 1 | tool_recyclebin | + | categorybinenable | 1 | tool_recyclebin | + | coursebinexpiry | 604800 | tool_recyclebin | | categorybinexpiry | 1209600 | tool_recyclebin | - | autohide | 0 | tool_recyclebin | + | autohide | 0 | tool_recyclebin | + And the following "core_badges > Badges" exist: + | name | course | description | image | status | type | + | My course 1 badge | C1 | Badge description | badges/tests/behat/badge.png | active | 2 | + | My course 2 badge | C2 | Badge description | badges/tests/behat/badge.png | active | 2 | + And the following "core_badges > Criterias" exist: + | badge | role | + | My course 1 badge | editingteacher | + | My course 2 badge | editingteacher | Scenario: Restore a deleted assignment Given I log in as "teacher1" @@ -58,6 +66,14 @@ Feature: Basic recycle bin functionality And I wait to be redirected And I am on "Course 1" course homepage And I should see "Test assign 1" in the "Section 1" "section" + # Check badges were not duplicated. + And I navigate to "Badges" in current page administration + And the following should exist in the "reportbuilder-table" table: + | Name | Badge status | + | My course 1 badge | Available | + And the following should not exist in the "reportbuilder-table" table: + | Name | Badge status | + | My course 1 badge | Not available | @javascript Scenario: Restore a deleted course @@ -83,6 +99,11 @@ Feature: Basic recycle bin functionality And "Student 1" "text" should exist in the "Group A" "table_row" And "Student 2" "text" should exist in the "Group A" "table_row" And "Student 2" "text" should exist in the "Group B" "table_row" + # Check badges are restored. + And I navigate to "Badges" in current page administration + And the following should exist in the "reportbuilder-table" table: + | Name | Badge status | + | My course 2 badge | Not available | @javascript Scenario: Deleting a single item from the recycle bin From 8b9a31798204d20fae8f803d4626185daecc9a8f Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Mon, 4 Aug 2025 15:15:13 +0100 Subject: [PATCH 011/553] MDL-82867 output: consistently space external link pixicon. --- .../files/classes/redactor/services/exifremover_service.php | 4 ++-- public/lib/classes/output/core_renderer.php | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/public/files/classes/redactor/services/exifremover_service.php b/public/files/classes/redactor/services/exifremover_service.php index 9687e757ce002..c9c71a2efaa1e 100644 --- a/public/files/classes/redactor/services/exifremover_service.php +++ b/public/files/classes/redactor/services/exifremover_service.php @@ -392,11 +392,11 @@ public static function add_settings(\admin_settingpage $settings): void { } } - $icon = $OUTPUT->pix_icon('i/externallink', get_string('opensinnewwindow')); + $icon = $OUTPUT->pix_icon('i/externallink', get_string('opensinnewwindow'), attributes: ['class' => 'ms-1']); $a = (object) [ 'link' => html_writer::link( url: 'https://exiftool.sourceforge.net/install.html', - text: "https://exiftool.sourceforge.net/install.html $icon", + text: 'https://exiftool.sourceforge.net/install.html' . $icon, attributes: ['role' => 'opener', 'rel' => 'noreferrer', 'target' => '_blank'], ), ]; diff --git a/public/lib/classes/output/core_renderer.php b/public/lib/classes/output/core_renderer.php index 9d8680d81fe02..bd87ae144fe13 100644 --- a/public/lib/classes/output/core_renderer.php +++ b/public/lib/classes/output/core_renderer.php @@ -1941,8 +1941,7 @@ public function doc_link($path, $text = '', $forcepopup = false, array $attribut $newwindowicon = $this->pix_icon( 'i/externallink', get_string('opensinnewwindow'), - 'moodle', - ['class' => 'fa fa-externallink fa-fw'] + attributes: ['class' => 'ms-1'], ); } From 9cf695d7bc93b903b48f70d37a21cedd08ddddde Mon Sep 17 00:00:00 2001 From: Rajneel Totaram Date: Thu, 10 Jul 2025 12:28:48 +1200 Subject: [PATCH 012/553] MDL-66579 mod_book: Make chapter numbering consistent --- public/mod/book/locallib.php | 36 +++++++--- .../tests/behat/chapter_numbering.feature | 66 +++++++++++++++++++ 2 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 public/mod/book/tests/behat/chapter_numbering.feature diff --git a/public/mod/book/locallib.php b/public/mod/book/locallib.php index 1627c3e16e62c..ec1867720df86 100644 --- a/public/mod/book/locallib.php +++ b/public/mod/book/locallib.php @@ -352,9 +352,6 @@ function book_get_toc($chapters, $chapter, $book, $cm, $edit) { $titleunescaped = trim(format_string($ch->title, true, array('context' => $context, 'escape' => false))); if (!$ch->hidden || ($ch->hidden && $viewhidden)) { if (!$ch->subchapter) { - $nch++; - $ns = 0; - if ($first) { $toc .= html_writer::start_tag('li'); } else { @@ -363,12 +360,20 @@ function book_get_toc($chapters, $chapter, $book, $cm, $edit) { $toc .= html_writer::start_tag('li'); } - if ($book->numbering == BOOK_NUM_NUMBERS) { - $title = "$nch. $title"; + // Don't show numbering for hidden chapters, so that numbering is consistent with what students see + // and the edit mode. + if (!$ch->hidden) { + $nch++; + $ns = 0; + if ($book->numbering == BOOK_NUM_NUMBERS) { + $title = "$nch. $title"; + } + } else { + if ($book->numbering == BOOK_NUM_NUMBERS) { + $title = "x. $title"; + } } } else { - $ns++; - if ($first) { $toc .= html_writer::start_tag('li'); $toc .= html_writer::start_tag('ul'); @@ -377,8 +382,21 @@ function book_get_toc($chapters, $chapter, $book, $cm, $edit) { $toc .= html_writer::start_tag('li'); } - if ($book->numbering == BOOK_NUM_NUMBERS) { - $title = "$nch.$ns. $title"; + // Don't show numbering for hidden subchapters, so that numbering is consistent with what students see + // and the edit mode. + if (!$ch->hidden) { + $ns++; + if ($book->numbering == BOOK_NUM_NUMBERS) { + $title = "$nch.$ns. $title"; + } + } else { + if ($book->numbering == BOOK_NUM_NUMBERS) { + if (empty($chapters[$ch->parent]->hidden)) { + $title = "$nch.x. $title"; + } else { + $title = "x.x. $title"; + } + } } } diff --git a/public/mod/book/tests/behat/chapter_numbering.feature b/public/mod/book/tests/behat/chapter_numbering.feature new file mode 100644 index 0000000000000..48261474e7b63 --- /dev/null +++ b/public/mod/book/tests/behat/chapter_numbering.feature @@ -0,0 +1,66 @@ +@mod @mod_book +Feature: Book chapter numbering should be consistent for users + In order to correctly refer to book chapters + As a teacher or student + I should be able to see the same chapter numbering as other users + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | One | student1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activity" exists: + | course | C1 | + | activity | book | + | name | Test book | + And the following "mod_book > chapters" exist: + | book | title | content | pagenum |subchapter | hidden | + | Test book | Intro | Intro chapter | 1 | 0 | 0 | + | Test book | First chapter | First chapter | 2 | 0 | 0 | + | Test book | Sub chapter A | Sub chapter A | 3 | 1 | 0 | + | Test book | Sub chapter B | Sub chapter B | 4 | 1 | 1 | + | Test book | Sub chapter C | Sub chapter C | 5 | 1 | 0 | + | Test book | Second chapter | Second chapter | 6 | 0 | 1 | + | Test book | Sub chapter D | Sub chapter C | 7 | 1 | 1 | + | Test book | Third chapter | Third chapter | 8 | 0 | 0 | + + Scenario Outline: Chapter numbering for teachers is consistent in editing and view mode + Given I am on the "Course 1" course page logged in as teacher1 + And I turn editing mode + And I am on the "Test book" "book activity" page + # Check chapter numbering + When I follow "2.1. Sub chapter A" + Then I should see "2.1. Sub chapter A" in the ".book_content" "css_element" + And I follow "2.x. Sub chapter B" + And I should see "2.x. Sub chapter B" in the ".book_content" "css_element" + And I follow "2.2. Sub chapter C" + And I should see "2.2. Sub chapter C" in the ".book_content" "css_element" + And I follow "x. Second chapter" + And I should see "x. Second chapter" in the ".book_content" "css_element" + And I follow "x.x. Sub chapter D" + And I should see "x.x. Sub chapter D" in the ".book_content" "css_element" + And I follow "3. Third chapter" + And I should see "3. Third chapter" in the ".book_content" "css_element" + + Examples: + | editmode | + | on | + | off | + + Scenario: Chapter numbering for students is consistent with what teachers see + Given I am on the "Course 1" course page logged in as student1 + And I am on the "Test book" "book activity" page + # Check chapter numbering + When I follow "2.1. Sub chapter A" + Then I should see "2.1. Sub chapter A" in the ".book_content" "css_element" + And I follow "2.2. Sub chapter C" + And I should see "2.2. Sub chapter C" in the ".book_content" "css_element" + And I follow "3. Third chapter" + And I should see "3. Third chapter" in the ".book_content" "css_element" From 16e42d4bd5dadc62b3d757ee361c733afe3de7e8 Mon Sep 17 00:00:00 2001 From: Rajneel Totaram Date: Fri, 11 Jul 2025 09:01:30 +1200 Subject: [PATCH 013/553] MDL-85988 mod_book: Fix chapter content overlaps navigation buttons --- public/mod/book/styles.css | 2 +- public/theme/classic/scss/moodle.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/mod/book/styles.css b/public/mod/book/styles.css index bff952db56c66..009cb151801fc 100644 --- a/public/mod/book/styles.css +++ b/public/mod/book/styles.css @@ -61,7 +61,7 @@ } .path-mod-book .book_content { - margin: 0 5px; + margin: 0 15px; padding-right: 15px; padding-left: 15px; position: relative; /* Chapter navigation should not float on top of content. */ diff --git a/public/theme/classic/scss/moodle.scss b/public/theme/classic/scss/moodle.scss index 1966680c1c94f..de782af5e6730 100644 --- a/public/theme/classic/scss/moodle.scss +++ b/public/theme/classic/scss/moodle.scss @@ -32,7 +32,7 @@ img.userpicture { } .book_content { - margin: 0 30px; + margin: 0 40px; @include media-breakpoint-down(md) { margin-left: -10px; From e028ea9232e0dd417ae29d88e2fdb165cd2e27d5 Mon Sep 17 00:00:00 2001 From: AMOS bot Date: Thu, 9 Oct 2025 00:09:19 +0000 Subject: [PATCH 014/553] Automatically generated installer lang files --- public/install/lang/fo/install.php | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 public/install/lang/fo/install.php diff --git a/public/install/lang/fo/install.php b/public/install/lang/fo/install.php new file mode 100644 index 0000000000000..aaac9482e4d21 --- /dev/null +++ b/public/install/lang/fo/install.php @@ -0,0 +1,32 @@ +. + +/** + * Automatically generated strings for Moodle installer + * + * Do not edit this file manually! It contains just a subset of strings + * needed during the very first steps of installation. This file was + * generated automatically by export-installer.php (which is part of AMOS + * {@link https://moodledev.io/general/projects/api/amos}) using the + * list of strings defined in public/install/stringnames.txt file. + * + * @package installer + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['welcomep10'] = '{$a->installername} ({$a->installerversion})'; From 8f7c9ae1b509beb778bb7498fd157514c1488f7d Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Sun, 7 Sep 2025 18:44:02 +0200 Subject: [PATCH 015/553] MDL-86574 tool_cohortroles: format cohort data during privacy export. --- .../cohortroles/classes/privacy/provider.php | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/public/admin/tool/cohortroles/classes/privacy/provider.php b/public/admin/tool/cohortroles/classes/privacy/provider.php index 9896a620610af..e55794c755520 100644 --- a/public/admin/tool/cohortroles/classes/privacy/provider.php +++ b/public/admin/tool/cohortroles/classes/privacy/provider.php @@ -166,10 +166,12 @@ public static function export_user_data(approved_contextlist $contextlist) { // Retrieve the tool_cohortroles records created for the user. $sql = "SELECT cr.id as cohortroleid, + cr.cohortid, c.name as cohortname, c.idnumber as cohortidnumber, - c.description as cohortdescription, - c.contextid as contextid, + c.description, + c.descriptionformat, + c.contextid, r.shortname as roleshortname, cr.userid as userid, cr.timecreated as timecreated, @@ -185,27 +187,39 @@ public static function export_user_data(approved_contextlist $contextlist) { $cohortroles = $DB->get_records_sql($sql, $params); foreach ($cohortroles as $cohortrole) { + $context = \context::instance_by_id($cohortrole->contextid); + // The tool_cohortroles data export is organised in: // {User Context}/Cohort roles management/{cohort name}/{role shortname}/data.json. $subcontext = [ get_string('pluginname', 'tool_cohortroles'), - $cohortrole->cohortname, + format_string($cohortrole->cohortname, options: ['context' => $context]), $cohortrole->roleshortname ]; $data = (object) [ - 'cohortname' => $cohortrole->cohortname, + 'cohortname' => format_string($cohortrole->cohortname, options: ['context' => $context]), 'cohortidnumber' => $cohortrole->cohortidnumber, - 'cohortdescription' => $cohortrole->cohortdescription, + 'cohortdescription' => format_text( + writer::with_context($context)->rewrite_pluginfile_urls( + $subcontext, + 'cohort', + 'description', + $cohortrole->cohortid, + $cohortrole->description, + ), + $cohortrole->descriptionformat, + ['context' => $context], + ), 'roleshortname' => $cohortrole->roleshortname, 'userid' => transform::user($cohortrole->userid), 'timecreated' => transform::datetime($cohortrole->timecreated), 'timemodified' => transform::datetime($cohortrole->timemodified) ]; - $context = \context::instance_by_id($cohortrole->contextid); - - writer::with_context($context)->export_data($subcontext, $data); + writer::with_context($context) + ->export_area_files($subcontext, 'cohort', 'description', $cohortrole->cohortid) + ->export_data($subcontext, $data); } } From 431040b341a40cc5d3fbef79d2980f280e1a68d7 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Sun, 7 Sep 2025 19:26:13 +0200 Subject: [PATCH 016/553] MDL-86575 cohort: format cohort names in external service return data. --- public/cohort/externallib.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/cohort/externallib.php b/public/cohort/externallib.php index 83ea6249cb8a6..0a759b5f55076 100644 --- a/public/cohort/externallib.php +++ b/public/cohort/externallib.php @@ -145,6 +145,8 @@ public static function create_cohorts($cohorts) { $cohort->id = cohort_add_cohort($cohort); + $cohort->name = \core_external\util::format_string($cohort->name, $context); + list($cohort->description, $cohort->descriptionformat) = \core_external\util::format_text($cohort->description, $cohort->descriptionformat, $context, 'cohort', 'description', $cohort->id); @@ -284,6 +286,8 @@ public static function get_cohorts($cohortids = array()) { throw new required_capability_exception($context, 'moodle/cohort:view', 'nopermissions', ''); } + $cohort->name = \core_external\util::format_string($cohort->name, $context); + // Only return theme when $CFG->allowcohortthemes is enabled. if (!empty($cohort->theme) && empty($CFG->allowcohortthemes)) { $cohort->theme = null; @@ -427,6 +431,8 @@ public static function search_cohorts($query, $context, $includes = 'parents', $ foreach ($results as $key => $cohort) { $cohortcontext = context::instance_by_id($cohort->contextid); + $cohort->name = \core_external\util::format_string($cohort->name, $cohortcontext); + // Only return theme when $CFG->allowcohortthemes is enabled. if (!empty($cohort->theme) && empty($CFG->allowcohortthemes)) { $cohort->theme = null; From 58e27b443091184b2827bc8eb6e33c4236f41f72 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Fri, 26 Sep 2025 11:38:23 +0100 Subject: [PATCH 017/553] MDL-86736 block_myoverview: remove stray template content. Changes made in c530e4c4 left some stray elements in the templates, which manifested themselves as displaying misplaced pipe characters depending on unrelated site config (`$CFG->courselistshortnames`). --- public/blocks/myoverview/templates/view-cards.mustache | 5 ----- public/blocks/myoverview/templates/view-list.mustache | 3 --- public/blocks/myoverview/templates/view-summary.mustache | 3 --- 3 files changed, 11 deletions(-) diff --git a/public/blocks/myoverview/templates/view-cards.mustache b/public/blocks/myoverview/templates/view-cards.mustache index 7391038065051..b82bb74f12b4b 100644 --- a/public/blocks/myoverview/templates/view-cards.mustache +++ b/public/blocks/myoverview/templates/view-cards.mustache @@ -67,9 +67,4 @@ {{/showcoursecategory}} {{/coursecategory}} - {{$divider}} - {{#showcoursecategory}} -
|
- {{/showcoursecategory}} - {{/divider}} {{/ core_course/coursecards }} diff --git a/public/blocks/myoverview/templates/view-list.mustache b/public/blocks/myoverview/templates/view-list.mustache index 7b8c73c093c3c..4e8a021364910 100644 --- a/public/blocks/myoverview/templates/view-list.mustache +++ b/public/blocks/myoverview/templates/view-list.mustache @@ -52,9 +52,6 @@
{{#showshortname}}
- {{#showcoursecategory}} -
|
- {{/showcoursecategory}} {{#str}}aria:courseshortname, core_course{{/str}} diff --git a/public/blocks/myoverview/templates/view-summary.mustache b/public/blocks/myoverview/templates/view-summary.mustache index 7d7480528ab80..b3221fcaa0010 100644 --- a/public/blocks/myoverview/templates/view-summary.mustache +++ b/public/blocks/myoverview/templates/view-summary.mustache @@ -52,9 +52,6 @@
{{#showshortname}}
- {{#showcoursecategory}} -
|
- {{/showcoursecategory}} {{#str}}aria:courseshortname, core_course{{/str}} From aca127b16dee7963ebb4bbed4082a5ea0a72776c Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Mon, 29 Sep 2025 10:40:15 +0100 Subject: [PATCH 018/553] MDL-86777 customfield: show formatted category name in move dialog. --- public/customfield/amd/build/form.min.js | 2 +- public/customfield/amd/build/form.min.js.map | 2 +- public/customfield/amd/src/form.js | 3 +-- public/customfield/classes/output/management.php | 1 + public/customfield/externallib.php | 1 + public/customfield/templates/list.mustache | 4 +++- 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/public/customfield/amd/build/form.min.js b/public/customfield/amd/build/form.min.js index 0b037753a91b4..bae1ff17a5990 100644 --- a/public/customfield/amd/build/form.min.js +++ b/public/customfield/amd/build/form.min.js @@ -5,6 +5,6 @@ define("core_customfield/form",["exports","core/inplace_editable","core/ajax","c * @module core_customfield/form * @copyright 2018 Toni Barbera * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modalform=_interopRequireDefault(_modalform),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_sortable_list=_interopRequireDefault(_sortable_list),_templates=_interopRequireDefault(_templates),_jquery=_interopRequireDefault(_jquery);const confirmDelete=(id,type,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:confirmDelete");(0,_str.getStrings)([{key:"confirm"},{key:"confirmdelete"+type,component:"core_customfield"},{key:"yes"},{key:"no"}]).then((strings=>_notification.default.confirm(strings[0],strings[1],strings[2],strings[3],(function(){const pendingDeletePromise=new _pending.default("core_customfield/form:confirmDelete");(0,_ajax.call)([{methodname:"field"===type?"core_customfield_delete_field":"core_customfield_delete_category",args:{id:id}},{methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}}])[1].then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then(pendingDeletePromise.resolve).catch(_notification.default.exception)})))).then(pendingPromise.resolve).catch(_notification.default.exception)},getCategoryNameFor=nodeElement=>nodeElement.closest("[data-category-id]").find("[data-inplaceeditable][data-itemtype=category][data-component=core_customfield]").attr("data-value");_exports.init=()=>{const rootNode=document.querySelector("#customfield_catlist"),component=rootNode.dataset.component,area=rootNode.dataset.area,itemid=rootNode.dataset.itemid;rootNode.addEventListener("click",(e=>{const roleHolder=e.target.closest("[data-role]");if(roleHolder)return"deletefield"===roleHolder.dataset.role?(e.preventDefault(),void confirmDelete(roleHolder.dataset.id,"field",component,area,itemid)):"deletecategory"===roleHolder.dataset.role?(e.preventDefault(),void confirmDelete(roleHolder.dataset.id,"category",component,area,itemid)):"addnewcategory"===roleHolder.dataset.role?(e.preventDefault(),void((component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:createNewCategory");(0,_ajax.call)([{methodname:"core_customfield_create_category",args:{component:component,area:area,itemid:itemid}},{methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}}])[1].then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)})(component,area,itemid)):"addfield"===roleHolder.dataset.role?(e.preventDefault(),void((element,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:createNewField"),returnFocus=element.closest(".action-menu").querySelector(".dropdown-toggle"),form=new _modalform.default({formClass:"core_customfield\\field_config_form",args:{categoryid:element.getAttribute("data-categoryid"),type:element.getAttribute("data-type")},modalConfig:{title:(0,_str.getString)("addingnewcustomfield","core_customfield",element.getAttribute("data-typename"))},returnFocus:returnFocus});form.addEventListener(form.events.FORM_SUBMITTED,(()=>{const pendingCreatedPromise=new _pending.default("core_customfield/form:createdNewField");(0,_ajax.call)([{methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}}])[0].then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then((()=>pendingCreatedPromise.resolve())).catch((()=>window.location.reload()))})),form.show(),pendingPromise.resolve()})(roleHolder,component,area,itemid)):"editfield"===roleHolder.dataset.role?(e.preventDefault(),void((element,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:editField"),form=new _modalform.default({formClass:"core_customfield\\field_config_form",args:{id:element.getAttribute("data-id")},modalConfig:{title:(0,_str.getString)("editingfield","core_customfield",element.getAttribute("data-name"))},returnFocus:element});form.addEventListener(form.events.FORM_SUBMITTED,(()=>{const pendingCreatedPromise=new _pending.default("core_customfield/form:createdNewField");(0,_ajax.call)([{methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}}])[0].then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then((()=>pendingCreatedPromise.resolve())).catch((()=>window.location.reload()))})),form.show(),pendingPromise.resolve()})(roleHolder,component,area,itemid)):void 0})),(rootNode=>{new _sortable_list.default("#customfield_catlist .categorieslist",{moveHandlerSelector:".movecategory [data-drag-type=move]"}).getElementName=nodeElement=>Promise.resolve(getCategoryNameFor(nodeElement)),(0,_jquery.default)("[data-category-id]").on(_sortable_list.default.EVENTS.DROP,((evt,info)=>{if(info.positionChanged){const pendingPromise=new _pending.default("core_customfield/form:categoryid:on:sortablelist-drop");(0,_ajax.call)([{methodname:"core_customfield_move_category",args:{id:info.element.data("category-id"),beforeid:info.targetNextElement.data("category-id")}}])[0].then(pendingPromise.resolve).catch(_notification.default.exception)}evt.stopPropagation()})),new _sortable_list.default("#customfield_catlist .fieldslist tbody",{moveHandlerSelector:".movefield [data-drag-type=move]"}).getDestinationName=(parentElement,afterElement)=>afterElement.length?afterElement.attr("data-field-name")?(0,_str.getString)("afterfield","customfield",afterElement.attr("data-field-name")):Promise.resolve(""):(0,_str.getString)("totopofcategory","customfield",getCategoryNameFor(parentElement)),(0,_jquery.default)("[data-field-name]").on(_sortable_list.default.EVENTS.DROP,((evt,info)=>{if(info.positionChanged){const pendingPromise=new _pending.default("core_customfield/form:fieldname:on:sortablelist-drop");(0,_ajax.call)([{methodname:"core_customfield_move_field",args:{id:info.element.data("field-id"),beforeid:info.targetNextElement.data("field-id"),categoryid:Number(info.targetList.closest("[data-category-id]").attr("data-category-id"))}}])[0].then(pendingPromise.resolve).catch(_notification.default.exception)}evt.stopPropagation()})),(0,_jquery.default)("[data-field-name]").on(_sortable_list.default.EVENTS.DRAG,(evt=>{var pendingPromise=new _pending.default("core_customfield/form:fieldname:on:sortablelist-drag");evt.stopPropagation(),_templates.default.render("core_customfield/nofields",{}).then((html=>{rootNode.querySelectorAll(".categorieslist > *").forEach((category=>{const fields=category.querySelectorAll(".field:not(.sortable-list-is-dragged)"),noFields=category.querySelector(".nofields");fields.length||noFields?fields.length&&noFields&&noFields.remove():category.querySelector("tbody").innerHTML=html}))})).then(pendingPromise.resolve).catch(_notification.default.exception)})),(0,_jquery.default)("[data-category-id], [data-field-name]").on(_sortable_list.default.EVENTS.DRAGSTART,((evt,info)=>{setTimeout((()=>{(0,_jquery.default)(".sortable-list-is-dragged").width(info.element.width())}),501)}))})(rootNode)}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modalform=_interopRequireDefault(_modalform),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_sortable_list=_interopRequireDefault(_sortable_list),_templates=_interopRequireDefault(_templates),_jquery=_interopRequireDefault(_jquery);const confirmDelete=(id,type,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:confirmDelete");(0,_str.getStrings)([{key:"confirm"},{key:"confirmdelete"+type,component:"core_customfield"},{key:"yes"},{key:"no"}]).then((strings=>_notification.default.confirm(strings[0],strings[1],strings[2],strings[3],(function(){const pendingDeletePromise=new _pending.default("core_customfield/form:confirmDelete");(0,_ajax.call)([{methodname:"field"===type?"core_customfield_delete_field":"core_customfield_delete_category",args:{id:id}},{methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}}])[1].then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then(pendingDeletePromise.resolve).catch(_notification.default.exception)})))).then(pendingPromise.resolve).catch(_notification.default.exception)},getCategoryNameFor=nodeElement=>nodeElement.closest("[data-category-id]").attr("data-category-name");_exports.init=()=>{const rootNode=document.querySelector("#customfield_catlist"),component=rootNode.dataset.component,area=rootNode.dataset.area,itemid=rootNode.dataset.itemid;rootNode.addEventListener("click",(e=>{const roleHolder=e.target.closest("[data-role]");if(roleHolder)return"deletefield"===roleHolder.dataset.role?(e.preventDefault(),void confirmDelete(roleHolder.dataset.id,"field",component,area,itemid)):"deletecategory"===roleHolder.dataset.role?(e.preventDefault(),void confirmDelete(roleHolder.dataset.id,"category",component,area,itemid)):"addnewcategory"===roleHolder.dataset.role?(e.preventDefault(),void((component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:createNewCategory");(0,_ajax.call)([{methodname:"core_customfield_create_category",args:{component:component,area:area,itemid:itemid}},{methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}}])[1].then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then((()=>pendingPromise.resolve())).catch(_notification.default.exception)})(component,area,itemid)):"addfield"===roleHolder.dataset.role?(e.preventDefault(),void((element,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:createNewField"),returnFocus=element.closest(".action-menu").querySelector(".dropdown-toggle"),form=new _modalform.default({formClass:"core_customfield\\field_config_form",args:{categoryid:element.getAttribute("data-categoryid"),type:element.getAttribute("data-type")},modalConfig:{title:(0,_str.getString)("addingnewcustomfield","core_customfield",element.getAttribute("data-typename"))},returnFocus:returnFocus});form.addEventListener(form.events.FORM_SUBMITTED,(()=>{const pendingCreatedPromise=new _pending.default("core_customfield/form:createdNewField");(0,_ajax.call)([{methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}}])[0].then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then((()=>pendingCreatedPromise.resolve())).catch((()=>window.location.reload()))})),form.show(),pendingPromise.resolve()})(roleHolder,component,area,itemid)):"editfield"===roleHolder.dataset.role?(e.preventDefault(),void((element,component,area,itemid)=>{const pendingPromise=new _pending.default("core_customfield/form:editField"),form=new _modalform.default({formClass:"core_customfield\\field_config_form",args:{id:element.getAttribute("data-id")},modalConfig:{title:(0,_str.getString)("editingfield","core_customfield",element.getAttribute("data-name"))},returnFocus:element});form.addEventListener(form.events.FORM_SUBMITTED,(()=>{const pendingCreatedPromise=new _pending.default("core_customfield/form:createdNewField");(0,_ajax.call)([{methodname:"core_customfield_reload_template",args:{component:component,area:area,itemid:itemid}}])[0].then((response=>_templates.default.render("core_customfield/list",response))).then(((html,js)=>_templates.default.replaceNode((0,_jquery.default)('[data-region="list-page"]'),html,js))).then((()=>pendingCreatedPromise.resolve())).catch((()=>window.location.reload()))})),form.show(),pendingPromise.resolve()})(roleHolder,component,area,itemid)):void 0})),(rootNode=>{new _sortable_list.default("#customfield_catlist .categorieslist",{moveHandlerSelector:".movecategory [data-drag-type=move]"}).getElementName=nodeElement=>Promise.resolve(getCategoryNameFor(nodeElement)),(0,_jquery.default)("[data-category-id]").on(_sortable_list.default.EVENTS.DROP,((evt,info)=>{if(info.positionChanged){const pendingPromise=new _pending.default("core_customfield/form:categoryid:on:sortablelist-drop");(0,_ajax.call)([{methodname:"core_customfield_move_category",args:{id:info.element.data("category-id"),beforeid:info.targetNextElement.data("category-id")}}])[0].then(pendingPromise.resolve).catch(_notification.default.exception)}evt.stopPropagation()})),new _sortable_list.default("#customfield_catlist .fieldslist tbody",{moveHandlerSelector:".movefield [data-drag-type=move]"}).getDestinationName=(parentElement,afterElement)=>afterElement.length?afterElement.attr("data-field-name")?(0,_str.getString)("afterfield","customfield",afterElement.attr("data-field-name")):Promise.resolve(""):(0,_str.getString)("totopofcategory","customfield",getCategoryNameFor(parentElement)),(0,_jquery.default)("[data-field-name]").on(_sortable_list.default.EVENTS.DROP,((evt,info)=>{if(info.positionChanged){const pendingPromise=new _pending.default("core_customfield/form:fieldname:on:sortablelist-drop");(0,_ajax.call)([{methodname:"core_customfield_move_field",args:{id:info.element.data("field-id"),beforeid:info.targetNextElement.data("field-id"),categoryid:Number(info.targetList.closest("[data-category-id]").attr("data-category-id"))}}])[0].then(pendingPromise.resolve).catch(_notification.default.exception)}evt.stopPropagation()})),(0,_jquery.default)("[data-field-name]").on(_sortable_list.default.EVENTS.DRAG,(evt=>{var pendingPromise=new _pending.default("core_customfield/form:fieldname:on:sortablelist-drag");evt.stopPropagation(),_templates.default.render("core_customfield/nofields",{}).then((html=>{rootNode.querySelectorAll(".categorieslist > *").forEach((category=>{const fields=category.querySelectorAll(".field:not(.sortable-list-is-dragged)"),noFields=category.querySelector(".nofields");fields.length||noFields?fields.length&&noFields&&noFields.remove():category.querySelector("tbody").innerHTML=html}))})).then(pendingPromise.resolve).catch(_notification.default.exception)})),(0,_jquery.default)("[data-category-id], [data-field-name]").on(_sortable_list.default.EVENTS.DRAGSTART,((evt,info)=>{setTimeout((()=>{(0,_jquery.default)(".sortable-list-is-dragged").width(info.element.width())}),501)}))})(rootNode)}})); //# sourceMappingURL=form.min.js.map \ No newline at end of file diff --git a/public/customfield/amd/build/form.min.js.map b/public/customfield/amd/build/form.min.js.map index 6c09edb203d78..1d5b299f8c5ce 100644 --- a/public/customfield/amd/build/form.min.js.map +++ b/public/customfield/amd/build/form.min.js.map @@ -1 +1 @@ -{"version":3,"file":"form.min.js","sources":["../src/form.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 * Custom Field interaction management for Moodle.\n *\n * @module core_customfield/form\n * @copyright 2018 Toni Barbera\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport 'core/inplace_editable';\nimport {call as fetchMany} from 'core/ajax';\nimport {\n getString,\n getStrings,\n} from 'core/str';\nimport ModalForm from 'core_form/modalform';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport SortableList from 'core/sortable_list';\nimport Templates from 'core/templates';\nimport jQuery from 'jquery';\n\n/**\n * Display confirmation dialogue\n *\n * @param {Number} id\n * @param {String} type\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst confirmDelete = (id, type, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:confirmDelete');\n\n getStrings([\n {'key': 'confirm'},\n {'key': 'confirmdelete' + type, component: 'core_customfield'},\n {'key': 'yes'},\n {'key': 'no'},\n ])\n .then(strings => {\n return Notification.confirm(strings[0], strings[1], strings[2], strings[3], function() {\n const pendingDeletePromise = new Pending('core_customfield/form:confirmDelete');\n fetchMany([\n {\n methodname: (type === 'field') ? 'core_customfield_delete_field' : 'core_customfield_delete_category',\n args: {id},\n },\n {methodname: 'core_customfield_reload_template', args: {component, area, itemid}}\n ])[1]\n .then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(pendingDeletePromise.resolve)\n .catch(Notification.exception);\n });\n })\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n};\n\n\n/**\n * Creates a new custom fields category with default name and updates the list\n *\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst createNewCategory = (component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:createNewCategory');\n const promises = fetchMany([\n {methodname: 'core_customfield_create_category', args: {component, area, itemid}},\n {methodname: 'core_customfield_reload_template', args: {component, area, itemid}}\n ]);\n\n promises[1].then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n};\n\n/**\n * Create new custom field\n *\n * @param {HTMLElement} element\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst createNewField = (element, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:createNewField');\n\n const returnFocus = element.closest(\".action-menu\").querySelector(\".dropdown-toggle\");\n const form = new ModalForm({\n formClass: \"core_customfield\\\\field_config_form\",\n args: {\n categoryid: element.getAttribute('data-categoryid'),\n type: element.getAttribute('data-type'),\n },\n modalConfig: {\n title: getString('addingnewcustomfield', 'core_customfield', element.getAttribute('data-typename')),\n },\n returnFocus,\n });\n\n form.addEventListener(form.events.FORM_SUBMITTED, () => {\n const pendingCreatedPromise = new Pending('core_customfield/form:createdNewField');\n const promises = fetchMany([\n {methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}\n ]);\n\n promises[0].then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(() => pendingCreatedPromise.resolve())\n .catch(() => window.location.reload());\n });\n\n form.show();\n\n pendingPromise.resolve();\n};\n\n/**\n * Edit custom field\n *\n * @param {HTMLElement} element\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst editField = (element, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:editField');\n\n const form = new ModalForm({\n formClass: \"core_customfield\\\\field_config_form\",\n args: {\n id: element.getAttribute('data-id'),\n },\n modalConfig: {\n title: getString('editingfield', 'core_customfield', element.getAttribute('data-name')),\n },\n returnFocus: element,\n });\n\n form.addEventListener(form.events.FORM_SUBMITTED, () => {\n const pendingCreatedPromise = new Pending('core_customfield/form:createdNewField');\n const promises = fetchMany([\n {methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}\n ]);\n\n promises[0].then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(() => pendingCreatedPromise.resolve())\n .catch(() => window.location.reload());\n });\n\n form.show();\n\n pendingPromise.resolve();\n};\n\n/**\n * Fetch the category name from an inplace editable, given a child node of that field.\n *\n * @param {NodeElement} nodeElement\n * @returns {String}\n */\nconst getCategoryNameFor = nodeElement => nodeElement\n .closest('[data-category-id]')\n .find('[data-inplaceeditable][data-itemtype=category][data-component=core_customfield]')\n .attr('data-value');\n\nconst setupSortableLists = rootNode => {\n // Sort category.\n const sortCat = new SortableList(\n '#customfield_catlist .categorieslist',\n {\n moveHandlerSelector: '.movecategory [data-drag-type=move]',\n }\n );\n sortCat.getElementName = nodeElement => Promise.resolve(getCategoryNameFor(nodeElement));\n\n // Note: The sortable list currently uses jQuery events.\n jQuery('[data-category-id]').on(SortableList.EVENTS.DROP, (evt, info) => {\n if (info.positionChanged) {\n const pendingPromise = new Pending('core_customfield/form:categoryid:on:sortablelist-drop');\n fetchMany([{\n methodname: 'core_customfield_move_category',\n args: {\n id: info.element.data('category-id'),\n beforeid: info.targetNextElement.data('category-id')\n }\n\n }])[0]\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n }\n evt.stopPropagation(); // Important for nested lists to prevent multiple targets.\n });\n\n // Sort fields.\n var sort = new SortableList(\n '#customfield_catlist .fieldslist tbody',\n {\n moveHandlerSelector: '.movefield [data-drag-type=move]',\n }\n );\n\n sort.getDestinationName = (parentElement, afterElement) => {\n if (!afterElement.length) {\n return getString('totopofcategory', 'customfield', getCategoryNameFor(parentElement));\n } else if (afterElement.attr('data-field-name')) {\n return getString('afterfield', 'customfield', afterElement.attr('data-field-name'));\n } else {\n return Promise.resolve('');\n }\n };\n\n jQuery('[data-field-name]').on(SortableList.EVENTS.DROP, (evt, info) => {\n if (info.positionChanged) {\n const pendingPromise = new Pending('core_customfield/form:fieldname:on:sortablelist-drop');\n fetchMany([{\n methodname: 'core_customfield_move_field',\n args: {\n id: info.element.data('field-id'),\n beforeid: info.targetNextElement.data('field-id'),\n categoryid: Number(info.targetList.closest('[data-category-id]').attr('data-category-id'))\n },\n }])[0]\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n }\n evt.stopPropagation(); // Important for nested lists to prevent multiple targets.\n });\n\n jQuery('[data-field-name]').on(SortableList.EVENTS.DRAG, evt => {\n var pendingPromise = new Pending('core_customfield/form:fieldname:on:sortablelist-drag');\n\n evt.stopPropagation(); // Important for nested lists to prevent multiple targets.\n\n // Refreshing fields tables.\n Templates.render('core_customfield/nofields', {})\n .then(html => {\n rootNode.querySelectorAll('.categorieslist > *')\n .forEach(category => {\n const fields = category.querySelectorAll('.field:not(.sortable-list-is-dragged)');\n const noFields = category.querySelector('.nofields');\n\n if (!fields.length && !noFields) {\n category.querySelector('tbody').innerHTML = html;\n } else if (fields.length && noFields) {\n noFields.remove();\n }\n });\n return;\n })\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n });\n\n jQuery('[data-category-id], [data-field-name]').on(SortableList.EVENTS.DRAGSTART, (evt, info) => {\n setTimeout(() => {\n jQuery('.sortable-list-is-dragged').width(info.element.width());\n }, 501);\n });\n};\n\n/**\n * Initialise the custom fields manager.\n */\nexport const init = () => {\n const rootNode = document.querySelector('#customfield_catlist');\n\n const component = rootNode.dataset.component;\n const area = rootNode.dataset.area;\n const itemid = rootNode.dataset.itemid;\n\n rootNode.addEventListener('click', e => {\n const roleHolder = e.target.closest('[data-role]');\n if (!roleHolder) {\n return;\n }\n\n if (roleHolder.dataset.role === 'deletefield') {\n e.preventDefault();\n\n confirmDelete(roleHolder.dataset.id, 'field', component, area, itemid);\n return;\n }\n\n if (roleHolder.dataset.role === 'deletecategory') {\n e.preventDefault();\n\n confirmDelete(roleHolder.dataset.id, 'category', component, area, itemid);\n return;\n }\n\n if (roleHolder.dataset.role === 'addnewcategory') {\n e.preventDefault();\n createNewCategory(component, area, itemid);\n\n return;\n }\n\n if (roleHolder.dataset.role === 'addfield') {\n e.preventDefault();\n createNewField(roleHolder, component, area, itemid);\n\n return;\n }\n\n if (roleHolder.dataset.role === 'editfield') {\n e.preventDefault();\n editField(roleHolder, component, area, itemid);\n\n return;\n }\n });\n\n setupSortableLists(rootNode, component, area, itemid);\n};\n"],"names":["confirmDelete","id","type","component","area","itemid","pendingPromise","Pending","then","strings","Notification","confirm","pendingDeletePromise","methodname","args","response","Templates","render","html","js","replaceNode","resolve","catch","exception","getCategoryNameFor","nodeElement","closest","find","attr","rootNode","document","querySelector","dataset","addEventListener","e","roleHolder","target","role","preventDefault","createNewCategory","element","returnFocus","form","ModalForm","formClass","categoryid","getAttribute","modalConfig","title","events","FORM_SUBMITTED","pendingCreatedPromise","window","location","reload","show","createNewField","editField","SortableList","moveHandlerSelector","getElementName","Promise","on","EVENTS","DROP","evt","info","positionChanged","data","beforeid","targetNextElement","stopPropagation","getDestinationName","parentElement","afterElement","length","Number","targetList","DRAG","querySelectorAll","forEach","category","fields","noFields","remove","innerHTML","DRAGSTART","setTimeout","width","setupSortableLists"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"form.min.js","sources":["../src/form.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 * Custom Field interaction management for Moodle.\n *\n * @module core_customfield/form\n * @copyright 2018 Toni Barbera\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport 'core/inplace_editable';\nimport {call as fetchMany} from 'core/ajax';\nimport {\n getString,\n getStrings,\n} from 'core/str';\nimport ModalForm from 'core_form/modalform';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport SortableList from 'core/sortable_list';\nimport Templates from 'core/templates';\nimport jQuery from 'jquery';\n\n/**\n * Display confirmation dialogue\n *\n * @param {Number} id\n * @param {String} type\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst confirmDelete = (id, type, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:confirmDelete');\n\n getStrings([\n {'key': 'confirm'},\n {'key': 'confirmdelete' + type, component: 'core_customfield'},\n {'key': 'yes'},\n {'key': 'no'},\n ])\n .then(strings => {\n return Notification.confirm(strings[0], strings[1], strings[2], strings[3], function() {\n const pendingDeletePromise = new Pending('core_customfield/form:confirmDelete');\n fetchMany([\n {\n methodname: (type === 'field') ? 'core_customfield_delete_field' : 'core_customfield_delete_category',\n args: {id},\n },\n {methodname: 'core_customfield_reload_template', args: {component, area, itemid}}\n ])[1]\n .then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(pendingDeletePromise.resolve)\n .catch(Notification.exception);\n });\n })\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n};\n\n\n/**\n * Creates a new custom fields category with default name and updates the list\n *\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst createNewCategory = (component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:createNewCategory');\n const promises = fetchMany([\n {methodname: 'core_customfield_create_category', args: {component, area, itemid}},\n {methodname: 'core_customfield_reload_template', args: {component, area, itemid}}\n ]);\n\n promises[1].then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(() => pendingPromise.resolve())\n .catch(Notification.exception);\n};\n\n/**\n * Create new custom field\n *\n * @param {HTMLElement} element\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst createNewField = (element, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:createNewField');\n\n const returnFocus = element.closest(\".action-menu\").querySelector(\".dropdown-toggle\");\n const form = new ModalForm({\n formClass: \"core_customfield\\\\field_config_form\",\n args: {\n categoryid: element.getAttribute('data-categoryid'),\n type: element.getAttribute('data-type'),\n },\n modalConfig: {\n title: getString('addingnewcustomfield', 'core_customfield', element.getAttribute('data-typename')),\n },\n returnFocus,\n });\n\n form.addEventListener(form.events.FORM_SUBMITTED, () => {\n const pendingCreatedPromise = new Pending('core_customfield/form:createdNewField');\n const promises = fetchMany([\n {methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}\n ]);\n\n promises[0].then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(() => pendingCreatedPromise.resolve())\n .catch(() => window.location.reload());\n });\n\n form.show();\n\n pendingPromise.resolve();\n};\n\n/**\n * Edit custom field\n *\n * @param {HTMLElement} element\n * @param {String} component\n * @param {String} area\n * @param {Number} itemid\n */\nconst editField = (element, component, area, itemid) => {\n const pendingPromise = new Pending('core_customfield/form:editField');\n\n const form = new ModalForm({\n formClass: \"core_customfield\\\\field_config_form\",\n args: {\n id: element.getAttribute('data-id'),\n },\n modalConfig: {\n title: getString('editingfield', 'core_customfield', element.getAttribute('data-name')),\n },\n returnFocus: element,\n });\n\n form.addEventListener(form.events.FORM_SUBMITTED, () => {\n const pendingCreatedPromise = new Pending('core_customfield/form:createdNewField');\n const promises = fetchMany([\n {methodname: 'core_customfield_reload_template', args: {component: component, area: area, itemid: itemid}}\n ]);\n\n promises[0].then(response => Templates.render('core_customfield/list', response))\n .then((html, js) => Templates.replaceNode(jQuery('[data-region=\"list-page\"]'), html, js))\n .then(() => pendingCreatedPromise.resolve())\n .catch(() => window.location.reload());\n });\n\n form.show();\n\n pendingPromise.resolve();\n};\n\n/**\n * Fetch the category name from an inplace editable, given a child node of that field.\n *\n * @param {NodeElement} nodeElement\n * @returns {String}\n */\nconst getCategoryNameFor = nodeElement => nodeElement\n .closest('[data-category-id]')\n .attr('data-category-name');\n\nconst setupSortableLists = rootNode => {\n // Sort category.\n const sortCat = new SortableList(\n '#customfield_catlist .categorieslist',\n {\n moveHandlerSelector: '.movecategory [data-drag-type=move]',\n }\n );\n sortCat.getElementName = nodeElement => Promise.resolve(getCategoryNameFor(nodeElement));\n\n // Note: The sortable list currently uses jQuery events.\n jQuery('[data-category-id]').on(SortableList.EVENTS.DROP, (evt, info) => {\n if (info.positionChanged) {\n const pendingPromise = new Pending('core_customfield/form:categoryid:on:sortablelist-drop');\n fetchMany([{\n methodname: 'core_customfield_move_category',\n args: {\n id: info.element.data('category-id'),\n beforeid: info.targetNextElement.data('category-id')\n }\n\n }])[0]\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n }\n evt.stopPropagation(); // Important for nested lists to prevent multiple targets.\n });\n\n // Sort fields.\n var sort = new SortableList(\n '#customfield_catlist .fieldslist tbody',\n {\n moveHandlerSelector: '.movefield [data-drag-type=move]',\n }\n );\n\n sort.getDestinationName = (parentElement, afterElement) => {\n if (!afterElement.length) {\n return getString('totopofcategory', 'customfield', getCategoryNameFor(parentElement));\n } else if (afterElement.attr('data-field-name')) {\n return getString('afterfield', 'customfield', afterElement.attr('data-field-name'));\n } else {\n return Promise.resolve('');\n }\n };\n\n jQuery('[data-field-name]').on(SortableList.EVENTS.DROP, (evt, info) => {\n if (info.positionChanged) {\n const pendingPromise = new Pending('core_customfield/form:fieldname:on:sortablelist-drop');\n fetchMany([{\n methodname: 'core_customfield_move_field',\n args: {\n id: info.element.data('field-id'),\n beforeid: info.targetNextElement.data('field-id'),\n categoryid: Number(info.targetList.closest('[data-category-id]').attr('data-category-id'))\n },\n }])[0]\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n }\n evt.stopPropagation(); // Important for nested lists to prevent multiple targets.\n });\n\n jQuery('[data-field-name]').on(SortableList.EVENTS.DRAG, evt => {\n var pendingPromise = new Pending('core_customfield/form:fieldname:on:sortablelist-drag');\n\n evt.stopPropagation(); // Important for nested lists to prevent multiple targets.\n\n // Refreshing fields tables.\n Templates.render('core_customfield/nofields', {})\n .then(html => {\n rootNode.querySelectorAll('.categorieslist > *')\n .forEach(category => {\n const fields = category.querySelectorAll('.field:not(.sortable-list-is-dragged)');\n const noFields = category.querySelector('.nofields');\n\n if (!fields.length && !noFields) {\n category.querySelector('tbody').innerHTML = html;\n } else if (fields.length && noFields) {\n noFields.remove();\n }\n });\n return;\n })\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n });\n\n jQuery('[data-category-id], [data-field-name]').on(SortableList.EVENTS.DRAGSTART, (evt, info) => {\n setTimeout(() => {\n jQuery('.sortable-list-is-dragged').width(info.element.width());\n }, 501);\n });\n};\n\n/**\n * Initialise the custom fields manager.\n */\nexport const init = () => {\n const rootNode = document.querySelector('#customfield_catlist');\n\n const component = rootNode.dataset.component;\n const area = rootNode.dataset.area;\n const itemid = rootNode.dataset.itemid;\n\n rootNode.addEventListener('click', e => {\n const roleHolder = e.target.closest('[data-role]');\n if (!roleHolder) {\n return;\n }\n\n if (roleHolder.dataset.role === 'deletefield') {\n e.preventDefault();\n\n confirmDelete(roleHolder.dataset.id, 'field', component, area, itemid);\n return;\n }\n\n if (roleHolder.dataset.role === 'deletecategory') {\n e.preventDefault();\n\n confirmDelete(roleHolder.dataset.id, 'category', component, area, itemid);\n return;\n }\n\n if (roleHolder.dataset.role === 'addnewcategory') {\n e.preventDefault();\n createNewCategory(component, area, itemid);\n\n return;\n }\n\n if (roleHolder.dataset.role === 'addfield') {\n e.preventDefault();\n createNewField(roleHolder, component, area, itemid);\n\n return;\n }\n\n if (roleHolder.dataset.role === 'editfield') {\n e.preventDefault();\n editField(roleHolder, component, area, itemid);\n\n return;\n }\n });\n\n setupSortableLists(rootNode, component, area, itemid);\n};\n"],"names":["confirmDelete","id","type","component","area","itemid","pendingPromise","Pending","then","strings","Notification","confirm","pendingDeletePromise","methodname","args","response","Templates","render","html","js","replaceNode","resolve","catch","exception","getCategoryNameFor","nodeElement","closest","attr","rootNode","document","querySelector","dataset","addEventListener","e","roleHolder","target","role","preventDefault","createNewCategory","element","returnFocus","form","ModalForm","formClass","categoryid","getAttribute","modalConfig","title","events","FORM_SUBMITTED","pendingCreatedPromise","window","location","reload","show","createNewField","editField","SortableList","moveHandlerSelector","getElementName","Promise","on","EVENTS","DROP","evt","info","positionChanged","data","beforeid","targetNextElement","stopPropagation","getDestinationName","parentElement","afterElement","length","Number","targetList","DRAG","querySelectorAll","forEach","category","fields","noFields","remove","innerHTML","DRAGSTART","setTimeout","width","setupSortableLists"],"mappings":"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"} \ No newline at end of file diff --git a/public/customfield/amd/src/form.js b/public/customfield/amd/src/form.js index f0bcb2798865c..dbd461a4ca6f5 100644 --- a/public/customfield/amd/src/form.js +++ b/public/customfield/amd/src/form.js @@ -181,8 +181,7 @@ const editField = (element, component, area, itemid) => { */ const getCategoryNameFor = nodeElement => nodeElement .closest('[data-category-id]') - .find('[data-inplaceeditable][data-itemtype=category][data-component=core_customfield]') - .attr('data-value'); + .attr('data-category-name'); const setupSortableLists = rootNode => { // Sort category. diff --git a/public/customfield/classes/output/management.php b/public/customfield/classes/output/management.php index b5e86d3df1a42..f37e1e6de09c3 100644 --- a/public/customfield/classes/output/management.php +++ b/public/customfield/classes/output/management.php @@ -93,6 +93,7 @@ public function export_for_template(\renderer_base $output) { $categoryarray = array(); $categoryarray['id'] = $category->get('id'); + $categoryarray['name'] = $category->get_formatted_name(); $categoryarray['nameeditable'] = $canedit ? $output->render(api::get_category_inplace_editable($category, true)) : $category->get_formatted_name(); $categoryarray['movetitle'] = get_string('movecategory', 'core_customfield', diff --git a/public/customfield/externallib.php b/public/customfield/externallib.php index 57d2ab2201408..2f5d8da28fa47 100644 --- a/public/customfield/externallib.php +++ b/public/customfield/externallib.php @@ -118,6 +118,7 @@ public static function reload_template_returns() { new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'id'), + 'name' => new external_value(PARAM_TEXT, 'name'), 'nameeditable' => new external_value(PARAM_RAW, 'inplace editable name'), 'addfieldmenu' => new external_value(PARAM_RAW, 'addfieldmenu'), 'canedit' => new external_value(PARAM_BOOL, 'can edit'), diff --git a/public/customfield/templates/list.mustache b/public/customfield/templates/list.mustache index e0689043cff41..6bb11cd70ed39 100644 --- a/public/customfield/templates/list.mustache +++ b/public/customfield/templates/list.mustache @@ -42,6 +42,7 @@ "canmovefields": 1, "categories": [ { "id": "0", + "name": "Other fields", "nameeditable": "Other fields", "addfieldmenu": "Add field", "canedit": true, @@ -51,6 +52,7 @@ ] }, { "id": "00", + "name": "Empty category", "nameeditable": "Empty category", "addfieldmenu": "Add field", "canedit": true, @@ -77,7 +79,7 @@ {{#categories}} {{#canedit}}
{{/canedit}} -
+
{{#usescategories}} From 25614d858baac4e6740018bb86e9d4af03bbfeea Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Thu, 9 Oct 2025 21:58:52 +0700 Subject: [PATCH 019/553] weekly release 5.1+ --- public/version.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/version.php b/public/version.php index 67e1ed8b5c2b5..688690f4964c6 100644 --- a/public/version.php +++ b/public/version.php @@ -29,9 +29,9 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2025100600.00; // 20251006 = branching date YYYYMMDD - do not modify! +$version = 2025100600.01; // 20251006 = branching date YYYYMMDD - do not modify! // RR = release increments - 00 in DEV branches. // .XX = incremental changes. -$release = '5.1 (Build: 20251006)'; // Human-friendly version name +$release = '5.1+ (Build: 20251009)'; // Human-friendly version name $branch = '501'; // This version's branch. $maturity = MATURITY_STABLE; // This version's maturity level. From 1ae4254fca147dfae080a450f3af8b0945ebb332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luca=20B=C3=B6sch?= Date: Sat, 27 Sep 2025 11:42:39 +0200 Subject: [PATCH 020/553] MDL-86767 forum: let forum use standard buttons. --- public/mod/forum/report/summary/templates/filter_dates.mustache | 2 +- .../mod/forum/report/summary/templates/filter_groups.mustache | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/mod/forum/report/summary/templates/filter_dates.mustache b/public/mod/forum/report/summary/templates/filter_dates.mustache index d39c89774bce1..11918603cd5e3 100644 --- a/public/mod/forum/report/summary/templates/filter_dates.mustache +++ b/public/mod/forum/report/summary/templates/filter_dates.mustache @@ -38,7 +38,7 @@ }} - diff --git a/public/mod/forum/report/summary/templates/filter_groups.mustache b/public/mod/forum/report/summary/templates/filter_groups.mustache index cc20d4a20fa20..ee9ae3932f01a 100644 --- a/public/mod/forum/report/summary/templates/filter_groups.mustache +++ b/public/mod/forum/report/summary/templates/filter_groups.mustache @@ -39,7 +39,7 @@ }} {{#hasgroups}} - From d8758c83676bdd3af6c2634ec411485556fa578f Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 19 Jun 2025 11:08:30 +0100 Subject: [PATCH 021/553] MDL-85820 formslib: don't add '-' to client-side valiation --- public/lib/pear/HTML/QuickForm/RuleRegistry.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/lib/pear/HTML/QuickForm/RuleRegistry.php b/public/lib/pear/HTML/QuickForm/RuleRegistry.php index baa506d8b1ff4..b3e5b1ae26bab 100644 --- a/public/lib/pear/HTML/QuickForm/RuleRegistry.php +++ b/public/lib/pear/HTML/QuickForm/RuleRegistry.php @@ -179,7 +179,7 @@ function getValidationScript(&$element, $elementName, $ruleData) $js = $jsValue . "\n" . $jsPrefix . " if (" . str_replace('{jsVar}', 'value', $jsCheck) . " && !errFlag['{$jsField}']) {\n" . " errFlag['{$jsField}'] = true;\n" . - " _qfMsg = _qfMsg + '\\n - {$ruleData['message']}';\n" . + " _qfMsg = _qfMsg + '{$ruleData['message']}';\n" . $jsReset . " }\n"; } else { @@ -192,7 +192,7 @@ function getValidationScript(&$element, $elementName, $ruleData) " }\n" . " if (res < {$ruleData['howmany']} && !errFlag['{$jsField}']) {\n" . " errFlag['{$jsField}'] = true;\n" . - " _qfMsg = _qfMsg + '\\n - {$ruleData['message']}';\n" . + " _qfMsg = _qfMsg + '{$ruleData['message']}';\n" . $jsReset . " }\n"; } From fa0ef43e8e95ba6bab13407747687234ca8f95e7 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Fri, 10 Oct 2025 11:01:13 +0800 Subject: [PATCH 022/553] MDL-85820 lib: Update pear/readme_moodle.txt --- public/lib/pear/readme_moodle.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/lib/pear/readme_moodle.txt b/public/lib/pear/readme_moodle.txt index 6c6b4de003d8a..f4f5807e45338 100644 --- a/public/lib/pear/readme_moodle.txt +++ b/public/lib/pear/readme_moodle.txt @@ -42,6 +42,8 @@ MDL-78527 - Adding a sixth parameter to allow groups to use attributes. MDL-80818 - Freezing all elements with the same name (e.g. radio buttons) MDL-80820 - PHPdocs corrections MDL-73700 - remove old PHP version check and dead code +MDL-85820 - Remove unnecessary `-` character beside each client validation error message for consistency with the server side + validation error messages. Pear ==== From 0bd7d0767b4d9527f05ba5dc7b5a9f02c5466271 Mon Sep 17 00:00:00 2001 From: ferran Date: Fri, 10 Oct 2025 14:05:56 +0200 Subject: [PATCH 023/553] MDL-86879 core_course: optimise subsections loading --- public/mod/subsection/lib.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/public/mod/subsection/lib.php b/public/mod/subsection/lib.php index 3aab2be224f29..be8e8fbd66b09 100644 --- a/public/mod/subsection/lib.php +++ b/public/mod/subsection/lib.php @@ -256,10 +256,13 @@ function subsection_cm_info_view(cm_info $cm) { global $DB, $PAGE; $cm->set_custom_cmlist_item(true); - $course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST); + $course = $cm->get_course(); - // Get the section info. - $delegatedsection = manager::create_from_coursemodule($cm)->get_delegated_section_info(); + $delegatedsection = $cm->get_delegated_section_info(); + if (!$delegatedsection) { + // Some restorations can produce a situation where the section is not found. + $delegatedsection = manager::create_from_coursemodule($cm)->get_delegated_section_info(); + } // Render the delegated section. $format = course_get_format($course); From 50bdce3494154ad4e39694659bfeeb70daf607d3 Mon Sep 17 00:00:00 2001 From: Julien Boulen Date: Sun, 12 Oct 2025 21:35:03 +0200 Subject: [PATCH 024/553] MDL-83543 core: Move download complete status handling to Excel library When a form is validated, all validation buttons are automatically locked to prevent them from being sent twice by mistake. When the form returns a file instead of redirecting to a new HTML page, a mechanism (\core_form\util::form_download_complete()) must be called to reactivate the form's validation buttons. This mechanism is called for CSV files (file lib/csvlib.class.php), but not when downloading xls files. The mechanism is only called when downloading grades in Excel format (file grade/export/xls/grade_export_xls.php). The patch proposes to call this mechanism globally for all Excel file downloads (file lib/excellib.class.php), and not just for grade downloads (file grade/export/xls/grade_export_xls.php). --- public/grade/export/xls/grade_export_xls.php | 3 --- public/lib/excellib.class.php | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/grade/export/xls/grade_export_xls.php b/public/grade/export/xls/grade_export_xls.php index ec4bbcd2d15ba..f81b0e25846ec 100644 --- a/public/grade/export/xls/grade_export_xls.php +++ b/public/grade/export/xls/grade_export_xls.php @@ -45,9 +45,6 @@ public function print_grades() { $strgrades = get_string('grades'); - // If this file was requested from a form, then mark download as complete (before sending headers). - \core_form\util::form_download_complete(); - // Calculate file name $shortname = format_string($this->course->shortname, true, array('context' => context_course::instance($this->course->id))); $downloadfilename = clean_filename("$shortname $strgrades.xls"); diff --git a/public/lib/excellib.class.php b/public/lib/excellib.class.php index ff300752c77a9..812535df4ed6c 100644 --- a/public/lib/excellib.class.php +++ b/public/lib/excellib.class.php @@ -110,6 +110,9 @@ public function add_format($properties = array()) { public function close() { global $CFG; + // If this file was requested from a form, then mark download as complete. + \core_form\util::form_download_complete(); + foreach ($this->objspreadsheet->getAllSheets() as $sheet) { $sheet->setSelectedCells('A1'); } From e01123cbd54971257e028db9158f4859eb3fe5b0 Mon Sep 17 00:00:00 2001 From: Julien Boulen Date: Sun, 12 Oct 2025 21:49:15 +0200 Subject: [PATCH 025/553] MDL-86322 phpdoc: Fix the given example in the phpdoc --- .../classes/output/requirements/page_requirements_manager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/lib/classes/output/requirements/page_requirements_manager.php b/public/lib/classes/output/requirements/page_requirements_manager.php index 9771504f43c04..71052f7ef6b87 100644 --- a/public/lib/classes/output/requirements/page_requirements_manager.php +++ b/public/lib/classes/output/requirements/page_requirements_manager.php @@ -990,7 +990,7 @@ protected function js_module_loaded($module) { * * @param string $stylesheet The path to the .css file, relative to $CFG->wwwroot. * For example: - * $PAGE->requires->css('mod/data/css.php?d='.$data->id); + * $PAGE->requires->css('/mod/data/css.php?d='.$data->id); */ public function css($stylesheet) { global $CFG; From 8ad01a9abee755bafe51d13c257d50da17c7370b Mon Sep 17 00:00:00 2001 From: Julien Boulen Date: Sun, 12 Oct 2025 21:55:21 +0200 Subject: [PATCH 026/553] MDL-86320 core: Fix unit test qrcode_test --- public/lib/tests/qrcode_test.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/lib/tests/qrcode_test.php b/public/lib/tests/qrcode_test.php index faf14e446149a..8450026a3623c 100644 --- a/public/lib/tests/qrcode_test.php +++ b/public/lib/tests/qrcode_test.php @@ -26,8 +26,8 @@ * @author * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +#[\PHPUnit\Framework\Attributes\CoversClass(core_qrcode::class)] final class qrcode_test extends \basic_testcase { - /** * Basic test to generate a QR code and check that the library is not broken. */ @@ -36,9 +36,9 @@ public function test_generate_basic_qr(): void { // binary file can be different. This is why tests are limited. $text = 'abc'; - $color = 'black'; - $qrcode = new core_qrcode($text, $color); - $svgdata = $qrcode->getBarcodeSVGcode(1, 1); + $color = 'green'; + $qrcode = new core_qrcode($text); + $svgdata = $qrcode->getBarcodeSVGcode(1, 1, $color); // Just check the SVG was generated. $this->assertStringContainsString('' . $text . '', $svgdata); From 60db17bea97b45b8fe4a430e721f091ebf660355 Mon Sep 17 00:00:00 2001 From: Julien Boulen Date: Sun, 12 Oct 2025 22:16:58 +0200 Subject: [PATCH 027/553] MDL-77137 filelib: Honour proxybypass option for multiple Curl queries --- public/lib/filelib.php | 4 +++ public/lib/tests/filelib_test.php | 41 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/public/lib/filelib.php b/public/lib/filelib.php index e46df988fce27..840ed79b6aa5b 100644 --- a/public/lib/filelib.php +++ b/public/lib/filelib.php @@ -3639,6 +3639,10 @@ protected function multi($requests, $options = array()) { $options[$n] = $v; } $handles[$i] = curl_init($requests[$i]['url']); + + // Set the URL as a curl option. + $this->setopt(['CURLOPT_URL' => $requests[$i]['url']]); + $this->apply_opt($handles[$i], $options); curl_multi_add_handle($main, $handles[$i]); } diff --git a/public/lib/tests/filelib_test.php b/public/lib/tests/filelib_test.php index f0414ad74733c..ae7a87edb09ae 100644 --- a/public/lib/tests/filelib_test.php +++ b/public/lib/tests/filelib_test.php @@ -511,6 +511,27 @@ public function test_curl_proxybypass(): void { $this->assertNotEquals(0, $curl->get_errno()); $this->assertNotEquals('47250a973d1b88d9445f94db4ef2c97a', md5($contents)); + // Test multiple queries with proxy. + $curl = new \curl(['debug' => 1]); + ob_start(); + $requests = [[ + 'nobody' => true, + 'header' => 1, + 'url' => $testurl, + 'returntransfer' => true, + ], + [ + 'nobody' => true, + 'header' => 1, + 'url' => $testurl, + 'returntransfer' => true, + ]]; + $curl->download($requests); + $output = ob_get_contents(); + ob_end_clean(); + // We must have exactly 2 occurrences of ["CURLOPT_PROXY"]. + $this->assertMatchesRegularExpression('/(\["CURLOPT_PROXY"\].*){2}/ms', $output); + // Test with proxy bypass. $testurlhost = parse_url($testurl, PHP_URL_HOST); $CFG->proxybypass = $testurlhost; @@ -519,6 +540,26 @@ public function test_curl_proxybypass(): void { $this->assertSame(0, $curl->get_errno()); $this->assertSame('47250a973d1b88d9445f94db4ef2c97a', md5($contents)); + // Test multiple queries with proxy bypass. + $curl = new \curl(['debug' => 1]); + ob_start(); + $requests = [[ + 'nobody' => true, + 'header' => 1, + 'url' => $testurl, + 'returntransfer' => true, + ], + [ + 'nobody' => true, + 'header' => 1, + 'url' => $testurl, + 'returntransfer' => true, + ]]; + $curl->download($requests); + $output = ob_get_contents(); + ob_end_clean(); + $this->assertStringNotContainsString('["CURLOPT_PROXY"]', $output); + $CFG->proxyhost = $oldproxy; $CFG->proxybypass = $oldproxybypass; } From 49bea1dc22797c61ea7527fbdc2ba04c99d100b3 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Mon, 13 Oct 2025 14:57:03 +0200 Subject: [PATCH 028/553] MDL-86011 feedback: Adjust display of Responded column in Overview --- .../classes/courseformat/overview.php | 2 +- .../tests/courseformat/overview_test.php | 27 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/public/mod/feedback/classes/courseformat/overview.php b/public/mod/feedback/classes/courseformat/overview.php index de180b7f6c0fa..166469aa8c362 100644 --- a/public/mod/feedback/classes/courseformat/overview.php +++ b/public/mod/feedback/classes/courseformat/overview.php @@ -120,7 +120,7 @@ private function get_extra_responses_overview(): ?overviewitem { private function get_extra_submitted_overview(): ?overviewitem { global $USER; - if (!has_capability('mod/feedback:complete', $this->context)) { + if (!has_capability('mod/feedback:complete', $this->context, $USER, false)) { return null; } diff --git a/public/mod/feedback/tests/courseformat/overview_test.php b/public/mod/feedback/tests/courseformat/overview_test.php index 4be1121da201d..a74814f2c7d79 100644 --- a/public/mod/feedback/tests/courseformat/overview_test.php +++ b/public/mod/feedback/tests/courseformat/overview_test.php @@ -446,7 +446,7 @@ public static function provider_feedback_get_extra_responses_overview_with_group * @param bool $hasresponses */ #[\PHPUnit\Framework\Attributes\DataProvider('provider_test_get_extra_submitted_overview')] - public function test_get_extra_submitted_overview(string $user, bool $expectnull, bool $hasresponses): void { + public function test_get_extra_submitted_overview(string $user, bool $expectnull, bool $hasresponses = false): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); @@ -471,8 +471,11 @@ public function test_get_extra_submitted_overview(string $user, bool $expectnull ]); } - $currentuser = ($user == 'teacher') ? $teacher : $student; - $this->setUser($currentuser); + if ($user == 'admin') { + $this->setAdminUser(); + } else { + $this->setUser(($user == 'teacher') ? $teacher : $student); + } $overview = overviewfactory::create($cm); $reflection = new \ReflectionClass($overview); @@ -497,25 +500,23 @@ public function test_get_extra_submitted_overview(string $user, bool $expectnull * @return \Generator */ public static function provider_test_get_extra_submitted_overview(): \Generator { - yield 'Teacher with responses' => [ + yield 'Teacher' => [ 'user' => 'teacher', 'expectnull' => true, - 'hasresponses' => true, ]; - yield 'Student with responses' => [ - 'user' => 'student', - 'expectnull' => false, - 'hasresponses' => true, - ]; - yield 'Teacher without responses' => [ - 'user' => 'teacher', + yield 'Admin' => [ + 'user' => 'admin', 'expectnull' => true, - 'hasresponses' => false, ]; yield 'Student without responses' => [ 'user' => 'student', 'expectnull' => false, 'hasresponses' => false, ]; + yield 'Student with responses' => [ + 'user' => 'student', + 'expectnull' => false, + 'hasresponses' => true, + ]; } } From 4cc058fc23b359634a349c66fd716e860f0bb187 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Mon, 13 Oct 2025 16:38:21 +0200 Subject: [PATCH 029/553] MDL-86011 feedback: Prevent admin answering unless enrolled as student --- public/mod/feedback/classes/completion.php | 2 +- public/mod/feedback/tests/behat/questions.feature | 9 +++++++++ public/mod/feedback/tests/lib_test.php | 6 ++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/public/mod/feedback/classes/completion.php b/public/mod/feedback/classes/completion.php index 860d822594f12..ccc72ddc3d921 100644 --- a/public/mod/feedback/classes/completion.php +++ b/public/mod/feedback/classes/completion.php @@ -613,7 +613,7 @@ public function can_complete() { global $CFG, $USER; $context = context_module::instance($this->cm->id); - if (has_capability('mod/feedback:complete', $context, $this->userid)) { + if (has_capability('mod/feedback:complete', $context, $this->userid, false)) { return true; } diff --git a/public/mod/feedback/tests/behat/questions.feature b/public/mod/feedback/tests/behat/questions.feature index 86b64eabd86a8..52d932c6fd2a9 100644 --- a/public/mod/feedback/tests/behat/questions.feature +++ b/public/mod/feedback/tests/behat/questions.feature @@ -91,3 +91,12 @@ Feature: Managing feedback questions And I click on "After \"(q3) I can see it in your smile\"" "link" in the "Move this question" "dialogue" And I click on "Move this question" "button" in the "Is it me you're looking for?" "mod_feedback > Question" And I click on "To the top of the list" "link" in the "Move this question" "dialogue" + + Scenario: Admin cannot answer questions if not enrolled as student + When I am on the "Learning experience course 1" "feedback activity" page logged in as admin + Then I should not see "Answer the questions" + But the following "course enrolments" exist: + | user | course | role | + | admin | C1 | student | + And I am on the "Learning experience course 1" "feedback activity" page logged in as admin + And I should see "Answer the questions" diff --git a/public/mod/feedback/tests/lib_test.php b/public/mod/feedback/tests/lib_test.php index 26aa41a4c35b1..2674ee3cfca80 100644 --- a/public/mod/feedback/tests/lib_test.php +++ b/public/mod/feedback/tests/lib_test.php @@ -183,6 +183,8 @@ public function test_feedback_core_calendar_provide_event_action_open(): void { $now = time(); $course = $this->getDataGenerator()->create_course(); + // Enrol admin as a student so they can complete the feedback. + $this->getDataGenerator()->enrol_user(get_admin()->id, $course->id, 'student'); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id, 'timeopen' => $now - DAYSECS, 'timeclose' => $now + DAYSECS]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); @@ -294,6 +296,8 @@ public function test_feedback_core_calendar_provide_event_action_open_in_future( $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); + // Enrol admin as a student so they can complete the feedback. + $this->getDataGenerator()->enrol_user(get_admin()->id, $course->id, 'student'); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id, 'timeopen' => time() + DAYSECS]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); @@ -356,6 +360,8 @@ public function test_feedback_core_calendar_provide_event_action_no_time_specifi $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); + // Enrol admin as a student so they can complete the feedback. + $this->getDataGenerator()->enrol_user(get_admin()->id, $course->id, 'student'); $feedback = $this->getDataGenerator()->create_module('feedback', ['course' => $course->id]); $event = $this->create_action_event($course->id, $feedback->id, FEEDBACK_EVENT_TYPE_OPEN); From 03aa72e9637b49bdf96a1a19af602e42135c9ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikel=20Mart=C3=ADn?= Date: Tue, 14 Oct 2025 11:01:40 +0200 Subject: [PATCH 030/553] MDL-85486 theme_boost: Fix single section page inplace editor When course format is displayed as single section per page, the inplace editable in the section page heading was incorrectly displaye, and the instructions panel was not displayed. Adding some specific styles to fix that behaviour. --- public/theme/boost/scss/moodle/course.scss | 10 ++++++++-- public/theme/boost/style/moodle.css | 3 +++ public/theme/classic/style/moodle.css | 3 +++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/public/theme/boost/scss/moodle/course.scss b/public/theme/boost/scss/moodle/course.scss index 1081ad21d4cd8..dc970bf644838 100644 --- a/public/theme/boost/scss/moodle/course.scss +++ b/public/theme/boost/scss/moodle/course.scss @@ -667,8 +667,14 @@ $divider-hover-color: $primary !default; } } -.single-section-page .header-action { - display: inline-block; +.single-section-page { + .header-action { + display: inline-block; + } + // Revert page header styles to avoid conflict with inplace editable. + .page-context-header { + overflow: visible; + } } /* Default activity completion page */ diff --git a/public/theme/boost/style/moodle.css b/public/theme/boost/style/moodle.css index 6925634354d04..38bd52a05771b 100644 --- a/public/theme/boost/style/moodle.css +++ b/public/theme/boost/style/moodle.css @@ -30991,6 +30991,9 @@ table.calendartable caption { .single-section-page .header-action { display: inline-block; } +.single-section-page .page-context-header { + overflow: visible; +} /* Default activity completion page */ .defaultactivitycompletion-item a { diff --git a/public/theme/classic/style/moodle.css b/public/theme/classic/style/moodle.css index 1354eafd2c2b8..b7c91366cc68c 100644 --- a/public/theme/classic/style/moodle.css +++ b/public/theme/classic/style/moodle.css @@ -30991,6 +30991,9 @@ table.calendartable caption { .single-section-page .header-action { display: inline-block; } +.single-section-page .page-context-header { + overflow: visible; +} /* Default activity completion page */ .defaultactivitycompletion-item a { From e817d28f23c7db444071f58c06614d975cf4b93b Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Fri, 12 Sep 2025 13:37:06 +0100 Subject: [PATCH 031/553] MDL-86627 cohort: format name correctly in report entity column. --- .../reportbuilder/local/entities/cohort.php | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/public/cohort/classes/reportbuilder/local/entities/cohort.php b/public/cohort/classes/reportbuilder/local/entities/cohort.php index 37ea472262fe9..e8d4493215b53 100644 --- a/public/cohort/classes/reportbuilder/local/entities/cohort.php +++ b/public/cohort/classes/reportbuilder/local/entities/cohort.php @@ -18,20 +18,14 @@ namespace core_cohort\reportbuilder\local\entities; -use lang_string; use stdClass; use theme_config; use core\{context, context_helper}; +use core\lang_string; use core_reportbuilder\local\entities\base; -use core_reportbuilder\local\filters\boolean_select; -use core_reportbuilder\local\filters\cohort as cohort_filter; -use core_reportbuilder\local\filters\date; -use core_reportbuilder\local\filters\select; -use core_reportbuilder\local\filters\text; -use core_reportbuilder\local\helpers\custom_fields; -use core_reportbuilder\local\helpers\format; -use core_reportbuilder\local\report\column; -use core_reportbuilder\local\report\filter; +use core_reportbuilder\local\filters\{boolean_select, cohort as cohort_filter, date, select, text}; +use core_reportbuilder\local\helpers\{custom_fields, format}; +use core_reportbuilder\local\report\{column, filter}; /** * Cohort entity @@ -132,8 +126,20 @@ protected function get_all_columns(): array { $this->get_entity_name() )) ->add_joins($this->get_joins()) - ->add_fields("{$tablealias}.name") - ->set_is_sortable(true); + ->add_join($this->get_context_join()) + ->add_field("{$tablealias}.name") + ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) + ->set_is_sortable(true) + ->set_callback(static function (?string $name, stdClass $cohort): string { + if ($name === null || $cohort->ctxid === null) { + return ''; + } + + context_helper::preload_from_record(clone $cohort); + $context = context::instance_by_id($cohort->ctxid); + + return format_string($name, options: ['context' => $context]); + }); // ID number column. $columns[] = (new column( From c81b75e0e0048c8f34fd30a590bb26a0d37a72b6 Mon Sep 17 00:00:00 2001 From: AMOS bot Date: Wed, 15 Oct 2025 00:09:39 +0000 Subject: [PATCH 032/553] Automatically generated installer lang files --- public/install/lang/fo/admin.php | 1 + 1 file changed, 1 insertion(+) diff --git a/public/install/lang/fo/admin.php b/public/install/lang/fo/admin.php index a619c73b71275..01b938c7f34db 100644 --- a/public/install/lang/fo/admin.php +++ b/public/install/lang/fo/admin.php @@ -31,3 +31,4 @@ $string['clianswerno'] = ''; $string['cliansweryes'] = ''; +$string['cliincorrectvalueretry'] = 'Skeivt virði, vinarliga royn aftur'; From 2253a568d0a3ca58a711b3b3a5567eb477523cc8 Mon Sep 17 00:00:00 2001 From: hieuvu Date: Fri, 10 Oct 2025 12:03:09 +0700 Subject: [PATCH 033/553] MDL-86621 core_question: remove incorrect navigation nodes. --- .../behat/adminnistration_navigation.feature | 36 +++++++++++++++++++ .../navigation/settings_navigation.php | 4 +-- public/lib/questionlib.php | 17 +++++++-- 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 public/blocks/navigation/tests/behat/adminnistration_navigation.feature diff --git a/public/blocks/navigation/tests/behat/adminnistration_navigation.feature b/public/blocks/navigation/tests/behat/adminnistration_navigation.feature new file mode 100644 index 0000000000000..b43346b029eca --- /dev/null +++ b/public/blocks/navigation/tests/behat/adminnistration_navigation.feature @@ -0,0 +1,36 @@ +@block @block_navigation +Feature: Test that admin can see related nodes in Administration block + In order to manage + As an admin + I need to be able to see related nodes in Administration block + + Background: + Given the following "categories" exist: + | name | category | idnumber | visible | + | cat1 | 0 | cat1 | 1 | + And the following "courses" exist: + | fullname | shortname | category | visible | + | Course 1 | c1 | cat1 | 1 | + And the following config values are set as admin: + | unaddableblocks | | theme_boost | + And I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I add the "Administration" block if not present + And I configure the "Administration" block + And I set the following fields to these values: + | Page contexts | Display throughout the entire site | + And I press "Save changes" + + @javascript + Scenario: As admin I must not see question related nodes in Administration. + Given the following "activities" exist: + | activity | name | intro | course | idnumber | + | quiz | Quiz 1 | Quiz 1 for testing the Add menu | c1 | quiz1 | + And I am on "Course 1" course homepage + Then I should see "Question bank" + And I should not see "Questions" + And I am on the "Quiz 1" "mod_quiz > view" page + And "Question bank" "link" should exist + And "Questions" "link" should exist + And "Categories" "link" should exist diff --git a/public/lib/classes/navigation/settings_navigation.php b/public/lib/classes/navigation/settings_navigation.php index a943b34d120d5..d01238e51caf3 100644 --- a/public/lib/classes/navigation/settings_navigation.php +++ b/public/lib/classes/navigation/settings_navigation.php @@ -596,7 +596,7 @@ protected function load_course_settings($forceopen = false) { // Questions. require_once($CFG->libdir . '/questionlib.php'); $baseurl = \core_question\local\bank\question_bank_helper::get_url_for_qbank_list($course->id); - question_extend_settings_navigation($coursenode, $coursecontext, $baseurl)->trim_if_empty(); + question_extend_settings_navigation($coursenode, $coursecontext, $baseurl); if ($adminoptions->update) { // Repository Instances. @@ -1834,7 +1834,7 @@ protected function load_front_page_settings($forceopen = false) { // Questions. require_once($CFG->libdir . '/questionlib.php'); $baseurl = \core_question\local\bank\question_bank_helper::get_url_for_qbank_list($course->id); - question_extend_settings_navigation($frontpage, $coursecontext, $baseurl)->trim_if_empty(); + question_extend_settings_navigation($frontpage, $coursecontext, $baseurl); // Manage files. if ($adminoptions->files) { diff --git a/public/lib/questionlib.php b/public/lib/questionlib.php index 4868e961aa20e..7f93acfa8a08f 100644 --- a/public/lib/questionlib.php +++ b/public/lib/questionlib.php @@ -1417,7 +1417,13 @@ function question_extend_settings_navigation(navigation_node $navigationnode, $c $iscourse = $context->contextlevel === CONTEXT_COURSE; if ($iscourse) { - $params = ['courseid' => $context->instanceid]; + return $navigationnode->add( + get_string('questionbank_plural', 'question'), + new moodle_url($baseurl, ['courseid' => $context->instanceid]), + navigation_node::TYPE_CONTAINER, + null, + 'questionbank' + ); } else if ($context->contextlevel == CONTEXT_MODULE) { $params = ['cmid' => $context->instanceid]; } else { @@ -1428,8 +1434,13 @@ function question_extend_settings_navigation(navigation_node $navigationnode, $c $params['cat'] = $cat; } - $questionnode = $navigationnode->add(get_string($iscourse ? 'questionbank_plural' : 'questionbank', 'question'), - new moodle_url($baseurl, $params), navigation_node::TYPE_CONTAINER, null, 'questionbank'); + $questionnode = $navigationnode->add( + get_string('questionbank', 'question'), + new moodle_url($baseurl, $params), + navigation_node::TYPE_CONTAINER, + null, + 'questionbank' + ); $corenavigations = [ 'questions' => [ From a9ea34dc3f91121b933159e2d7ebb013c7241b9d Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Tue, 21 May 2024 10:48:45 +0100 Subject: [PATCH 034/553] MDL-81514 groups: Add participationonly option to activity group menu groups_print_activity_menu() and groups_get_activity_group() now include an additional $participationonly parameter, which is true by default. This can be set false when we want the user to be able to select a non-participation group within an activity, for example if a teacher wants to filter assignment submissions by non-participation groups. It should never be used when the menu is displayed to students, as this may allow them to participate using non-participation groups. groups_sort_menu_options() now has a $splitparticipation parameter, which will split non-participation groups out into their own optgroup at the end of the menu. --- .upgradenotes/MDL-81514-2024061009100437.yml | 13 ++ .../output/actionbar/group_selector.php | 18 ++- .../amd/build/comboboxsearch/group.min.js | 2 +- .../amd/build/comboboxsearch/group.min.js.map | 2 +- public/group/amd/src/comboboxsearch/group.js | 1 + .../external/get_groups_for_selector.php | 8 +- .../comboboxsearch/resultitem.mustache | 5 + .../comboboxsearch/resultset.mustache | 12 +- public/lang/en/group.php | 1 + public/lib/grouplib.php | 97 ++++++++++++--- public/lib/tests/grouplib_test.php | 82 ++++++++++++- public/theme/boost/scss/moodle/dropdown.scss | 5 +- public/theme/boost/style/moodle.css | 112 +++++++++++++++--- public/theme/classic/style/moodle.css | 112 +++++++++++++++--- 14 files changed, 405 insertions(+), 65 deletions(-) create mode 100644 .upgradenotes/MDL-81514-2024061009100437.yml diff --git a/.upgradenotes/MDL-81514-2024061009100437.yml b/.upgradenotes/MDL-81514-2024061009100437.yml new file mode 100644 index 0000000000000..adbcd912c012f --- /dev/null +++ b/.upgradenotes/MDL-81514-2024061009100437.yml @@ -0,0 +1,13 @@ +issueNumber: MDL-81514 +notes: + core_group: + - message: > + `groups_print_activity_menu()` and `groups_get_activity_group()` now + include an additional `$participationonly` parameter, which is true by + default. This can be set false when we want the user to be able to + select a non-participation group within an activity, for example if a + teacher wants to filter assignment submissions by non-participation + groups. It should never be used when the menu is displayed to students, + as this may allow them to participate using non-participation groups. + Non-participation groups are labeled as such. + type: improved diff --git a/public/course/classes/output/actionbar/group_selector.php b/public/course/classes/output/actionbar/group_selector.php index df0b35697cea4..a439497c46b26 100644 --- a/public/course/classes/output/actionbar/group_selector.php +++ b/public/course/classes/output/actionbar/group_selector.php @@ -35,8 +35,18 @@ class group_selector extends comboboxsearch { * The class constructor. * * @param stdClass $context The context object. + * @param bool $participationonly Only include participation groups? */ - public function __construct(private stdClass $context) { + public function __construct( + /** + * @var stdClass The context object. + */ + private stdClass $context, + /** + * @var bool Only include participation groups? + */ + protected bool $participationonly = true, + ) { $this->activegroup = $this->get_active_group(); $this->label = $this->get_label(); @@ -100,22 +110,20 @@ private function get_active_group(): int|bool { if ($this->context->contextlevel === CONTEXT_MODULE) { $cm = get_coursemodule_from_id(false, $this->context->instanceid); $groupingid = $cm->groupingid; - $participationonly = true; } else { $cm = null; $groupingid = $course->defaultgroupingid; - $participationonly = false; } $allowedgroups = groups_get_all_groups( courseid: $course->id, userid: $userid, groupingid: $groupingid, - participationonly: $participationonly + participationonly: $this->participationonly, ); if ($cm) { - return groups_get_activity_group($cm, true, $allowedgroups); + return groups_get_activity_group($cm, true, $allowedgroups, $this->participationonly); } return groups_get_course_group($course, true, $allowedgroups); } diff --git a/public/group/amd/build/comboboxsearch/group.min.js b/public/group/amd/build/comboboxsearch/group.min.js index 522cab10779aa..28e7d1f34c7ea 100644 --- a/public/group/amd/build/comboboxsearch/group.min.js +++ b/public/group/amd/build/comboboxsearch/group.min.js @@ -1,3 +1,3 @@ -define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){let cmid=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"cmID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.instance=component.querySelector(this.selectors.instance).dataset.instance,this.cmID=cmid;const searchValueElement=this.component.querySelector("#".concat(this.searchInput.dataset.inputElement));searchValueElement.addEventListener("change",(()=>{this.toggleDropdown();const valueElement=this.component.querySelector("#".concat(this.combobox.dataset.inputElement));valueElement.value!==searchValueElement.value&&(valueElement.value=searchValueElement.value,valueElement.dispatchEvent(new Event("change",{bubbles:!0}))),searchValueElement.value=""})),this.component.addEventListener("hide.bs.dropdown",(()=>{this.searchInput.removeAttribute("aria-activedescendant");const listbox=document.querySelector("#".concat(this.searchInput.getAttribute("aria-controls"),'[role="listbox"]'));listbox.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")})),listbox.scrollTop=0,setTimeout((()=>{""!==this.searchInput.value&&(this.searchInput.value="",this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}))})),this.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch(arguments.length>0&&void 0!==arguments[0]?arguments[0]:null)}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,instance:this.instance,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js),this.searchInput.removeAttribute("aria-activedescendant")}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes()}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID,this.cmID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,groupimageurl:group.groupimageurl}))))}async clickHandler(e){e.target.closest(this.selectors.clearSearch)&&(e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),this.searchInput.focus(),this.clearSearchButton.classList.add("d-none"),await this.filterrenderpipe())}changeHandler(e){window.location=this.selectOneLink(e.target.value)}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}selectOneLink(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default})); +define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){let cmid=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"cmID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.instance=component.querySelector(this.selectors.instance).dataset.instance,this.cmID=cmid;const searchValueElement=this.component.querySelector("#".concat(this.searchInput.dataset.inputElement));searchValueElement.addEventListener("change",(()=>{this.toggleDropdown();const valueElement=this.component.querySelector("#".concat(this.combobox.dataset.inputElement));valueElement.value!==searchValueElement.value&&(valueElement.value=searchValueElement.value,valueElement.dispatchEvent(new Event("change",{bubbles:!0}))),searchValueElement.value=""})),this.component.addEventListener("hide.bs.dropdown",(()=>{this.searchInput.removeAttribute("aria-activedescendant");const listbox=document.querySelector("#".concat(this.searchInput.getAttribute("aria-controls"),'[role="listbox"]'));listbox.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")})),listbox.scrollTop=0,setTimeout((()=>{""!==this.searchInput.value&&(this.searchInput.value="",this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}))})),this.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch(arguments.length>0&&void 0!==arguments[0]?arguments[0]:null)}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,instance:this.instance,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js),this.searchInput.removeAttribute("aria-activedescendant")}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes()}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID,this.cmID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,groupimageurl:group.groupimageurl,participation:group.participation}))))}async clickHandler(e){e.target.closest(this.selectors.clearSearch)&&(e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),this.searchInput.focus(),this.clearSearchButton.classList.add("d-none"),await this.filterrenderpipe())}changeHandler(e){window.location=this.selectOneLink(e.target.value)}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}selectOneLink(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default})); //# sourceMappingURL=group.min.js.map \ No newline at end of file diff --git a/public/group/amd/build/comboboxsearch/group.min.js.map b/public/group/amd/build/comboboxsearch/group.min.js.map index 6d43dfa720a2c..601bd76bef3a8 100644 --- a/public/group/amd/build/comboboxsearch/group.min.js.map +++ b/public/group/amd/build/comboboxsearch/group.min.js.map @@ -1 +1 @@ -{"version":3,"file":"group.min.js","sources":["../../src/comboboxsearch/group.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 * Allow the user to search for groups.\n *\n * @module core_group/comboboxsearch/group\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {groupFetch} from 'core_group/comboboxsearch/repository';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport Notification from 'core/notification';\n\nexport default class GroupSearch extends search_combobox {\n\n courseID;\n cmID;\n bannedFilterFields = ['id', 'link', 'groupimageurl'];\n\n /**\n * Construct the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n constructor(cmid = null) {\n super();\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n placeholder: '.groupsearchdropdown [data-region=\"searchplaceholder\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n // Override the instance since the body is built outside the constructor for the combobox.\n this.instance = component.querySelector(this.selectors.instance).dataset.instance;\n this.cmID = cmid;\n\n const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`);\n searchValueElement.addEventListener('change', () => {\n this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);\n if (valueElement.value !== searchValueElement.value) {\n valueElement.value = searchValueElement.value;\n valueElement.dispatchEvent(new Event('change', {bubbles: true}));\n }\n\n searchValueElement.value = '';\n });\n\n this.component.addEventListener('hide.bs.dropdown', () => {\n this.searchInput.removeAttribute('aria-activedescendant');\n\n const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role=\"listbox\"]`);\n listbox.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n listbox.scrollTop = 0;\n\n // Use setTimeout to make sure the following code is executed after the click event is handled.\n setTimeout(() => {\n if (this.searchInput.value !== '') {\n this.searchInput.value = '';\n this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));\n }\n });\n });\n\n this.renderDefault().catch(Notification.exception);\n }\n\n /**\n * Initialise an instance of the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n static init(cmid = null) {\n return new GroupSearch(cmid);\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.group-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.groupsearchdropdown';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {\n groups: this.getMatchedResults(),\n hasresults: this.getMatchedResults().length > 0,\n instance: this.instance,\n searchterm: this.getSearchTerm(),\n });\n replaceNodeContents(this.selectors.placeholder, html, js);\n // Remove aria-activedescendant when the available options change.\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n\n /**\n * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n this.filterMatchDataset();\n\n await this.renderDropdown();\n\n this.updateNodes();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n async fetchDataset() {\n return await groupFetch(this.courseID, this.cmID).then((r) => r.groups);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n // Sometimes we just want to show everything.\n if (this.getPreppedSearchTerm() === '') {\n return filterableData;\n }\n return filterableData.filter((group) => Object.keys(group).some((key) => {\n if (group[key] === \"\" || this.bannedFilterFields.includes(key)) {\n return false;\n }\n return group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n */\n filterMatchDataset() {\n this.setMatchedResults(\n this.getMatchedResults().map((group) => {\n return {\n id: group.id,\n name: group.name,\n groupimageurl: group.groupimageurl,\n };\n })\n );\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n async clickHandler(e) {\n if (e.target.closest(this.selectors.clearSearch)) {\n e.stopPropagation();\n // Clear the entered search query in the search bar.\n this.searchInput.value = '';\n this.setSearchTerms(this.searchInput.value);\n this.searchInput.focus();\n this.clearSearchButton.classList.add('d-none');\n // Display results.\n await this.filterrenderpipe();\n }\n }\n\n /**\n * The handler for when a user changes the value of the component (selects an option from the dropdown).\n *\n * @param {Event} e The change event.\n */\n changeHandler(e) {\n window.location = this.selectOneLink(e.target.value);\n }\n\n /**\n * Override the input event listener for the text input area.\n */\n registerInputHandlers() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.getSearchTerm() === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n // User has given something for us to filter against.\n await this.filterrenderpipe();\n }, 300));\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n * We will call this function when a user interacts with the combobox to redirect them to show their results in the page.\n *\n * @param {Number} groupID The ID of the group selected.\n */\n selectOneLink(groupID) {\n throw new Error(`selectOneLink(${groupID}) must be implemented in ${this.constructor.name}`);\n }\n}\n"],"names":["GroupSearch","search_combobox","constructor","cmid","selectors","this","courseid","placeholder","component","document","querySelector","componentSelector","courseID","dataset","instance","cmID","searchValueElement","searchInput","inputElement","addEventListener","toggleDropdown","valueElement","combobox","value","dispatchEvent","Event","bubbles","removeAttribute","listbox","getAttribute","querySelectorAll","forEach","option","classList","remove","scrollTop","setTimeout","renderDefault","catch","Notification","exception","dropdownSelector","html","js","groups","getMatchedResults","hasresults","length","searchterm","getSearchTerm","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","then","r","filterableData","getPreppedSearchTerm","filter","group","Object","keys","some","key","bannedFilterFields","includes","toString","toLowerCase","map","id","name","groupimageurl","e","target","closest","clearSearch","stopPropagation","setSearchTerms","focus","clearSearchButton","add","filterrenderpipe","changeHandler","window","location","selectOneLink","registerInputHandlers","async","groupID","Error"],"mappings":"+rBA4BqBA,oBAAoBC,yBAWrCC,kBAAYC,4DAAO,mIAPE,CAAC,KAAM,OAAQ,uBAS3BC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,2BACVC,YAAa,gEAEXC,UAAYC,SAASC,cAAcL,KAAKM,0BACzCC,SAAWJ,UAAUE,cAAcL,KAAKD,UAAUE,UAAUO,QAAQP,cAEpEQ,SAAWN,UAAUE,cAAcL,KAAKD,UAAUU,UAAUD,QAAQC,cACpEC,KAAOZ,WAENa,mBAAqBX,KAAKG,UAAUE,yBAAkBL,KAAKY,YAAYJ,QAAQK,eACrFF,mBAAmBG,iBAAiB,UAAU,UACrCC,uBAECC,aAAehB,KAAKG,UAAUE,yBAAkBL,KAAKiB,SAAST,QAAQK,eACxEG,aAAaE,QAAUP,mBAAmBO,QAC1CF,aAAaE,MAAQP,mBAAmBO,MACxCF,aAAaG,cAAc,IAAIC,MAAM,SAAU,CAACC,SAAS,MAG7DV,mBAAmBO,MAAQ,WAG1Bf,UAAUW,iBAAiB,oBAAoB,UAC3CF,YAAYU,gBAAgB,+BAE3BC,QAAUnB,SAASC,yBAAkBL,KAAKY,YAAYY,aAAa,sCACzED,QAAQE,iBAAiB,0BAA0BC,SAAQC,SACvDA,OAAOC,UAAUC,OAAO,aAE5BN,QAAQO,UAAY,EAGpBC,YAAW,KACwB,KAA3B/B,KAAKY,YAAYM,aACZN,YAAYM,MAAQ,QACpBN,YAAYO,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,iBAKnEW,gBAAgBC,MAAMC,sBAAaC,gCASjC,IAAIxC,mEADI,MASnBW,0BACW,gBAQX8B,yBACW,oDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,OAAQvC,KAAKwC,oBACbC,WAAYzC,KAAKwC,oBAAoBE,OAAS,EAC9CjC,SAAUT,KAAKS,SACfkC,WAAY3C,KAAK4C,qDAED5C,KAAKD,UAAUG,YAAamC,KAAMC,SAEjD1B,YAAYU,gBAAgB,oDAO5BuB,wBAAwB7C,KAAK8C,oBAAoB9C,KAAK+C,oBACtDC,2BAEChD,KAAKiD,sBAENC,gDASQ,0BAAWlD,KAAKO,SAAUP,KAAKU,MAAMyC,MAAMC,GAAMA,EAAEb,6BAShDc,sBAEoB,KAAhCrD,KAAKsD,uBACED,eAEJA,eAAeE,QAAQC,OAAUC,OAAOC,KAAKF,OAAOG,MAAMC,KAC1C,KAAfJ,MAAMI,OAAe5D,KAAK6D,mBAAmBC,SAASF,MAGnDJ,MAAMI,KAAKG,WAAWC,cAAcF,SAAS9D,KAAKsD,4BAOjEN,0BACSH,kBACD7C,KAAKwC,oBAAoByB,KAAKT,QACnB,CACHU,GAAIV,MAAMU,GACVC,KAAMX,MAAMW,KACZC,cAAeZ,MAAMY,sCAWlBC,GACXA,EAAEC,OAAOC,QAAQvE,KAAKD,UAAUyE,eAChCH,EAAEI,uBAEG7D,YAAYM,MAAQ,QACpBwD,eAAe1E,KAAKY,YAAYM,YAChCN,YAAY+D,aACZC,kBAAkBhD,UAAUiD,IAAI,gBAE/B7E,KAAK8E,oBASnBC,cAAcV,GACVW,OAAOC,SAAWjF,KAAKkF,cAAcb,EAAEC,OAAOpD,OAMlDiE,6BAESvE,YAAYE,iBAAiB,SAAS,oBAASsE,eAC3CV,eAAe1E,KAAKY,YAAYM,OAER,KAAzBlB,KAAK4C,qBAEAgC,kBAAkBhD,UAAUiD,IAAI,eAGhCD,kBAAkBhD,UAAUC,OAAO,gBAGtC7B,KAAK8E,qBACZ,MASPI,cAAcG,eACJ,IAAIC,8BAAuBD,4CAAmCrF,KAAKH,YAAYsE"} \ No newline at end of file +{"version":3,"file":"group.min.js","sources":["../../src/comboboxsearch/group.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 * Allow the user to search for groups.\n *\n * @module core_group/comboboxsearch/group\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {groupFetch} from 'core_group/comboboxsearch/repository';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport Notification from 'core/notification';\n\nexport default class GroupSearch extends search_combobox {\n\n courseID;\n cmID;\n bannedFilterFields = ['id', 'link', 'groupimageurl'];\n\n /**\n * Construct the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n constructor(cmid = null) {\n super();\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n placeholder: '.groupsearchdropdown [data-region=\"searchplaceholder\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n // Override the instance since the body is built outside the constructor for the combobox.\n this.instance = component.querySelector(this.selectors.instance).dataset.instance;\n this.cmID = cmid;\n\n const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`);\n searchValueElement.addEventListener('change', () => {\n this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);\n if (valueElement.value !== searchValueElement.value) {\n valueElement.value = searchValueElement.value;\n valueElement.dispatchEvent(new Event('change', {bubbles: true}));\n }\n\n searchValueElement.value = '';\n });\n\n this.component.addEventListener('hide.bs.dropdown', () => {\n this.searchInput.removeAttribute('aria-activedescendant');\n\n const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role=\"listbox\"]`);\n listbox.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n listbox.scrollTop = 0;\n\n // Use setTimeout to make sure the following code is executed after the click event is handled.\n setTimeout(() => {\n if (this.searchInput.value !== '') {\n this.searchInput.value = '';\n this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));\n }\n });\n });\n\n this.renderDefault().catch(Notification.exception);\n }\n\n /**\n * Initialise an instance of the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n static init(cmid = null) {\n return new GroupSearch(cmid);\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.group-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.groupsearchdropdown';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {\n groups: this.getMatchedResults(),\n hasresults: this.getMatchedResults().length > 0,\n instance: this.instance,\n searchterm: this.getSearchTerm(),\n });\n replaceNodeContents(this.selectors.placeholder, html, js);\n // Remove aria-activedescendant when the available options change.\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n\n /**\n * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n this.filterMatchDataset();\n\n await this.renderDropdown();\n\n this.updateNodes();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n async fetchDataset() {\n return await groupFetch(this.courseID, this.cmID).then((r) => r.groups);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n // Sometimes we just want to show everything.\n if (this.getPreppedSearchTerm() === '') {\n return filterableData;\n }\n return filterableData.filter((group) => Object.keys(group).some((key) => {\n if (group[key] === \"\" || this.bannedFilterFields.includes(key)) {\n return false;\n }\n return group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n */\n filterMatchDataset() {\n this.setMatchedResults(\n this.getMatchedResults().map((group) => {\n return {\n id: group.id,\n name: group.name,\n groupimageurl: group.groupimageurl,\n participation: group.participation,\n };\n })\n );\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n async clickHandler(e) {\n if (e.target.closest(this.selectors.clearSearch)) {\n e.stopPropagation();\n // Clear the entered search query in the search bar.\n this.searchInput.value = '';\n this.setSearchTerms(this.searchInput.value);\n this.searchInput.focus();\n this.clearSearchButton.classList.add('d-none');\n // Display results.\n await this.filterrenderpipe();\n }\n }\n\n /**\n * The handler for when a user changes the value of the component (selects an option from the dropdown).\n *\n * @param {Event} e The change event.\n */\n changeHandler(e) {\n window.location = this.selectOneLink(e.target.value);\n }\n\n /**\n * Override the input event listener for the text input area.\n */\n registerInputHandlers() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.getSearchTerm() === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n // User has given something for us to filter against.\n await this.filterrenderpipe();\n }, 300));\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n * We will call this function when a user interacts with the combobox to redirect them to show their results in the page.\n *\n * @param {Number} groupID The ID of the group selected.\n */\n selectOneLink(groupID) {\n throw new Error(`selectOneLink(${groupID}) must be implemented in ${this.constructor.name}`);\n }\n}\n"],"names":["GroupSearch","search_combobox","constructor","cmid","selectors","this","courseid","placeholder","component","document","querySelector","componentSelector","courseID","dataset","instance","cmID","searchValueElement","searchInput","inputElement","addEventListener","toggleDropdown","valueElement","combobox","value","dispatchEvent","Event","bubbles","removeAttribute","listbox","getAttribute","querySelectorAll","forEach","option","classList","remove","scrollTop","setTimeout","renderDefault","catch","Notification","exception","dropdownSelector","html","js","groups","getMatchedResults","hasresults","length","searchterm","getSearchTerm","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","then","r","filterableData","getPreppedSearchTerm","filter","group","Object","keys","some","key","bannedFilterFields","includes","toString","toLowerCase","map","id","name","groupimageurl","participation","e","target","closest","clearSearch","stopPropagation","setSearchTerms","focus","clearSearchButton","add","filterrenderpipe","changeHandler","window","location","selectOneLink","registerInputHandlers","async","groupID","Error"],"mappings":"+rBA4BqBA,oBAAoBC,yBAWrCC,kBAAYC,4DAAO,mIAPE,CAAC,KAAM,OAAQ,uBAS3BC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,2BACVC,YAAa,gEAEXC,UAAYC,SAASC,cAAcL,KAAKM,0BACzCC,SAAWJ,UAAUE,cAAcL,KAAKD,UAAUE,UAAUO,QAAQP,cAEpEQ,SAAWN,UAAUE,cAAcL,KAAKD,UAAUU,UAAUD,QAAQC,cACpEC,KAAOZ,WAENa,mBAAqBX,KAAKG,UAAUE,yBAAkBL,KAAKY,YAAYJ,QAAQK,eACrFF,mBAAmBG,iBAAiB,UAAU,UACrCC,uBAECC,aAAehB,KAAKG,UAAUE,yBAAkBL,KAAKiB,SAAST,QAAQK,eACxEG,aAAaE,QAAUP,mBAAmBO,QAC1CF,aAAaE,MAAQP,mBAAmBO,MACxCF,aAAaG,cAAc,IAAIC,MAAM,SAAU,CAACC,SAAS,MAG7DV,mBAAmBO,MAAQ,WAG1Bf,UAAUW,iBAAiB,oBAAoB,UAC3CF,YAAYU,gBAAgB,+BAE3BC,QAAUnB,SAASC,yBAAkBL,KAAKY,YAAYY,aAAa,sCACzED,QAAQE,iBAAiB,0BAA0BC,SAAQC,SACvDA,OAAOC,UAAUC,OAAO,aAE5BN,QAAQO,UAAY,EAGpBC,YAAW,KACwB,KAA3B/B,KAAKY,YAAYM,aACZN,YAAYM,MAAQ,QACpBN,YAAYO,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,iBAKnEW,gBAAgBC,MAAMC,sBAAaC,gCASjC,IAAIxC,mEADI,MASnBW,0BACW,gBAQX8B,yBACW,oDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,OAAQvC,KAAKwC,oBACbC,WAAYzC,KAAKwC,oBAAoBE,OAAS,EAC9CjC,SAAUT,KAAKS,SACfkC,WAAY3C,KAAK4C,qDAED5C,KAAKD,UAAUG,YAAamC,KAAMC,SAEjD1B,YAAYU,gBAAgB,oDAO5BuB,wBAAwB7C,KAAK8C,oBAAoB9C,KAAK+C,oBACtDC,2BAEChD,KAAKiD,sBAENC,gDASQ,0BAAWlD,KAAKO,SAAUP,KAAKU,MAAMyC,MAAMC,GAAMA,EAAEb,6BAShDc,sBAEoB,KAAhCrD,KAAKsD,uBACED,eAEJA,eAAeE,QAAQC,OAAUC,OAAOC,KAAKF,OAAOG,MAAMC,KAC1C,KAAfJ,MAAMI,OAAe5D,KAAK6D,mBAAmBC,SAASF,MAGnDJ,MAAMI,KAAKG,WAAWC,cAAcF,SAAS9D,KAAKsD,4BAOjEN,0BACSH,kBACD7C,KAAKwC,oBAAoByB,KAAKT,QACnB,CACHU,GAAIV,MAAMU,GACVC,KAAMX,MAAMW,KACZC,cAAeZ,MAAMY,cACrBC,cAAeb,MAAMa,sCAWlBC,GACXA,EAAEC,OAAOC,QAAQxE,KAAKD,UAAU0E,eAChCH,EAAEI,uBAEG9D,YAAYM,MAAQ,QACpByD,eAAe3E,KAAKY,YAAYM,YAChCN,YAAYgE,aACZC,kBAAkBjD,UAAUkD,IAAI,gBAE/B9E,KAAK+E,oBASnBC,cAAcV,GACVW,OAAOC,SAAWlF,KAAKmF,cAAcb,EAAEC,OAAOrD,OAMlDkE,6BAESxE,YAAYE,iBAAiB,SAAS,oBAASuE,eAC3CV,eAAe3E,KAAKY,YAAYM,OAER,KAAzBlB,KAAK4C,qBAEAiC,kBAAkBjD,UAAUkD,IAAI,eAGhCD,kBAAkBjD,UAAUC,OAAO,gBAGtC7B,KAAK+E,qBACZ,MASPI,cAAcG,eACJ,IAAIC,8BAAuBD,4CAAmCtF,KAAKH,YAAYsE"} \ No newline at end of file diff --git a/public/group/amd/src/comboboxsearch/group.js b/public/group/amd/src/comboboxsearch/group.js index d21cd94c8d57b..4626cd49e42ea 100644 --- a/public/group/amd/src/comboboxsearch/group.js +++ b/public/group/amd/src/comboboxsearch/group.js @@ -175,6 +175,7 @@ export default class GroupSearch extends search_combobox { id: group.id, name: group.name, groupimageurl: group.groupimageurl, + participation: group.participation, }; }) ); diff --git a/public/group/classes/external/get_groups_for_selector.php b/public/group/classes/external/get_groups_for_selector.php index cb2a9ee40c245..3e12a82019248 100644 --- a/public/group/classes/external/get_groups_for_selector.php +++ b/public/group/classes/external/get_groups_for_selector.php @@ -85,7 +85,7 @@ public static function execute(int $courseid, ?int $cmid = null): array { $cm = get_coursemodule_from_id('', $params['cmid']); $groupmode = groups_get_activity_groupmode($cm, $course); $groupingid = $cm->groupingid; - $participationonly = true; + $participationonly = false; } else { $context = context_course::instance($params['courseid']); $groupmode = $course->groupmode; @@ -130,19 +130,22 @@ public static function execute(int $courseid, ?int $cmid = null): array { ]); } - $mappedgroups = array_map(function($group) use ($context, $OUTPUT) { + $mappedgroups = array_map(function ($group) use ($context, $OUTPUT) { if ($group->id) { // Particular group. Get the group picture if it exists, otherwise return a generic image. $picture = get_group_picture_url($group, $group->courseid, true) ?? moodle_url::make_pluginfile_url($context->get_course_context()->id, 'group', 'generated', $group->id, '/', 'group.svg'); + $participation = $group->participation; } else { // All participants. $picture = $OUTPUT->image_url('g/g1'); + $participation = true; } return (object) [ 'id' => $group->id, 'name' => format_string($group->name, true, ['context' => $context]), 'groupimageurl' => $picture->out(false), + 'participation' => $participation, ]; }, $groupsmenu); } @@ -175,6 +178,7 @@ public static function group_description(): external_description { 'id' => new external_value(PARAM_ALPHANUM, 'An ID for the group', VALUE_REQUIRED), 'name' => new external_value(PARAM_TEXT, 'The full name of the group', VALUE_REQUIRED), 'groupimageurl' => new external_value(PARAM_URL, 'Group image URL', VALUE_OPTIONAL), + 'participation' => new external_value(PARAM_BOOL, 'Is this a participation group?', VALUE_REQUIRED), ]; return new external_single_structure($groupfields); } diff --git a/public/group/templates/comboboxsearch/resultitem.mustache b/public/group/templates/comboboxsearch/resultitem.mustache index 2dd40d997d28c..d9eb80a00a815 100644 --- a/public/group/templates/comboboxsearch/resultitem.mustache +++ b/public/group/templates/comboboxsearch/resultitem.mustache @@ -37,6 +37,11 @@ {{name}} + {{^participation}} + + {{#str}} nonparticipation, group {{/str}} + + {{/participation}}
{{/content}} {{/core/local/comboboxsearch/resultitem}} diff --git a/public/group/templates/comboboxsearch/resultset.mustache b/public/group/templates/comboboxsearch/resultset.mustache index fe077b9841d0b..b87b90c99818e 100644 --- a/public/group/templates/comboboxsearch/resultset.mustache +++ b/public/group/templates/comboboxsearch/resultset.mustache @@ -28,12 +28,20 @@ { "id": 2, "name": "Foo bar", - "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2" + "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2", + "participation": true }, { "id": 3, "name": "Bar Foo", - "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3" + "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3", + "participation": true + }, + { + "id": 4, + "name": "Baz Bar Foo", + "link": "http://foo.bar/grade/report/grader/index.php?id=43&userid=2", + "participation": false } ], "instance": 25, diff --git a/public/lang/en/group.php b/public/lang/en/group.php index fa9d4ee55224f..8e46a50fcb370 100644 --- a/public/lang/en/group.php +++ b/public/lang/en/group.php @@ -178,6 +178,7 @@ $string['messagingdisabled'] = 'Successfully disabled messaging in {$a} group(s)'; $string['messagingenabled'] = 'Successfully enabled messaging in {$a} group(s)'; $string['mygroups'] = 'My groups'; +$string['nonparticipation'] = 'Non-participation'; $string['othergroups'] = 'Other groups'; $string['overview'] = 'Overview'; $string['participation'] = 'Show group in dropdown menu for activities in group mode'; diff --git a/public/lib/grouplib.php b/public/lib/grouplib.php index 01e60f4553eb0..cecbb97ebef2f 100644 --- a/public/lib/grouplib.php +++ b/public/lib/grouplib.php @@ -833,11 +833,17 @@ function groups_list_to_menu($groups) { * Own groups are removed from allowed groups * @param array $allowedgroups All groups user is allowed to see * @param array $usergroups Groups user belongs to + * @param bool $splitparticipation If true, split each optgroup into "Participation" and "Non-participation" optgroups. * @return array */ -function groups_sort_menu_options($allowedgroups, $usergroups) { - $useroptions = array(); +function groups_sort_menu_options($allowedgroups, $usergroups, bool $splitparticipation = false) { + $nonparticipationgroups = []; + $useroptions = []; if ($usergroups) { + if ($splitparticipation) { + [$usergroups, $usernonparticipation] = groups_split_participation_groups($usergroups); + $nonparticipationgroups = array_merge($nonparticipationgroups, $usernonparticipation); + } $useroptions = groups_list_to_menu($usergroups); // Remove user groups from other groups list. @@ -846,21 +852,54 @@ function groups_sort_menu_options($allowedgroups, $usergroups) { } } - $allowedoptions = array(); + $allowedoptions = []; if ($allowedgroups) { + if ($splitparticipation) { + [$allowedgroups, $allowednonparticipation] = groups_split_participation_groups($allowedgroups); + $nonparticipationgroups = array_merge($nonparticipationgroups, $allowednonparticipation); + } $allowedoptions = groups_list_to_menu($allowedgroups); } if ($useroptions && $allowedoptions) { - return array( - 1 => array(get_string('mygroups', 'group') => $useroptions), - 2 => array(get_string('othergroups', 'group') => $allowedoptions) - ); + $options = [ + 1 => [ + get_string('mygroups', 'group') => $useroptions, + ], + 2 => [ + get_string('othergroups', 'group') => $allowedoptions, + ], + ]; } else if ($useroptions) { - return $useroptions; + $options = $useroptions; } else { - return $allowedoptions; + $options = $allowedoptions; + } + if ($nonparticipationgroups) { + $options[3] = [ + get_string('nonparticipation', 'group') => groups_list_to_menu($nonparticipationgroups), + ]; + } + return $options; +} + +/** + * Split the list of groups into participation and non-participation groups. + * + * @param array $groups List of group records + * @return array[] Menu options for the records, split into "Participation" and "Non-participation" optgroups. + */ +function groups_split_participation_groups(array $groups): array { + $participation = []; + $nonparticipation = []; + foreach ($groups as $group) { + if ($group->participation) { + $participation[] = $group; + } else { + $nonparticipation[] = $group; + } } + return [$participation, $nonparticipation]; } /** @@ -934,9 +973,20 @@ function groups_allgroups_course_menu($course, $urlroot, $update = false, $activ * selecting this option does not prevent groups_get_activity_group from * returning 0; it will still do that if the user has chosen 'all participants' * in another activity, or not chosen anything.) + * @param bool $participationonly By default, this menu will only contain groups with the "participation" + * flag set true. Setting this argument to false will return all groups that the user is allowed to see. + * This should only be used for cases such as a teacher wanting to filter submissions by group, not for + * students choosing a group to submit their work under, otherwise it negates the point of the participation + * flag. * @return mixed void or string depending on $return param */ -function groups_print_activity_menu($cm, $urlroot, $return=false, $hideallparticipants=false) { +function groups_print_activity_menu( + $cm, + $urlroot, + $return = false, + $hideallparticipants = false, + bool $participationonly = true, +) { global $USER, $OUTPUT; if ($urlroot instanceof moodle_url) { @@ -966,22 +1016,23 @@ function groups_print_activity_menu($cm, $urlroot, $return=false, $hideallpartic $usergroups = array(); if ($groupmode == VISIBLEGROUPS or $aag) { - $allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid, 'g.*', false, true); // Any group in grouping. + // Any group in grouping. + $allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid, 'g.*', false, $participationonly); // Get user's own groups and put to the top. - $usergroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid, 'g.*', false, true); + $usergroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid, 'g.*', false, $participationonly); } else { // Only assigned groups. - $allowedgroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid, 'g.*', false, true); + $allowedgroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid, 'g.*', false, $participationonly); } - $activegroup = groups_get_activity_group($cm, true, $allowedgroups); + $activegroup = groups_get_activity_group($cm, true, $allowedgroups, $participationonly); $groupsmenu = array(); if ((!$allowedgroups or $groupmode == VISIBLEGROUPS or $aag) and !$hideallparticipants) { $groupsmenu[0] = get_string('allparticipants'); } - $groupsmenu += groups_sort_menu_options($allowedgroups, $usergroups); + $groupsmenu += groups_sort_menu_options($allowedgroups, $usergroups, !$participationonly); if ($groupmode == VISIBLEGROUPS) { $grouplabel = get_string('groupsvisible'); @@ -1068,13 +1119,21 @@ function groups_get_course_group($course, $update=false, $allowedgroups=null) { /** * Returns group active in activity, changes the group by default if 'group' page param present * - * @category group * @param stdClass|cm_info $cm course module object * @param bool $update change active group if group param submitted * @param array $allowedgroups list of groups user may access (INTERNAL, to be used only from groups_print_activity_menu()) + * @param bool $participationonly By default, only allow groups with the "participation" + * flag set true. Setting this argument to false will allow setting a non-participation group as the active group. + * This should be used with care, and only for users who should be able to see non-participation groups within an activity. * @return mixed false if groups not used, int if groups used, 0 means all groups (access must be verified in SEPARATE mode) + * @category group */ -function groups_get_activity_group($cm, $update=false, $allowedgroups=null) { +function groups_get_activity_group( + $cm, + $update = false, + $allowedgroups = null, + bool $participationonly = true, +) { global $USER, $SESSION; if (!$groupmode = groups_get_activity_groupmode($cm)) { @@ -1089,9 +1148,9 @@ function groups_get_activity_group($cm, $update=false, $allowedgroups=null) { if (!is_array($allowedgroups)) { if ($groupmode == VISIBLEGROUPS or $groupmode === 'aag') { - $allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid, 'g.*', false, true); + $allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid, 'g.*', false, $participationonly); } else { - $allowedgroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid, 'g.*', false, true); + $allowedgroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid, 'g.*', false, $participationonly); } } diff --git a/public/lib/tests/grouplib_test.php b/public/lib/tests/grouplib_test.php index e34d6ce74e3ba..35a93226a0885 100644 --- a/public/lib/tests/grouplib_test.php +++ b/public/lib/tests/grouplib_test.php @@ -1236,14 +1236,18 @@ public function test_groups_get_user_groups(): void { /** * Create dummy groups array for use in menu tests * @param int $number + * @param bool $alternateparticipation If true, set participation = 1 for even groups, and 0 for odd groups. * @return array */ - protected function make_group_list($number) { + protected function make_group_list($number, $alternateparticipation = false) { $testgroups = array(); for ($a = 0; $a < $number; $a++) { $grp = new \stdClass(); $grp->id = 100 + $a; $grp->name = 'test group ' . $grp->id; + if ($alternateparticipation) { + $grp->participation = ($a % 2 == 0); + } $testgroups[$grp->id] = $grp; } return $testgroups; @@ -1302,6 +1306,82 @@ public function test_groups_sort_menu_options_user_both_many_groups(): void { ), groups_sort_menu_options($this->make_group_list(13), $this->make_group_list(2))); } + /** + * Splitting allowed groups by participation returns an optgroup for non-participation groups. + * + * @covers ::groups_sort_menu_options() + * @return void + */ + public function test_groups_split_participation_allowed_groups_only(): void { + $this->assertEquals( + [ + 100 => 'test group 100', + 3 => [ + get_string('nonparticipation', 'group') => [ + 101 => 'test group 101', + ], + ], + ], + groups_sort_menu_options($this->make_group_list(2, true), [], true), + ); + } + + /** + * Splitting user groups by participation returns an optgroup for non-participation groups. + * + * @covers ::groups_sort_menu_options() + * @return void + */ + public function test_groups_split_participation_options_user_groups_only(): void { + $this->assertEquals( + [ + 100 => 'test group 100', + 3 => [ + get_string('nonparticipation', 'group') => [ + 101 => 'test group 101', + ], + ], + ], + groups_sort_menu_options([], $this->make_group_list(2, true), true), + ); + } + + /** + * Splitting allowed and user groups by participation returns optgroups. + * + * One optgroup for user participation groups, one for other participation groups, and one for non-participation groups. + * + * @covers ::groups_sort_menu_options() + * @return void + */ + public function test_groups_split_participation_options_user_both(): void { + $this->assertEquals( + [ + 1 => [ + get_string('mygroups', 'group') => [ + 100 => 'test group 100', + ], + ], + 2 => [ + get_string('othergroups', 'group') => [ + 102 => 'test group 102', + ], + ], + 3 => [ + get_string('nonparticipation', 'group') => [ + 101 => 'test group 101', + 103 => 'test group 103', + ], + ], + ], + groups_sort_menu_options( + $this->make_group_list(4, true), + $this->make_group_list(2, true), + true + ), + ); + } + /** * Tests for groups_user_groups_visible. */ diff --git a/public/theme/boost/scss/moodle/dropdown.scss b/public/theme/boost/scss/moodle/dropdown.scss index f5cdd70719f5f..7c66263457234 100644 --- a/public/theme/boost/scss/moodle/dropdown.scss +++ b/public/theme/boost/scss/moodle/dropdown.scss @@ -44,12 +44,13 @@ } // Add dropdown menu items styles for each theme color mantainning default hover colour for contrast. -@each $color, $value in $theme-colors { +@each $color, $value in map-merge($theme-colors, $utilities-text-emphasis-colors) { .dropdown-item { &:hover, &:focus { &.text-#{$color}, - a.text-#{$color} { + a.text-#{$color}, + span.text-#{$color} { color: $dropdown-link-hover-color !important; /* stylelint-disable-line declaration-no-important */ } } diff --git a/public/theme/boost/style/moodle.css b/public/theme/boost/style/moodle.css index 6925634354d04..4bc929a2b82c2 100644 --- a/public/theme/boost/style/moodle.css +++ b/public/theme/boost/style/moodle.css @@ -41032,50 +41032,130 @@ div.editor_atto_toolbar button .icon { } .dropdown-item:hover.text-primary, -.dropdown-item:hover a.text-primary, .dropdown-item:focus.text-primary, -.dropdown-item:focus a.text-primary { +.dropdown-item:hover a.text-primary, +.dropdown-item:hover span.text-primary, .dropdown-item:focus.text-primary, +.dropdown-item:focus a.text-primary, +.dropdown-item:focus span.text-primary { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-secondary, -.dropdown-item:hover a.text-secondary, .dropdown-item:focus.text-secondary, -.dropdown-item:focus a.text-secondary { +.dropdown-item:hover a.text-secondary, +.dropdown-item:hover span.text-secondary, .dropdown-item:focus.text-secondary, +.dropdown-item:focus a.text-secondary, +.dropdown-item:focus span.text-secondary { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-success, -.dropdown-item:hover a.text-success, .dropdown-item:focus.text-success, -.dropdown-item:focus a.text-success { +.dropdown-item:hover a.text-success, +.dropdown-item:hover span.text-success, .dropdown-item:focus.text-success, +.dropdown-item:focus a.text-success, +.dropdown-item:focus span.text-success { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-info, -.dropdown-item:hover a.text-info, .dropdown-item:focus.text-info, -.dropdown-item:focus a.text-info { +.dropdown-item:hover a.text-info, +.dropdown-item:hover span.text-info, .dropdown-item:focus.text-info, +.dropdown-item:focus a.text-info, +.dropdown-item:focus span.text-info { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-warning, -.dropdown-item:hover a.text-warning, .dropdown-item:focus.text-warning, -.dropdown-item:focus a.text-warning { +.dropdown-item:hover a.text-warning, +.dropdown-item:hover span.text-warning, .dropdown-item:focus.text-warning, +.dropdown-item:focus a.text-warning, +.dropdown-item:focus span.text-warning { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-danger, -.dropdown-item:hover a.text-danger, .dropdown-item:focus.text-danger, -.dropdown-item:focus a.text-danger { +.dropdown-item:hover a.text-danger, +.dropdown-item:hover span.text-danger, .dropdown-item:focus.text-danger, +.dropdown-item:focus a.text-danger, +.dropdown-item:focus span.text-danger { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-light, -.dropdown-item:hover a.text-light, .dropdown-item:focus.text-light, -.dropdown-item:focus a.text-light { +.dropdown-item:hover a.text-light, +.dropdown-item:hover span.text-light, .dropdown-item:focus.text-light, +.dropdown-item:focus a.text-light, +.dropdown-item:focus span.text-light { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-dark, -.dropdown-item:hover a.text-dark, .dropdown-item:focus.text-dark, -.dropdown-item:focus a.text-dark { +.dropdown-item:hover a.text-dark, +.dropdown-item:hover span.text-dark, .dropdown-item:focus.text-dark, +.dropdown-item:focus a.text-dark, +.dropdown-item:focus span.text-dark { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-primary-emphasis, +.dropdown-item:hover a.text-primary-emphasis, +.dropdown-item:hover span.text-primary-emphasis, .dropdown-item:focus.text-primary-emphasis, +.dropdown-item:focus a.text-primary-emphasis, +.dropdown-item:focus span.text-primary-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-secondary-emphasis, +.dropdown-item:hover a.text-secondary-emphasis, +.dropdown-item:hover span.text-secondary-emphasis, .dropdown-item:focus.text-secondary-emphasis, +.dropdown-item:focus a.text-secondary-emphasis, +.dropdown-item:focus span.text-secondary-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-success-emphasis, +.dropdown-item:hover a.text-success-emphasis, +.dropdown-item:hover span.text-success-emphasis, .dropdown-item:focus.text-success-emphasis, +.dropdown-item:focus a.text-success-emphasis, +.dropdown-item:focus span.text-success-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-info-emphasis, +.dropdown-item:hover a.text-info-emphasis, +.dropdown-item:hover span.text-info-emphasis, .dropdown-item:focus.text-info-emphasis, +.dropdown-item:focus a.text-info-emphasis, +.dropdown-item:focus span.text-info-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-warning-emphasis, +.dropdown-item:hover a.text-warning-emphasis, +.dropdown-item:hover span.text-warning-emphasis, .dropdown-item:focus.text-warning-emphasis, +.dropdown-item:focus a.text-warning-emphasis, +.dropdown-item:focus span.text-warning-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-danger-emphasis, +.dropdown-item:hover a.text-danger-emphasis, +.dropdown-item:hover span.text-danger-emphasis, .dropdown-item:focus.text-danger-emphasis, +.dropdown-item:focus a.text-danger-emphasis, +.dropdown-item:focus span.text-danger-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-light-emphasis, +.dropdown-item:hover a.text-light-emphasis, +.dropdown-item:hover span.text-light-emphasis, .dropdown-item:focus.text-light-emphasis, +.dropdown-item:focus a.text-light-emphasis, +.dropdown-item:focus span.text-light-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-dark-emphasis, +.dropdown-item:hover a.text-dark-emphasis, +.dropdown-item:hover span.text-dark-emphasis, .dropdown-item:focus.text-dark-emphasis, +.dropdown-item:focus a.text-dark-emphasis, +.dropdown-item:focus span.text-dark-emphasis { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } diff --git a/public/theme/classic/style/moodle.css b/public/theme/classic/style/moodle.css index 1354eafd2c2b8..7d05ff39e84eb 100644 --- a/public/theme/classic/style/moodle.css +++ b/public/theme/classic/style/moodle.css @@ -40966,50 +40966,130 @@ div.editor_atto_toolbar button .icon { } .dropdown-item:hover.text-primary, -.dropdown-item:hover a.text-primary, .dropdown-item:focus.text-primary, -.dropdown-item:focus a.text-primary { +.dropdown-item:hover a.text-primary, +.dropdown-item:hover span.text-primary, .dropdown-item:focus.text-primary, +.dropdown-item:focus a.text-primary, +.dropdown-item:focus span.text-primary { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-secondary, -.dropdown-item:hover a.text-secondary, .dropdown-item:focus.text-secondary, -.dropdown-item:focus a.text-secondary { +.dropdown-item:hover a.text-secondary, +.dropdown-item:hover span.text-secondary, .dropdown-item:focus.text-secondary, +.dropdown-item:focus a.text-secondary, +.dropdown-item:focus span.text-secondary { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-success, -.dropdown-item:hover a.text-success, .dropdown-item:focus.text-success, -.dropdown-item:focus a.text-success { +.dropdown-item:hover a.text-success, +.dropdown-item:hover span.text-success, .dropdown-item:focus.text-success, +.dropdown-item:focus a.text-success, +.dropdown-item:focus span.text-success { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-info, -.dropdown-item:hover a.text-info, .dropdown-item:focus.text-info, -.dropdown-item:focus a.text-info { +.dropdown-item:hover a.text-info, +.dropdown-item:hover span.text-info, .dropdown-item:focus.text-info, +.dropdown-item:focus a.text-info, +.dropdown-item:focus span.text-info { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-warning, -.dropdown-item:hover a.text-warning, .dropdown-item:focus.text-warning, -.dropdown-item:focus a.text-warning { +.dropdown-item:hover a.text-warning, +.dropdown-item:hover span.text-warning, .dropdown-item:focus.text-warning, +.dropdown-item:focus a.text-warning, +.dropdown-item:focus span.text-warning { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-danger, -.dropdown-item:hover a.text-danger, .dropdown-item:focus.text-danger, -.dropdown-item:focus a.text-danger { +.dropdown-item:hover a.text-danger, +.dropdown-item:hover span.text-danger, .dropdown-item:focus.text-danger, +.dropdown-item:focus a.text-danger, +.dropdown-item:focus span.text-danger { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-light, -.dropdown-item:hover a.text-light, .dropdown-item:focus.text-light, -.dropdown-item:focus a.text-light { +.dropdown-item:hover a.text-light, +.dropdown-item:hover span.text-light, .dropdown-item:focus.text-light, +.dropdown-item:focus a.text-light, +.dropdown-item:focus span.text-light { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } .dropdown-item:hover.text-dark, -.dropdown-item:hover a.text-dark, .dropdown-item:focus.text-dark, -.dropdown-item:focus a.text-dark { +.dropdown-item:hover a.text-dark, +.dropdown-item:hover span.text-dark, .dropdown-item:focus.text-dark, +.dropdown-item:focus a.text-dark, +.dropdown-item:focus span.text-dark { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-primary-emphasis, +.dropdown-item:hover a.text-primary-emphasis, +.dropdown-item:hover span.text-primary-emphasis, .dropdown-item:focus.text-primary-emphasis, +.dropdown-item:focus a.text-primary-emphasis, +.dropdown-item:focus span.text-primary-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-secondary-emphasis, +.dropdown-item:hover a.text-secondary-emphasis, +.dropdown-item:hover span.text-secondary-emphasis, .dropdown-item:focus.text-secondary-emphasis, +.dropdown-item:focus a.text-secondary-emphasis, +.dropdown-item:focus span.text-secondary-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-success-emphasis, +.dropdown-item:hover a.text-success-emphasis, +.dropdown-item:hover span.text-success-emphasis, .dropdown-item:focus.text-success-emphasis, +.dropdown-item:focus a.text-success-emphasis, +.dropdown-item:focus span.text-success-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-info-emphasis, +.dropdown-item:hover a.text-info-emphasis, +.dropdown-item:hover span.text-info-emphasis, .dropdown-item:focus.text-info-emphasis, +.dropdown-item:focus a.text-info-emphasis, +.dropdown-item:focus span.text-info-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-warning-emphasis, +.dropdown-item:hover a.text-warning-emphasis, +.dropdown-item:hover span.text-warning-emphasis, .dropdown-item:focus.text-warning-emphasis, +.dropdown-item:focus a.text-warning-emphasis, +.dropdown-item:focus span.text-warning-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-danger-emphasis, +.dropdown-item:hover a.text-danger-emphasis, +.dropdown-item:hover span.text-danger-emphasis, .dropdown-item:focus.text-danger-emphasis, +.dropdown-item:focus a.text-danger-emphasis, +.dropdown-item:focus span.text-danger-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-light-emphasis, +.dropdown-item:hover a.text-light-emphasis, +.dropdown-item:hover span.text-light-emphasis, .dropdown-item:focus.text-light-emphasis, +.dropdown-item:focus a.text-light-emphasis, +.dropdown-item:focus span.text-light-emphasis { + color: #fff !important; /* stylelint-disable-line declaration-no-important */ +} + +.dropdown-item:hover.text-dark-emphasis, +.dropdown-item:hover a.text-dark-emphasis, +.dropdown-item:hover span.text-dark-emphasis, .dropdown-item:focus.text-dark-emphasis, +.dropdown-item:focus a.text-dark-emphasis, +.dropdown-item:focus span.text-dark-emphasis { color: #fff !important; /* stylelint-disable-line declaration-no-important */ } From 6cec07a34eb6c3bd91d4920668cbff8307280fc6 Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Tue, 21 May 2024 10:50:16 +0100 Subject: [PATCH 035/553] MDL-81514 assign: Enable filtering by non-participation groups This adds non-participation groups to the group menu on the Submissions screen, so that submissions can be filtered by these groups. --- .../classes/output/grading_actionmenu.php | 2 +- public/mod/assign/gradingtable.php | 2 +- .../tests/behat/group_submission.feature | 38 ++++++++++++++++++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/public/mod/assign/classes/output/grading_actionmenu.php b/public/mod/assign/classes/output/grading_actionmenu.php index 190dea8484fe6..537b4457e0b6f 100644 --- a/public/mod/assign/classes/output/grading_actionmenu.php +++ b/public/mod/assign/classes/output/grading_actionmenu.php @@ -146,7 +146,7 @@ public function export_for_template(\renderer_base $output): array { $data['initialselector'] = $initialselector->export_for_template($output); if (groups_get_activity_groupmode($cm, $course)) { - $gs = new group_selector($PAGE->context); + $gs = new group_selector($PAGE->context, false); $data['groupselector'] = $gs->export_for_template($output); } diff --git a/public/mod/assign/gradingtable.php b/public/mod/assign/gradingtable.php index 3e4d25aeed6c5..d73c2f3d70902 100644 --- a/public/mod/assign/gradingtable.php +++ b/public/mod/assign/gradingtable.php @@ -109,7 +109,7 @@ public function __construct(assign $assignment, $this->define_baseurl($url); // Do some business - then set the sql. - $currentgroup = groups_get_activity_group($assignment->get_course_module(), true); + $currentgroup = groups_get_activity_group($assignment->get_course_module(), true, participationonly: false); if ($rowoffset) { $this->rownum = $rowoffset - 1; diff --git a/public/mod/assign/tests/behat/group_submission.feature b/public/mod/assign/tests/behat/group_submission.feature index c1f5855af1ea5..83c24f01380c0 100644 --- a/public/mod/assign/tests/behat/group_submission.feature +++ b/public/mod/assign/tests/behat/group_submission.feature @@ -369,7 +369,7 @@ Feature: Group assignment submissions And I should see "Submitted for grading" in the "Submission status" "table_row" And I should not see "Users who need to submit" - Scenario: Group submission does not use non-participation groups + Scenario: Students cannot make a group submission under a non-participation group Given the following "groups" exist: | name | course | idnumber | participation | | Group A | C1 | CG1 | 0 | @@ -386,3 +386,39 @@ Feature: Group assignment submissions When I am on the "Test assignment name" Activity page logged in as student1 Then I should see "Default group" And I should not see "Group A" + + @javascript + Scenario: All groups including non-participation groups can be used for filtering submissions + Given the following "groups" exist: + | name | course | idnumber | participation | visibility | + | Group 2 | C1 | G2 | 0 | 0 | + | Group 3 | C1 | G3 | 0 | 3 | + And the following "group members" exist: + | group | user | + | G1 | student1 | + | G2 | student1 | + | G1 | student2 | + | G2 | student3 | + And the following "activity" exists: + | activity | assign | + | course | C1 | + | name | Test assignment name | + | submissiondrafts | 0 | + | teamsubmission | 1 | + | groupmode | 1 | + And the following "mod_assign > submissions" exist: + | assign | user | onlinetext | + | Test assignment name | student1 | I'm the student's first submission | + | Test assignment name | student3 | I'm the student's first submission | + When I am on the "Test assignment name" Activity page logged in as teacher1 + And I follow "Submissions" + And I confirm "Group 1" exists in the "Search groups" search combo box + And I confirm "Group 2" exists in the "Search groups" search combo box + And I confirm "Group 3" exists in the "Search groups" search combo box + And I should not see "Non-participation" in the "Group 1" "list_item" + And I should see "Non-participation" in the "Group 2" "list_item" + And I should see "Non-participation" in the "Group 3" "list_item" + And I click on "Group 2" in the "Search groups" search combo box + Then I should see "Student 1" + And I should see "Student 3" + And I should not see "Student 2" From f3174ef370493be12f5c5876efe5293799e89a96 Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Thu, 4 Sep 2025 14:53:06 +0100 Subject: [PATCH 036/553] MDL-81514 assign: Show non-participation groups in activity menu This sets the flag in the activity menu for selecting the grade summaries for each groups, to include non-participation groups as well. --- public/mod/assign/classes/output/renderer.php | 2 +- .../tests/behat/group_submission.feature | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/public/mod/assign/classes/output/renderer.php b/public/mod/assign/classes/output/renderer.php index 7fdc8b98630be..1dd9f89ec76c9 100644 --- a/public/mod/assign/classes/output/renderer.php +++ b/public/mod/assign/classes/output/renderer.php @@ -298,7 +298,7 @@ public function render_assign_grading_summary(\assign_grading_summary $summary) if (isset($summary->cm)) { $currenturl = new \moodle_url('/mod/assign/view.php', array('id' => $summary->cm->id)); - $o .= groups_print_activity_menu($summary->cm, $currenturl->out(), true); + $o .= groups_print_activity_menu($summary->cm, $currenturl->out(), true, participationonly: false); } $o .= $this->output->box_start('boxaligncenter gradingsummarytable'); diff --git a/public/mod/assign/tests/behat/group_submission.feature b/public/mod/assign/tests/behat/group_submission.feature index 83c24f01380c0..2ecf07d62cbcb 100644 --- a/public/mod/assign/tests/behat/group_submission.feature +++ b/public/mod/assign/tests/behat/group_submission.feature @@ -422,3 +422,33 @@ Feature: Group assignment submissions Then I should see "Student 1" And I should see "Student 3" And I should not see "Student 2" + + @javascript + Scenario: All groups including non-participation groups can be selected when viewing the grade summary + Given the following "groups" exist: + | name | course | idnumber | participation | visibility | + | Group 2 | C1 | G2 | 0 | 0 | + | Group 3 | C1 | G3 | 0 | 3 | + And the following "group members" exist: + | group | user | + | G1 | student1 | + | G2 | student1 | + | G1 | student2 | + | G2 | student3 | + And the following "activity" exists: + | activity | assign | + | course | C1 | + | name | Test assignment name | + | submissiondrafts | 0 | + | teamsubmission | 1 | + | groupmode | 1 | + And the following "mod_assign > submissions" exist: + | assign | user | onlinetext | + | Test assignment name | student1 | I'm the student's first submission | + | Test assignment name | student3 | I'm the student's first submission | + When I am on the "Test assignment name" Activity page logged in as teacher1 + Then "Non-participation" "optgroup" should exist in the "select[name=group]" "css_element" + And "Group 1" "option" should exist in the "select[name=group]" "css_element" + And "Group 2" "option" should exist in the "optgroup[label=Non-participation]" "css_element" + And "Group 3" "option" should exist in the "optgroup[label=Non-participation]" "css_element" + And "Group 1" "option" should not exist in the "optgroup[label=Non-participation]" "css_element" From d3022c17d54b9e833a2757bfee2f2b3b0c10df98 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Wed, 1 Oct 2025 14:51:44 +0700 Subject: [PATCH 037/553] MDL-86762 repository_flickr: Fix Flickr fetching and downloading --- public/repository/flickr/lib.php | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/public/repository/flickr/lib.php b/public/repository/flickr/lib.php index 7f33ae18dcfdc..3fcc87109dfa0 100644 --- a/public/repository/flickr/lib.php +++ b/public/repository/flickr/lib.php @@ -236,10 +236,11 @@ public function search($searchtext, $page = 0) { if (substr($p->title, strlen($p->title) - strlen($format)) != $format) { $p->title .= $format; } + $source = $this->flickr->get_photo_url($p->id); // Perform a HEAD request to the image to obtain it's Content-Length. $curl = new curl(); - $curl->head($p->url_o); + $curl->head($source); $ret['list'][] = [ 'title' => $p->title, @@ -248,7 +249,7 @@ public function search($searchtext, $page = 0) { 'thumbnail' => $p->url_sq, 'datecreated' => $p->dateupload, 'datemodified' => $p->lastupdate, - 'url' => $p->url_o, + 'url' => $source, 'author' => $p->ownername, 'size' => (int)($curl->get_info()['download_content_length']), 'image_width' => $p->width_o, @@ -298,14 +299,26 @@ public function get_link($photoid) { return $this->flickr->get_photo_url($photoid); } - /** - * - * @param string $photoid - * @param string $file - * @return string - */ + #[\Override] public function get_file($photoid, $file = '') { - return parent::get_file($this->flickr->get_photo_url($photoid), $file); + global $CFG; + + $url = $this->flickr->get_photo_url($photoid); + + $path = $this->prepare_file($file); + $c = new curl(); + $c->setopt([ + 'CURLOPT_USERAGENT' => flickr_client::user_agent(), + ]); + + $result = $c->download_one($url, null, [ + 'filepath' => $path, + 'timeout' => $CFG->repositorygetfiletimeout, + ]); + if ($result !== true) { + throw new moodle_exception('errorwhiledownload', 'repository', '', $result); + } + return ['path' => $path, 'url' => $url]; } /** From 1014dd0084c7d90fd20c89387c2074b83348eb04 Mon Sep 17 00:00:00 2001 From: Philipp Memmel Date: Mon, 18 Mar 2024 06:41:26 +0000 Subject: [PATCH 038/553] MDL-81263 grunt: Respect local .eslintignore and .stylelintignore --- .grunt/tasks/ignorefiles.js | 43 +++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/.grunt/tasks/ignorefiles.js b/.grunt/tasks/ignorefiles.js index c90fae11283bd..4c3cad5a20d99 100644 --- a/.grunt/tasks/ignorefiles.js +++ b/.grunt/tasks/ignorefiles.js @@ -58,6 +58,31 @@ module.exports = grunt => { }) + "\n"); }; + /** + * Extracts ignore entries from a local ignore file. + * + * @param {string} componentPath the file path to the component, relative to the code base directory + * @param {string} ignoreFilePath the path to the ignore file + * @return {array} array of ignore paths to be included in the global ignore files + */ + const getEntriesFromLocalIgnoreFile = (componentPath, ignoreFilePath) => { + const ignorePaths = []; + if (grunt.file.exists(ignoreFilePath)) { + const ignoreFile = grunt.file.read(ignoreFilePath); + const entries = ignoreFile.split('\n'); + entries.forEach(entry => { + entry = entry.trim(); + if (entry.length > 0 && !entry.startsWith('#') && !entry.startsWith('!')) { + while (entry.startsWith('/')) { + entry = entry.substring(1); + } + ignorePaths.push(componentPath + '/' + entry); + } + }); + } + return ignorePaths; + }; + /** * Generate ignore files (utilising thirdpartylibs.xml data) */ @@ -67,6 +92,20 @@ module.exports = grunt => { // An array of paths to third party directories. const thirdPartyPaths = ComponentList.getThirdPartyPaths(); + const localStylelintIgnorePaths = []; + const localEslintIgnorePaths = []; + ComponentList.getComponentPaths(process.cwd() + '/').forEach(componentPath => { + const localEslintIgnorePath = process.cwd() + '/' + componentPath + '/.eslintignore'; + const localEslintIgnoreEntries = getEntriesFromLocalIgnoreFile(componentPath, localEslintIgnorePath); + if (localEslintIgnoreEntries.length > 0) { + localEslintIgnorePaths.push(...localEslintIgnoreEntries); + } + const localStylelintIgnorePath = process.cwd() + '/' + componentPath + '/.stylelintignore'; + const localStylelintIgnoreEntries = getEntriesFromLocalIgnoreFile(componentPath, localStylelintIgnorePath); + if (localStylelintIgnoreEntries.length > 0) { + localStylelintIgnorePaths.push(...localStylelintIgnoreEntries); + } + }); // Generate .eslintignore. const eslintIgnores = [ @@ -77,7 +116,7 @@ module.exports = grunt => { // Ignore all yui/src meta directories and build directories. '*/**/yui/src/*/meta/', '*/**/build/', - ].concat(thirdPartyPaths); + ].concat(thirdPartyPaths).concat(localEslintIgnorePaths); grunt.file.write('.eslintignore', eslintIgnores.join('\n') + '\n'); // Generate .stylelintignore. @@ -88,7 +127,7 @@ module.exports = grunt => { 'public/theme/classic/style/moodle.css', 'jsdoc/styles/*.css', 'public/admin/tool/componentlibrary/hugo/dist/css/docs.css', - ].concat(thirdPartyPaths); + ].concat(thirdPartyPaths).concat(localStylelintIgnorePaths); grunt.file.write('.stylelintignore', stylelintIgnores.join('\n') + '\n'); phpcsIgnore(thirdPartyPaths); From b57d39e1feb9df3a46262c0f094e7a4a688dba44 Mon Sep 17 00:00:00 2001 From: Simey Lameze Date: Mon, 4 Aug 2025 11:15:53 +0800 Subject: [PATCH 039/553] MDL-86186 behat: enlarge window size before asserting --- public/admin/tests/behat/browse_users.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/public/admin/tests/behat/browse_users.feature b/public/admin/tests/behat/browse_users.feature index cdbcfbeef585e..2c7d3de127500 100644 --- a/public/admin/tests/behat/browse_users.feature +++ b/public/admin/tests/behat/browse_users.feature @@ -90,6 +90,7 @@ Feature: An administrator can browse user accounts | username | firstname | lastname | email | confirmed | | user3 | User | Three | three@example.com | 0 | And I navigate to "Users > Accounts > Browse list of users" in site administration + And I change window size to "large" Then I should see "Confirmation pending" in the "User Three" "table_row" And I press "Resend confirmation email" action in the "User Three" report row And I should see "Confirmation email sent successfully" From 265f472f58e528c70e9c1967961ea883088b521f Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 12 Aug 2025 21:30:57 +0100 Subject: [PATCH 040/553] MDL-86147 block_myoverview: consistent zero-state view styling. Make the zero state template consistent with the populated course template from 6415776d, specifically in regards to borders. --- public/blocks/myoverview/templates/zero-state.mustache | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/blocks/myoverview/templates/zero-state.mustache b/public/blocks/myoverview/templates/zero-state.mustache index b719518362ca0..403679c93353b 100644 --- a/public/blocks/myoverview/templates/zero-state.mustache +++ b/public/blocks/myoverview/templates/zero-state.mustache @@ -45,8 +45,6 @@ }}

', + '
' + this.get('headerContent') + '
', Y.WidgetStdMod.REPLACE); // Initialise the element cache. diff --git a/public/lib/yui/src/notification/js/dialogue.js b/public/lib/yui/src/notification/js/dialogue.js index bfad4c1a5f867..fa06a00018215 100644 --- a/public/lib/yui/src/notification/js/dialogue.js +++ b/public/lib/yui/src/notification/js/dialogue.js @@ -110,7 +110,7 @@ Y.extend(DIALOGUE, Y.Panel, { } this.setStdModContent(Y.WidgetStdMod.HEADER, - '
' + this.get('headerContent') + '
', + '
' + this.get('headerContent') + '
', Y.WidgetStdMod.REPLACE); // Initialise the element cache. From 95a1e4dea27b8114d6badbda4f2c1198698df141 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Fri, 17 Oct 2025 14:06:46 +0100 Subject: [PATCH 074/553] MDL-86952 output: truncate long text in drag/drop dialog options. --- .../build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js | 3 ++- .../build/moodle-core-dragdrop/moodle-core-dragdrop-min.js | 4 ++-- .../yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js | 3 ++- public/lib/yui/src/dragdrop/js/dragdrop.js | 3 ++- public/theme/boost/scss/moodle/core.scss | 5 +++++ public/theme/boost/style/moodle.css | 5 +++++ public/theme/classic/style/moodle.css | 5 +++++ 7 files changed, 23 insertions(+), 5 deletions(-) diff --git a/public/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js b/public/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js index 5b118d2358fe0..0d0613b42718d 100644 --- a/public/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js +++ b/public/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js @@ -429,7 +429,7 @@ Y.extend(DRAGDROP, Y.Base, { // Build the list of drop targets. var droplist = Y.Node.create('
    '); - droplist.addClass('dragdrop-keyboard-drag'); + droplist.addClass('dragdrop-keyboard-drag ps-2'); var listitem, listlink, listitemtext; // Search for possible drop targets. @@ -479,6 +479,7 @@ Y.extend(DRAGDROP, Y.Base, { } listlink.setContent(listitemtext); + listlink.setAttribute('class', 'aalink d-inline-block mw-100 text-truncate'); // Add a data attribute so we can get the real drop target. listlink.setAttribute('data-drop-target', node.get('id')); // Allow tabbing to the link. diff --git a/public/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js b/public/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js index 6788968f55c84..19dffe2c3084c 100644 --- a/public/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js +++ b/public/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js @@ -1,2 +1,2 @@ -YUI.add("moodle-core-dragdrop",function(h,t){var d="moodle-core-dragdrop-draghandle",e=function(){e.superclass.constructor.apply(this,arguments)};h.extend(e,h.Base,{goingup:null,absgoingup:null,samenodeclass:null,parentnodeclass:null,samenodelabel:null,parentnodelabel:null,groups:[],lastdroptarget:null,detectkeyboarddirection:!1,listeners:null,initializer:function(){this.listeners=[],this.listeners.push(h.DD.DDM.on("drag:start",this.global_drag_start,this)),this.listeners.push(h.DD.DDM.on("drag:over",this.globalDragOver,this)),this.listeners.push(h.DD.DDM.on("drag:end",this.global_drag_end,this)),this.listeners.push(h.DD.DDM.on("drag:drag",this.global_drag_drag,this)),this.listeners.push(h.DD.DDM.on("drop:over",this.global_drop_over,this)),this.listeners.push(h.DD.DDM.on("drop:hit",this.global_drop_hit,this)),this.listeners.push(h.DD.DDM.on("drag:dropmiss",this.global_drag_dropmiss,this)),this.listeners.push(h.one(h.config.doc.body).delegate("key",this.global_keydown,"down:32, enter, esc","."+d,this)),this.listeners.push(h.one(h.config.doc.body).delegate("click",this.global_keydown,"."+d,this))},destructor:function(){new h.EventHandle(this.listeners).detach()},get_drag_handle:function(t,e,o){var r=h.Node.create("").addClass(e).setAttribute("title",t).setAttribute("tabIndex",0).setAttribute("data-draggroups",this.groups).setAttribute("role","button");return r.addClass(d),window.require(["core/templates"],function(t){t.renderPix("i/move_2d","core").then(function(t){t=h.Node.create(t);t.setStyle("cursor","move"),void 0!==o&&t.addClass(o),r.appendChild(t)})}),r},lock_drag_handle:function(t,e){t.removeHandle("."+e)},unlock_drag_handle:function(t,e){t.addHandle("."+e),t.get("activeHandle").focus()},ajax_failure:function(t){t={name:t.status+" "+t.statusText,message:t.responseText};return new M.core.exception(t)},in_group:function(e){var o=!1;return h.each(this.groups,function(t){e._groups[t]&&(o=!0)},this),o},global_drag_start:function(t){var e=t.target;this.in_group(e)&&(this.originalstyle=e.get("node").getAttribute("style"),e.get("node").setStyle("opacity",".25"),e.get("dragNode").setStyles({opacity:".75",borderColor:e.get("node").getStyle("borderColor"),backgroundColor:e.get("node").getStyle("backgroundColor")}),e.get("dragNode").empty(),this.drag_start(t))},globalDragOver:function(t){this.dragOver(t)},global_drag_end:function(t){var e=t.target;this.in_group(e)&&(e.get("node").setAttribute("style",this.originalstyle),this.drag_end(t))},global_drag_drag:function(t){var e=t.target,o=t.info;this.in_group(e)&&(o.start[1]o.xy[1]&&(this.absgoingup=!1),o.delta[1]<0?this.goingup=!0:0")).addClass("dragdrop-keyboard-drag"),h.all("."+this.samenodeclass+", ."+this.parentnodeclass).each(function(t){var e,o,r,a=!1,n=t,i=t.getAttribute("class").split(" ").join(", .");if(t.drop&&t.drop.inGroup(this.groups)&&t.drop.get("node")!==d&&(t.next(i)!==d||this.detectkeyboarddirection))a=!0;else for(e=t.getAttribute("data-draggroups").split(" "),o=0;o"),p=h.Node.create(""),s=this.find_element_text(n),u=this.samenodelabel&&t.hasClass(this.samenodeclass)?M.util.get_string(this.samenodelabel.identifier,this.samenodelabel.component,s):this.parentnodelabel&&t.hasClass(this.parentnodeclass)?M.util.get_string(this.parentnodelabel.identifier,this.parentnodelabel.component,s):M.util.get_string("tocontent","moodle",s),p.setContent(u),p.setAttribute("data-drop-target",t.get("id")),p.setAttribute("tabindex","0"),p.setAttribute("role","button"),p.on("click",this.global_keyboard_drop,this),p.on("key",this.global_keyboard_drop,"down:enter,32",this),g.append(p),l.append(g))},this),M.core.dragdrop.dropui=new M.core.dialogue({headerContent:e,bodyContent:l,draggable:!0,visible:!0,center:!0,modal:!0}),M.core.dragdrop.dropui.after("visibleChange",function(t){t.prevVal&&!t.newVal&&this.global_cancel_keyboard_drag()},this),l.one("a")&&l.one("a").focus()},simulated_drag_drop_event:function(t,e){var o=function(t){this.node=t};o.prototype.get=function(t){return"node"===t||"dragNode"===t||"dropNode"===t?this.node:"activeHandle"===t?this.node.one(".editing_move"):null},o.prototype.inGroup=function(){return!0},o.prototype.addHandle=function(){},o.prototype.removeHandle=function(){},this.drop=new o(e),this.drag=new o(t),this.target=this.drop},global_keyboard_drop:function(t){var e,o=M.core.dragdrop.keydragcontainer,r=h.one("#"+t.target.getAttribute("data-drop-target")); -M.core.dragdrop.dropui.hide(),t.preventDefault(),this.detectkeyboarddirection&&o.getY()>r.getY()?(this.absgoingup=!0,this.goingup=!0):(this.absgoingup=!1,this.goingup=!1),t=new this.simulated_drag_drop_event(o,o),e=new this.simulated_drag_drop_event(o,r),this.drag_start(t),this.global_drop_over(e),r.hasClass(this.parentnodeclass)&&r.contains(o)&&r.prepend(o),this.global_drop_hit(e)},global_cancel_keyboard_drag:function(){M.core.dragdrop.keydragcontainer&&(M.core.dragdrop.keydraghandle.focus(),M.core.dragdrop.keydragcontainer=null),M.core.dragdrop.dropui&&M.core.dragdrop.dropui.destroy()},global_keydown:function(t){var e,o,r,a,n,i=t.target.ancestor("."+d,!0);if(null!==i){if(27===t.keyCode)return this.global_cancel_keyboard_drag(),void t.preventDefault();if(i.hasClass(d)&&(13===t.keyCode||32===t.keyCode||"click"===t.type)){for(o=i.getAttribute("data-draggroups").split(" "),n=!1,r=0;r").addClass(e).setAttribute("title",t).setAttribute("tabIndex",0).setAttribute("data-draggroups",this.groups).setAttribute("role","button");return r.addClass(d),window.require(["core/templates"],function(t){t.renderPix("i/move_2d","core").then(function(t){t=h.Node.create(t);t.setStyle("cursor","move"),void 0!==o&&t.addClass(o),r.appendChild(t)})}),r},lock_drag_handle:function(t,e){t.removeHandle("."+e)},unlock_drag_handle:function(t,e){t.addHandle("."+e),t.get("activeHandle").focus()},ajax_failure:function(t){t={name:t.status+" "+t.statusText,message:t.responseText};return new M.core.exception(t)},in_group:function(e){var o=!1;return h.each(this.groups,function(t){e._groups[t]&&(o=!0)},this),o},global_drag_start:function(t){var e=t.target;this.in_group(e)&&(this.originalstyle=e.get("node").getAttribute("style"),e.get("node").setStyle("opacity",".25"),e.get("dragNode").setStyles({opacity:".75",borderColor:e.get("node").getStyle("borderColor"),backgroundColor:e.get("node").getStyle("backgroundColor")}),e.get("dragNode").empty(),this.drag_start(t))},globalDragOver:function(t){this.dragOver(t)},global_drag_end:function(t){var e=t.target;this.in_group(e)&&(e.get("node").setAttribute("style",this.originalstyle),this.drag_end(t))},global_drag_drag:function(t){var e=t.target,o=t.info;this.in_group(e)&&(o.start[1]o.xy[1]&&(this.absgoingup=!1),o.delta[1]<0?this.goingup=!0:0")).addClass("dragdrop-keyboard-drag ps-2"),h.all("."+this.samenodeclass+", ."+this.parentnodeclass).each(function(t){var e,o,r,a=!1,n=t,i=t.getAttribute("class").split(" ").join(", .");if(t.drop&&t.drop.inGroup(this.groups)&&t.drop.get("node")!==d&&(t.next(i)!==d||this.detectkeyboarddirection))a=!0;else for(e=t.getAttribute("data-draggroups").split(" "),o=0;o"),p=h.Node.create(""),s=this.find_element_text(n),u=this.samenodelabel&&t.hasClass(this.samenodeclass)?M.util.get_string(this.samenodelabel.identifier,this.samenodelabel.component,s):this.parentnodelabel&&t.hasClass(this.parentnodeclass)?M.util.get_string(this.parentnodelabel.identifier,this.parentnodelabel.component,s):M.util.get_string("tocontent","moodle",s),p.setContent(u),p.setAttribute("class","aalink d-inline-block mw-100 text-truncate"),p.setAttribute("data-drop-target",t.get("id")),p.setAttribute("tabindex","0"),p.setAttribute("role","button"),p.on("click",this.global_keyboard_drop,this),p.on("key",this.global_keyboard_drop,"down:enter,32",this),g.append(p),l.append(g))},this),M.core.dragdrop.dropui=new M.core.dialogue({headerContent:e,bodyContent:l,draggable:!0,visible:!0,center:!0,modal:!0}),M.core.dragdrop.dropui.after("visibleChange",function(t){t.prevVal&&!t.newVal&&this.global_cancel_keyboard_drag()},this),l.one("a")&&l.one("a").focus()},simulated_drag_drop_event:function(t,e){var o=function(t){this.node=t};o.prototype.get=function(t){return"node"===t||"dragNode"===t||"dropNode"===t?this.node:"activeHandle"===t?this.node.one(".editing_move"):null},o.prototype.inGroup=function(){return!0},o.prototype.addHandle=function(){},o.prototype.removeHandle=function(){},this.drop=new o(e),this.drag=new o(t),this.target=this.drop},global_keyboard_drop:function(t){var e, +o=M.core.dragdrop.keydragcontainer,r=h.one("#"+t.target.getAttribute("data-drop-target"));M.core.dragdrop.dropui.hide(),t.preventDefault(),this.detectkeyboarddirection&&o.getY()>r.getY()?(this.absgoingup=!0,this.goingup=!0):(this.absgoingup=!1,this.goingup=!1),t=new this.simulated_drag_drop_event(o,o),e=new this.simulated_drag_drop_event(o,r),this.drag_start(t),this.global_drop_over(e),r.hasClass(this.parentnodeclass)&&r.contains(o)&&r.prepend(o),this.global_drop_hit(e)},global_cancel_keyboard_drag:function(){M.core.dragdrop.keydragcontainer&&(M.core.dragdrop.keydraghandle.focus(),M.core.dragdrop.keydragcontainer=null),M.core.dragdrop.dropui&&M.core.dragdrop.dropui.destroy()},global_keydown:function(t){var e,o,r,a,n,i=t.target.ancestor("."+d,!0);if(null!==i){if(27===t.keyCode)return this.global_cancel_keyboard_drag(),void t.preventDefault();if(i.hasClass(d)&&(13===t.keyCode||32===t.keyCode||"click"===t.type)){for(o=i.getAttribute("data-draggroups").split(" "),n=!1,r=0;r'); - droplist.addClass('dragdrop-keyboard-drag'); + droplist.addClass('dragdrop-keyboard-drag ps-2'); var listitem, listlink, listitemtext; // Search for possible drop targets. @@ -479,6 +479,7 @@ Y.extend(DRAGDROP, Y.Base, { } listlink.setContent(listitemtext); + listlink.setAttribute('class', 'aalink d-inline-block mw-100 text-truncate'); // Add a data attribute so we can get the real drop target. listlink.setAttribute('data-drop-target', node.get('id')); // Allow tabbing to the link. diff --git a/public/lib/yui/src/dragdrop/js/dragdrop.js b/public/lib/yui/src/dragdrop/js/dragdrop.js index 68b0b02aad2bc..b93cc627ec1a6 100644 --- a/public/lib/yui/src/dragdrop/js/dragdrop.js +++ b/public/lib/yui/src/dragdrop/js/dragdrop.js @@ -427,7 +427,7 @@ Y.extend(DRAGDROP, Y.Base, { // Build the list of drop targets. var droplist = Y.Node.create('
      '); - droplist.addClass('dragdrop-keyboard-drag'); + droplist.addClass('dragdrop-keyboard-drag ps-2'); var listitem, listlink, listitemtext; // Search for possible drop targets. @@ -477,6 +477,7 @@ Y.extend(DRAGDROP, Y.Base, { } listlink.setContent(listitemtext); + listlink.setAttribute('class', 'aalink d-inline-block mw-100 text-truncate'); // Add a data attribute so we can get the real drop target. listlink.setAttribute('data-drop-target', node.get('id')); // Allow tabbing to the link. diff --git a/public/theme/boost/scss/moodle/core.scss b/public/theme/boost/scss/moodle/core.scss index 585acb210f2d9..6992d65a8955b 100644 --- a/public/theme/boost/scss/moodle/core.scss +++ b/public/theme/boost/scss/moodle/core.scss @@ -1764,6 +1764,11 @@ nav.navbar .logo img { ul.dragdrop-keyboard-drag li { list-style-type: none; + a, + a:hover { + color: inherit; + text-decoration: none; + } } a.disabled:hover, diff --git a/public/theme/boost/style/moodle.css b/public/theme/boost/style/moodle.css index 452a0e5f0cc22..9df134d874069 100644 --- a/public/theme/boost/style/moodle.css +++ b/public/theme/boost/style/moodle.css @@ -27360,6 +27360,11 @@ nav.navbar .logo img { ul.dragdrop-keyboard-drag li { list-style-type: none; } +ul.dragdrop-keyboard-drag li a, +ul.dragdrop-keyboard-drag li a:hover { + color: inherit; + text-decoration: none; +} a.disabled:hover, a.disabled { diff --git a/public/theme/classic/style/moodle.css b/public/theme/classic/style/moodle.css index 80de42a04351d..a5ed9ca9f66b2 100644 --- a/public/theme/classic/style/moodle.css +++ b/public/theme/classic/style/moodle.css @@ -27360,6 +27360,11 @@ nav.navbar .logo img { ul.dragdrop-keyboard-drag li { list-style-type: none; } +ul.dragdrop-keyboard-drag li a, +ul.dragdrop-keyboard-drag li a:hover { + color: inherit; + text-decoration: none; +} a.disabled:hover, a.disabled { From 50678b67d81108bfad9ef3ca9b1628f62dfa5a57 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Fri, 17 Oct 2025 14:43:02 +0100 Subject: [PATCH 075/553] MDL-86953 blocks: constrain block controls menu on narrow screens. Extension of work in 93cadaba that constrained the dropdown menu, we now set a breakpoint to account for overflowing when narrow. --- public/theme/boost/scss/moodle/blocks.scss | 3 +++ public/theme/boost/style/moodle.css | 5 +++++ public/theme/classic/style/moodle.css | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/public/theme/boost/scss/moodle/blocks.scss b/public/theme/boost/scss/moodle/blocks.scss index 3697d576a613b..4d63471e92102 100644 --- a/public/theme/boost/scss/moodle/blocks.scss +++ b/public/theme/boost/scss/moodle/blocks.scss @@ -32,6 +32,9 @@ } .dropdown-menu { max-width: 500px; + @include media-breakpoint-down(sm) { + max-width: 85vw; + } .dropdown-item { @include text-truncate; } diff --git a/public/theme/boost/style/moodle.css b/public/theme/boost/style/moodle.css index 452a0e5f0cc22..4a7cb4dcfcda1 100644 --- a/public/theme/boost/style/moodle.css +++ b/public/theme/boost/style/moodle.css @@ -29456,6 +29456,11 @@ img.icon { .block .block-controls .dropdown-menu { max-width: 500px; } +@media (max-width: 575.98px) { + .block .block-controls .dropdown-menu { + max-width: 85vw; + } +} .block .block-controls .dropdown-menu .dropdown-item { overflow: hidden; text-overflow: ellipsis; diff --git a/public/theme/classic/style/moodle.css b/public/theme/classic/style/moodle.css index 80de42a04351d..f51485414c9eb 100644 --- a/public/theme/classic/style/moodle.css +++ b/public/theme/classic/style/moodle.css @@ -29456,6 +29456,11 @@ img.icon { .block .block-controls .dropdown-menu { max-width: 500px; } +@media (max-width: 575.98px) { + .block .block-controls .dropdown-menu { + max-width: 85vw; + } +} .block .block-controls .dropdown-menu .dropdown-item { overflow: hidden; text-overflow: ellipsis; From 52d7c269220dcf734e2c8ea1e9bea6971ae2b9bf Mon Sep 17 00:00:00 2001 From: Gareth Barnard <1058419+gjb2048@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:18:09 +0100 Subject: [PATCH 076/553] MDL-86435 course: i/groupn.svg is not the same size as other icons. --- public/pix/i/groupn.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/pix/i/groupn.svg b/public/pix/i/groupn.svg index 46e33db34ed50..1efa70f8ff9b4 100644 --- a/public/pix/i/groupn.svg +++ b/public/pix/i/groupn.svg @@ -1,3 +1,3 @@ - - + + From 8da0cef43579b102ba3c7acc78a6585661dcc5dd Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Thu, 26 Jun 2025 21:13:38 +0800 Subject: [PATCH 077/553] MDL-85592 output: export missing node `sort` attribute for template. --- public/lib/classes/navigation/output/primary.php | 1 + 1 file changed, 1 insertion(+) diff --git a/public/lib/classes/navigation/output/primary.php b/public/lib/classes/navigation/output/primary.php index ddbf22285b088..672bf93473706 100644 --- a/public/lib/classes/navigation/output/primary.php +++ b/public/lib/classes/navigation/output/primary.php @@ -96,6 +96,7 @@ protected function get_primary_nav($parent = null): array { 'icon' => $node->icon, 'isactive' => $node->isactive || !empty($activechildren), 'key' => $node->key, + 'sort' => $node->key, 'children' => $children, 'haschildren' => !empty($children) ? 1 : 0, ]; From 30d7d03664a10ba1738475fc12882c77c642dda0 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Mon, 4 Aug 2025 17:25:53 +0100 Subject: [PATCH 078/553] MDL-86217 badges: gracefully handle missing recipient data. --- public/badges/badge.php | 2 +- public/badges/classes/output/issued_badge.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/badges/badge.php b/public/badges/badge.php index a6e6f28a8ac83..1d46520e0b281 100644 --- a/public/badges/badge.php +++ b/public/badges/badge.php @@ -80,7 +80,7 @@ $eventparams = array('context' => $PAGE->context, 'other' => $other); // If the badge does not belong to this user, log it appropriately. -if (($badge->recipient->id != $USER->id)) { +if ($badge->recipient && $badge->recipient->id != $USER->id) { $eventparams['relateduserid'] = $badge->recipient->id; } diff --git a/public/badges/classes/output/issued_badge.php b/public/badges/classes/output/issued_badge.php index 3022c99778d25..4bc58c9511234 100644 --- a/public/badges/classes/output/issued_badge.php +++ b/public/badges/classes/output/issued_badge.php @@ -74,7 +74,7 @@ public function __construct($hash) { $this->hash = $hash; $assertion = new \core_badges_assertion($hash, badges_open_badges_backpack_api()); $this->issued = $assertion->get_badge_assertion(); - if (!is_numeric($this->issued['issuedOn'])) { + if (array_key_exists('issuedOn', $this->issued) && !is_numeric($this->issued['issuedOn'])) { $this->issued['issuedOn'] = strtotime($this->issued['issuedOn']); } $this->badgeclass = $assertion->get_badge_class(); From 40e65cc0328da6b6e34f3715692175977a0f6811 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Thu, 17 Jul 2025 22:16:06 +0100 Subject: [PATCH 079/553] MDL-86063 customfield: internally validate numeric data in persistent. Move previous validation from the data controller, added in 89dbe63d, into the persistent class itself so that it can internally validate itself rather than relying on callers. This resolves problems with empty/null numeric fields contained within course backups (e.g. during course copy). --- public/customfield/classes/data.php | 45 ++++++++++++++----- .../customfield/classes/data_controller.php | 20 ++------- public/lib/classes/persistent.php | 2 +- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/public/customfield/classes/data.php b/public/customfield/classes/data.php index 99d5a205faca9..da1fabf9e5a97 100644 --- a/public/customfield/classes/data.php +++ b/public/customfield/classes/data.php @@ -14,24 +14,14 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Data persistent class - * - * @package core_customfield - * @copyright 2018 Toni Barbera - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace core_customfield; use core\persistent; -defined('MOODLE_INTERNAL') || die; - /** - * Class data + * Data persistent class * - * @package core_customfield + * @package core_customfield * @copyright 2018 Toni Barbera * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -121,4 +111,35 @@ protected static function define_properties(): array { ); } + /** + * For integer data field, persistent won't allow empty string, swap for null + * + * @param string|null $value + * @return self + */ + protected function set_intvalue(?string $value): self { + $value = (string) $value === '' ? null : (int) $value; + return $this->raw_set('intvalue', $value); + } + + /** + * For decimal data field, persistent won't allow empty string, swap for null + * + * @param string|null $value + * @return self + */ + protected function set_decvalue(?string $value): self { + $value = (string) $value === '' ? null : (float) $value; + return $this->raw_set('decvalue', $value); + } + + /** + * Ensure value field observes non-nullability + * + * @param string|null $value + * @return self + */ + protected function set_value(?string $value): self { + return $this->raw_set('value', (string) $value); + } } diff --git a/public/customfield/classes/data_controller.php b/public/customfield/classes/data_controller.php index ead81c30ff85c..77d4b01015d7b 100644 --- a/public/customfield/classes/data_controller.php +++ b/public/customfield/classes/data_controller.php @@ -14,21 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Customfield component data controller abstract class - * - * @package core_customfield - * @copyright 2018 Toni Barbera - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace core_customfield; use backup_nested_element; use core_customfield\output\field_data; -defined('MOODLE_INTERNAL') || die; - /** * Base class for custom fields data controllers * @@ -38,7 +28,7 @@ * Custom field plugins must define a class * \{pluginname}\data_controller extends \core_customfield\data_controller * - * @package core_customfield + * @package core_customfield * @copyright 2018 Toni Barbera * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -212,15 +202,11 @@ public function instance_form_save(\stdClass $datanew) { if (!property_exists($datanew, $elementname)) { return; } - $datafieldvalue = $value = $datanew->{$elementname}; - // For numeric datafields, persistent won't allow empty string, swap for null. $datafield = $this->datafield(); - if ($datafield === 'intvalue' || $datafield === 'decvalue') { - $datafieldvalue = $datafieldvalue === '' ? null : $datafieldvalue; - } + $value = $datanew->{$elementname}; - $this->data->set($datafield, $datafieldvalue); + $this->data->set($datafield, $value); $this->data->set('value', $value); // Set component, area and itemid from the handler. diff --git a/public/lib/classes/persistent.php b/public/lib/classes/persistent.php index 8308cd7988aee..5ebe007605562 100644 --- a/public/lib/classes/persistent.php +++ b/public/lib/classes/persistent.php @@ -95,7 +95,7 @@ final protected function verify_protected_methods() { * Data setter. * * This is the main setter for all the properties. Developers can implement their own setters (set_propertyname) - * and they will be called by this function. Custom setters should call internal_set() to finally set the value. + * and they will be called by this function. Custom setters should call {@see raw_set} to finally set the value. * Internally this is not used {@link self::to_record()} or * {@link self::from_record()} because the data is not expected to be validated or changed when reading/writing * raw records from the DB. From d74ce2b179fa7ca010bce471fc9f528f8d07ddf8 Mon Sep 17 00:00:00 2001 From: Mark Johnson Date: Wed, 9 Jul 2025 09:47:46 +0100 Subject: [PATCH 080/553] MDL-85975 backup: Handle nulls and arrays in restored questiondata unset_excluded_fields() was using isset() to determine if the field targeted for removal is present in the provided data structure. However, isset() returns false if the field exists, but contains null. This means the field will not be unset when it should be. This resolves this by changing the isset() to a property_exists() check for objects, and array_key_exists() check for arrays. It also expands the function to properly handle arrays of arrays, or arrays of values, which was not fully covered before and is used by some third-party question types. --- .upgradenotes/MDL-85975-2025092314370040.yml | 17 ++ .../moodle2/restore_qtype_plugin.class.php | 62 ++++++-- .../tests/restore_qtype_plugin_test.php | 147 ++++++++++++++++++ 3 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 .upgradenotes/MDL-85975-2025092314370040.yml create mode 100644 public/backup/moodle2/tests/restore_qtype_plugin_test.php diff --git a/.upgradenotes/MDL-85975-2025092314370040.yml b/.upgradenotes/MDL-85975-2025092314370040.yml new file mode 100644 index 0000000000000..263b753aca2d0 --- /dev/null +++ b/.upgradenotes/MDL-85975-2025092314370040.yml @@ -0,0 +1,17 @@ +issueNumber: MDL-85975 +notes: + core: + - message: > + `restore_qtype_plugin::unset_excluded_fields` now returns the modified + questiondata structure, + + in order to support structures that contain arrays. + + If your qtype plugin overrides + `restore_qtype_plugin::remove_excluded_question_data` without + + calling the parent method, you may need to modify your overridden method + to use the returned + + value. + type: fixed diff --git a/public/backup/moodle2/restore_qtype_plugin.class.php b/public/backup/moodle2/restore_qtype_plugin.class.php index 52b10c6a5488c..4b80a13a02233 100644 --- a/public/backup/moodle2/restore_qtype_plugin.class.php +++ b/public/backup/moodle2/restore_qtype_plugin.class.php @@ -568,8 +568,7 @@ public static function remove_excluded_question_data(stdClass $questiondata, arr foreach ($excludefields as $excludefield) { $pathparts = explode('/', ltrim($excludefield, '/')); - $data = $questiondata; - self::unset_excluded_fields($data, $pathparts); + $questiondata = self::unset_excluded_fields($questiondata, $pathparts); } return $questiondata; @@ -581,25 +580,60 @@ public static function remove_excluded_question_data(stdClass $questiondata, arr * If any of the elements in the path is an array, this is called recursively on each element in the array to unset fields * in each child of the array. * - * @param stdClass|array $data The questiondata object, or a subsection of it. + * @param stdClass|array $data The questiondata structure, or a subsection of it. * @param array $pathparts The remaining elements in the path to the excluded field. - * @return void + * @return stdClass|array The $data structure with excluded fields removed. */ - private static function unset_excluded_fields(stdClass|array $data, array $pathparts): void { + private static function unset_excluded_fields(stdClass|array $data, array $pathparts): stdClass|array { $element = array_shift($pathparts); - if (!isset($data->{$element})) { - // This element is not present in the data structure, nothing to unset. - return; + $unset = false; + // Get the current element from the data structure. + if (is_object($data)) { + if (!property_exists($data, $element)) { + // This element is not present in the data structure, nothing to unset. + return $data; + } + $dataelement = $data->{$element}; + } else { // It's an array. + if (!array_key_exists($element, $data)) { + return $data; + } + $dataelement = $data[$element]; } - if (is_object($data->{$element})) { - self::unset_excluded_fields($data->{$element}, $pathparts); - } else if (is_array($data->{$element})) { - foreach ($data->{$element} as $item) { - self::unset_excluded_fields($item, $pathparts); + // Check if we need to recur, or unset this element. + if (is_object($dataelement)) { + $dataelement = self::unset_excluded_fields($dataelement, $pathparts); + } else if (is_array($dataelement)) { + foreach ($dataelement as $key => $item) { + if (is_object($item) || is_array($item)) { + // This is an array of objects or arrays, recur. + $dataelement[$key] = self::unset_excluded_fields($item, $pathparts); + } else { + // This is an associative array of values, check if they should be removed. + $subelement = reset($pathparts); + if ($key == $subelement) { + unset($dataelement[$key]); + } + } } } else if (empty($pathparts)) { // This is the last element of the path and it's a scalar value, unset it. - unset($data->{$element}); + $unset = true; + } + // Write the modified element back to the data structure, or unset it. + if (is_object($data)) { + if ($unset) { + unset($data->{$element}); + } else { + $data->{$element} = $dataelement; + } + } else { + if ($unset) { + unset($data[$element]); + } else { + $data[$element] = $dataelement; + } } + return $data; } } diff --git a/public/backup/moodle2/tests/restore_qtype_plugin_test.php b/public/backup/moodle2/tests/restore_qtype_plugin_test.php new file mode 100644 index 0000000000000..527763281fec7 --- /dev/null +++ b/public/backup/moodle2/tests/restore_qtype_plugin_test.php @@ -0,0 +1,147 @@ +. + +namespace core; + +/** + * Tests for question type restore methods + * + * @package core + * @copyright 2025 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \restore_qtype_plugin + */ +final class restore_qtype_plugin_test extends \basic_testcase { + /** + * All default and specified fields should be removed from the provided data structure. + */ + public function test_remove_excluded_question_data(): void { + global $CFG; + require_once($CFG->dirroot . '/backup/moodle2/restore_plugin.class.php'); + require_once($CFG->dirroot . '/backup/moodle2/restore_qtype_plugin.class.php'); + $data = (object) [ + // Default excluded fields should be removed. + 'id' => 1, + 'createdby' => 2, + 'modifiedby' => 3, + // This field is not specified for removal, it should remain. + 'questiontext' => 'Some question text', + // Excluded paths that address an array should operate on all items in the array. + 'hints' => [ + (object) [ + 'id' => 4, + 'questionid' => 1, + // This field is not specified for removal. + 'text' => 'Lorem ipsum', + ], + (object) [ + 'id' => 5, + 'questionid' => 1, + 'text' => 'Lorem ipsum', + ], + ], + 'options' => [ // This is an array of arrays, rather than an array of objects. It should be handled the same. + [ + 'id' => 6, + 'questionid' => 1, + // This field is not specified for removal. + 'option' => true, + ], + [ + 'id' => 7, + 'questionid' => 1, + 'option' => false, + ], + [ + 'id' => 8, + 'questionid' => 1, + 'option' => false, + ], + ], + 'custom1' => 'Some custom text', + // This field is not specified for removal. + 'custom2' => 'Some custom text2', + // Fields specified for removal should be removed even if they contain null values. + 'custom3' => null, + 'customarray' => [ + (object) [ + // Null values should also be removed. + 'id' => null, + // This field is not specified for removal. + 'text' => 'Custom item text', + ], + (object) [ + 'id' => null, + 'text' => 'Custom item text2', + ], + ], + 'customstructure' => [ // This array contains scalar values, not a list of objects/arrays. + 'id' => null, + 'text' => 'Custom structure text', + 'number' => 1, + 'bool' => true, + ], + ]; + + $expecteddata = (object) [ + 'questiontext' => 'Some question text', + 'hints' => [ + (object) [ + 'text' => 'Lorem ipsum', + ], + (object) [ + 'text' => 'Lorem ipsum', + ], + ], + 'options' => [ + [ + 'option' => true, + ], + [ + 'option' => false, + ], + [ + 'option' => false, + ], + ], + 'custom2' => 'Some custom text2', + 'customarray' => [ + (object) [ + 'text' => 'Custom item text', + ], + (object) [ + 'text' => 'Custom item text2', + ], + ], + 'customstructure' => [ + 'text' => 'Custom structure text', + 'number' => 1, + ], + ]; + + $excludedfields = [ + '/custom1', + '/custom3', + '/customarray/id', + '/customstructure/id', + '/customstructure/bool', + // A field that is not in the data structure will be ignored. + '/custom4', + ]; + $this->assertEquals($expecteddata, \restore_qtype_plugin::remove_excluded_question_data($data, $excludedfields)); + } +} From fcc9b77f9c1c2b965e901cff321e66fd15aa0866 Mon Sep 17 00:00:00 2001 From: Andi Permana Date: Mon, 13 Oct 2025 11:14:47 +0700 Subject: [PATCH 081/553] MDL-86559 filter_activitynames: Fix auto links with double spaces --- .../activitynames/classes/text_filter.php | 31 ++++++++- .../activitynames/tests/text_filter_test.php | 67 ++++++++++++++++++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/public/filter/activitynames/classes/text_filter.php b/public/filter/activitynames/classes/text_filter.php index 2939c828ad413..ca8e321d25deb 100644 --- a/public/filter/activitynames/classes/text_filter.php +++ b/public/filter/activitynames/classes/text_filter.php @@ -54,12 +54,36 @@ public function filter($text, array $options = []) { } if ($filterslist) { - return $text = filter_phrases($text, $filterslist); + // Modify each filter's regex pattern to match any whitespace sequence where there are spaces. + // This allows matching activity names regardless of whether users type single spaces, double spaces, + // or non-breaking spaces (which HTML editors often insert). + $filterslist = filter_prepare_phrases_for_filtering($filterslist); + foreach ($filterslist as $filterobject) { + if ($filterobject->workregexp !== null) { + $filterobject->workregexp = self::replace_spaces_with_whitespace($filterobject->workregexp); + } + } + + return filter_phrases($text, $filterslist, null, null, false, true); } else { return $text; } } + /** + * Replace literal spaces in a regex with general whitespace match. + * + * @param string $regex The regex pattern containing literal spaces. + * @return string The regex pattern with spaces replaced. + */ + protected static function replace_spaces_with_whitespace($regex): string { + return preg_replace_callback('/ +/', function($matches): string { + $count = strlen($matches[0]); + // Matches regular space, non-breaking space (U+00A0), or other whitespace. + return '(?:[\s\xC2\xA0]{' . $count . '})'; + }, $regex); + } + /** * Get all the cached activity list for a course * @@ -115,6 +139,9 @@ protected function get_activity_list($courseid) { foreach ($sortedactivities as $cm) { $title = s(trim(strip_tags($cm->name))); $currentname = trim($cm->name); + // Normalize whitespace in activity names to handle double spaces and non-breaking spaces. + // This ensures that activities with multiple consecutive spaces can still be matched + // even when users type the name with different whitespace (e.g., single space, NBSP). $entitisedname = s($currentname); // Avoid empty or unlinkable activity names. if (!empty($title)) { @@ -133,4 +160,4 @@ protected function get_activity_list($courseid) { } return $activitylist; } -} +} \ No newline at end of file diff --git a/public/filter/activitynames/tests/text_filter_test.php b/public/filter/activitynames/tests/text_filter_test.php index b6713515ac1fa..a86bd2faa87f5 100644 --- a/public/filter/activitynames/tests/text_filter_test.php +++ b/public/filter/activitynames/tests/text_filter_test.php @@ -102,6 +102,71 @@ public function test_links_activity_named_hyphen(): void { $this->assertEquals($page->name, $matches[3][0]); } + /** + * Data provider for the test_links_with_whitespace. + * + * @return array + */ + public static function links_with_whitespace_provider(): array { + return [ + 'Regular spaces' => [ + 'Assignment 1', + '

      Go to Assignment 1

      ', + true, + ], + 'Two regular spaces' => [ + 'Assignment 1', + '

      Go to Assignment 1

      ', + true, + ], + 'NBSP + regular spaces' => [ + 'Assignment 1', + "

      Go to Assignment\xC2\xA0 1

      ", + true, + ], + 'Multiple spaces' => [ + 'Assignment 1 - History', + '

      Go to Assignment 1 - History

      ', + true, + ], + 'Mismatched spaces 1' => [ + 'Assignment 1', + '

      Go to Assignment 1

      ', + false, + ], + 'Mismatched spaces 2' => [ + 'Assignment 1', + '

      Go to Assignment 1

      ', + false, + ], + ]; + } + + /** + * Test that links can be matched with various whitespace combinations. + * + * @dataProvider links_with_whitespace_provider + * @param string $activityname + * @param string $html + * @param bool $expectedresult + */ + public function test_links_with_whitespace(string $activityname, string $html, bool $expectedresult): void { + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $context = \context_course::instance($course->id); + + $this->getDataGenerator()->create_module( + 'page', + ['course' => $course->id, 'name' => $activityname] + ); + + $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); + $haslink = strpos($filtered, 'assertEquals($expectedresult, $haslink); + } + public function test_cache(): void { $this->resetAfterTest(true); @@ -166,4 +231,4 @@ public function test_cache(): void { $this->assertEquals($page1->name, $matches[1][0]); $this->assertEquals($page2->name, $matches[1][1]); } -} +} \ No newline at end of file From b2801136441d34711453b078ba122d0109c1e3b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luca=20B=C3=B6sch?= Date: Tue, 29 Jul 2025 16:20:36 +0200 Subject: [PATCH 082/553] MDL-85511 course: back button for course management course search. --- .../classes/output/manage_categories_action_bar.php | 8 +++++++- .../templates/manage_category_actionbar.mustache | 11 +++++++++++ public/course/tests/behat/course_search.feature | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/public/course/classes/output/manage_categories_action_bar.php b/public/course/classes/output/manage_categories_action_bar.php index 44882bc141ca8..01314a2d6bfb4 100644 --- a/public/course/classes/output/manage_categories_action_bar.php +++ b/public/course/classes/output/manage_categories_action_bar.php @@ -154,11 +154,17 @@ protected function get_search_form(): array { * - renderedcontent Rendered content to be displayed in line with the tertiary nav */ public function export_for_template(\renderer_base $output): array { - return [ + $data = [ 'urlselect' => $this->get_dropdown($output), 'categoryselect' => $this->get_category_select($output), 'search' => $this->get_search_form(), 'heading' => $this->heading, ]; + + if ($this->searchvalue !== '') { + $backbutton = new \single_button(new moodle_url('/course/management.php'), get_string('back'), 'get'); + $data['backbutton'] = $backbutton->export_for_template($output); + } + return $data; } } diff --git a/public/course/templates/manage_category_actionbar.mustache b/public/course/templates/manage_category_actionbar.mustache index 9ed3cc04d7e70..f35d067c78cef 100644 --- a/public/course/templates/manage_category_actionbar.mustache +++ b/public/course/templates/manage_category_actionbar.mustache @@ -78,6 +78,12 @@ "helpicon":false, "attributes":[] }, + "backbutton": { + "url": "https://moodle.local/course/management.php", + "icon": "", + "title": "Back", + "classes": "btn btn-secondary" + }, "search": { "action": "https://moodle.local/admin/search.php", "extraclasses": "my-2", @@ -109,6 +115,11 @@ {{> core/url_select }}
      {{/categoryselect}} + {{#backbutton}} + + {{/backbutton}} {{#search}} {{#sectiontitle}}
      - {{sectiontitle}} + {{{sectiontitle}}}
      {{/sectiontitle}}
      diff --git a/public/course/tests/behat/course_overview.feature b/public/course/tests/behat/course_overview.feature index 8ed4ee0691f6f..e64d498cea878 100644 --- a/public/course/tests/behat/course_overview.feature +++ b/public/course/tests/behat/course_overview.feature @@ -281,6 +281,17 @@ Feature: Users can access the course activities overview page When I am on the "Course 1" "course > activities > assign" page logged in as "teacher1" Then I should not see "span" in the "assign_overview_collapsible" "region" + Scenario: Section name is properly filtered and rendered + Given the following config values are set as admin: + | formatstringstriptags | 0 | + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I click on "Edit settings" "link" in the "Section 1" "core_courseformat > Section actions menu" + And I set the field "Section name" to "Announcements$$(a+b)=2$$$$(a+b)=2$$" + And I press "Save changes" + When I am on the "Course 1" "course > activities > assign" page + Then I should not see "span" in the "assign_overview_collapsible" "region" + @javascript Scenario: Users in no group that cannot view all groups see an error on 'Separate groups' activities Given the following "users" exist: From 2030a3d87b23a18ee256ea54aa4bb244b50cdcb1 Mon Sep 17 00:00:00 2001 From: Jonathon Fowler Date: Wed, 23 Apr 2025 09:33:52 +1000 Subject: [PATCH 088/553] MDL-85474 assign: fix call to undefined 'view_error_page' method And while doing so, fix $notices parameters that ought to be call-by- reference to collect notices produced during calls to process_submit_other_for_grading() and process_submit_for_grading(). --- public/mod/assign/locallib.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/public/mod/assign/locallib.php b/public/mod/assign/locallib.php index c113e9ae59fd5..537b936399b3b 100644 --- a/public/mod/assign/locallib.php +++ b/public/mod/assign/locallib.php @@ -705,7 +705,7 @@ public function view($action='', $args = array()) { } else if ($action == 'viewbatchmarkingallocation') { $o .= $this->view_batch_markingallocation(); } else if ($action == 'viewsubmitforgradingerror') { - $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices); + $o .= $this->view_notices(get_string('submitforgrading', 'assign'), $notices); } else if ($action == 'fixrescalednullgrades') { $o .= $this->view_fix_rescaled_null_grades(); } else { @@ -7003,9 +7003,12 @@ public function submit_submission(stdClass $submission, int $userid): void { /** * A students submission is submitted for grading by a teacher. * + * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform. + * It can be null. + * @param array $notices Receives error messages to display on an error condition. * @return bool */ - protected function process_submit_other_for_grading($mform, $notices) { + protected function process_submit_other_for_grading($mform, &$notices) { global $USER, $CFG; require_sesskey(); @@ -7026,9 +7029,10 @@ protected function process_submit_other_for_grading($mform, $notices) { * * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform. * It can be null. + * @param array $notices Receives error messages to display on an error condition. * @return bool Return false if the validation fails. This affects which page is displayed next. */ - protected function process_submit_for_grading($mform, $notices) { + protected function process_submit_for_grading($mform, &$notices) { global $CFG; require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php'); From 3d46fc66eb403113187020a5dbe36ae35daa8820 Mon Sep 17 00:00:00 2001 From: Philipp Memmel Date: Fri, 11 Apr 2025 08:13:53 +0200 Subject: [PATCH 089/553] MDL-85168 lib: Move courseid filter to participants_filter.js --- public/lib/amd/build/datafilter.min.js | 2 +- public/lib/amd/build/datafilter.min.js.map | 2 +- public/lib/amd/src/datafilter.js | 5 +---- public/user/amd/build/participants_filter.min.js | 4 ++-- public/user/amd/build/participants_filter.min.js.map | 2 +- public/user/amd/src/participants_filter.js | 2 ++ 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/public/lib/amd/build/datafilter.min.js b/public/lib/amd/build/datafilter.min.js index 98353ea8c493e..dfd985da79b2a 100644 --- a/public/lib/amd/build/datafilter.min.js +++ b/public/lib/amd/build/datafilter.min.js @@ -1,3 +1,3 @@ -define("core/datafilter",["exports","core/datafilter/filtertypes/courseid","core/datafilter/filtertype","core/str","core/notification","core/pending","core/datafilter/selectors","core/templates","core/custom_interaction_events","jquery"],(function(_exports,_courseid,_filtertype,_str,_notification,_pending,_selectors,_templates,_custom_interaction_events,_jquery){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_courseid=_interopRequireDefault(_courseid),_filtertype=_interopRequireDefault(_filtertype),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_jquery=_interopRequireDefault(_jquery);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}return _exports.default=class{constructor(filterSet,applyCallback){this.filterSet=filterSet,this.applyCallback=applyCallback,this.activeFilters={courseid:new _courseid.default("courseid",filterSet)}}init(){this.filterSet.querySelector(_selectors.default.filterset.region).addEventListener("click",(e=>{e.target.closest(_selectors.default.filterset.actions.addRow)&&(e.preventDefault(),this.addFilterRow()),e.target.closest(_selectors.default.filterset.actions.applyFilters)&&(e.preventDefault(),this.updateTableFromFilter()),e.target.closest(_selectors.default.filterset.actions.resetFilters)&&(e.preventDefault(),this.removeAllFilters())})),this.filterSet.querySelector(_selectors.default.filterset.regions.filterlist).addEventListener("click",(e=>{e.target.closest(_selectors.default.filter.actions.remove)&&(e.preventDefault(),this.removeOrReplaceFilterRow(e.target.closest(_selectors.default.filter.region),!0))}));let filterRegion=(0,_jquery.default)(this.getFilterRegion());_custom_interaction_events.default.define(filterRegion,[_custom_interaction_events.default.events.accessibleChange]),filterRegion.on(_custom_interaction_events.default.events.accessibleChange,(e=>{const typeField=e.target.closest(_selectors.default.filter.fields.type);if(typeField&&typeField.value){const filter=e.target.closest(_selectors.default.filter.region);this.addFilter(filter,typeField.value)}})),this.filterSet.querySelector(_selectors.default.filterset.fields.join).addEventListener("change",(e=>{this.filterSet.dataset.filterverb=e.target.value}))}getFilterRegion(){return this.filterSet.querySelector(_selectors.default.filterset.regions.filterlist)}addFilterRow(){var _filterdata$rownum;let filterdata=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const pendingPromise=new _pending.default("core/datafilter:addFilterRow"),rownum=null!==(_filterdata$rownum=filterdata.rownum)&&void 0!==_filterdata$rownum?_filterdata$rownum:1+this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).length;return _templates.default.renderForPromise("core/datafilter/filter_row",{rownumber:rownum}).then((_ref=>{let{html:html,js:js}=_ref;return _templates.default.appendNodeContents(this.getFilterRegion(),html,js)})).then((filterRow=>{const typeList=this.filterSet.querySelector(_selectors.default.data.typeList);return filterRow.forEach((contentNode=>{const contentTypeList=contentNode.querySelector(_selectors.default.filter.fields.type);contentTypeList&&(contentTypeList.innerHTML=typeList.innerHTML)})),filterRow})).then((filterRow=>(this.updateFiltersOptions(),filterRow))).then((result=>(pendingPromise.resolve(),filterdata.filtertype&&result.forEach((filter=>{this.addFilter(filter,filterdata.filtertype,filterdata.values,filterdata.jointype,filterdata.filteroptions)})),result))).catch(_notification.default.exception)}getFilterDataSource(filterType){return this.filterSet.querySelector(_selectors.default.filterset.regions.datasource).querySelector(_selectors.default.data.fields.byName(filterType))}async addFilter(filterRow,filterType,initialFilterValues,filterJoin,filterOptions){filterRow.dataset.filterType=filterType;const filterDataNode=this.getFilterDataSource(filterType);let Filter=_filtertype.default;if(filterDataNode.dataset.filterTypeClass)try{Filter=await("function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([filterDataNode.dataset.filterTypeClass],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(filterDataNode.dataset.filterTypeClass)):Promise.resolve(_systemImportTransformerGlobalIdentifier[filterDataNode.dataset.filterTypeClass]))}catch(error){_notification.default.exception(error)}this.activeFilters[filterType]=new Filter(filterType,this.filterSet,initialFilterValues,filterOptions);const typeField=filterRow.querySelector(_selectors.default.filter.fields.type);typeField.value=filterType,typeField.disabled="disabled",this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList),filterRow);const joinField=filterRow.querySelector(_selectors.default.filter.fields.join);return isNaN(filterJoin)||(joinField.value=filterJoin),this.updateFiltersOptions(),this.activeFilters[filterType]}getFilterObject(name){return this.activeFilters[name]}removeOrReplaceFilterRow(filterRow,refreshContent){1===this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).length?this.replaceFilterRow(filterRow,refreshContent):this.removeFilterRow(filterRow,refreshContent)}async removeFilterRow(filterRow){let refreshContent=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];if(filterRow.querySelector(_selectors.default.data.required))return;const hasFilterValue=!!filterRow.querySelector(_selectors.default.filter.fields.type).value;this.removeFilterObject(filterRow.dataset.filterType),filterRow.remove(),this.updateFiltersOptions(),hasFilterValue&&refreshContent&&this.updateTableFromFilter();const filterLegends=await this.getAvailableFilterLegends();this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach(((filterRow,index)=>{filterRow.querySelector("legend").innerText=filterLegends[index]}))}replaceFilterRow(filterRow){let refreshContent=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],rowNum=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1;if(!filterRow.querySelector(_selectors.default.data.required))return this.removeFilterObject(filterRow.dataset.filterType),_templates.default.renderForPromise("core/datafilter/filter_row",{rownumber:rowNum}).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.replaceNode(filterRow,html,js)})).then((filterRow=>{const typeList=this.filterSet.querySelector(_selectors.default.data.typeList);return filterRow.forEach((contentNode=>{const contentTypeList=contentNode.querySelector(_selectors.default.filter.fields.type);contentTypeList&&(contentTypeList.innerHTML=typeList.innerHTML)})),filterRow})).then((filterRow=>(this.updateFiltersOptions(),filterRow))).then((filterRow=>refreshContent?this.updateTableFromFilter():filterRow)).catch(_notification.default.exception)}removeFilterObject(filterName){if(filterName){const filter=this.getFilterObject(filterName);filter&&(filter.tearDown(),delete this.activeFilters[filterName])}}removeAllFilters(){return this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach((filterRow=>this.removeOrReplaceFilterRow(filterRow,!1))),this.updateTableFromFilter()}removeEmptyFilters(){this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach((filterRow=>{filterRow.querySelector(_selectors.default.filter.fields.type).value||this.removeOrReplaceFilterRow(filterRow,!1)}))}updateFiltersOptions(){const filters=this.getFilterRegion().querySelectorAll(_selectors.default.filter.region);filters.forEach((filterRow=>{filterRow.querySelectorAll(_selectors.default.filter.fields.type+" option").forEach((option=>{option.value===filterRow.dataset.filterType?(option.classList.remove("hidden"),option.disabled=!1):this.activeFilters[option.value]?(option.classList.add("hidden"),option.disabled=!0):(option.classList.remove("hidden"),option.disabled=!1)}))}));const addRowButton=this.filterSet.querySelector(_selectors.default.filterset.actions.addRow);this.filterSet.querySelectorAll(_selectors.default.data.fields.all).length<=filters.length?addRowButton.setAttribute("disabled","disabled"):addRowButton.removeAttribute("disabled"),1===filters.length?(this.filterSet.querySelector(_selectors.default.filterset.regions.filtermatch).classList.add("hidden"),this.filterSet.querySelector(_selectors.default.filterset.fields.join).value=2,this.filterSet.dataset.filterverb=2):this.filterSet.querySelector(_selectors.default.filterset.regions.filtermatch).classList.remove("hidden")}updateTableFromFilter(){let validate=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];const pendingPromise=new _pending.default("core/datafilter:updateTableFromFilter"),filters={};let valid=!0;Object.values(this.activeFilters).forEach((filter=>{validate&&(valid=valid&&filter.validate()),filters[filter.filterValue.name]=filter.filterValue})),validate&&(valid=valid&&document.querySelector(_selectors.default.filter.region).closest("form").reportValidity()),this.applyCallback&&valid?this.applyCallback(filters,pendingPromise):pendingPromise.resolve()}async getAvailableFilterLegends(){const maxFilters=document.querySelector(_selectors.default.data.typeListSelect).length-1;let requests=[];[...Array(maxFilters)].forEach(((_,rowIndex)=>{requests.push({key:"filterrowlegend",component:"core",param:rowIndex+1})}));return await(0,_str.getStrings)(requests).then((fetchedStrings=>fetchedStrings)).catch(_notification.default.exception)}updateJoinList(filterJoinList,filterRow){const regularJoinList=[0,1,2];if(0!==filterJoinList.length){const joinField=filterRow.querySelector(_selectors.default.filter.fields.join);regularJoinList.forEach((join=>{filterJoinList.includes(join)||(joinField.options[join].classList.add("hidden"),joinField.options[join].disabled=!0)})),joinField.options.forEach(((element,index)=>{element.disabled&&(joinField.options[index]=null)})),1===joinField.options.length&&(joinField.hidden=!0)}}},_exports.default})); +define("core/datafilter",["exports","core/datafilter/filtertype","core/str","core/notification","core/pending","core/datafilter/selectors","core/templates","core/custom_interaction_events","jquery"],(function(_exports,_filtertype,_str,_notification,_pending,_selectors,_templates,_custom_interaction_events,_jquery){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=_interopRequireDefault(_filtertype),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_jquery=_interopRequireDefault(_jquery);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}return _exports.default=class{constructor(filterSet,applyCallback){this.filterSet=filterSet,this.applyCallback=applyCallback,this.activeFilters={}}init(){this.filterSet.querySelector(_selectors.default.filterset.region).addEventListener("click",(e=>{e.target.closest(_selectors.default.filterset.actions.addRow)&&(e.preventDefault(),this.addFilterRow()),e.target.closest(_selectors.default.filterset.actions.applyFilters)&&(e.preventDefault(),this.updateTableFromFilter()),e.target.closest(_selectors.default.filterset.actions.resetFilters)&&(e.preventDefault(),this.removeAllFilters())})),this.filterSet.querySelector(_selectors.default.filterset.regions.filterlist).addEventListener("click",(e=>{e.target.closest(_selectors.default.filter.actions.remove)&&(e.preventDefault(),this.removeOrReplaceFilterRow(e.target.closest(_selectors.default.filter.region),!0))}));let filterRegion=(0,_jquery.default)(this.getFilterRegion());_custom_interaction_events.default.define(filterRegion,[_custom_interaction_events.default.events.accessibleChange]),filterRegion.on(_custom_interaction_events.default.events.accessibleChange,(e=>{const typeField=e.target.closest(_selectors.default.filter.fields.type);if(typeField&&typeField.value){const filter=e.target.closest(_selectors.default.filter.region);this.addFilter(filter,typeField.value)}})),this.filterSet.querySelector(_selectors.default.filterset.fields.join).addEventListener("change",(e=>{this.filterSet.dataset.filterverb=e.target.value}))}getFilterRegion(){return this.filterSet.querySelector(_selectors.default.filterset.regions.filterlist)}addFilterRow(){var _filterdata$rownum;let filterdata=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const pendingPromise=new _pending.default("core/datafilter:addFilterRow"),rownum=null!==(_filterdata$rownum=filterdata.rownum)&&void 0!==_filterdata$rownum?_filterdata$rownum:1+this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).length;return _templates.default.renderForPromise("core/datafilter/filter_row",{rownumber:rownum}).then((_ref=>{let{html:html,js:js}=_ref;return _templates.default.appendNodeContents(this.getFilterRegion(),html,js)})).then((filterRow=>{const typeList=this.filterSet.querySelector(_selectors.default.data.typeList);return filterRow.forEach((contentNode=>{const contentTypeList=contentNode.querySelector(_selectors.default.filter.fields.type);contentTypeList&&(contentTypeList.innerHTML=typeList.innerHTML)})),filterRow})).then((filterRow=>(this.updateFiltersOptions(),filterRow))).then((result=>(pendingPromise.resolve(),filterdata.filtertype&&result.forEach((filter=>{this.addFilter(filter,filterdata.filtertype,filterdata.values,filterdata.jointype,filterdata.filteroptions)})),result))).catch(_notification.default.exception)}getFilterDataSource(filterType){return this.filterSet.querySelector(_selectors.default.filterset.regions.datasource).querySelector(_selectors.default.data.fields.byName(filterType))}async addFilter(filterRow,filterType,initialFilterValues,filterJoin,filterOptions){filterRow.dataset.filterType=filterType;const filterDataNode=this.getFilterDataSource(filterType);let Filter=_filtertype.default;if(filterDataNode.dataset.filterTypeClass)try{Filter=await("function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([filterDataNode.dataset.filterTypeClass],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(filterDataNode.dataset.filterTypeClass)):Promise.resolve(_systemImportTransformerGlobalIdentifier[filterDataNode.dataset.filterTypeClass]))}catch(error){_notification.default.exception(error)}this.activeFilters[filterType]=new Filter(filterType,this.filterSet,initialFilterValues,filterOptions);const typeField=filterRow.querySelector(_selectors.default.filter.fields.type);typeField.value=filterType,typeField.disabled="disabled",this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList),filterRow);const joinField=filterRow.querySelector(_selectors.default.filter.fields.join);return isNaN(filterJoin)||(joinField.value=filterJoin),this.updateFiltersOptions(),this.activeFilters[filterType]}getFilterObject(name){return this.activeFilters[name]}removeOrReplaceFilterRow(filterRow,refreshContent){1===this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).length?this.replaceFilterRow(filterRow,refreshContent):this.removeFilterRow(filterRow,refreshContent)}async removeFilterRow(filterRow){let refreshContent=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];if(filterRow.querySelector(_selectors.default.data.required))return;const hasFilterValue=!!filterRow.querySelector(_selectors.default.filter.fields.type).value;this.removeFilterObject(filterRow.dataset.filterType),filterRow.remove(),this.updateFiltersOptions(),hasFilterValue&&refreshContent&&this.updateTableFromFilter();const filterLegends=await this.getAvailableFilterLegends();this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach(((filterRow,index)=>{filterRow.querySelector("legend").innerText=filterLegends[index]}))}replaceFilterRow(filterRow){let refreshContent=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],rowNum=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1;if(!filterRow.querySelector(_selectors.default.data.required))return this.removeFilterObject(filterRow.dataset.filterType),_templates.default.renderForPromise("core/datafilter/filter_row",{rownumber:rowNum}).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.replaceNode(filterRow,html,js)})).then((filterRow=>{const typeList=this.filterSet.querySelector(_selectors.default.data.typeList);return filterRow.forEach((contentNode=>{const contentTypeList=contentNode.querySelector(_selectors.default.filter.fields.type);contentTypeList&&(contentTypeList.innerHTML=typeList.innerHTML)})),filterRow})).then((filterRow=>(this.updateFiltersOptions(),filterRow))).then((filterRow=>refreshContent?this.updateTableFromFilter():filterRow)).catch(_notification.default.exception)}removeFilterObject(filterName){if(filterName){const filter=this.getFilterObject(filterName);filter&&(filter.tearDown(),delete this.activeFilters[filterName])}}removeAllFilters(){return this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach((filterRow=>this.removeOrReplaceFilterRow(filterRow,!1))),this.updateTableFromFilter()}removeEmptyFilters(){this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach((filterRow=>{filterRow.querySelector(_selectors.default.filter.fields.type).value||this.removeOrReplaceFilterRow(filterRow,!1)}))}updateFiltersOptions(){const filters=this.getFilterRegion().querySelectorAll(_selectors.default.filter.region);filters.forEach((filterRow=>{filterRow.querySelectorAll(_selectors.default.filter.fields.type+" option").forEach((option=>{option.value===filterRow.dataset.filterType?(option.classList.remove("hidden"),option.disabled=!1):this.activeFilters[option.value]?(option.classList.add("hidden"),option.disabled=!0):(option.classList.remove("hidden"),option.disabled=!1)}))}));const addRowButton=this.filterSet.querySelector(_selectors.default.filterset.actions.addRow);this.filterSet.querySelectorAll(_selectors.default.data.fields.all).length<=filters.length?addRowButton.setAttribute("disabled","disabled"):addRowButton.removeAttribute("disabled"),1===filters.length?(this.filterSet.querySelector(_selectors.default.filterset.regions.filtermatch).classList.add("hidden"),this.filterSet.querySelector(_selectors.default.filterset.fields.join).value=2,this.filterSet.dataset.filterverb=2):this.filterSet.querySelector(_selectors.default.filterset.regions.filtermatch).classList.remove("hidden")}updateTableFromFilter(){let validate=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];const pendingPromise=new _pending.default("core/datafilter:updateTableFromFilter"),filters={};let valid=!0;Object.values(this.activeFilters).forEach((filter=>{validate&&(valid=valid&&filter.validate()),filters[filter.filterValue.name]=filter.filterValue})),validate&&(valid=valid&&document.querySelector(_selectors.default.filter.region).closest("form").reportValidity()),this.applyCallback&&valid?this.applyCallback(filters,pendingPromise):pendingPromise.resolve()}async getAvailableFilterLegends(){const maxFilters=document.querySelector(_selectors.default.data.typeListSelect).length-1;let requests=[];[...Array(maxFilters)].forEach(((_,rowIndex)=>{requests.push({key:"filterrowlegend",component:"core",param:rowIndex+1})}));return await(0,_str.getStrings)(requests).then((fetchedStrings=>fetchedStrings)).catch(_notification.default.exception)}updateJoinList(filterJoinList,filterRow){const regularJoinList=[0,1,2];if(0!==filterJoinList.length){const joinField=filterRow.querySelector(_selectors.default.filter.fields.join);regularJoinList.forEach((join=>{filterJoinList.includes(join)||(joinField.options[join].classList.add("hidden"),joinField.options[join].disabled=!0)})),joinField.options.forEach(((element,index)=>{element.disabled&&(joinField.options[index]=null)})),1===joinField.options.length&&(joinField.hidden=!0)}}},_exports.default})); //# sourceMappingURL=datafilter.min.js.map \ No newline at end of file diff --git a/public/lib/amd/build/datafilter.min.js.map b/public/lib/amd/build/datafilter.min.js.map index c51f0a1a00028..1dc0d6fe0301f 100644 --- a/public/lib/amd/build/datafilter.min.js.map +++ b/public/lib/amd/build/datafilter.min.js.map @@ -1 +1 @@ -{"version":3,"file":"datafilter.min.js","sources":["../src/datafilter.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 * Data filter management.\n *\n * @module core/datafilter\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport CourseFilter from 'core/datafilter/filtertypes/courseid';\nimport GenericFilter from 'core/datafilter/filtertype';\nimport {getStrings} from 'core/str';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Selectors from 'core/datafilter/selectors';\nimport Templates from 'core/templates';\nimport CustomEvents from 'core/custom_interaction_events';\nimport jQuery from 'jquery';\n\nexport default class {\n\n /**\n * Initialise the filter on the element with the given filterSet and callback.\n *\n * @param {HTMLElement} filterSet The filter element.\n * @param {Function} applyCallback Callback function when updateTableFromFilter\n */\n constructor(filterSet, applyCallback) {\n\n this.filterSet = filterSet;\n this.applyCallback = applyCallback;\n // Keep a reference to all of the active filters.\n this.activeFilters = {\n courseid: new CourseFilter('courseid', filterSet),\n };\n }\n\n /**\n * Initialise event listeners to the filter.\n */\n init() {\n // Add listeners for the main actions.\n this.filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {\n if (e.target.closest(Selectors.filterset.actions.addRow)) {\n e.preventDefault();\n\n this.addFilterRow();\n }\n\n if (e.target.closest(Selectors.filterset.actions.applyFilters)) {\n e.preventDefault();\n this.updateTableFromFilter();\n }\n\n if (e.target.closest(Selectors.filterset.actions.resetFilters)) {\n e.preventDefault();\n\n this.removeAllFilters();\n }\n });\n\n // Add the listener to remove a single filter.\n this.filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => {\n if (e.target.closest(Selectors.filter.actions.remove)) {\n e.preventDefault();\n\n this.removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region), true);\n }\n });\n\n // Add listeners for the filter type selection.\n let filterRegion = jQuery(this.getFilterRegion());\n CustomEvents.define(filterRegion, [CustomEvents.events.accessibleChange]);\n filterRegion.on(CustomEvents.events.accessibleChange, e => {\n const typeField = e.target.closest(Selectors.filter.fields.type);\n if (typeField && typeField.value) {\n const filter = e.target.closest(Selectors.filter.region);\n\n this.addFilter(filter, typeField.value);\n }\n });\n\n this.filterSet.querySelector(Selectors.filterset.fields.join).addEventListener('change', e => {\n this.filterSet.dataset.filterverb = e.target.value;\n });\n }\n\n /**\n * Get the filter list region.\n *\n * @return {HTMLElement}\n */\n getFilterRegion() {\n return this.filterSet.querySelector(Selectors.filterset.regions.filterlist);\n }\n\n /**\n * Add a filter row.\n *\n * @param {Object} filterdata Optional, data for adding for row with an existing filter.\n * @return {Promise}\n */\n addFilterRow(filterdata = {}) {\n const pendingPromise = new Pending('core/datafilter:addFilterRow');\n const rownum = filterdata.rownum ?? 1 + this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n return Templates.renderForPromise('core/datafilter/filter_row', {\"rownumber\": rownum})\n .then(({html, js}) => {\n const newContentNodes = Templates.appendNodeContents(this.getFilterRegion(), html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = this.filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n this.updateFiltersOptions();\n\n return filterRow;\n })\n .then(result => {\n pendingPromise.resolve();\n\n // If an existing filter is passed in, add it. Otherwise, leave the row empty.\n if (filterdata.filtertype) {\n result.forEach(filter => {\n this.addFilter(filter, filterdata.filtertype, filterdata.values,\n filterdata.jointype, filterdata.filteroptions);\n });\n }\n return result;\n })\n .catch(Notification.exception);\n }\n\n /**\n * Get the filter data source node fro the specified filter type.\n *\n * @param {String} filterType\n * @return {HTMLElement}\n */\n getFilterDataSource(filterType) {\n const filterDataNode = this.filterSet.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(filterType));\n }\n\n /**\n * Add a filter to the list of active filters, performing any necessary setup.\n *\n * @param {HTMLElement} filterRow\n * @param {String} filterType\n * @param {Array} initialFilterValues The initially selected values for the filter\n * @param {String} filterJoin\n * @param {Object} filterOptions\n * @returns {Filter}\n */\n async addFilter(filterRow, filterType, initialFilterValues, filterJoin, filterOptions) {\n // Name the filter on the filter row.\n filterRow.dataset.filterType = filterType;\n\n const filterDataNode = this.getFilterDataSource(filterType);\n\n // Instantiate the Filter class.\n let Filter = GenericFilter;\n if (filterDataNode.dataset.filterTypeClass) {\n\n // Ensure the filter class passed through exists, otherwise the filtering will break.\n try {\n Filter = await import(filterDataNode.dataset.filterTypeClass);\n } catch (error) {\n Notification.exception(error);\n }\n\n }\n this.activeFilters[filterType] = new Filter(filterType, this.filterSet, initialFilterValues, filterOptions);\n\n // Disable the select.\n const typeField = filterRow.querySelector(Selectors.filter.fields.type);\n typeField.value = filterType;\n typeField.disabled = 'disabled';\n // Update the join list.\n this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList), filterRow);\n const joinField = filterRow.querySelector(Selectors.filter.fields.join);\n if (!isNaN(filterJoin)) {\n joinField.value = filterJoin;\n }\n // Update the list of available filter types.\n this.updateFiltersOptions();\n\n return this.activeFilters[filterType];\n }\n\n /**\n * Get the registered filter class for the named filter.\n *\n * @param {String} name\n * @return {Object} See the Filter class.\n */\n getFilterObject(name) {\n return this.activeFilters[name];\n }\n\n /**\n * Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row,\n * that it is replaced instead of being removed.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n */\n removeOrReplaceFilterRow(filterRow, refreshContent) {\n const filterCount = this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n if (filterCount === 1) {\n this.replaceFilterRow(filterRow, refreshContent);\n } else {\n this.removeFilterRow(filterRow, refreshContent);\n }\n }\n\n /**\n * Remove the specified filter row and associated class.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n */\n async removeFilterRow(filterRow, refreshContent = true) {\n if (filterRow.querySelector(Selectors.data.required)) {\n return;\n }\n const filterType = filterRow.querySelector(Selectors.filter.fields.type);\n const hasFilterValue = !!filterType.value;\n\n // Remove the filter object.\n this.removeFilterObject(filterRow.dataset.filterType);\n\n // Remove the actual filter HTML.\n filterRow.remove();\n\n // Update the list of available filter types.\n this.updateFiltersOptions();\n\n if (hasFilterValue && refreshContent) {\n // Refresh the table if there was any content in this row.\n this.updateTableFromFilter();\n }\n\n // Update filter fieldset legends.\n const filterLegends = await this.getAvailableFilterLegends();\n\n this.getFilterRegion().querySelectorAll(Selectors.filter.region).forEach((filterRow, index) => {\n filterRow.querySelector('legend').innerText = filterLegends[index];\n });\n\n }\n\n /**\n * Replace the specified filter row with a new one.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n * @param {Number} rowNum The number used to label the filter fieldset legend (eg Row 1). Defaults to 1 (the first filter).\n * @return {Promise}\n */\n replaceFilterRow(filterRow, refreshContent = true, rowNum = 1) {\n if (filterRow.querySelector(Selectors.data.required)) {\n return;\n }\n // Remove the filter object.\n this.removeFilterObject(filterRow.dataset.filterType);\n\n return Templates.renderForPromise('core/datafilter/filter_row', {\"rownumber\": rowNum})\n .then(({html, js}) => {\n const newContentNodes = Templates.replaceNode(filterRow, html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = this.filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n this.updateFiltersOptions();\n\n return filterRow;\n })\n .then(filterRow => {\n // Refresh the table.\n if (refreshContent) {\n return this.updateTableFromFilter();\n } else {\n return filterRow;\n }\n })\n .catch(Notification.exception);\n }\n\n /**\n * Remove the Filter Object from the register.\n *\n * @param {string} filterName The name of the filter to be removed\n */\n removeFilterObject(filterName) {\n if (filterName) {\n const filter = this.getFilterObject(filterName);\n if (filter) {\n filter.tearDown();\n\n // Remove from the list of active filters.\n delete this.activeFilters[filterName];\n }\n }\n }\n\n /**\n * Remove all filters.\n *\n * @returns {Promise}\n */\n removeAllFilters() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => this.removeOrReplaceFilterRow(filterRow, false));\n\n // Refresh the table.\n return this.updateTableFromFilter();\n }\n\n /**\n * Remove any empty filters.\n */\n removeEmptyFilters() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const filterType = filterRow.querySelector(Selectors.filter.fields.type);\n if (!filterType.value) {\n this.removeOrReplaceFilterRow(filterRow, false);\n }\n });\n }\n\n /**\n * Update the list of filter types to filter out those already selected.\n */\n updateFiltersOptions() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option');\n options.forEach(option => {\n if (option.value === filterRow.dataset.filterType) {\n option.classList.remove('hidden');\n option.disabled = false;\n } else if (this.activeFilters[option.value]) {\n option.classList.add('hidden');\n option.disabled = true;\n } else {\n option.classList.remove('hidden');\n option.disabled = false;\n }\n });\n });\n\n // Configure the state of the \"Add row\" button.\n // This button is disabled when there is a filter row available for each condition.\n const addRowButton = this.filterSet.querySelector(Selectors.filterset.actions.addRow);\n const filterDataNode = this.filterSet.querySelectorAll(Selectors.data.fields.all);\n if (filterDataNode.length <= filters.length) {\n addRowButton.setAttribute('disabled', 'disabled');\n } else {\n addRowButton.removeAttribute('disabled');\n }\n\n if (filters.length === 1) {\n this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.add('hidden');\n this.filterSet.querySelector(Selectors.filterset.fields.join).value = 2;\n this.filterSet.dataset.filterverb = 2;\n } else {\n this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.remove('hidden');\n }\n }\n\n /**\n * Update the Dynamic table based upon the current filter.\n *\n * @param {bool} validate Should we validate the filters? We might want to skip this if the filters won't have changed,\n * for example for pagination/sorting.\n */\n updateTableFromFilter(validate = true) {\n const pendingPromise = new Pending('core/datafilter:updateTableFromFilter');\n\n const filters = {};\n let valid = true;\n Object.values(this.activeFilters).forEach(filter => {\n if (validate) {\n valid = valid && filter.validate();\n }\n filters[filter.filterValue.name] = filter.filterValue;\n });\n if (validate) {\n valid = valid && document.querySelector(Selectors.filter.region).closest('form').reportValidity();\n }\n if (this.applyCallback && valid) {\n this.applyCallback(filters, pendingPromise);\n } else {\n pendingPromise.resolve();\n }\n }\n\n /**\n * Fetch the strings used to populate the fieldset legends for the maximum number of filters possible.\n *\n * @return {array}\n */\n async getAvailableFilterLegends() {\n const maxFilters = document.querySelector(Selectors.data.typeListSelect).length - 1;\n let requests = [];\n\n [...Array(maxFilters)].forEach((_, rowIndex) => {\n requests.push({\n \"key\": \"filterrowlegend\",\n \"component\": \"core\",\n // Add 1 since rows begin at 1 (index begins at zero).\n \"param\": rowIndex + 1\n });\n });\n\n const legendStrings = await getStrings(requests)\n .then(fetchedStrings => {\n return fetchedStrings;\n })\n .catch(Notification.exception);\n\n return legendStrings;\n }\n\n /**\n * Update the list of join types for a filter.\n *\n * This will update the list of join types based on the allowed types defined for a filter.\n * If only one type is allowed, the list will be hidden.\n *\n * @param {Array} filterJoinList Array of join types, a subset of the regularJoinList array in this function.\n * @param {Element} filterRow The row being updated.\n */\n updateJoinList(filterJoinList, filterRow) {\n const regularJoinList = [0, 1, 2];\n // If a join list was specified for this filter, find the default join list and disable the options that are not allowed\n // for this filter.\n if (filterJoinList.length !== 0) {\n const joinField = filterRow.querySelector(Selectors.filter.fields.join);\n // Check each option from the default list, and disable the option in this filter row if it is not allowed\n // for this filter.\n regularJoinList.forEach((join) => {\n if (!filterJoinList.includes(join)) {\n joinField.options[join].classList.add('hidden');\n joinField.options[join].disabled = true;\n }\n });\n // Now remove the disabled options, and hide the select list of there is only one option left.\n joinField.options.forEach((element, index) => {\n if (element.disabled) {\n joinField.options[index] = null;\n }\n });\n if (joinField.options.length === 1) {\n joinField.hidden = true;\n }\n }\n }\n}\n"],"names":["constructor","filterSet","applyCallback","activeFilters","courseid","CourseFilter","init","querySelector","Selectors","filterset","region","addEventListener","e","target","closest","actions","addRow","preventDefault","addFilterRow","applyFilters","updateTableFromFilter","resetFilters","removeAllFilters","regions","filterlist","filter","remove","removeOrReplaceFilterRow","filterRegion","this","getFilterRegion","define","CustomEvents","events","accessibleChange","on","typeField","fields","type","value","addFilter","join","dataset","filterverb","filterdata","pendingPromise","Pending","rownum","querySelectorAll","length","Templates","renderForPromise","then","_ref","html","js","appendNodeContents","filterRow","typeList","data","forEach","contentNode","contentTypeList","innerHTML","updateFiltersOptions","result","resolve","filtertype","values","jointype","filteroptions","catch","Notification","exception","getFilterDataSource","filterType","datasource","byName","initialFilterValues","filterJoin","filterOptions","filterDataNode","Filter","GenericFilter","filterTypeClass","error","disabled","updateJoinList","JSON","parse","joinList","joinField","isNaN","getFilterObject","name","refreshContent","replaceFilterRow","removeFilterRow","required","hasFilterValue","removeFilterObject","filterLegends","getAvailableFilterLegends","index","innerText","rowNum","_ref2","replaceNode","filterName","tearDown","removeEmptyFilters","filters","option","classList","add","addRowButton","all","setAttribute","removeAttribute","filtermatch","validate","valid","Object","filterValue","document","reportValidity","maxFilters","typeListSelect","requests","Array","_","rowIndex","push","fetchedStrings","filterJoinList","regularJoinList","includes","options","element","hidden"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"datafilter.min.js","sources":["../src/datafilter.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 * Data filter management.\n *\n * @module core/datafilter\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport GenericFilter from 'core/datafilter/filtertype';\nimport {getStrings} from 'core/str';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Selectors from 'core/datafilter/selectors';\nimport Templates from 'core/templates';\nimport CustomEvents from 'core/custom_interaction_events';\nimport jQuery from 'jquery';\n\nexport default class {\n\n /**\n * Initialise the filter on the element with the given filterSet and callback.\n *\n * @param {HTMLElement} filterSet The filter element.\n * @param {Function} applyCallback Callback function when updateTableFromFilter\n */\n constructor(filterSet, applyCallback) {\n\n this.filterSet = filterSet;\n this.applyCallback = applyCallback;\n // Keep a reference to all of the active filters.\n this.activeFilters = {};\n }\n\n /**\n * Initialise event listeners to the filter.\n */\n init() {\n // Add listeners for the main actions.\n this.filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {\n if (e.target.closest(Selectors.filterset.actions.addRow)) {\n e.preventDefault();\n\n this.addFilterRow();\n }\n\n if (e.target.closest(Selectors.filterset.actions.applyFilters)) {\n e.preventDefault();\n this.updateTableFromFilter();\n }\n\n if (e.target.closest(Selectors.filterset.actions.resetFilters)) {\n e.preventDefault();\n\n this.removeAllFilters();\n }\n });\n\n // Add the listener to remove a single filter.\n this.filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => {\n if (e.target.closest(Selectors.filter.actions.remove)) {\n e.preventDefault();\n\n this.removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region), true);\n }\n });\n\n // Add listeners for the filter type selection.\n let filterRegion = jQuery(this.getFilterRegion());\n CustomEvents.define(filterRegion, [CustomEvents.events.accessibleChange]);\n filterRegion.on(CustomEvents.events.accessibleChange, e => {\n const typeField = e.target.closest(Selectors.filter.fields.type);\n if (typeField && typeField.value) {\n const filter = e.target.closest(Selectors.filter.region);\n\n this.addFilter(filter, typeField.value);\n }\n });\n\n this.filterSet.querySelector(Selectors.filterset.fields.join).addEventListener('change', e => {\n this.filterSet.dataset.filterverb = e.target.value;\n });\n }\n\n /**\n * Get the filter list region.\n *\n * @return {HTMLElement}\n */\n getFilterRegion() {\n return this.filterSet.querySelector(Selectors.filterset.regions.filterlist);\n }\n\n /**\n * Add a filter row.\n *\n * @param {Object} filterdata Optional, data for adding for row with an existing filter.\n * @return {Promise}\n */\n addFilterRow(filterdata = {}) {\n const pendingPromise = new Pending('core/datafilter:addFilterRow');\n const rownum = filterdata.rownum ?? 1 + this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n return Templates.renderForPromise('core/datafilter/filter_row', {\"rownumber\": rownum})\n .then(({html, js}) => {\n const newContentNodes = Templates.appendNodeContents(this.getFilterRegion(), html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = this.filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n this.updateFiltersOptions();\n\n return filterRow;\n })\n .then(result => {\n pendingPromise.resolve();\n\n // If an existing filter is passed in, add it. Otherwise, leave the row empty.\n if (filterdata.filtertype) {\n result.forEach(filter => {\n this.addFilter(filter, filterdata.filtertype, filterdata.values,\n filterdata.jointype, filterdata.filteroptions);\n });\n }\n return result;\n })\n .catch(Notification.exception);\n }\n\n /**\n * Get the filter data source node fro the specified filter type.\n *\n * @param {String} filterType\n * @return {HTMLElement}\n */\n getFilterDataSource(filterType) {\n const filterDataNode = this.filterSet.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(filterType));\n }\n\n /**\n * Add a filter to the list of active filters, performing any necessary setup.\n *\n * @param {HTMLElement} filterRow\n * @param {String} filterType\n * @param {Array} initialFilterValues The initially selected values for the filter\n * @param {String} filterJoin\n * @param {Object} filterOptions\n * @returns {Filter}\n */\n async addFilter(filterRow, filterType, initialFilterValues, filterJoin, filterOptions) {\n // Name the filter on the filter row.\n filterRow.dataset.filterType = filterType;\n\n const filterDataNode = this.getFilterDataSource(filterType);\n\n // Instantiate the Filter class.\n let Filter = GenericFilter;\n if (filterDataNode.dataset.filterTypeClass) {\n\n // Ensure the filter class passed through exists, otherwise the filtering will break.\n try {\n Filter = await import(filterDataNode.dataset.filterTypeClass);\n } catch (error) {\n Notification.exception(error);\n }\n\n }\n this.activeFilters[filterType] = new Filter(filterType, this.filterSet, initialFilterValues, filterOptions);\n\n // Disable the select.\n const typeField = filterRow.querySelector(Selectors.filter.fields.type);\n typeField.value = filterType;\n typeField.disabled = 'disabled';\n // Update the join list.\n this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList), filterRow);\n const joinField = filterRow.querySelector(Selectors.filter.fields.join);\n if (!isNaN(filterJoin)) {\n joinField.value = filterJoin;\n }\n // Update the list of available filter types.\n this.updateFiltersOptions();\n\n return this.activeFilters[filterType];\n }\n\n /**\n * Get the registered filter class for the named filter.\n *\n * @param {String} name\n * @return {Object} See the Filter class.\n */\n getFilterObject(name) {\n return this.activeFilters[name];\n }\n\n /**\n * Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row,\n * that it is replaced instead of being removed.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n */\n removeOrReplaceFilterRow(filterRow, refreshContent) {\n const filterCount = this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n if (filterCount === 1) {\n this.replaceFilterRow(filterRow, refreshContent);\n } else {\n this.removeFilterRow(filterRow, refreshContent);\n }\n }\n\n /**\n * Remove the specified filter row and associated class.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n */\n async removeFilterRow(filterRow, refreshContent = true) {\n if (filterRow.querySelector(Selectors.data.required)) {\n return;\n }\n const filterType = filterRow.querySelector(Selectors.filter.fields.type);\n const hasFilterValue = !!filterType.value;\n\n // Remove the filter object.\n this.removeFilterObject(filterRow.dataset.filterType);\n\n // Remove the actual filter HTML.\n filterRow.remove();\n\n // Update the list of available filter types.\n this.updateFiltersOptions();\n\n if (hasFilterValue && refreshContent) {\n // Refresh the table if there was any content in this row.\n this.updateTableFromFilter();\n }\n\n // Update filter fieldset legends.\n const filterLegends = await this.getAvailableFilterLegends();\n\n this.getFilterRegion().querySelectorAll(Selectors.filter.region).forEach((filterRow, index) => {\n filterRow.querySelector('legend').innerText = filterLegends[index];\n });\n\n }\n\n /**\n * Replace the specified filter row with a new one.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n * @param {Number} rowNum The number used to label the filter fieldset legend (eg Row 1). Defaults to 1 (the first filter).\n * @return {Promise}\n */\n replaceFilterRow(filterRow, refreshContent = true, rowNum = 1) {\n if (filterRow.querySelector(Selectors.data.required)) {\n return;\n }\n // Remove the filter object.\n this.removeFilterObject(filterRow.dataset.filterType);\n\n return Templates.renderForPromise('core/datafilter/filter_row', {\"rownumber\": rowNum})\n .then(({html, js}) => {\n const newContentNodes = Templates.replaceNode(filterRow, html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = this.filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n this.updateFiltersOptions();\n\n return filterRow;\n })\n .then(filterRow => {\n // Refresh the table.\n if (refreshContent) {\n return this.updateTableFromFilter();\n } else {\n return filterRow;\n }\n })\n .catch(Notification.exception);\n }\n\n /**\n * Remove the Filter Object from the register.\n *\n * @param {string} filterName The name of the filter to be removed\n */\n removeFilterObject(filterName) {\n if (filterName) {\n const filter = this.getFilterObject(filterName);\n if (filter) {\n filter.tearDown();\n\n // Remove from the list of active filters.\n delete this.activeFilters[filterName];\n }\n }\n }\n\n /**\n * Remove all filters.\n *\n * @returns {Promise}\n */\n removeAllFilters() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => this.removeOrReplaceFilterRow(filterRow, false));\n\n // Refresh the table.\n return this.updateTableFromFilter();\n }\n\n /**\n * Remove any empty filters.\n */\n removeEmptyFilters() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const filterType = filterRow.querySelector(Selectors.filter.fields.type);\n if (!filterType.value) {\n this.removeOrReplaceFilterRow(filterRow, false);\n }\n });\n }\n\n /**\n * Update the list of filter types to filter out those already selected.\n */\n updateFiltersOptions() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option');\n options.forEach(option => {\n if (option.value === filterRow.dataset.filterType) {\n option.classList.remove('hidden');\n option.disabled = false;\n } else if (this.activeFilters[option.value]) {\n option.classList.add('hidden');\n option.disabled = true;\n } else {\n option.classList.remove('hidden');\n option.disabled = false;\n }\n });\n });\n\n // Configure the state of the \"Add row\" button.\n // This button is disabled when there is a filter row available for each condition.\n const addRowButton = this.filterSet.querySelector(Selectors.filterset.actions.addRow);\n const filterDataNode = this.filterSet.querySelectorAll(Selectors.data.fields.all);\n if (filterDataNode.length <= filters.length) {\n addRowButton.setAttribute('disabled', 'disabled');\n } else {\n addRowButton.removeAttribute('disabled');\n }\n\n if (filters.length === 1) {\n this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.add('hidden');\n this.filterSet.querySelector(Selectors.filterset.fields.join).value = 2;\n this.filterSet.dataset.filterverb = 2;\n } else {\n this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.remove('hidden');\n }\n }\n\n /**\n * Update the Dynamic table based upon the current filter.\n *\n * @param {bool} validate Should we validate the filters? We might want to skip this if the filters won't have changed,\n * for example for pagination/sorting.\n */\n updateTableFromFilter(validate = true) {\n const pendingPromise = new Pending('core/datafilter:updateTableFromFilter');\n\n const filters = {};\n let valid = true;\n Object.values(this.activeFilters).forEach(filter => {\n if (validate) {\n valid = valid && filter.validate();\n }\n filters[filter.filterValue.name] = filter.filterValue;\n });\n if (validate) {\n valid = valid && document.querySelector(Selectors.filter.region).closest('form').reportValidity();\n }\n if (this.applyCallback && valid) {\n this.applyCallback(filters, pendingPromise);\n } else {\n pendingPromise.resolve();\n }\n }\n\n /**\n * Fetch the strings used to populate the fieldset legends for the maximum number of filters possible.\n *\n * @return {array}\n */\n async getAvailableFilterLegends() {\n const maxFilters = document.querySelector(Selectors.data.typeListSelect).length - 1;\n let requests = [];\n\n [...Array(maxFilters)].forEach((_, rowIndex) => {\n requests.push({\n \"key\": \"filterrowlegend\",\n \"component\": \"core\",\n // Add 1 since rows begin at 1 (index begins at zero).\n \"param\": rowIndex + 1\n });\n });\n\n const legendStrings = await getStrings(requests)\n .then(fetchedStrings => {\n return fetchedStrings;\n })\n .catch(Notification.exception);\n\n return legendStrings;\n }\n\n /**\n * Update the list of join types for a filter.\n *\n * This will update the list of join types based on the allowed types defined for a filter.\n * If only one type is allowed, the list will be hidden.\n *\n * @param {Array} filterJoinList Array of join types, a subset of the regularJoinList array in this function.\n * @param {Element} filterRow The row being updated.\n */\n updateJoinList(filterJoinList, filterRow) {\n const regularJoinList = [0, 1, 2];\n // If a join list was specified for this filter, find the default join list and disable the options that are not allowed\n // for this filter.\n if (filterJoinList.length !== 0) {\n const joinField = filterRow.querySelector(Selectors.filter.fields.join);\n // Check each option from the default list, and disable the option in this filter row if it is not allowed\n // for this filter.\n regularJoinList.forEach((join) => {\n if (!filterJoinList.includes(join)) {\n joinField.options[join].classList.add('hidden');\n joinField.options[join].disabled = true;\n }\n });\n // Now remove the disabled options, and hide the select list of there is only one option left.\n joinField.options.forEach((element, index) => {\n if (element.disabled) {\n joinField.options[index] = null;\n }\n });\n if (joinField.options.length === 1) {\n joinField.hidden = true;\n }\n }\n }\n}\n"],"names":["constructor","filterSet","applyCallback","activeFilters","init","querySelector","Selectors","filterset","region","addEventListener","e","target","closest","actions","addRow","preventDefault","addFilterRow","applyFilters","updateTableFromFilter","resetFilters","removeAllFilters","regions","filterlist","filter","remove","removeOrReplaceFilterRow","filterRegion","this","getFilterRegion","define","CustomEvents","events","accessibleChange","on","typeField","fields","type","value","addFilter","join","dataset","filterverb","filterdata","pendingPromise","Pending","rownum","querySelectorAll","length","Templates","renderForPromise","then","_ref","html","js","appendNodeContents","filterRow","typeList","data","forEach","contentNode","contentTypeList","innerHTML","updateFiltersOptions","result","resolve","filtertype","values","jointype","filteroptions","catch","Notification","exception","getFilterDataSource","filterType","datasource","byName","initialFilterValues","filterJoin","filterOptions","filterDataNode","Filter","GenericFilter","filterTypeClass","error","disabled","updateJoinList","JSON","parse","joinList","joinField","isNaN","getFilterObject","name","refreshContent","replaceFilterRow","removeFilterRow","required","hasFilterValue","removeFilterObject","filterLegends","getAvailableFilterLegends","index","innerText","rowNum","_ref2","replaceNode","filterName","tearDown","removeEmptyFilters","filters","option","classList","add","addRowButton","all","setAttribute","removeAttribute","filtermatch","validate","valid","Object","filterValue","document","reportValidity","maxFilters","typeListSelect","requests","Array","_","rowIndex","push","fetchedStrings","filterJoinList","regularJoinList","includes","options","element","hidden"],"mappings":"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"} \ No newline at end of file diff --git a/public/lib/amd/src/datafilter.js b/public/lib/amd/src/datafilter.js index d3fa3562e7293..d6a6a8a7c4ab2 100644 --- a/public/lib/amd/src/datafilter.js +++ b/public/lib/amd/src/datafilter.js @@ -21,7 +21,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import CourseFilter from 'core/datafilter/filtertypes/courseid'; import GenericFilter from 'core/datafilter/filtertype'; import {getStrings} from 'core/str'; import Notification from 'core/notification'; @@ -44,9 +43,7 @@ export default class { this.filterSet = filterSet; this.applyCallback = applyCallback; // Keep a reference to all of the active filters. - this.activeFilters = { - courseid: new CourseFilter('courseid', filterSet), - }; + this.activeFilters = {}; } /** diff --git a/public/user/amd/build/participants_filter.min.js b/public/user/amd/build/participants_filter.min.js index f30f6d8abe5b3..7ed3398b857dd 100644 --- a/public/user/amd/build/participants_filter.min.js +++ b/public/user/amd/build/participants_filter.min.js @@ -1,10 +1,10 @@ -define("core_user/participants_filter",["exports","core/datafilter","core_table/dynamic","core/datafilter/selectors","core/notification","core/pending"],(function(_exports,_datafilter,DynamicTable,_selectors,_notification,_pending){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_user/participants_filter",["exports","core/datafilter","core/datafilter/filtertypes/courseid","core_table/dynamic","core/datafilter/selectors","core/notification","core/pending"],(function(_exports,_datafilter,_courseid,DynamicTable,_selectors,_notification,_pending){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}} /** * Participants filter management. * * @module core_user/participants_filter * @copyright 2021 Tomo Tsuyuki * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_datafilter=_interopRequireDefault(_datafilter),DynamicTable=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}(DynamicTable),_selectors=_interopRequireDefault(_selectors),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);_exports.init=filterRegionId=>{const filterSet=document.getElementById(filterRegionId),coreFilter=new _datafilter.default(filterSet,(function(filters,pendingPromise){DynamicTable.setFilters(DynamicTable.getTableFromId(filterSet.dataset.tableRegion),{jointype:parseInt(filterSet.querySelector(_selectors.default.filterset.fields.join).value,10),filters:filters}).then((result=>(pendingPromise.resolve(),result))).catch(_notification.default.exception)}));coreFilter.init();const tableRoot=DynamicTable.getTableFromId(filterSet.dataset.tableRegion),initialFilters=DynamicTable.getFilters(tableRoot);if(initialFilters){const initialFilterPromise=new _pending.default("core/filter:setFilterFromConfig");(config=>{const filterConfig=Object.entries(config.filters);if(!filterConfig.length)return Promise.resolve();filterSet.querySelector(_selectors.default.filterset.fields.join).value=config.jointype;const filterPromises=filterConfig.map((_ref=>{let[filterType,filterData]=_ref;if("courseid"===filterType)return!1;const filterValues=filterData.values;return!!filterValues.length&&coreFilter.addFilterRow().then((_ref2=>{let[filterRow]=_ref2;coreFilter.addFilter(filterRow,filterType,filterValues)}))})).filter((promise=>promise));return filterPromises.length?Promise.all(filterPromises).then((()=>coreFilter.removeEmptyFilters())).then((()=>{coreFilter.updateFiltersOptions()})).then((()=>{coreFilter.updateTableFromFilter()})):Promise.resolve()})(initialFilters).then((()=>initialFilterPromise.resolve())).catch()}}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_datafilter=_interopRequireDefault(_datafilter),_courseid=_interopRequireDefault(_courseid),DynamicTable=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}(DynamicTable),_selectors=_interopRequireDefault(_selectors),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);_exports.init=filterRegionId=>{const filterSet=document.getElementById(filterRegionId),coreFilter=new _datafilter.default(filterSet,(function(filters,pendingPromise){DynamicTable.setFilters(DynamicTable.getTableFromId(filterSet.dataset.tableRegion),{jointype:parseInt(filterSet.querySelector(_selectors.default.filterset.fields.join).value,10),filters:filters}).then((result=>(pendingPromise.resolve(),result))).catch(_notification.default.exception)}));coreFilter.activeFilters.courseid=new _courseid.default("courseid",filterSet),coreFilter.init();const tableRoot=DynamicTable.getTableFromId(filterSet.dataset.tableRegion),initialFilters=DynamicTable.getFilters(tableRoot);if(initialFilters){const initialFilterPromise=new _pending.default("core/filter:setFilterFromConfig");(config=>{const filterConfig=Object.entries(config.filters);if(!filterConfig.length)return Promise.resolve();filterSet.querySelector(_selectors.default.filterset.fields.join).value=config.jointype;const filterPromises=filterConfig.map((_ref=>{let[filterType,filterData]=_ref;if("courseid"===filterType)return!1;const filterValues=filterData.values;return!!filterValues.length&&coreFilter.addFilterRow().then((_ref2=>{let[filterRow]=_ref2;coreFilter.addFilter(filterRow,filterType,filterValues)}))})).filter((promise=>promise));return filterPromises.length?Promise.all(filterPromises).then((()=>coreFilter.removeEmptyFilters())).then((()=>{coreFilter.updateFiltersOptions()})).then((()=>{coreFilter.updateTableFromFilter()})):Promise.resolve()})(initialFilters).then((()=>initialFilterPromise.resolve())).catch()}}})); //# sourceMappingURL=participants_filter.min.js.map \ No newline at end of file diff --git a/public/user/amd/build/participants_filter.min.js.map b/public/user/amd/build/participants_filter.min.js.map index 500e157922b47..733f2a8255a95 100644 --- a/public/user/amd/build/participants_filter.min.js.map +++ b/public/user/amd/build/participants_filter.min.js.map @@ -1 +1 @@ -{"version":3,"file":"participants_filter.min.js","sources":["../src/participants_filter.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 * Participants filter management.\n *\n * @module core_user/participants_filter\n * @copyright 2021 Tomo Tsuyuki \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport CoreFilter from 'core/datafilter';\nimport * as DynamicTable from 'core_table/dynamic';\nimport Selectors from 'core/datafilter/selectors';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\n\n/**\n * Initialise the participants filter on the element with the given id.\n *\n * @param {String} filterRegionId The id for the filter element.\n */\nexport const init = filterRegionId => {\n\n const filterSet = document.getElementById(filterRegionId);\n\n // Create and initialize filter.\n const coreFilter = new CoreFilter(filterSet, function(filters, pendingPromise) {\n DynamicTable.setFilters(\n DynamicTable.getTableFromId(filterSet.dataset.tableRegion),\n {\n jointype: parseInt(filterSet.querySelector(Selectors.filterset.fields.join).value, 10),\n filters,\n }\n )\n .then(result => {\n pendingPromise.resolve();\n\n return result;\n })\n .catch(Notification.exception);\n });\n coreFilter.init();\n\n /**\n * Set the current filter options based on a provided configuration.\n *\n * @param {Object} config\n * @param {Number} config.jointype\n * @param {Object} config.filters\n * @returns {Promise}\n */\n const setFilterFromConfig = config => {\n const filterConfig = Object.entries(config.filters);\n\n if (!filterConfig.length) {\n // There are no filters to set from.\n return Promise.resolve();\n }\n\n // Set the main join type.\n filterSet.querySelector(Selectors.filterset.fields.join).value = config.jointype;\n\n const filterPromises = filterConfig.map(([filterType, filterData]) => {\n if (filterType === 'courseid') {\n // The courseid is a special case.\n return false;\n }\n\n const filterValues = filterData.values;\n\n if (!filterValues.length) {\n // There are no values for this filter.\n // Skip it.\n return false;\n }\n return coreFilter.addFilterRow()\n .then(([filterRow]) => {\n coreFilter.addFilter(filterRow, filterType, filterValues);\n return;\n });\n }).filter(promise => promise);\n\n if (!filterPromises.length) {\n return Promise.resolve();\n }\n\n return Promise.all(filterPromises)\n .then(() => {\n return coreFilter.removeEmptyFilters();\n })\n .then(() => {\n coreFilter.updateFiltersOptions();\n return;\n })\n .then(() => {\n coreFilter.updateTableFromFilter();\n return;\n });\n };\n\n // Initialize DynamicTable for showing result.\n const tableRoot = DynamicTable.getTableFromId(filterSet.dataset.tableRegion);\n const initialFilters = DynamicTable.getFilters(tableRoot);\n if (initialFilters) {\n const initialFilterPromise = new Pending('core/filter:setFilterFromConfig');\n // Apply the initial filter configuration.\n setFilterFromConfig(initialFilters)\n .then(() => initialFilterPromise.resolve())\n .catch();\n }\n};\n\n"],"names":["filterRegionId","filterSet","document","getElementById","coreFilter","CoreFilter","filters","pendingPromise","DynamicTable","setFilters","getTableFromId","dataset","tableRegion","jointype","parseInt","querySelector","Selectors","filterset","fields","join","value","then","result","resolve","catch","Notification","exception","init","tableRoot","initialFilters","getFilters","initialFilterPromise","Pending","config","filterConfig","Object","entries","length","Promise","filterPromises","map","_ref","filterType","filterData","filterValues","values","addFilterRow","_ref2","filterRow","addFilter","filter","promise","all","removeEmptyFilters","updateFiltersOptions","updateTableFromFilter","setFilterFromConfig"],"mappings":";;;;;;;o8BAkCoBA,uBAEVC,UAAYC,SAASC,eAAeH,gBAGpCI,WAAa,IAAIC,oBAAWJ,WAAW,SAASK,QAASC,gBAC3DC,aAAaC,WACTD,aAAaE,eAAeT,UAAUU,QAAQC,aAC9C,CACIC,SAAUC,SAASb,UAAUc,cAAcC,mBAAUC,UAAUC,OAAOC,MAAMC,MAAO,IACnFd,QAAAA,UAGHe,MAAKC,SACFf,eAAegB,UAERD,UAEVE,MAAMC,sBAAaC,cAE5BtB,WAAWuB,aA4DLC,UAAYpB,aAAaE,eAAeT,UAAUU,QAAQC,aAC1DiB,eAAiBrB,aAAasB,WAAWF,cAC3CC,eAAgB,OACVE,qBAAuB,IAAIC,iBAAQ,mCArDjBC,CAAAA,eAClBC,aAAeC,OAAOC,QAAQH,OAAO3B,aAEtC4B,aAAaG,cAEPC,QAAQf,UAInBtB,UAAUc,cAAcC,mBAAUC,UAAUC,OAAOC,MAAMC,MAAQa,OAAOpB,eAElE0B,eAAiBL,aAAaM,KAAIC,WAAEC,WAAYC,oBAC/B,aAAfD,kBAEO,QAGLE,aAAeD,WAAWE,eAE3BD,aAAaP,QAKXjC,WAAW0C,eACbzB,MAAK0B,YAAEC,iBACJ5C,WAAW6C,UAAUD,UAAWN,WAAYE,oBAGrDM,QAAOC,SAAWA,iBAEhBZ,eAAeF,OAIbC,QAAQc,IAAIb,gBACdlB,MAAK,IACKjB,WAAWiD,uBAErBhC,MAAK,KACFjB,WAAWkD,0BAGdjC,MAAK,KACFjB,WAAWmD,2BAZRjB,QAAQf,WAuBnBiC,CAAoB3B,gBACfR,MAAK,IAAMU,qBAAqBR,YAChCC"} \ No newline at end of file +{"version":3,"file":"participants_filter.min.js","sources":["../src/participants_filter.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 * Participants filter management.\n *\n * @module core_user/participants_filter\n * @copyright 2021 Tomo Tsuyuki \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport CoreFilter from 'core/datafilter';\nimport CourseFilter from 'core/datafilter/filtertypes/courseid';\nimport * as DynamicTable from 'core_table/dynamic';\nimport Selectors from 'core/datafilter/selectors';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\n\n/**\n * Initialise the participants filter on the element with the given id.\n *\n * @param {String} filterRegionId The id for the filter element.\n */\nexport const init = filterRegionId => {\n\n const filterSet = document.getElementById(filterRegionId);\n\n // Create and initialize filter.\n const coreFilter = new CoreFilter(filterSet, function(filters, pendingPromise) {\n DynamicTable.setFilters(\n DynamicTable.getTableFromId(filterSet.dataset.tableRegion),\n {\n jointype: parseInt(filterSet.querySelector(Selectors.filterset.fields.join).value, 10),\n filters,\n }\n )\n .then(result => {\n pendingPromise.resolve();\n\n return result;\n })\n .catch(Notification.exception);\n });\n coreFilter.activeFilters.courseid = new CourseFilter('courseid', filterSet);\n coreFilter.init();\n\n /**\n * Set the current filter options based on a provided configuration.\n *\n * @param {Object} config\n * @param {Number} config.jointype\n * @param {Object} config.filters\n * @returns {Promise}\n */\n const setFilterFromConfig = config => {\n const filterConfig = Object.entries(config.filters);\n\n if (!filterConfig.length) {\n // There are no filters to set from.\n return Promise.resolve();\n }\n\n // Set the main join type.\n filterSet.querySelector(Selectors.filterset.fields.join).value = config.jointype;\n\n const filterPromises = filterConfig.map(([filterType, filterData]) => {\n if (filterType === 'courseid') {\n // The courseid is a special case.\n return false;\n }\n\n const filterValues = filterData.values;\n\n if (!filterValues.length) {\n // There are no values for this filter.\n // Skip it.\n return false;\n }\n return coreFilter.addFilterRow()\n .then(([filterRow]) => {\n coreFilter.addFilter(filterRow, filterType, filterValues);\n return;\n });\n }).filter(promise => promise);\n\n if (!filterPromises.length) {\n return Promise.resolve();\n }\n\n return Promise.all(filterPromises)\n .then(() => {\n return coreFilter.removeEmptyFilters();\n })\n .then(() => {\n coreFilter.updateFiltersOptions();\n return;\n })\n .then(() => {\n coreFilter.updateTableFromFilter();\n return;\n });\n };\n\n // Initialize DynamicTable for showing result.\n const tableRoot = DynamicTable.getTableFromId(filterSet.dataset.tableRegion);\n const initialFilters = DynamicTable.getFilters(tableRoot);\n if (initialFilters) {\n const initialFilterPromise = new Pending('core/filter:setFilterFromConfig');\n // Apply the initial filter configuration.\n setFilterFromConfig(initialFilters)\n .then(() => initialFilterPromise.resolve())\n .catch();\n }\n};\n\n"],"names":["filterRegionId","filterSet","document","getElementById","coreFilter","CoreFilter","filters","pendingPromise","DynamicTable","setFilters","getTableFromId","dataset","tableRegion","jointype","parseInt","querySelector","Selectors","filterset","fields","join","value","then","result","resolve","catch","Notification","exception","activeFilters","courseid","CourseFilter","init","tableRoot","initialFilters","getFilters","initialFilterPromise","Pending","config","filterConfig","Object","entries","length","Promise","filterPromises","map","_ref","filterType","filterData","filterValues","values","addFilterRow","_ref2","filterRow","addFilter","filter","promise","all","removeEmptyFilters","updateFiltersOptions","updateTableFromFilter","setFilterFromConfig"],"mappings":";;;;;;;g/BAmCoBA,uBAEVC,UAAYC,SAASC,eAAeH,gBAGpCI,WAAa,IAAIC,oBAAWJ,WAAW,SAASK,QAASC,gBAC3DC,aAAaC,WACTD,aAAaE,eAAeT,UAAUU,QAAQC,aAC9C,CACIC,SAAUC,SAASb,UAAUc,cAAcC,mBAAUC,UAAUC,OAAOC,MAAMC,MAAO,IACnFd,QAAAA,UAGHe,MAAKC,SACFf,eAAegB,UAERD,UAEVE,MAAMC,sBAAaC,cAE5BtB,WAAWuB,cAAcC,SAAW,IAAIC,kBAAa,WAAY5B,WACjEG,WAAW0B,aA4DLC,UAAYvB,aAAaE,eAAeT,UAAUU,QAAQC,aAC1DoB,eAAiBxB,aAAayB,WAAWF,cAC3CC,eAAgB,OACVE,qBAAuB,IAAIC,iBAAQ,mCArDjBC,CAAAA,eAClBC,aAAeC,OAAOC,QAAQH,OAAO9B,aAEtC+B,aAAaG,cAEPC,QAAQlB,UAInBtB,UAAUc,cAAcC,mBAAUC,UAAUC,OAAOC,MAAMC,MAAQgB,OAAOvB,eAElE6B,eAAiBL,aAAaM,KAAIC,WAAEC,WAAYC,oBAC/B,aAAfD,kBAEO,QAGLE,aAAeD,WAAWE,eAE3BD,aAAaP,QAKXpC,WAAW6C,eACb5B,MAAK6B,YAAEC,iBACJ/C,WAAWgD,UAAUD,UAAWN,WAAYE,oBAGrDM,QAAOC,SAAWA,iBAEhBZ,eAAeF,OAIbC,QAAQc,IAAIb,gBACdrB,MAAK,IACKjB,WAAWoD,uBAErBnC,MAAK,KACFjB,WAAWqD,0BAGdpC,MAAK,KACFjB,WAAWsD,2BAZRjB,QAAQlB,WAuBnBoC,CAAoB3B,gBACfX,MAAK,IAAMa,qBAAqBX,YAChCC"} \ No newline at end of file diff --git a/public/user/amd/src/participants_filter.js b/public/user/amd/src/participants_filter.js index 6255c4723eef9..8348fc0486dc7 100644 --- a/public/user/amd/src/participants_filter.js +++ b/public/user/amd/src/participants_filter.js @@ -22,6 +22,7 @@ */ import CoreFilter from 'core/datafilter'; +import CourseFilter from 'core/datafilter/filtertypes/courseid'; import * as DynamicTable from 'core_table/dynamic'; import Selectors from 'core/datafilter/selectors'; import Notification from 'core/notification'; @@ -52,6 +53,7 @@ export const init = filterRegionId => { }) .catch(Notification.exception); }); + coreFilter.activeFilters.courseid = new CourseFilter('courseid', filterSet); coreFilter.init(); /** From f38ff4bd491e61cdea218ba63805bc239056d456 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 21 Oct 2025 10:27:56 +0100 Subject: [PATCH 090/553] MDL-86255 behat: implement methods to assert date/time field value. --- .../lib/behat/form_field/behat_form_date.php | 45 ++++++++++++++----- .../behat/form_field/behat_form_date_time.php | 23 ++++++---- public/lib/form/tests/behat/dates.feature | 30 ++++++++++++- .../tests/behat/schedules.feature | 2 +- 4 files changed, 76 insertions(+), 24 deletions(-) diff --git a/public/lib/behat/form_field/behat_form_date.php b/public/lib/behat/form_field/behat_form_date.php index 903c15bdcdad5..cfdbb34738094 100644 --- a/public/lib/behat/form_field/behat_form_date.php +++ b/public/lib/behat/form_field/behat_form_date.php @@ -14,15 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Date form field class. - * - * @package core_form - * @category test - * @copyright 2013 David Monllaó - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/behat_form_group.php'); @@ -41,17 +32,14 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_form_date extends behat_form_group { - /** * Sets the value to a date field. * * @param string $value The value to be assigned to the date selector field. The string value must be either * parsable into a UNIX timestamp or equal to 'disabled' (if disabling the date selector). - * @return void * @throws ExpectationException If the value is invalid. */ public function set_value($value) { - if ($value === 'disabled') { // Disable the given date selector field. $this->set_child_field_value('enabled', false); @@ -77,6 +65,29 @@ public function set_value($value) { } } + /** + * Returns the current value of the field + * + * @return int + */ + public function get_value() { + return make_timestamp( + $this->get_child_field_value('year'), + $this->get_child_field_value('month'), + $this->get_child_field_value('day'), + ); + } + + /** + * Matches the provided value against the current field value + * + * @param mixed $expectedvalue + * @return bool + */ + public function matches($expectedvalue) { + return (int) $expectedvalue === $this->get_value(); + } + /** * Returns the date field identifiers and the values that should be assigned to them. * @@ -115,4 +126,14 @@ private function set_child_field_value(string $childname, $childvalue) { $childinstance->set_value($childvalue); } } + + /** + * Gets a value of a child element in the date form field + * + * @param string $childname + * @return string + */ + protected function get_child_field_value(string $childname): string { + return $this->field->find('css', "*[name$='[{$childname}]']")->getValue(); + } } diff --git a/public/lib/behat/form_field/behat_form_date_time.php b/public/lib/behat/form_field/behat_form_date_time.php index f1344603adfdd..8c078d2605596 100644 --- a/public/lib/behat/form_field/behat_form_date_time.php +++ b/public/lib/behat/form_field/behat_form_date_time.php @@ -14,15 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Date time form field class. - * - * @package core_form - * @category test - * @copyright 2013 David Monllaó - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/behat_form_date.php'); @@ -39,6 +30,20 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_form_date_time extends behat_form_date { + /** + * Returns the current value of the field + * + * @return string + */ + public function get_value() { + return make_timestamp( + $this->get_child_field_value('year'), + $this->get_child_field_value('month'), + $this->get_child_field_value('day'), + $this->get_child_field_value('hour'), + $this->get_child_field_value('minute'), + ); + } /** * Returns the date field identifiers and the values that should be assigned to them. diff --git a/public/lib/form/tests/behat/dates.feature b/public/lib/form/tests/behat/dates.feature index ecb9bd7bb9e2f..e052f8403185f 100644 --- a/public/lib/form/tests/behat/dates.feature +++ b/public/lib/form/tests/behat/dates.feature @@ -27,7 +27,20 @@ Feature: Setting and validating date fields | dategroup2[group2optionaldatetime][enabled] | 1 | | Group2 optional date and time | ## 2023-08-31 14:45 ## | When I press "Send form" - Then I should see "simpledateonly: 1690732800" + Then the following fields match these values: + | Simple only date | ## 2023-07-31 ## | + | Simple optional only date | ## 2023-08-31 ## | + | Simple date and time | ## 2023-07-31 11:15 ## | + | Simple optional date and time | ## 2023-08-31 14:45 ## | + | Group1 only date | ## 2023-07-31 ## | + | Group1 optional only date | ## 2023-08-31 ## | + | Group1 date and time | ## 2023-07-31 11:15 ## | + | Group1 optional date and time | ## 2023-08-31 14:45 ## | + | Group2 only date | ## 2023-07-31 ## | + | Group2 optional only date | ## 2023-08-31 ## | + | Group2 date and time | ## 2023-07-31 11:15 ## | + | Group2 optional date and time | ## 2023-08-31 14:45 ## | + And I should see "simpledateonly: 1690732800" And I should see "simpleoptionaldateonly: 1693411200" And I should see "simpledatetime: 1690773300" And I should see "simpleoptionaldatetime: 1693464300" @@ -62,7 +75,20 @@ Feature: Setting and validating date fields | dategroup2[group2optionaldatetime][enabled] | 1 | | dategroup2[group2optionaldatetime] | ## 2023-08-31 14:45 ## | When I press "Send form" - Then I should see "simpledateonly: 1690732800" + Then the following fields match these values: + | simpledateonly | ## 2023-07-31 ## | + | simpleoptionaldateonly | ## 2023-08-31 ## | + | simpledatetime | ## 2023-07-31 11:15 ## | + | simpleoptionaldatetime | ## 2023-08-31 14:45 ## | + | group1dateonly | ## 2023-07-31 ## | + | group1optionaldateonly | ## 2023-08-31 ## | + | group1datetime | ## 2023-07-31 11:15 ## | + | group1optionaldatetime | ## 2023-08-31 14:45 ## | + | dategroup2[group2dateonly] | ## 2023-07-31 ## | + | dategroup2[group2optionaldateonly] | ## 2023-08-31 ## | + | dategroup2[group2datetime] | ## 2023-07-31 11:15 ## | + | dategroup2[group2optionaldatetime] | ## 2023-08-31 14:45 ## | + And I should see "simpledateonly: 1690732800" And I should see "simpleoptionaldateonly: 1693411200" And I should see "simpledatetime: 1690773300" And I should see "simpleoptionaldatetime: 1693464300" diff --git a/public/reportbuilder/tests/behat/schedules.feature b/public/reportbuilder/tests/behat/schedules.feature index 5be4927ae12ae..4c6763e9673b8 100644 --- a/public/reportbuilder/tests/behat/schedules.feature +++ b/public/reportbuilder/tests/behat/schedules.feature @@ -160,7 +160,7 @@ Feature: Manage custom report schedules And I press "Edit schedule details" action in the "My updated schedule" report row And the following fields in the "Edit schedule details" "dialogue" match these values: | Name | My updated schedule | - # | Starting from | ##tomorrow 11:00## | Nope, can't: MDL-86255. + | Starting from | ##tomorrow 11:00## | | All users: All site users | 1 | | Subject | Tell me how to win your heart | | Body | For I haven't got a clue | From e69e34174cea0fbf726eb2b797cbd5897ca1b59d Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 14 Oct 2025 14:48:53 +0100 Subject: [PATCH 091/553] MDL-86915 core: define "core" plugin type string. Per ae7f3dfd this is now required for all plugin types. In this specific case, it's used by the privacy plugin registry. --- public/lang/en/plugin.php | 2 ++ public/lib/tests/plugin_manager_test.php | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/public/lang/en/plugin.php b/public/lang/en/plugin.php index 824fdfa3c2066..35d65d8e489d6 100644 --- a/public/lang/en/plugin.php +++ b/public/lang/en/plugin.php @@ -135,6 +135,8 @@ $string['type_communication_plural'] = 'Communication providers'; $string['type_contenttype'] = 'Content bank'; $string['type_contenttype_plural'] = 'Content bank plugins'; +$string['type_core'] = 'Core sub-system'; +$string['type_core_plural'] = 'Core sub-systems'; $string['type_customfield'] = 'Custom field'; $string['type_customfield_plural'] = 'Custom fields'; $string['type_coursereport'] = 'Course report'; diff --git a/public/lib/tests/plugin_manager_test.php b/public/lib/tests/plugin_manager_test.php index f5c5e07fd5e36..cdd59b5f1a44a 100644 --- a/public/lib/tests/plugin_manager_test.php +++ b/public/lib/tests/plugin_manager_test.php @@ -247,6 +247,16 @@ public function test_plugintype_name_plural(): void { $this->assertSame(get_string('type_editor_plural', 'core_plugin'), $name); } + public function test_plugintype_name_core(): void { + $name = core_plugin_manager::instance()->plugintype_name('core'); + $this->assertSame(get_string('type_core', 'core_plugin'), $name); + } + + public function test_plugintype_name_core_plural(): void { + $name = core_plugin_manager::instance()->plugintype_name_plural('core'); + $this->assertSame(get_string('type_core_plural', 'core_plugin'), $name); + } + public function test_get_plugin_info(): void { global $CFG; From 87b1c27659965c9f6f5ce47141fdb403ca51bb93 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Wed, 22 Oct 2025 00:22:28 +0100 Subject: [PATCH 092/553] MDL-86975 mod_book: fix alignment of chapter editing delete icon. Mis-aligned since changes to previously used action icon in df486967. --- public/mod/book/locallib.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/public/mod/book/locallib.php b/public/mod/book/locallib.php index ec1867720df86..83c725c502099 100644 --- a/public/mod/book/locallib.php +++ b/public/mod/book/locallib.php @@ -305,17 +305,17 @@ function book_get_toc($chapters, $chapter, $book, $cm, $edit) { array('title' => get_string('editchapter', 'mod_book', $titleunescaped))); $deleteaction = new confirm_action(get_string('deletechapter', 'mod_book', $titleunescaped)); - $toc .= $OUTPUT->action_icon( - new moodle_url('delete.php', [ - 'id' => $cm->id, - 'chapterid' => $ch->id, - 'sesskey' => sesskey(), - 'confirm' => 1, - ]), - new pix_icon('t/delete', get_string('deletechapter', 'mod_book', $title)), - $deleteaction, - ['title' => get_string('deletechapter', 'mod_book', $titleunescaped)] - ); + $toc .= $OUTPUT->action_link( + new moodle_url('delete.php', [ + 'id' => $cm->id, + 'chapterid' => $ch->id, + 'sesskey' => sesskey(), + 'confirm' => 1, + ]), + $OUTPUT->pix_icon('t/delete', get_string('deletechapter', 'mod_book', $title)), + $deleteaction, + ['title' => get_string('deletechapter', 'mod_book', $titleunescaped)] + ); if ($ch->hidden) { $toc .= html_writer::link(new moodle_url('show.php', array('id' => $cm->id, 'chapterid' => $ch->id, 'sesskey' => $USER->sesskey)), From fdb7d82ea55f766593c0a57d2ac8db68d9c701d3 Mon Sep 17 00:00:00 2001 From: Jose Pico Date: Wed, 6 Aug 2025 15:32:16 +1000 Subject: [PATCH 093/553] MDL-86225 plagiarism: Update data type from object to array --- public/lib/plagiarismlib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/lib/plagiarismlib.php b/public/lib/plagiarismlib.php index b2d478b91022f..2761fb2766e72 100644 --- a/public/lib/plagiarismlib.php +++ b/public/lib/plagiarismlib.php @@ -31,7 +31,7 @@ /** * displays the similarity score and provides a link to the full report if allowed. * - * @param object $linkarray contains all relevant information for the plugin to generate a link + * @param array $linkarray contains all relevant information for the plugin to generate a link * @return string - url to allow login/viewing of a similarity report */ function plagiarism_get_links($linkarray) { From 7787e7ee82584a50a0e78c1abac74a9210c2381a Mon Sep 17 00:00:00 2001 From: Noemie Ariste Date: Fri, 1 Aug 2025 11:12:19 +1200 Subject: [PATCH 094/553] MDL-75764 mod_assign: remove capability check for bulk downloads --- public/mod/assign/classes/downloader.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/mod/assign/classes/downloader.php b/public/mod/assign/classes/downloader.php index 420f5fbd7ae17..9a73e0c16b57b 100644 --- a/public/mod/assign/classes/downloader.php +++ b/public/mod/assign/classes/downloader.php @@ -94,10 +94,9 @@ public function load_filelist(): bool { $manager->require_view_grades(); - // Load all users with submit. $students = get_enrolled_users( $manager->get_context(), - "mod/assign:submit", + '', 0, 'u.*', null, From f1c1815b97b967c256586d9ec5e90a925f2e24b9 Mon Sep 17 00:00:00 2001 From: Stephan Robotta Date: Tue, 12 Aug 2025 17:33:41 +0200 Subject: [PATCH 095/553] MDL-86282 navigation: Main navigation corrected for XMLDB tool --- public/admin/tool/xmldb/index.php | 1 + 1 file changed, 1 insertion(+) diff --git a/public/admin/tool/xmldb/index.php b/public/admin/tool/xmldb/index.php index 5a52dc961efa3..ab1508eff75ca 100644 --- a/public/admin/tool/xmldb/index.php +++ b/public/admin/tool/xmldb/index.php @@ -47,6 +47,7 @@ // Some previous checks $site = get_site(); +$PAGE->set_primary_active_tab('siteadminnode'); // Body of the script, based on action, we delegate the work $action = optional_param ('action', 'main_view', PARAM_ALPHAEXT); From fb29f55fa17b122c59737d12fda17410b33a1f61 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Fri, 3 Oct 2025 11:22:36 +0100 Subject: [PATCH 096/553] MDL-80524 rating: preserve activity idnumber when adding rating. See also e9a5485f for context regarding similar problem with grade updates. Co-authored-by: Julian Tovar --- public/lib/tests/gradelib_test.php | 2 +- public/rating/lib.php | 4 ++-- public/rating/rate.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/lib/tests/gradelib_test.php b/public/lib/tests/gradelib_test.php index 3d20c10f5e23e..a7e262ea7edca 100644 --- a/public/lib/tests/gradelib_test.php +++ b/public/lib/tests/gradelib_test.php @@ -52,7 +52,7 @@ public function test_grade_update_mod_grades(): void { // Function grade_update_mod_grades() requires 2 additional properties, cmidnumber and modname. $cm = get_coursemodule_from_instance('assign', $modinstance->id, 0, false, MUST_EXIST); - $modinstance->cmidnumber = $cm->id; + $modinstance->cmidnumber = $cm->idnumber; $modinstance->modname = 'assign'; $this->assertTrue(grade_update_mod_grades($modinstance)); diff --git a/public/rating/lib.php b/public/rating/lib.php index a8cc8cca4c11e..9d0a779a8b941 100644 --- a/public/rating/lib.php +++ b/public/rating/lib.php @@ -1133,10 +1133,10 @@ public function add_rating($cm, $context, $component, $ratingarea, $itemid, $sca // Future possible enhancement: add a setting to turn grade updating off for those who don't want them in gradebook. // Note that this would need to be done in both rate.php and rate_ajax.php. if ($context->contextlevel == CONTEXT_MODULE) { - // Tell the module that its grades have changed. + // Tell the module that its grades have changed (note that 'cmidnumber' is required in order to update grades). $modinstance = $DB->get_record($cm->modname, array('id' => $cm->instance)); if ($modinstance) { - $modinstance->cmidnumber = $cm->id; // MDL-12961. + $modinstance->cmidnumber = $cm->idnumber; $functionname = $cm->modname.'_update_grades'; require_once($CFG->dirroot."/mod/{$cm->modname}/lib.php"); if (function_exists($functionname)) { diff --git a/public/rating/rate.php b/public/rating/rate.php index a8bd265aa9b07..2f56dc1f20a71 100644 --- a/public/rating/rate.php +++ b/public/rating/rate.php @@ -99,9 +99,9 @@ } if (!empty($cm) && $context->contextlevel == CONTEXT_MODULE) { - // Tell the module that its grades have changed. + // Tell the module that its grades have changed (note that 'cmidnumber' is required in order to update grades). $modinstance = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST); - $modinstance->cmidnumber = $cm->id; // MDL-12961. + $modinstance->cmidnumber = $cm->idnumber; $functionname = $cm->modname.'_update_grades'; require_once($CFG->dirroot."/mod/{$cm->modname}/lib.php"); if (function_exists($functionname)) { From bd690ac79ab1d50a21dad80ad5b9f27595421d35 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Wed, 22 Oct 2025 20:14:06 +0800 Subject: [PATCH 097/553] MDL-86986 core: Add aria-label to the drag handle button --- public/lib/templates/drag_handle.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/lib/templates/drag_handle.mustache b/public/lib/templates/drag_handle.mustache index 4432bbb15b1cf..2a9cd8cc25c95 100644 --- a/public/lib/templates/drag_handle.mustache +++ b/public/lib/templates/drag_handle.mustache @@ -24,4 +24,4 @@ "movetitle": "Move this element" } }} -{{#pix}} i/dragdrop, core {{/pix}} +{{#pix}} i/dragdrop, core {{/pix}} From cf6087d890c79456b00818264c53c91ba664d9ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luca=20B=C3=B6sch?= Date: Wed, 22 Oct 2025 16:18:30 +0200 Subject: [PATCH 098/553] MDL-81804 dml: Passing parameters with -c key=val on PostgreSQL. Co-authored-by: Marcos Dos Santos De Oliveira --- public/lib/dml/pgsql_native_moodle_database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/lib/dml/pgsql_native_moodle_database.php b/public/lib/dml/pgsql_native_moodle_database.php index 13844654d9694..1c76c5c2cf07d 100644 --- a/public/lib/dml/pgsql_native_moodle_database.php +++ b/public/lib/dml/pgsql_native_moodle_database.php @@ -187,7 +187,7 @@ public function raw_connect(string $dbhost, string $dbuser, string $dbpass, stri if (empty($this->dboptions['dbhandlesoptions'])) { // ALTER USER and ALTER DATABASE are overridden by these settings. - $options = array('--client_encoding=utf8', '--standard_conforming_strings=on'); + $options = ['-c client_encoding=utf8', '-c standard_conforming_strings=on']; // Select schema if specified, otherwise the first one wins. if (!empty($this->dboptions['dbschema'])) { $options[] = "-c search_path=" . addcslashes($this->dboptions['dbschema'], "'\\"); From 70c6f5dca750a0d42bfd702cb2e0e373ba386d67 Mon Sep 17 00:00:00 2001 From: Leon Stringer Date: Wed, 3 Sep 2025 16:14:58 +0200 Subject: [PATCH 099/553] MDL-86434 core: Page title for invalid user ID --- public/user/profile.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/user/profile.php b/public/user/profile.php index b51b0b2c4a49b..9921bbbc238a3 100644 --- a/public/user/profile.php +++ b/public/user/profile.php @@ -50,6 +50,7 @@ require_login(); if (isguestuser()) { $PAGE->set_context(context_system::instance()); + $PAGE->set_title(get_string('user')); echo $OUTPUT->header(); echo $OUTPUT->confirm(get_string('guestcantaccessprofiles', 'error'), get_login_url(), @@ -63,6 +64,7 @@ if ((!$user = $DB->get_record('user', array('id' => $userid))) || ($user->deleted)) { $PAGE->set_context(context_system::instance()); + $PAGE->set_title(get_string('user')); echo $OUTPUT->header(); if (!$user) { echo $OUTPUT->notification(get_string('invaliduser', 'error')); From eeb9977692ab697fbaa57c8ca76247ba43b91eb2 Mon Sep 17 00:00:00 2001 From: Matthew Hilton Date: Thu, 3 Apr 2025 13:51:36 +1000 Subject: [PATCH 100/553] MDL-85075 files: Use core security helper first --- public/lib/filelib.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/public/lib/filelib.php b/public/lib/filelib.php index e46df988fce27..d0e72756f27b9 100644 --- a/public/lib/filelib.php +++ b/public/lib/filelib.php @@ -3709,6 +3709,15 @@ protected function check_securityhelper_blocklist(string $url): ?string { return null; } + // Check if the URL is blocked in core curl_security_helper or + // curl security helper that passed to curl class constructor. + // Note, we purposely check the configured helper first, + // as this may be being mocked for unit testing. + if ($this->securityhelper->url_is_blocked($url)) { + $this->error = $this->securityhelper->get_blocked_url_string(); + return $this->error; + } + // Augment all installed plugin's security helpers if there is any. // The plugin's function has to be defined as plugintype_pluginname_curl_security_helper in pluginname/lib.php. $plugintypes = get_plugins_with_function('curl_security_helper'); @@ -3727,13 +3736,6 @@ protected function check_securityhelper_blocklist(string $url): ?string { } } - // Check if the URL is blocked in core curl_security_helper or - // curl security helper that passed to curl class constructor. - if ($this->securityhelper->url_is_blocked($url)) { - $this->error = $this->securityhelper->get_blocked_url_string(); - return $this->error; - } - // Set allowed resolve info if the URL is not blocked. $this->curlresolveinfo = $this->securityhelper->get_resolve_info(); From fe7d26d33f2ef9560216d658d7e18ef226d491e5 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Thu, 23 Oct 2025 11:24:17 +0800 Subject: [PATCH 101/553] MDL-85774 login: Remove visually-hidden links Visually hidden links on login error/info disrupt tab order. We must remove them. Instead, announce the div containing the login error/info messages on page reload. The visually hidden signup link has also been removed. --- public/auth/tests/behat/loginform.feature | 4 ++- public/lib/templates/loginform.mustache | 36 ++++++++++++++++------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/public/auth/tests/behat/loginform.feature b/public/auth/tests/behat/loginform.feature index 7942678268dbe..2f6c37d7e2b3e 100644 --- a/public/auth/tests/behat/loginform.feature +++ b/public/auth/tests/behat/loginform.feature @@ -96,13 +96,15 @@ Feature: Test if the login form provides the correct feedback And I follow "Log in" Then the focused element is "Password" "field" + @accessibility Scenario: Test the login page focus after error feature Given I follow "Log in" And I set the field "Username" to "admin" And I set the field "Password" to "wrongpassword" And I press "Log in" - And I press the tab key + And I wait until the page is ready Then the focused element is "Username" "field" + And the page should meet accessibility standards with "best-practice" extra tests Scenario: Display the password visibility toggle icon Given the following config values are set as admin: diff --git a/public/lib/templates/loginform.mustache b/public/lib/templates/loginform.mustache index f57fb46039850..5b204b1c9a368 100644 --- a/public/lib/templates/loginform.mustache +++ b/public/lib/templates/loginform.mustache @@ -115,16 +115,11 @@
      {{/maintenance}} {{#error}} -
      {{error}} - + {{/error}} {{#info}} - {{info}} - +
      {{info}}
      {{/info}} - {{#cansignup}} - {{#str}} tocreatenewaccount {{/str}} - {{/cansignup}} {{#showloginform}}