���ѧۧݧ�ӧ�� �ާ֧ߧ֧էا֧� - ���֧էѧܧ�ڧ��ӧѧ�� - /home3/cpr76684/public_html/local.tar
���ѧ٧ѧ�
content.min.js 0000644 00000030740 15151264135 0007345 0 ustar 00 define("core_courseformat/local/content",["exports","core/reactive","core_courseformat/courseeditor","core/inplace_editable","core_courseformat/local/content/section","core_courseformat/local/content/section/cmitem","core_course/actions","core_courseformat/local/content/actions","core_course/events","jquery"],(function(_exports,_reactive,_courseeditor,_inplace_editable,_section,_cmitem,_actions,_actions2,CourseEvents,_jquery){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}} /** * Course index main component. * * @module core_courseformat/local/content * @class core_courseformat/local/content * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_inplace_editable=_interopRequireDefault(_inplace_editable),_section=_interopRequireDefault(_section),_cmitem=_interopRequireDefault(_cmitem),_actions=_interopRequireDefault(_actions),_actions2=_interopRequireDefault(_actions2),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),_jquery=_interopRequireDefault(_jquery);class Component extends _reactive.BaseComponent{create(descriptor){var _descriptor$sectionRe;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']",PAGE:"#page",TOGGLER:'[data-action="togglecoursecontentsection"]',COLLAPSE:'[data-toggle="collapse"]',TOGGLEALL:'[data-toggle="toggleall"]',ACTIVITYTAG:"li",SECTIONTAG:"li"},this.classes={COLLAPSED:"collapsed",ACTIVITY:"activity",STATEDREADY:"stateready",SECTION:"section"},this.dettachedCms={},this.dettachedSections={},this.sections={},this.cms={},this.sectionReturn=null!==(_descriptor$sectionRe=descriptor.sectionReturn)&&void 0!==_descriptor$sectionRe?_descriptor$sectionRe:0}static init(target,selectors,sectionReturn){return new Component({element:document.getElementById(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors,sectionReturn:sectionReturn})}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 _actions2.default(this),this.element.classList.add(this.classes.STATEDREADY)),this.addEventListener(this.element,CourseEvents.manualCompletionToggled,this._completionHandler),this.addEventListener(document.querySelector(this.selectors.PAGE),"scroll",this._scrollHandler)}_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),isCollapsed=null!==(_toggler$classList$co=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co&&_toggler$classList$co;if(isChevron||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(){return this.reactive.sectionReturn=this.sectionReturn,this.reactive.supportComponents?[{watch:"cm.visible:updated",handler:this._reloadCm},{watch:"cm.stealth:updated",handler:this._reloadCm},{watch:"cm.indent:updated",handler:this._reloadCm},{watch:"section.number:updated",handler:this._refreshSectionNumber},{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:"state:updated",handler:this._indexContents},{watch:"cm.visible:updated",handler:this._reloadCm},{watch:"cm.sectionid:updated",handler:this._reloadCm}]:[]}_refreshSectionCollapsed(_ref){var _toggler$classList$co2;let{state:state,element:element}=_ref;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;(0,_jquery.default)(collapsible).collapse(element.contentcollapsed?"hide":"show")}this._refreshAllSectionsToggler(state)}_refreshAllSectionsToggler(state){const target=this.getElement(this.selectors.TOGGLEALL);if(!target)return;let allcollapsed=!0,allexpanded=!0;state.section.forEach((section=>{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))}_startProcessing(){this.dettachedCms={},this.dettachedSections={}}_completionHandler(_ref2){let{detail:detail}=_ref2;void 0!==detail&&this.reactive.dispatch("cmCompletion",[detail.cmid],detail.completed)}_scrollHandler(){const pageOffset=document.querySelector(this.selectors.PAGE).scrollTop,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"cm"!==item.type||item.url||this.reactive.isEditing?(pageItem=item,pageOffset>=element.offsetTop):pageOffset>=element.offsetTop})),pageItem&&this.reactive.dispatch("setPageItem",pageItem.type,pageItem.id)}_refreshSectionNumber(_ref3){let{element:element}=_ref3;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))}}_refreshSectionCmlist(_ref4){var _element$cmlist;let{element:element}=_ref4;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)}_refreshCourseSectionlist(_ref5){var _element$sectionlist;let{element:element}=_ref5;if(0!=this.reactive.sectionReturn)return;const sectionlist=null!==(_element$sectionlist=element.sectionlist)&&void 0!==_element$sectionlist?_element$sectionlist:[],listparent=this.getElement(this.selectors.COURSE_SECTIONLIST),createSection=this._createSectionItem.bind(this);listparent&&this._fixOrder(listparent,sectionlist,this.selectors.SECTION,this.dettachedSections,createSection)}_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(_ref6){let{element:element}=_ref6;const cmitem=this.getElement(this.selectors.CM,element.id);if(cmitem){_actions.default.refreshModule(cmitem,element.id).then((()=>{this._indexContents()})).catch()}}_reloadSection(_ref7){let{element:element}=_ref7;const sectionitem=this.getElement(this.selectors.SECTION,element.id);if(sectionitem){_actions.default.refreshSection(sectionitem,element.id).then((()=>{this._indexContents()})).catch()}}_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="");let dndFakeActivity;for(container.classList.remove("hidden"),neworder.forEach(((itemid,index)=>{var _ref8,_this$getElement;let item=null!==(_ref8=null!==(_this$getElement=this.getElement(selector,itemid))&&void 0!==_this$getElement?_this$getElement:dettachedelements[itemid])&&void 0!==_ref8?_ref8: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)}));container.children.length>neworder.length;){var _lastchild$classList;const lastchild=container.lastChild;var _lastchild$dataset$id,_lastchild$dataset;if(null!=lastchild&&null!==(_lastchild$classList=lastchild.classList)&&void 0!==_lastchild$classList&&_lastchild$classList.contains("dndupload-preview"))dndFakeActivity=lastchild;else dettachedelements[null!==(_lastchild$dataset$id=null==lastchild||null===(_lastchild$dataset=lastchild.dataset)||void 0===_lastchild$dataset?void 0:_lastchild$dataset.id)&&void 0!==_lastchild$dataset$id?_lastchild$dataset$id:0]=lastchild;container.removeChild(lastchild)}dndFakeActivity&&container.append(dndFakeActivity)}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=content.min.js.map content.min.js.map 0000644 00000102035 15151264135 0010116 0 ustar 00 {"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 <http://www.gnu.org/licenses/>.\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 <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport inplaceeditable from 'core/inplace_editable';\nimport Section from 'core_courseformat/local/content/section';\nimport CmItem from 'core_courseformat/local/content/section/cmitem';\n// Course actions is needed for actions that are not migrated to components.\nimport courseActions from 'core_course/actions';\nimport DispatchActions from 'core_courseformat/local/content/actions';\nimport * as CourseEvents from 'core_course/events';\n// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.\nimport jQuery from 'jquery';\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 PAGE: `#page`,\n TOGGLER: `[data-action=\"togglecoursecontentsection\"]`,\n COLLAPSE: `[data-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 // 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 page section return.\n this.sectionReturn = descriptor.sectionReturn ?? 0;\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 content section return\n * @return {Component}\n */\n static init(target, selectors, sectionReturn) {\n return new Component({\n element: document.getElementById(target),\n reactive: getCurrentCourseEditor(),\n selectors,\n sectionReturn,\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.querySelector(this.selectors.PAGE),\n \"scroll\",\n this._scrollHandler\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 const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n\n if (isChevron || isCollapsed) {\n // Update the state.\n const sectionId = section.getAttribute('data-id');\n this.reactive.dispatch(\n 'sectionContentCollapsed',\n [sectionId],\n !isCollapsed\n );\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;\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.indent:updated`, handler: this._reloadCm},\n // Update section number and title.\n {watch: `section.number:updated`, handler: this._refreshSectionNumber},\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 // Reindex sections and cms.\n {watch: `state:updated`, handler: this._indexContents},\n // State changes thaty require to reload course modules.\n {watch: `cm.visible:updated`, handler: this._reloadCm},\n {watch: `cm.sectionid:updated`, handler: this._reloadCm},\n ];\n }\n\n /**\n * Update section collapsed state via bootstrap 4 if necessary.\n *\n * Formats that do not use bootstrap 4 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\n // Course index is based on Bootstrap 4 collapsibles. To collapse them we need jQuery to\n // interact with collapsibles methods. Hopefully, this will change in Bootstrap 5 because\n // it does not require jQuery anymore (when MDL-71979 is integrated).\n jQuery(collapsible).collapse(element.contentcollapsed ? 'hide' : 'show');\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 // Check if we have all sections collapsed/expanded.\n let allcollapsed = true;\n let allexpanded = true;\n state.section.forEach(\n section => {\n allcollapsed = allcollapsed && section.contentcollapsed;\n allexpanded = allexpanded && !section.contentcollapsed;\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 * 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 = document.querySelector(this.selectors.PAGE).scrollTop;\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 // Activities without url can only be page items in edit mode.\n if (item.type === 'cm' && !item.url && !this.reactive.isEditing) {\n return pageOffset >= element.offsetTop;\n }\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 * Refresh a section cm list.\n *\n * @param {Object} param\n * @param {Object} param.element details the update details.\n */\n _refreshSectionCmlist({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 }\n\n /**\n * Refresh the section list.\n *\n * @param {Object} param\n * @param {Object} param.element details the update details.\n */\n _refreshCourseSectionlist({element}) {\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 != 0) {\n return;\n }\n const sectionlist = element.sectionlist ?? [];\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 }\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 const cmitem = this.getElement(this.selectors.CM, element.id);\n if (cmitem) {\n const promise = courseActions.refreshModule(cmitem, element.id);\n promise.then(() => {\n this._indexContents();\n return;\n }).catch();\n }\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 sectionitem = this.getElement(this.selectors.SECTION, element.id);\n if (sectionitem) {\n const promise = courseActions.refreshSection(sectionitem, element.id);\n promise.then(() => {\n this._indexContents();\n return;\n }).catch();\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 // Dndupload add a fake element we need to keep.\n let dndFakeActivity;\n\n // Remove the remaining elements.\n while (container.children.length > neworder.length) {\n const lastchild = container.lastChild;\n if (lastchild?.classList?.contains('dndupload-preview')) {\n dndFakeActivity = lastchild;\n } else {\n dettachedelements[lastchild?.dataset?.id ?? 0] = lastchild;\n }\n container.removeChild(lastchild);\n }\n // Restore dndupload fake element.\n if (dndFakeActivity) {\n container.append(dndFakeActivity);\n }\n }\n}\n"],"names":["Component","BaseComponent","create","descriptor","name","selectors","SECTION","SECTION_ITEM","SECTION_CMLIST","COURSE_SECTIONLIST","CM","PAGE","TOGGLER","COLLAPSE","TOGGLEALL","ACTIVITYTAG","SECTIONTAG","classes","COLLAPSED","ACTIVITY","STATEDREADY","dettachedCms","dettachedSections","sections","cms","sectionReturn","target","element","document","getElementById","reactive","stateReady","state","_indexContents","addEventListener","this","_sectionTogglers","toogleAll","getElement","collapseElementIds","getElements","map","id","setAttribute","join","_allSectionToggler","e","key","_refreshAllSectionsToggler","supportComponents","isEditing","DispatchActions","classList","add","CourseEvents","manualCompletionToggled","_completionHandler","querySelector","_scrollHandler","event","sectionlink","closest","closestCollapse","isChevron","section","toggler","isCollapsed","contains","sectionId","getAttribute","dispatch","preventDefault","isAllCollapsed","course","get","sectionlist","getWatchers","watch","handler","_reloadCm","_refreshSectionNumber","_refreshSectionCollapsed","_startProcessing","_refreshCourseSectionlist","_refreshSectionCmlist","Error","contentcollapsed","collapsibleId","dataset","replace","collapsible","collapse","allcollapsed","allexpanded","forEach","remove","detail","undefined","cmid","completed","pageOffset","scrollTop","items","getExporter","allItemsArray","pageItem","every","item","index","type","url","offsetTop","number","sectionid","inplace","inplaceeditable","getInplaceEditable","currentvalue","getValue","currentitemid","getItemId","rawtitle","setValue","cmlist","listparent","createCm","_createCmItem","bind","_fixOrder","createSection","_createSectionItem","_scanIndex","Section","CmItem","selector","creationhandler","_item$dataset","unregister","indexed","cmitem","courseActions","refreshModule","then","catch","_reloadSection","sectionitem","refreshSection","container","newItem","createElement","for","append","neworder","dettachedelements","createMethod","length","innerHTML","dndFakeActivity","itemid","currentitem","children","insertBefore","lastchild","lastChild","_lastchild$classList","_lastchild$dataset","removeChild"],"mappings":";;;;;;;;+gCAoCqBA,kBAAkBC,wBAOnCC,OAAOC,2CAEEC,KAAO,qBAEPC,UAAY,CACbC,+BACAC,0CACAC,qCACAC,qDACAC,yBACAC,aACAC,qDACAC,oCACAC,sCAEAC,YAAa,KACbC,WAAY,WAGXC,QAAU,CACXC,sBAEAC,oBACAC,yBACAd,wBAGCe,aAAe,QACfC,kBAAoB,QAEpBC,SAAW,QACXC,IAAM,QAENC,4CAAgBtB,WAAWsB,qEAAiB,cAWzCC,OAAQrB,UAAWoB,sBACpB,IAAIzB,UAAU,CACjB2B,QAASC,SAASC,eAAeH,QACjCI,UAAU,0CACVzB,UAAAA,UACAoB,cAAAA,gBASRM,WAAWC,YACFC,sBAEAC,iBAAiBC,KAAKR,QAAS,QAASQ,KAAKC,wBAG5CC,UAAYF,KAAKG,WAAWH,KAAK9B,UAAUS,cAC7CuB,UAAW,OAILE,mBAAqB,IADFJ,KAAKK,YAAYL,KAAK9B,UAAUQ,WACR4B,KAAId,SAAWA,QAAQe,KACxEL,UAAUM,aAAa,gBAAiBJ,mBAAmBK,KAAK,WAE3DV,iBAAiBG,UAAW,QAASF,KAAKU,yBAC1CX,iBAAiBG,UAAW,WAAWS,IAE1B,MAAVA,EAAEC,UACGF,mBAAmBC,WAG3BE,2BAA2BhB,OAGhCG,KAAKL,SAASmB,oBAEVd,KAAKL,SAASoB,eACVC,kBAAgBhB,WAInBR,QAAQyB,UAAUC,IAAIlB,KAAKlB,QAAQG,mBAIvCc,iBACDC,KAAKR,QACL2B,aAAaC,wBACbpB,KAAKqB,yBAIJtB,iBACDN,SAAS6B,cAActB,KAAK9B,UAAUM,MACtC,SACAwB,KAAKuB,gBAYbtB,iBAAiBuB,aACPC,YAAcD,MAAMjC,OAAOmC,QAAQ1B,KAAK9B,UAAUO,SAClDkD,gBAAkBH,MAAMjC,OAAOmC,QAAQ1B,KAAK9B,UAAUQ,UAGtDkD,UAAYD,MAAAA,uBAAAA,gBAAiBD,QAAQ1B,KAAK9B,UAAUE,iBAEtDqD,aAAeG,UAAW,iCAEpBC,QAAUL,MAAMjC,OAAOmC,QAAQ1B,KAAK9B,UAAUC,SAC9C2D,QAAUD,QAAQP,cAActB,KAAK9B,UAAUQ,UAC/CqD,0CAAcD,MAAAA,eAAAA,QAASb,UAAUe,SAAShC,KAAKlB,QAAQC,sEAEzD6C,WAAaG,YAAa,OAEpBE,UAAYJ,QAAQK,aAAa,gBAClCvC,SAASwC,SACV,0BACA,CAACF,YACAF,eAcjBrB,mBAAmBc,+BACfA,MAAMY,uBAGAC,eADSb,MAAMjC,OAAOmC,QAAQ1B,KAAK9B,UAAUS,WACrBsC,UAAUe,SAAShC,KAAKlB,QAAQC,WAExDuD,OAAStC,KAAKL,SAAS4C,IAAI,eAC5B5C,SAASwC,SACV,sDACAG,OAAOE,+DAAe,IACrBH,gBASTI,0BAGS9C,SAASL,cAAgBU,KAAKV,cAG9BU,KAAKL,SAASmB,kBAGZ,CAEH,CAAC4B,2BAA6BC,QAAS3C,KAAK4C,WAC5C,CAACF,2BAA6BC,QAAS3C,KAAK4C,WAC5C,CAACF,0BAA4BC,QAAS3C,KAAK4C,WAE3C,CAACF,+BAAiCC,QAAS3C,KAAK6C,uBAEhD,CAACH,yCAA2CC,QAAS3C,KAAK8C,0BAE1D,CAACJ,0BAA4BC,QAAS3C,KAAK+C,kBAC3C,CAACL,mCAAqCC,QAAS3C,KAAKgD,2BACpD,CAACN,+BAAiCC,QAAS3C,KAAKiD,uBAEhD,CAACP,sBAAwBC,QAAS3C,KAAKF,gBAEvC,CAAC4C,2BAA6BC,QAAS3C,KAAK4C,WAC5C,CAACF,6BAA+BC,QAAS3C,KAAK4C,YAnBvC,GAiCfE,8DAAyBjD,MAACA,MAADL,QAAQA,oBACvBD,OAASS,KAAKG,WAAWH,KAAK9B,UAAUC,QAASqB,QAAQe,QAC1DhB,aACK,IAAI2D,wCAAiC1D,QAAQe,WAGjDuB,QAAUvC,OAAO+B,cAActB,KAAK9B,UAAUQ,UAC9CqD,2CAAcD,MAAAA,eAAAA,QAASb,UAAUe,SAAShC,KAAKlB,QAAQC,wEAEzDS,QAAQ2D,mBAAqBpB,YAAa,+BACtCqB,4CAAgBtB,QAAQuB,QAAQ9D,8DAAUuC,QAAQI,aAAa,YAC9DkB,qBAGLA,cAAgBA,cAAcE,QAAQ,IAAK,UACrCC,YAAc9D,SAASC,eAAe0D,mBACvCG,uCAOEA,aAAaC,SAAShE,QAAQ2D,iBAAmB,OAAS,aAGhEtC,2BAA2BhB,OAQpCgB,2BAA2BhB,aACjBN,OAASS,KAAKG,WAAWH,KAAK9B,UAAUS,eACzCY,kBAIDkE,cAAe,EACfC,aAAc,EAClB7D,MAAMgC,QAAQ8B,SACV9B,UACI4B,aAAeA,cAAgB5B,QAAQsB,iBACvCO,YAAcA,cAAgB7B,QAAQsB,oBAG1CM,eACAlE,OAAO0B,UAAUC,IAAIlB,KAAKlB,QAAQC,WAClCQ,OAAOiB,aAAa,iBAAiB,IAErCkD,cACAnE,OAAO0B,UAAU2C,OAAO5D,KAAKlB,QAAQC,WACrCQ,OAAOiB,aAAa,iBAAiB,IAW7CuC,wBAGS7D,aAAe,QACfC,kBAAoB,GAQ7BkC,8BAAmBwC,OAACA,mBACDC,IAAXD,aAGClE,SAASwC,SAAS,eAAgB,CAAC0B,OAAOE,MAAOF,OAAOG,WAMjEzC,uBACU0C,WAAaxE,SAAS6B,cAActB,KAAK9B,UAAUM,MAAM0F,UACzDC,MAAQnE,KAAKL,SAASyE,cAAcC,cAAcrE,KAAKL,SAASE,WAElEyE,SAAW,KACfH,MAAMI,OAAMC,aACFC,MAAuB,YAAdD,KAAKE,KAAsB1E,KAAKZ,SAAWY,KAAKX,YACxCyE,IAAnBW,MAAMD,KAAKjE,WACJ,QAGLf,QAAUiF,MAAMD,KAAKjE,IAAIf,cAEb,OAAdgF,KAAKE,MAAkBF,KAAKG,KAAQ3E,KAAKL,SAASoB,WAGtDuD,SAAWE,KACJP,YAAczE,QAAQoF,WAHlBX,YAAczE,QAAQoF,aAKjCN,eACK3E,SAASwC,SAAS,cAAemC,SAASI,KAAMJ,SAAS/D,IAiBtEsC,iCAAsBrD,QAACA,qBAEbD,OAASS,KAAKG,WAAWH,KAAK9B,UAAUC,QAASqB,QAAQe,QAC1DhB,cAKLA,OAAOgB,qBAAgBf,QAAQqF,QAI/BtF,OAAO8D,QAAQyB,UAAYtF,QAAQqF,OAEnCtF,OAAO8D,QAAQwB,OAASrF,QAAQqF,aAG1BE,QAAUC,0BAAgBC,mBAAmB1F,OAAO+B,cAActB,KAAK9B,UAAUE,kBACnF2G,QAAS,OAGHG,aAAeH,QAAQI,WACvBC,cAAgBL,QAAQM,YAEH,KAAvBN,QAAQI,aAEJC,eAAiB5F,QAAQe,IAAO2E,cAAgB1F,QAAQ8F,UAAgC,IAApB9F,QAAQ8F,UAC5EP,QAAQQ,SAAS/F,QAAQ8F,YAYzCrC,qDAAsBzD,QAACA,qBACbgG,+BAAShG,QAAQgG,kDAAU,GAC3B3D,QAAU7B,KAAKG,WAAWH,KAAK9B,UAAUC,QAASqB,QAAQe,IAC1DkF,WAAa5D,MAAAA,eAAAA,QAASP,cAActB,KAAK9B,UAAUG,gBAEnDqH,SAAW1F,KAAK2F,cAAcC,KAAK5F,MACrCyF,iBACKI,UAAUJ,WAAYD,OAAQxF,KAAK9B,UAAUK,GAAIyB,KAAKd,aAAcwG,UAUjF1C,8DAA0BxD,QAACA,kBAEY,GAA/BQ,KAAKL,SAASL,2BAGZkD,yCAAchD,QAAQgD,iEAAe,GACrCiD,WAAazF,KAAKG,WAAWH,KAAK9B,UAAUI,oBAE5CwH,cAAgB9F,KAAK+F,mBAAmBH,KAAK5F,MAC/CyF,iBACKI,UAAUJ,WAAYjD,YAAaxC,KAAK9B,UAAUC,QAAS6B,KAAKb,kBAAmB2G,eAShGhG,sBAESkG,WACDhG,KAAK9B,UAAUC,QACf6B,KAAKZ,UACJoF,MACU,IAAIyB,iBAAQzB,aAKtBwB,WACDhG,KAAK9B,UAAUK,GACfyB,KAAKX,KACJmF,MACU,IAAI0B,gBAAO1B,QAc9BwB,WAAWG,SAAU1B,MAAO2B,iBACVpG,KAAKK,sBAAe8F,kCAC5BxC,SAASa,yBACNA,MAAAA,4BAAAA,KAAMnB,kCAANgD,cAAe9F,UAIWuD,IAA3BW,MAAMD,KAAKnB,QAAQ9C,KACnBkE,MAAMD,KAAKnB,QAAQ9C,IAAI+F,aAG3B7B,MAAMD,KAAKnB,QAAQ9C,IAAM6F,gBAAgB,IAClCpG,KACHR,QAASgF,OAGbA,KAAKnB,QAAQkD,SAAU,MAa/B3D,qBAAUpD,QAACA,qBACDgH,OAASxG,KAAKG,WAAWH,KAAK9B,UAAUK,GAAIiB,QAAQe,OACtDiG,OAAQ,CACQC,iBAAcC,cAAcF,OAAQhH,QAAQe,IACpDoG,MAAK,UACJ7G,oBAEN8G,SAaXC,0BAAerH,QAACA,qBACNsH,YAAc9G,KAAKG,WAAWH,KAAK9B,UAAUC,QAASqB,QAAQe,OAChEuG,YAAa,CACGL,iBAAcM,eAAeD,YAAatH,QAAQe,IAC1DoG,MAAK,UACJ7G,oBAEN8G,SAcXjB,cAAcqB,UAAWjD,YACfkD,QAAUxH,SAASyH,cAAclH,KAAK9B,UAAUU,oBACtDqI,QAAQ5D,QAAQ8D,IAAM,SACtBF,QAAQ5D,QAAQ9C,GAAKwD,KAErBkD,QAAQ1G,oBAAewD,MACvBkD,QAAQhG,UAAUC,IAAIlB,KAAKlB,QAAQE,UACnCgI,UAAUI,OAAOH,cACZrE,UAAU,CACXpD,QAASQ,KAAKL,SAAS4C,IAAI,KAAMwB,QAE9BkD,QAaXlB,mBAAmBiB,UAAWlC,iBACpBjD,QAAU7B,KAAKL,SAAS4C,IAAI,UAAWuC,WACvCmC,QAAUxH,SAASyH,cAAclH,KAAK9B,UAAUW,mBACtDoI,QAAQ5D,QAAQ8D,IAAM,UACtBF,QAAQ5D,QAAQ9C,GAAKuE,UACrBmC,QAAQ5D,QAAQwB,OAAShD,QAAQgD,OAEjCoC,QAAQ1G,qBAAgBuE,WACxBmC,QAAQhG,UAAUC,IAAIlB,KAAKlB,QAAQX,SACnC6I,UAAUI,OAAOH,cACZJ,eAAe,CAChBrH,QAASqC,UAENoF,wBAYKD,UAAWK,SAAUlB,SAAUmB,kBAAmBC,sBAC5CzD,IAAdkD,qBAKCK,SAASG,cACVR,UAAU/F,UAAUC,IAAI,eACxB8F,UAAUS,UAAY,QA0BtBC,oBArBJV,UAAU/F,UAAU2C,OAAO,UAG3ByD,SAAS1D,SAAQ,CAACgE,OAAQlD,wCAClBD,4CAAOxE,KAAKG,WAAWgG,SAAUwB,qDAAWL,kBAAkBK,+BAAWJ,aAAaP,UAAWW,gBACxF7D,IAATU,kBAKEoD,YAAcZ,UAAUa,SAASpD,YACnBX,IAAhB8D,YAIAA,cAAgBpD,MAChBwC,UAAUc,aAAatD,KAAMoD,aAJ7BZ,UAAUI,OAAO5C,SAYlBwC,UAAUa,SAASL,OAASH,SAASG,QAAQ,gCAC1CO,UAAYf,UAAUgB,0DACxBD,MAAAA,wCAAAA,UAAW9G,2CAAXgH,qBAAsBjG,SAAS,qBAC/B0F,gBAAkBK,eAElBT,gDAAkBS,MAAAA,sCAAAA,UAAW1E,6CAAX6E,mBAAoB3H,0DAAM,GAAKwH,UAErDf,UAAUmB,YAAYJ,WAGtBL,iBACAV,UAAUI,OAAOM"} courseindex/section.min.js.map 0000644 00000021376 15151264135 0012450 0 ustar 00 {"version":3,"file":"section.min.js","sources":["../../../src/local/courseindex/section.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Course index section component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/section\n * @class core_courseformat/local/courseindex/section\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport SectionTitle from 'core_courseformat/local/courseindex/sectiontitle';\nimport DndSection from 'core_courseformat/local/courseeditor/dndsection';\n\nexport default class Component extends DndSection {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_section';\n // Default query selectors.\n this.selectors = {\n SECTION_ITEM: `[data-for='section_item']`,\n SECTION_TITLE: `[data-for='section_title']`,\n CM_LAST: `[data-for=\"cm\"]:last-child`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n LOCKED: 'editinprogress',\n RESTRICTIONS: 'restrictions',\n PAGEITEM: 'pageitem',\n };\n\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n this.isPageItem = false;\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 * @return {Component}\n */\n static init(target, selectors) {\n return new Component({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the initial state\n */\n stateReady(state) {\n this.configState(state);\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init the inner dragable element passing the full section as affected region.\n const titleitem = new SectionTitle({\n ...this,\n element: sectionItem,\n fullregion: this.element,\n });\n this.configDragDrop(titleitem);\n }\n // Check if the current url is the section url.\n const section = state.section.get(this.id);\n if (window.location.href == section.sectionurl.replace(/&/g, \"&\")) {\n this.reactive.dispatch('setPageItem', 'section', this.id);\n sectionItem.scrollIntoView();\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `section[${this.id}]:deleted`, handler: this.remove},\n {watch: `section[${this.id}]:updated`, handler: this._refreshSection},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n ];\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null}\n */\n getLastCm() {\n return this.getElement(this.selectors.CM_LAST);\n }\n\n /**\n * Update a course index section using the state information.\n *\n * @param {Object} param details the update details.\n * @param {Object} param.element the section element\n */\n _refreshSection({element}) {\n // Update classes.\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.SECTIONHIDDEN, !element.visible);\n sectionItem.classList.toggle(this.classes.RESTRICTIONS, element.hasrestrictions ?? false);\n this.element.classList.toggle(this.classes.SECTIONCURRENT, element.current);\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.locked = element.locked;\n // Update title.\n this.getElement(this.selectors.SECTION_TITLE).innerHTML = element.title;\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data.\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element, state}) {\n if (!element.pageItem) {\n return;\n }\n if (element.pageItem.sectionId !== this.id && this.isPageItem) {\n this.pageItem = false;\n this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);\n return;\n }\n const section = state.section.get(this.id);\n if (section.indexcollapsed && !element.pageItem?.isStatic) {\n this.pageItem = (element.pageItem?.sectionId == this.id);\n } else {\n this.pageItem = (element.pageItem.type == 'section' && element.pageItem.id == this.id);\n }\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n sectionItem.classList.toggle(this.classes.PAGEITEM, this.pageItem ?? false);\n if (this.pageItem && !this.reactive.isEditing) {\n this.element.scrollIntoView({block: \"nearest\"});\n }\n }\n}\n"],"names":["Component","DndSection","create","name","selectors","SECTION_ITEM","SECTION_TITLE","CM_LAST","classes","SECTIONHIDDEN","SECTIONCURRENT","LOCKED","RESTRICTIONS","PAGEITEM","id","this","element","dataset","isPageItem","target","document","getElementById","stateReady","state","configState","sectionItem","getElement","reactive","isEditing","supportComponents","titleitem","SectionTitle","fullregion","configDragDrop","section","get","window","location","href","sectionurl","replace","dispatch","scrollIntoView","getWatchers","watch","handler","remove","_refreshSection","_refreshPageItem","getLastCm","classList","toggle","visible","hasrestrictions","current","DRAGGING","dragging","locked","innerHTML","title","pageItem","sectionId","indexcollapsed","_element$pageItem","isStatic","type","block"],"mappings":";;;;;;;;;;+LA6BqBA,kBAAkBC,oBAKnCC,cAESC,KAAO,2BAEPC,UAAY,CACbC,yCACAC,2CACAC,2CAGCC,QAAU,CACXC,cAAe,SACfC,eAAgB,UAChBC,OAAQ,iBACRC,aAAc,eACdC,SAAU,iBAITC,GAAKC,KAAKC,QAAQC,QAAQH,QAC1BI,YAAa,cAUVC,OAAQf,kBACT,IAAIJ,UAAU,CACjBgB,QAASI,SAASC,eAAeF,QACjCf,UAAAA,YASRkB,WAAWC,YACFC,YAAYD,aACXE,YAAcV,KAAKW,WAAWX,KAAKX,UAAUC,iBAE/CU,KAAKY,SAASC,WAAab,KAAKY,SAASE,kBAAmB,OAEtDC,UAAY,IAAIC,sBAAa,IAC5BhB,KACHC,QAASS,YACTO,WAAYjB,KAAKC,eAEhBiB,eAAeH,iBAGlBI,QAAUX,MAAMW,QAAQC,IAAIpB,KAAKD,IACnCsB,OAAOC,SAASC,MAAQJ,QAAQK,WAAWC,QAAQ,SAAU,YACxDb,SAASc,SAAS,cAAe,UAAW1B,KAAKD,IACtDW,YAAYiB,kBASpBC,oBACW,CACH,CAACC,wBAAkB7B,KAAKD,gBAAe+B,QAAS9B,KAAK+B,QACrD,CAACF,wBAAkB7B,KAAKD,gBAAe+B,QAAS9B,KAAKgC,iBACrD,CAACH,gCAAkCC,QAAS9B,KAAKiC,mBASzDC,mBACWlC,KAAKW,WAAWX,KAAKX,UAAUG,SAS1CwC,sFAAgB/B,QAACA,oBAEPS,YAAcV,KAAKW,WAAWX,KAAKX,UAAUC,cACnDoB,YAAYyB,UAAUC,OAAOpC,KAAKP,QAAQC,eAAgBO,QAAQoC,SAClE3B,YAAYyB,UAAUC,OAAOpC,KAAKP,QAAQI,2CAAcI,QAAQqC,8EAC3DrC,QAAQkC,UAAUC,OAAOpC,KAAKP,QAAQE,eAAgBM,QAAQsC,cAC9DtC,QAAQkC,UAAUC,OAAOpC,KAAKP,QAAQ+C,mCAAUvC,QAAQwC,+DACxDxC,QAAQkC,UAAUC,OAAOpC,KAAKP,QAAQG,+BAAQK,QAAQyC,yDACtDA,OAASzC,QAAQyC,YAEjB/B,WAAWX,KAAKX,UAAUE,eAAeoD,UAAY1C,QAAQ2C,MAUtEX,iEAAiBhC,QAACA,QAADO,MAAUA,iBAClBP,QAAQ4C,mBAGT5C,QAAQ4C,SAASC,YAAc9C,KAAKD,IAAMC,KAAKG,uBAC1C0C,UAAW,YACXlC,WAAWX,KAAKX,UAAUC,cAAc6C,UAAUJ,OAAO/B,KAAKP,QAAQK,kCAG/DU,MAAMW,QAAQC,IAAIpB,KAAKD,IAC3BgD,0CAAmB9C,QAAQ4C,uCAARG,kBAAkBC,cAGxCJ,SAAqC,WAAzB5C,QAAQ4C,SAASK,MAAqBjD,QAAQ4C,SAAS9C,IAAMC,KAAKD,QAF9E8C,qCAAY5C,QAAQ4C,iEAAUC,YAAa9C,KAAKD,GAIrCC,KAAKW,WAAWX,KAAKX,UAAUC,cACvC6C,UAAUC,OAAOpC,KAAKP,QAAQK,gCAAUE,KAAK6C,oDACrD7C,KAAK6C,WAAa7C,KAAKY,SAASC,gBAC3BZ,QAAQ0B,eAAe,CAACwB,MAAO"} courseindex/sectiontitle.min.js 0000644 00000002450 15151264135 0012726 0 ustar 00 define("core_courseformat/local/courseindex/sectiontitle",["exports","core_courseformat/local/courseeditor/dndsectionitem"],(function(_exports,_dndsectionitem){var obj; /** * Course index section title component. * * This component is used to control specific course section interactions like drag and drop. * * @module core_courseformat/local/courseindex/sectiontitle * @class core_courseformat/local/courseindex/sectiontitle * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndsectionitem=(obj=_dndsectionitem)&&obj.__esModule?obj:{default:obj};class Component extends _dndsectionitem.default{create(descriptor){this.name="courseindex_sectiontitle",this.id=descriptor.id,this.section=descriptor.section,this.course=descriptor.course,this.fullregion=descriptor.fullregion,this.section.number>0&&(this.getDraggableData=this._getDraggableData)}static init(target,selectors){return new Component({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configDragDrop(this.id,state,this.fullregion)}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=sectiontitle.min.js.map courseindex/courseindex.min.js.map 0000644 00000044141 15151264135 0013327 0 ustar 00 {"version":3,"file":"courseindex.min.js","sources":["../../../src/local/courseindex/courseindex.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Course index main component.\n *\n * @module core_courseformat/local/courseindex/courseindex\n * @class core_courseformat/local/courseindex/courseindex\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport jQuery from 'jquery';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex';\n // Default query selectors.\n this.selectors = {\n SECTION: `[data-for='section']`,\n SECTION_CMLIST: `[data-for='cmlist']`,\n CM: `[data-for='cm']`,\n TOGGLER: `[data-action=\"togglecourseindexsection\"]`,\n COLLAPSE: `[data-toggle=\"collapse\"]`,\n DRAWER: `.drawer`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n SECTIONHIDDEN: 'dimmed',\n CMHIDDEN: 'dimmed',\n SECTIONCURRENT: 'current',\n COLLAPSED: `collapsed`,\n SHOW: `show`,\n };\n // Arrays to keep cms and sections elements.\n this.sections = {};\n this.cms = {};\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new Component({\n element: document.getElementById(target),\n reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data\n */\n stateReady(state) {\n // Activate section togglers.\n this.addEventListener(this.element, 'click', this._sectionTogglers);\n\n // Get cms and sections elements.\n const sections = this.getElements(this.selectors.SECTION);\n sections.forEach((section) => {\n this.sections[section.dataset.id] = section;\n });\n const cms = this.getElements(this.selectors.CM);\n cms.forEach((cm) => {\n this.cms[cm.dataset.id] = cm;\n });\n\n // Set the page item if any.\n this._refreshPageItem({element: state.course, state});\n\n // Configure Aria Tree.\n this.contentTree = new ContentTree(this.element, this.selectors, this.reactive.isEditing);\n }\n\n getWatchers() {\n return [\n {watch: `section.indexcollapsed:updated`, handler: this._refreshSectionCollapsed},\n {watch: `cm:created`, handler: this._createCm},\n {watch: `cm:deleted`, handler: this._deleteCm},\n {watch: `section:created`, handler: this._createSection},\n {watch: `section:deleted`, handler: this._deleteSection},\n {watch: `course.pageItem:created`, handler: this._refreshPageItem},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n // Sections and cm sorting.\n {watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},\n {watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},\n ];\n }\n\n /**\n * Setup sections toggler.\n *\n * Toggler click is delegated to the main course index 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 isChevron = event.target.closest(this.selectors.COLLAPSE);\n\n if (sectionlink || isChevron) {\n\n const section = event.target.closest(this.selectors.SECTION);\n const toggler = section.querySelector(this.selectors.COLLAPSE);\n const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;\n\n if (isChevron || isCollapsed) {\n // Update the state.\n const sectionId = section.getAttribute('data-id');\n this.reactive.dispatch(\n 'sectionIndexCollapsed',\n [sectionId],\n !isCollapsed\n );\n }\n }\n }\n\n /**\n * Update section collapsed.\n *\n * @param {object} args\n * @param {object} args.element The leement to be expanded\n */\n _refreshSectionCollapsed({element}) {\n const target = this.getElement(this.selectors.SECTION, element.id);\n if (!target) {\n throw new Error(`Unkown 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.indexcollapsed !== isCollapsed) {\n this._expandSectionNode(element);\n }\n }\n\n /**\n * Expand a section node.\n *\n * By default the method will use element.indexcollapsed to decide if the\n * section is opened or closed. However, using forceValue it is possible\n * to open or close a section independant from the indexcollapsed attribute.\n *\n * @param {Object} element the course module state element\n * @param {boolean} forceValue optional forced expanded value\n */\n _expandSectionNode(element, forceValue) {\n const target = this.getElement(this.selectors.SECTION, element.id);\n const toggler = target.querySelector(this.selectors.COLLAPSE);\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\n if (forceValue === undefined) {\n forceValue = (element.indexcollapsed) ? false : true;\n }\n\n // Course index is based on Bootstrap 4 collapsibles. To collapse them we need jQuery to\n // interact with collapsibles methods. Hopefully, this will change in Bootstrap 5 because\n // it does not require jQuery anymore (when MDL-71979 is integrated).\n const togglerValue = (forceValue) ? 'show' : 'hide';\n jQuery(collapsible).collapse(togglerValue);\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data.\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element, state}) {\n if (!element?.pageItem?.isStatic || element.pageItem.type != 'cm') {\n return;\n }\n // Check if we need to uncollapse the section and scroll to the element.\n const section = state.section.get(element.pageItem.sectionId);\n if (section.indexcollapsed) {\n this._expandSectionNode(section, true);\n setTimeout(\n () => this.cms[element.pageItem.id]?.scrollIntoView({block: \"nearest\"}),\n 250\n );\n }\n }\n\n /**\n * Create a newcm instance.\n *\n * @param {object} param\n * @param {Object} param.state\n * @param {Object} param.element\n */\n async _createCm({state, element}) {\n // Create a fake node while the component is loading.\n const fakeelement = document.createElement('li');\n fakeelement.classList.add('bg-pulse-grey', 'w-100');\n fakeelement.innerHTML = ' ';\n this.cms[element.id] = fakeelement;\n // Place the fake node on the correct position.\n this._refreshSectionCmlist({\n state,\n element: state.section.get(element.sectionid),\n });\n // Collect render data.\n const exporter = this.reactive.getExporter();\n const data = exporter.cm(state, element);\n // Create the new content.\n const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/cm', data);\n // Replace the fake node with the real content.\n const newelement = newcomponent.getElement();\n this.cms[element.id] = newelement;\n fakeelement.parentNode.replaceChild(newelement, fakeelement);\n }\n\n /**\n * Create a new section instance.\n *\n * @param {Object} details the update details.\n * @param {Object} details.state the state data.\n * @param {Object} details.element the element data.\n */\n async _createSection({state, element}) {\n // Create a fake node while the component is loading.\n const fakeelement = document.createElement('div');\n fakeelement.classList.add('bg-pulse-grey', 'w-100');\n fakeelement.innerHTML = ' ';\n this.sections[element.id] = fakeelement;\n // Place the fake node on the correct position.\n this._refreshCourseSectionlist({\n state,\n element: state.course,\n });\n // Collect render data.\n const exporter = this.reactive.getExporter();\n const data = exporter.section(state, element);\n // Create the new content.\n const newcomponent = await this.renderComponent(fakeelement, 'core_courseformat/local/courseindex/section', data);\n // Replace the fake node with the real content.\n const newelement = newcomponent.getElement();\n this.sections[element.id] = newelement;\n fakeelement.parentNode.replaceChild(newelement, fakeelement);\n }\n\n /**\n * Refresh a section cm list.\n *\n * @param {object} param\n * @param {Object} param.element\n */\n _refreshSectionCmlist({element}) {\n const cmlist = element.cmlist ?? [];\n const listparent = this.getElement(this.selectors.SECTION_CMLIST, element.id);\n this._fixOrder(listparent, cmlist, this.cms);\n }\n\n /**\n * Refresh the section list.\n *\n * @param {object} param\n * @param {Object} param.element\n */\n _refreshCourseSectionlist({element}) {\n const sectionlist = element.sectionlist ?? [];\n this._fixOrder(this.element, sectionlist, this.sections);\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 {Array} allitems the list of html elements that can be placed in the container\n */\n _fixOrder(container, neworder, allitems) {\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 const item = allitems[itemid];\n // Get the current element 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 // Remove the remaining elements.\n while (container.children.length > neworder.length) {\n container.removeChild(container.lastChild);\n }\n }\n\n /**\n * Remove a cm from the list.\n *\n * The actual DOM element removal is delegated to the cm component.\n *\n * @param {object} param\n * @param {Object} param.element\n */\n _deleteCm({element}) {\n delete this.cms[element.id];\n }\n\n /**\n * Remove a section from the list.\n *\n * The actual DOM element removal is delegated to the section component.\n *\n * @param {Object} details the update details.\n * @param {Object} details.element the element data.\n */\n _deleteSection({element}) {\n delete this.sections[element.id];\n }\n}\n"],"names":["Component","BaseComponent","create","name","selectors","SECTION","SECTION_CMLIST","CM","TOGGLER","COLLAPSE","DRAWER","classes","SECTIONHIDDEN","CMHIDDEN","SECTIONCURRENT","COLLAPSED","SHOW","sections","cms","target","element","document","getElementById","reactive","stateReady","state","addEventListener","this","_sectionTogglers","getElements","forEach","section","dataset","id","cm","_refreshPageItem","course","contentTree","ContentTree","isEditing","getWatchers","watch","handler","_refreshSectionCollapsed","_createCm","_deleteCm","_createSection","_deleteSection","_refreshCourseSectionlist","_refreshSectionCmlist","event","sectionlink","closest","isChevron","toggler","querySelector","isCollapsed","classList","contains","sectionId","getAttribute","dispatch","getElement","Error","indexcollapsed","_expandSectionNode","forceValue","collapsibleId","replace","collapsible","undefined","togglerValue","collapse","pageItem","_element$pageItem","isStatic","type","get","setTimeout","_this$cms$element$pag","scrollIntoView","block","fakeelement","createElement","add","innerHTML","sectionid","data","getExporter","newelement","renderComponent","parentNode","replaceChild","cmlist","listparent","_fixOrder","sectionlist","container","neworder","allitems","length","remove","itemid","index","item","currentitem","children","insertBefore","append","removeChild","lastChild"],"mappings":";;;;;;;;qLA6BqBA,kBAAkBC,wBAKnCC,cAESC,KAAO,mBAEPC,UAAY,CACbC,+BACAC,qCACAC,qBACAC,mDACAC,oCACAC,uBAGCC,QAAU,CACXC,cAAe,SACfC,SAAU,SACVC,eAAgB,UAChBC,sBACAC,kBAGCC,SAAW,QACXC,IAAM,eAUHC,OAAQf,kBACT,IAAIJ,UAAU,CACjBoB,QAASC,SAASC,eAAeH,QACjCI,UAAU,0CACVnB,UAAAA,YASRoB,WAAWC,YAEFC,iBAAiBC,KAAKP,QAAS,QAASO,KAAKC,kBAGjCD,KAAKE,YAAYF,KAAKvB,UAAUC,SACxCyB,SAASC,eACTd,SAASc,QAAQC,QAAQC,IAAMF,WAE5BJ,KAAKE,YAAYF,KAAKvB,UAAUG,IACxCuB,SAASI,UACJhB,IAAIgB,GAAGF,QAAQC,IAAMC,WAIzBC,iBAAiB,CAACf,QAASK,MAAMW,OAAQX,MAAAA,aAGzCY,YAAc,IAAIC,qBAAYX,KAAKP,QAASO,KAAKvB,UAAWuB,KAAKJ,SAASgB,WAGnFC,oBACW,CACH,CAACC,uCAAyCC,QAASf,KAAKgB,0BACxD,CAACF,mBAAqBC,QAASf,KAAKiB,WACpC,CAACH,mBAAqBC,QAASf,KAAKkB,WACpC,CAACJ,wBAA0BC,QAASf,KAAKmB,gBACzC,CAACL,wBAA0BC,QAASf,KAAKoB,gBACzC,CAACN,gCAAkCC,QAASf,KAAKQ,kBACjD,CAACM,gCAAkCC,QAASf,KAAKQ,kBAEjD,CAACM,mCAAqCC,QAASf,KAAKqB,2BACpD,CAACP,+BAAiCC,QAASf,KAAKsB,wBAYxDrB,iBAAiBsB,aACPC,YAAcD,MAAM/B,OAAOiC,QAAQzB,KAAKvB,UAAUI,SAClD6C,UAAYH,MAAM/B,OAAOiC,QAAQzB,KAAKvB,UAAUK,aAElD0C,aAAeE,UAAW,iCAEpBtB,QAAUmB,MAAM/B,OAAOiC,QAAQzB,KAAKvB,UAAUC,SAC9CiD,QAAUvB,QAAQwB,cAAc5B,KAAKvB,UAAUK,UAC/C+C,0CAAcF,MAAAA,eAAAA,QAASG,UAAUC,SAAS/B,KAAKhB,QAAQI,sEAEzDsC,WAAaG,YAAa,OAEpBG,UAAY5B,QAAQ6B,aAAa,gBAClCrC,SAASsC,SACV,wBACA,CAACF,YACAH,eAYjBb,8DAAyBvB,QAACA,oBAChBD,OAASQ,KAAKmC,WAAWnC,KAAKvB,UAAUC,QAASe,QAAQa,QAC1Dd,aACK,IAAI4C,uCAAgC3C,QAAQa,WAGhDqB,QAAUnC,OAAOoC,cAAc5B,KAAKvB,UAAUK,UAC9C+C,2CAAcF,MAAAA,eAAAA,QAASG,UAAUC,SAAS/B,KAAKhB,QAAQI,qEAEzDK,QAAQ4C,iBAAmBR,kBACtBS,mBAAmB7C,SAchC6C,mBAAmB7C,QAAS8C,4CAElBZ,QADS3B,KAAKmC,WAAWnC,KAAKvB,UAAUC,QAASe,QAAQa,IACxCsB,cAAc5B,KAAKvB,UAAUK,cAChD0D,4CAAgBb,QAAQtB,QAAQb,8DAAUmC,QAAQM,aAAa,YAC9DO,qBAGLA,cAAgBA,cAAcC,QAAQ,IAAK,UACrCC,YAAchD,SAASC,eAAe6C,mBACvCE,wBAIcC,IAAfJ,aACAA,YAAc9C,QAAQ4C,sBAMpBO,aAAgBL,WAAc,OAAS,2BACtCG,aAAaG,SAASD,cAUjCpC,kDAAiBf,QAACA,QAADK,MAAUA,gBAClBL,MAAAA,mCAAAA,QAASqD,wCAATC,kBAAmBC,UAAqC,MAAzBvD,QAAQqD,SAASG,kBAI/C7C,QAAUN,MAAMM,QAAQ8C,IAAIzD,QAAQqD,SAASd,WAC/C5B,QAAQiC,sBACHC,mBAAmBlC,SAAS,GACjC+C,YACI,oEAAMnD,KAAKT,IAAIE,QAAQqD,SAASxC,4CAA1B8C,sBAA+BC,eAAe,CAACC,MAAO,cAC5D,iCAYIxD,MAACA,MAADL,QAAQA,qBAEd8D,YAAc7D,SAAS8D,cAAc,MAC3CD,YAAYzB,UAAU2B,IAAI,gBAAiB,SAC3CF,YAAYG,UAAY,cACnBnE,IAAIE,QAAQa,IAAMiD,iBAElBjC,sBAAsB,CACvBxB,MAAAA,MACAL,QAASK,MAAMM,QAAQ8C,IAAIzD,QAAQkE,mBAIjCC,KADW5D,KAAKJ,SAASiE,cACTtD,GAAGT,MAAOL,SAI1BqE,kBAFqB9D,KAAK+D,gBAAgBR,YAAa,yCAA0CK,OAEvEzB,kBAC3B5C,IAAIE,QAAQa,IAAMwD,WACvBP,YAAYS,WAAWC,aAAaH,WAAYP,6CAU/BzD,MAACA,MAADL,QAAQA,qBAEnB8D,YAAc7D,SAAS8D,cAAc,OAC3CD,YAAYzB,UAAU2B,IAAI,gBAAiB,SAC3CF,YAAYG,UAAY,cACnBpE,SAASG,QAAQa,IAAMiD,iBAEvBlC,0BAA0B,CAC3BvB,MAAAA,MACAL,QAASK,MAAMW,eAIbmD,KADW5D,KAAKJ,SAASiE,cACTzD,QAAQN,MAAOL,SAI/BqE,kBAFqB9D,KAAK+D,gBAAgBR,YAAa,8CAA+CK,OAE5EzB,kBAC3B7C,SAASG,QAAQa,IAAMwD,WAC5BP,YAAYS,WAAWC,aAAaH,WAAYP,aASpDjC,qDAAsB7B,QAACA,qBACbyE,+BAASzE,QAAQyE,kDAAU,GAC3BC,WAAanE,KAAKmC,WAAWnC,KAAKvB,UAAUE,eAAgBc,QAAQa,SACrE8D,UAAUD,WAAYD,OAAQlE,KAAKT,KAS5C8B,8DAA0B5B,QAACA,qBACjB4E,yCAAc5E,QAAQ4E,iEAAe,QACtCD,UAAUpE,KAAKP,QAAS4E,YAAarE,KAAKV,UAUnD8E,UAAUE,UAAWC,SAAUC,cAGtBD,SAASE,cACVH,UAAUxC,UAAU2B,IAAI,eACxBa,UAAUZ,UAAY,QAK1BY,UAAUxC,UAAU4C,OAAO,UAG3BH,SAASpE,SAAQ,CAACwE,OAAQC,eAChBC,KAAOL,SAASG,QAEhBG,YAAcR,UAAUS,SAASH,YACnBjC,IAAhBmC,YAIAA,cAAgBD,MAChBP,UAAUU,aAAaH,KAAMC,aAJ7BR,UAAUW,OAAOJ,SAQlBP,UAAUS,SAASN,OAASF,SAASE,QACxCH,UAAUY,YAAYZ,UAAUa,WAYxCjE,qBAAUzB,QAACA,sBACAO,KAAKT,IAAIE,QAAQa,IAW5Bc,0BAAe3B,QAACA,sBACLO,KAAKV,SAASG,QAAQa"} courseindex/drawer.min.js.map 0000644 00000004427 15151264135 0012266 0 ustar 00 {"version":3,"file":"drawer.min.js","sources":["../../../src/local/courseindex/drawer.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Course index drawer wrap.\n *\n * This component is mostly used to ensure all subcomponents find a parent\n * compoment with a reactive instance defined.\n *\n * @module core_courseformat/local/courseindex/drawer\n * @class core_courseformat/local/courseindex/drawer\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex-drawer';\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new Component({\n element: document.getElementById(target),\n reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n}\n"],"names":["Component","BaseComponent","create","name","target","selectors","element","document","getElementById","reactive"],"mappings":";;;;;;;;;;;;MA8BqBA,kBAAkBC,wBAKnCC,cAESC,KAAO,iCAUJC,OAAQC,kBACT,IAAIL,UAAU,CACjBM,QAASC,SAASC,eAAeJ,QACjCK,UAAU,0CACVJ,UAAAA"} courseindex/cm.min.js 0000644 00000011037 15151264135 0010620 0 ustar 00 define("core_courseformat/local/courseindex/cm",["exports","core_courseformat/local/courseeditor/dndcmitem","core/templates","core/prefetch","core/config"],(function(_exports,_dndcmitem,_templates,_prefetch,_config){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Course index cm component. * * This component is used to control specific course modules interactions like drag and drop. * * @module core_courseformat/local/courseindex/cm * @class core_courseformat/local/courseindex/cm * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=_interopRequireDefault(_dndcmitem),_templates=_interopRequireDefault(_templates),_prefetch=_interopRequireDefault(_prefetch),_config=_interopRequireDefault(_config);_prefetch.default.prefetchTemplate("core_courseformat/local/courseindex/cmcompletion");class Component extends _dndcmitem.default{create(){this.name="courseindex_cm",this.selectors={CM_NAME:"[data-for='cm_name']",CM_COMPLETION:"[data-for='cm_completion']"},this.classes={CMHIDDEN:"dimmed",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",INDENTED:"indented"},this.id=this.element.dataset.id}static init(target,selectors){return new Component({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configDragDrop(this.id);const cm=state.cm.get(this.id),course=state.course;this._refreshCompletion({state:state,element:cm});const anchor=new URL(window.location.href).hash.replace("#","");(window.location.href==cm.url||window.location.href.includes(course.baseurl)&&anchor==cm.anchor)&&(this.reactive.dispatch("setPageItem","cm",this.id),this.element.scrollIntoView({block:"center"})),_config.default.contextid!=_config.default.courseContextId&&_config.default.contextInstanceId==this.id&&(this.reactive.dispatch("setPageItem","cm",this.id,!0),this.element.scrollIntoView({block:"center"})),cm.uservisible||this.addEventListener(this.getElement(this.selectors.CM_NAME),"click",this._activityAnchor)}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"cm[".concat(this.id,"].completionstate:updated"),handler:this._refreshCompletion},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}_refreshCm(_ref){var _element$dragging,_element$locked,_element$hascmrestric;let{element:element}=_ref;this.element.classList.toggle(this.classes.CMHIDDEN,!element.visible),this.getElement(this.selectors.CM_NAME).innerHTML=element.name,this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.element.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hascmrestric=element.hascmrestrictions)&&void 0!==_element$hascmrestric&&_element$hascmrestric),this.element.classList.toggle(this.classes.INDENTED,element.indent),this.locked=element.locked}_refreshPageItem(_ref2){let{element:element}=_ref2;if(!element.pageItem)return;const isPageId="cm"==element.pageItem.type&&element.pageItem.id==this.id;this.element.classList.toggle(this.classes.PAGEITEM,isPageId),isPageId&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async _refreshCompletion(_ref3){let{state:state,element:element}=_ref3;if(this.reactive.isEditing||!element.istrackeduser)return;const completionElement=this.getElement(this.selectors.CM_COMPLETION);if(completionElement.dataset.value==element.completionstate)return;const data=this.reactive.getExporter().cmCompletion(state,element);try{const{html:html,js:js}=await _templates.default.renderForPromise("core_courseformat/local/courseindex/cmcompletion",data);_templates.default.replaceNode(completionElement,html,js)}catch(error){throw error}}_activityAnchor(event){const cm=this.reactive.get("cm",this.id);if(document.getElementById(cm.anchor))return void setTimeout((()=>{this.reactive.dispatch("setPageItem","cm",cm.id)}),50);const course=this.reactive.get("course"),section=this.reactive.get("section",cm.sectionid);if(!section)return;const url="".concat(course.baseurl,"§ion=").concat(section.number,"#").concat(cm.anchor);event.preventDefault(),window.location=url}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=cm.min.js.map courseindex/placeholder.min.js.map 0000644 00000011520 15151264135 0013254 0 ustar 00 {"version":3,"file":"placeholder.min.js","sources":["../../../src/local/courseindex/placeholder.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Course index placeholder replacer.\n *\n * @module core_courseformat/local/courseindex/placeholder\n * @class core_courseformat/local/courseindex/placeholder\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport Templates from 'core/templates';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport Pending from 'core/pending';\n\nexport default class Component extends BaseComponent {\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new Component({\n element: document.getElementById(target),\n reactive: getCurrentCourseEditor(),\n selectors,\n });\n }\n\n /**\n * Component creation hook.\n */\n create() {\n // Add a pending operation waiting for the initial content.\n this.pendingContent = new Pending(`core_courseformat/placeholder:loadcourseindex`);\n }\n\n /**\n * Initial state ready method.\n *\n * This stateReady to be async because it loads the real courseindex.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n\n // Check if we have a static course index already loded from a previous page.\n if (!this.loadStaticContent()) {\n await this.loadTemplateContent(state);\n }\n }\n\n /**\n * Load the course index from the session storage if any.\n *\n * @return {boolean} true if the static version is loaded form the session\n */\n loadStaticContent() {\n // Load the previous static course index from the session cache.\n const index = this.reactive.getStorageValue(`courseIndex`);\n if (index.html && index.js) {\n Templates.replaceNode(this.element, index.html, index.js);\n this.pendingContent.resolve();\n return true;\n }\n return false;\n }\n\n /**\n * Load the course index template.\n *\n * @param {Object} state the initial state\n */\n async loadTemplateContent(state) {\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(state);\n try {\n // To render an HTML into our component we just use the regular Templates module.\n const {html, js} = await Templates.renderForPromise(\n 'core_courseformat/local/courseindex/courseindex',\n data,\n );\n Templates.replaceNode(this.element, html, js);\n this.pendingContent.resolve();\n\n // Save the rendered template into the session cache.\n this.reactive.setStorageValue(`courseIndex`, {html, js});\n } catch (error) {\n this.pendingContent.resolve(error);\n throw error;\n }\n }\n}\n"],"names":["Component","BaseComponent","target","selectors","element","document","getElementById","reactive","create","pendingContent","Pending","state","this","loadStaticContent","loadTemplateContent","index","getStorageValue","html","js","replaceNode","resolve","data","getExporter","course","Templates","renderForPromise","setStorageValue","error"],"mappings":";;;;;;;;mLA6BqBA,kBAAkBC,oCASvBC,OAAQC,kBACT,IAAIH,UAAU,CACjBI,QAASC,SAASC,eAAeJ,QACjCK,UAAU,0CACVJ,UAAAA,YAORK,cAESC,eAAiB,IAAIC,mFAUbC,OAGRC,KAAKC,2BACAD,KAAKE,oBAAoBH,OASvCE,0BAEUE,MAAQH,KAAKL,SAASS,wCACxBD,MAAME,OAAQF,MAAMG,yBACVC,YAAYP,KAAKR,QAASW,MAAME,KAAMF,MAAMG,SACjDT,eAAeW,WACb,6BAUWT,aAGhBU,KADWT,KAAKL,SAASe,cACTC,OAAOZ,iBAGnBM,KAACA,KAADC,GAAOA,UAAYM,mBAAUC,iBAC/B,kDACAJ,yBAEMF,YAAYP,KAAKR,QAASa,KAAMC,SACrCT,eAAeW,eAGfb,SAASmB,8BAA+B,CAACT,KAAAA,KAAMC,GAAAA,KACtD,MAAOS,kBACAlB,eAAeW,QAAQO,OACtBA"} courseindex/sectiontitle.min.js.map 0000644 00000006122 15151264135 0013502 0 ustar 00 {"version":3,"file":"sectiontitle.min.js","sources":["../../../src/local/courseindex/sectiontitle.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Course index section title component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/sectiontitle\n * @class core_courseformat/local/courseindex/sectiontitle\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DndSectionItem from 'core_courseformat/local/courseeditor/dndsectionitem';\n\nexport default class Component extends DndSectionItem {\n\n /**\n * Constructor hook.\n *\n * @param {Object} descriptor\n */\n create(descriptor) {\n // Optional component name for debugging.\n this.name = 'courseindex_sectiontitle';\n\n this.id = descriptor.id;\n this.section = descriptor.section;\n this.course = descriptor.course;\n this.fullregion = descriptor.fullregion;\n\n // Prevent topic zero from being draggable.\n if (this.section.number > 0) {\n this.getDraggableData = this._getDraggableData;\n }\n }\n\n /**\n * Static method to create a component instance form the mustahce template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new Component({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the initial state\n */\n stateReady(state) {\n this.configDragDrop(this.id, state, this.fullregion);\n }\n}\n"],"names":["Component","DndSectionItem","create","descriptor","name","id","section","course","fullregion","this","number","getDraggableData","_getDraggableData","target","selectors","element","document","getElementById","stateReady","state","configDragDrop"],"mappings":";;;;;;;;;;mKA4BqBA,kBAAkBC,wBAOnCC,OAAOC,iBAEEC,KAAO,gCAEPC,GAAKF,WAAWE,QAChBC,QAAUH,WAAWG,aACrBC,OAASJ,WAAWI,YACpBC,WAAaL,WAAWK,WAGzBC,KAAKH,QAAQI,OAAS,SACjBC,iBAAmBF,KAAKG,+BAWzBC,OAAQC,kBACT,IAAId,UAAU,CACjBe,QAASC,SAASC,eAAeJ,QACjCC,UAAAA,YASRI,WAAWC,YACFC,eAAeX,KAAKJ,GAAIc,MAAOV,KAAKD"} courseindex/drawer.min.js 0000644 00000001764 15151264135 0011513 0 ustar 00 define("core_courseformat/local/courseindex/drawer",["exports","core/reactive","core_courseformat/courseeditor"],(function(_exports,_reactive,_courseeditor){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; /** * Course index drawer wrap. * * This component is mostly used to ensure all subcomponents find a parent * compoment with a reactive instance defined. * * @module core_courseformat/local/courseindex/drawer * @class core_courseformat/local/courseindex/drawer * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class Component extends _reactive.BaseComponent{create(){this.name="courseindex-drawer"}static init(target,selectors){return new Component({element:document.getElementById(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=drawer.min.js.map courseindex/section.min.js 0000644 00000007736 15151264135 0011700 0 ustar 00 define("core_courseformat/local/courseindex/section",["exports","core_courseformat/local/courseindex/sectiontitle","core_courseformat/local/courseeditor/dndsection"],(function(_exports,_sectiontitle,_dndsection){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Course index section component. * * This component is used to control specific course section interactions like drag and drop. * * @module core_courseformat/local/courseindex/section * @class core_courseformat/local/courseindex/section * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_sectiontitle=_interopRequireDefault(_sectiontitle),_dndsection=_interopRequireDefault(_dndsection);class Component extends _dndsection.default{create(){this.name="courseindex_section",this.selectors={SECTION_ITEM:"[data-for='section_item']",SECTION_TITLE:"[data-for='section_title']",CM_LAST:'[data-for="cm"]:last-child'},this.classes={SECTIONHIDDEN:"dimmed",SECTIONCURRENT:"current",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem"},this.id=this.element.dataset.id,this.isPageItem=!1}static init(target,selectors){return new Component({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configState(state);const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(this.reactive.isEditing&&this.reactive.supportComponents){const titleitem=new _sectiontitle.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(titleitem)}const section=state.section.get(this.id);window.location.href==section.sectionurl.replace(/&/g,"&")&&(this.reactive.dispatch("setPageItem","section",this.id),sectionItem.scrollIntoView())}getWatchers(){return[{watch:"section[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}getLastCm(){return this.getElement(this.selectors.CM_LAST)}_refreshSection(_ref){var _element$hasrestricti,_element$dragging,_element$locked;let{element:element}=_ref;const sectionItem=this.getElement(this.selectors.SECTION_ITEM);sectionItem.classList.toggle(this.classes.SECTIONHIDDEN,!element.visible),sectionItem.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hasrestricti=element.hasrestrictions)&&void 0!==_element$hasrestricti&&_element$hasrestricti),this.element.classList.toggle(this.classes.SECTIONCURRENT,element.current),this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked,this.getElement(this.selectors.SECTION_TITLE).innerHTML=element.title}_refreshPageItem(_ref2){var _element$pageItem,_this$pageItem;let{element:element,state:state}=_ref2;if(!element.pageItem)return;if(element.pageItem.sectionId!==this.id&&this.isPageItem)return this.pageItem=!1,void this.getElement(this.selectors.SECTION_ITEM).classList.remove(this.classes.PAGEITEM);var _element$pageItem2;!state.section.get(this.id).indexcollapsed||null!==(_element$pageItem=element.pageItem)&&void 0!==_element$pageItem&&_element$pageItem.isStatic?this.pageItem="section"==element.pageItem.type&&element.pageItem.id==this.id:this.pageItem=(null===(_element$pageItem2=element.pageItem)||void 0===_element$pageItem2?void 0:_element$pageItem2.sectionId)==this.id;this.getElement(this.selectors.SECTION_ITEM).classList.toggle(this.classes.PAGEITEM,null!==(_this$pageItem=this.pageItem)&&void 0!==_this$pageItem&&_this$pageItem),this.pageItem&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=section.min.js.map courseindex/placeholder.min.js 0000644 00000003563 15151264135 0012510 0 ustar 00 define("core_courseformat/local/courseindex/placeholder",["exports","core/reactive","core/templates","core_courseformat/courseeditor","core/pending"],(function(_exports,_reactive,_templates,_courseeditor,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Course index placeholder replacer. * * @module core_courseformat/local/courseindex/placeholder * @class core_courseformat/local/courseindex/placeholder * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),_pending=_interopRequireDefault(_pending);class Component extends _reactive.BaseComponent{static init(target,selectors){return new Component({element:document.getElementById(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}create(){this.pendingContent=new _pending.default("core_courseformat/placeholder:loadcourseindex")}async stateReady(state){this.loadStaticContent()||await this.loadTemplateContent(state)}loadStaticContent(){const index=this.reactive.getStorageValue("courseIndex");return!(!index.html||!index.js)&&(_templates.default.replaceNode(this.element,index.html,index.js),this.pendingContent.resolve(),!0)}async loadTemplateContent(state){const data=this.reactive.getExporter().course(state);try{const{html:html,js:js}=await _templates.default.renderForPromise("core_courseformat/local/courseindex/courseindex",data);_templates.default.replaceNode(this.element,html,js),this.pendingContent.resolve(),this.reactive.setStorageValue("courseIndex",{html:html,js:js})}catch(error){throw this.pendingContent.resolve(error),error}}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=placeholder.min.js.map courseindex/courseindex.min.js 0000644 00000016005 15151264135 0012551 0 ustar 00 define("core_courseformat/local/courseindex/courseindex",["exports","core/reactive","core_courseformat/courseeditor","jquery","core_courseformat/local/courseeditor/contenttree"],(function(_exports,_reactive,_courseeditor,_jquery,_contenttree){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Course index main component. * * @module core_courseformat/local/courseindex/courseindex * @class core_courseformat/local/courseindex/courseindex * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_contenttree=_interopRequireDefault(_contenttree);class Component extends _reactive.BaseComponent{create(){this.name="courseindex",this.selectors={SECTION:"[data-for='section']",SECTION_CMLIST:"[data-for='cmlist']",CM:"[data-for='cm']",TOGGLER:'[data-action="togglecourseindexsection"]',COLLAPSE:'[data-toggle="collapse"]',DRAWER:".drawer"},this.classes={SECTIONHIDDEN:"dimmed",CMHIDDEN:"dimmed",SECTIONCURRENT:"current",COLLAPSED:"collapsed",SHOW:"show"},this.sections={},this.cms={}}static init(target,selectors){return new Component({element:document.getElementById(target),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}stateReady(state){this.addEventListener(this.element,"click",this._sectionTogglers);this.getElements(this.selectors.SECTION).forEach((section=>{this.sections[section.dataset.id]=section}));this.getElements(this.selectors.CM).forEach((cm=>{this.cms[cm.dataset.id]=cm})),this._refreshPageItem({element:state.course,state:state}),this.contentTree=new _contenttree.default(this.element,this.selectors,this.reactive.isEditing)}getWatchers(){return[{watch:"section.indexcollapsed:updated",handler:this._refreshSectionCollapsed},{watch:"cm:created",handler:this._createCm},{watch:"cm:deleted",handler:this._deleteCm},{watch:"section:created",handler:this._createSection},{watch:"section:deleted",handler:this._deleteSection},{watch:"course.pageItem:created",handler:this._refreshPageItem},{watch:"course.pageItem:updated",handler:this._refreshPageItem},{watch:"course.sectionlist:updated",handler:this._refreshCourseSectionlist},{watch:"section.cmlist:updated",handler:this._refreshSectionCmlist}]}_sectionTogglers(event){const sectionlink=event.target.closest(this.selectors.TOGGLER),isChevron=event.target.closest(this.selectors.COLLAPSE);if(sectionlink||isChevron){var _toggler$classList$co;const section=event.target.closest(this.selectors.SECTION),toggler=section.querySelector(this.selectors.COLLAPSE),isCollapsed=null!==(_toggler$classList$co=null==toggler?void 0:toggler.classList.contains(this.classes.COLLAPSED))&&void 0!==_toggler$classList$co&&_toggler$classList$co;if(isChevron||isCollapsed){const sectionId=section.getAttribute("data-id");this.reactive.dispatch("sectionIndexCollapsed",[sectionId],!isCollapsed)}}}_refreshSectionCollapsed(_ref){var _toggler$classList$co2;let{element:element}=_ref;const target=this.getElement(this.selectors.SECTION,element.id);if(!target)throw new Error("Unkown 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;element.indexcollapsed!==isCollapsed&&this._expandSectionNode(element)}_expandSectionNode(element,forceValue){var _toggler$dataset$targ;const toggler=this.getElement(this.selectors.SECTION,element.id).querySelector(this.selectors.COLLAPSE);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;void 0===forceValue&&(forceValue=!element.indexcollapsed);const togglerValue=forceValue?"show":"hide";(0,_jquery.default)(collapsible).collapse(togglerValue)}_refreshPageItem(_ref2){var _element$pageItem;let{element:element,state:state}=_ref2;if(null==element||null===(_element$pageItem=element.pageItem)||void 0===_element$pageItem||!_element$pageItem.isStatic||"cm"!=element.pageItem.type)return;const section=state.section.get(element.pageItem.sectionId);section.indexcollapsed&&(this._expandSectionNode(section,!0),setTimeout((()=>{var _this$cms$element$pag;return null===(_this$cms$element$pag=this.cms[element.pageItem.id])||void 0===_this$cms$element$pag?void 0:_this$cms$element$pag.scrollIntoView({block:"nearest"})}),250))}async _createCm(_ref3){let{state:state,element:element}=_ref3;const fakeelement=document.createElement("li");fakeelement.classList.add("bg-pulse-grey","w-100"),fakeelement.innerHTML=" ",this.cms[element.id]=fakeelement,this._refreshSectionCmlist({state:state,element:state.section.get(element.sectionid)});const data=this.reactive.getExporter().cm(state,element),newelement=(await this.renderComponent(fakeelement,"core_courseformat/local/courseindex/cm",data)).getElement();this.cms[element.id]=newelement,fakeelement.parentNode.replaceChild(newelement,fakeelement)}async _createSection(_ref4){let{state:state,element:element}=_ref4;const fakeelement=document.createElement("div");fakeelement.classList.add("bg-pulse-grey","w-100"),fakeelement.innerHTML=" ",this.sections[element.id]=fakeelement,this._refreshCourseSectionlist({state:state,element:state.course});const data=this.reactive.getExporter().section(state,element),newelement=(await this.renderComponent(fakeelement,"core_courseformat/local/courseindex/section",data)).getElement();this.sections[element.id]=newelement,fakeelement.parentNode.replaceChild(newelement,fakeelement)}_refreshSectionCmlist(_ref5){var _element$cmlist;let{element:element}=_ref5;const cmlist=null!==(_element$cmlist=element.cmlist)&&void 0!==_element$cmlist?_element$cmlist:[],listparent=this.getElement(this.selectors.SECTION_CMLIST,element.id);this._fixOrder(listparent,cmlist,this.cms)}_refreshCourseSectionlist(_ref6){var _element$sectionlist;let{element:element}=_ref6;const sectionlist=null!==(_element$sectionlist=element.sectionlist)&&void 0!==_element$sectionlist?_element$sectionlist:[];this._fixOrder(this.element,sectionlist,this.sections)}_fixOrder(container,neworder,allitems){if(!neworder.length)return container.classList.add("hidden"),void(container.innerHTML="");for(container.classList.remove("hidden"),neworder.forEach(((itemid,index)=>{const item=allitems[itemid],currentitem=container.children[index];void 0!==currentitem?currentitem!==item&&container.insertBefore(item,currentitem):container.append(item)}));container.children.length>neworder.length;)container.removeChild(container.lastChild)}_deleteCm(_ref7){let{element:element}=_ref7;delete this.cms[element.id]}_deleteSection(_ref8){let{element:element}=_ref8;delete this.sections[element.id]}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=courseindex.min.js.map courseindex/cm.min.js.map 0000644 00000026201 15151264135 0011373 0 ustar 00 {"version":3,"file":"cm.min.js","sources":["../../../src/local/courseindex/cm.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Course index cm component.\n *\n * This component is used to control specific course modules interactions like drag and drop.\n *\n * @module core_courseformat/local/courseindex/cm\n * @class core_courseformat/local/courseindex/cm\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DndCmItem from 'core_courseformat/local/courseeditor/dndcmitem';\nimport Templates from 'core/templates';\nimport Prefetch from 'core/prefetch';\nimport Config from 'core/config';\n\n// Prefetch the completion icons template.\nconst completionTemplate = 'core_courseformat/local/courseindex/cmcompletion';\nPrefetch.prefetchTemplate(completionTemplate);\n\nexport default class Component extends DndCmItem {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'courseindex_cm';\n // Default query selectors.\n this.selectors = {\n CM_NAME: `[data-for='cm_name']`,\n CM_COMPLETION: `[data-for='cm_completion']`,\n };\n // Default classes to toggle on refresh.\n this.classes = {\n CMHIDDEN: 'dimmed',\n LOCKED: 'editinprogress',\n RESTRICTIONS: 'restrictions',\n PAGEITEM: 'pageitem',\n INDENTED: 'indented',\n };\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {element|string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new Component({\n element: document.getElementById(target),\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the course state.\n */\n stateReady(state) {\n this.configDragDrop(this.id);\n const cm = state.cm.get(this.id);\n const course = state.course;\n // Refresh completion icon.\n this._refreshCompletion({\n state,\n element: cm,\n });\n const url = new URL(window.location.href);\n const anchor = url.hash.replace('#', '');\n // Check if the current url is the cm url.\n if (window.location.href == cm.url\n || (window.location.href.includes(course.baseurl) && anchor == cm.anchor)\n ) {\n this.reactive.dispatch('setPageItem', 'cm', this.id);\n this.element.scrollIntoView({block: \"center\"});\n }\n // Check if this we are displaying this activity page.\n if (Config.contextid != Config.courseContextId && Config.contextInstanceId == this.id) {\n this.reactive.dispatch('setPageItem', 'cm', this.id, true);\n this.element.scrollIntoView({block: \"center\"});\n }\n // Add anchor logic if the element is not user visible.\n if (!cm.uservisible) {\n this.addEventListener(\n this.getElement(this.selectors.CM_NAME),\n 'click',\n this._activityAnchor,\n );\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `cm[${this.id}]:deleted`, handler: this.remove},\n {watch: `cm[${this.id}]:updated`, handler: this._refreshCm},\n {watch: `cm[${this.id}].completionstate:updated`, handler: this._refreshCompletion},\n {watch: `course.pageItem:updated`, handler: this._refreshPageItem},\n ];\n }\n\n /**\n * Update a course index cm using the state information.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshCm({element}) {\n // Update classes.\n this.element.classList.toggle(this.classes.CMHIDDEN, !element.visible);\n this.getElement(this.selectors.CM_NAME).innerHTML = element.name;\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.element.classList.toggle(this.classes.RESTRICTIONS, element.hascmrestrictions ?? false);\n this.element.classList.toggle(this.classes.INDENTED, element.indent);\n this.locked = element.locked;\n }\n\n /**\n * Handle a page item update.\n *\n * @param {Object} details the update details\n * @param {Object} details.element the course state data.\n */\n _refreshPageItem({element}) {\n if (!element.pageItem) {\n return;\n }\n const isPageId = (element.pageItem.type == 'cm' && element.pageItem.id == this.id);\n this.element.classList.toggle(this.classes.PAGEITEM, isPageId);\n if (isPageId && !this.reactive.isEditing) {\n this.element.scrollIntoView({block: \"nearest\"});\n }\n }\n\n /**\n * Update the activity completion icon.\n *\n * @param {Object} details the update details\n * @param {Object} details.state the state data\n * @param {Object} details.element the element data\n */\n async _refreshCompletion({state, element}) {\n // No completion icons are displayed in edit mode.\n if (this.reactive.isEditing || !element.istrackeduser) {\n return;\n }\n // Check if the completion value has changed.\n const completionElement = this.getElement(this.selectors.CM_COMPLETION);\n if (completionElement.dataset.value == element.completionstate) {\n return;\n }\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.cmCompletion(state, element);\n\n try {\n const {html, js} = await Templates.renderForPromise(completionTemplate, data);\n Templates.replaceNode(completionElement, html, js);\n } catch (error) {\n throw error;\n }\n }\n\n /**\n * The activity anchor event.\n *\n * @param {Event} event\n */\n _activityAnchor(event) {\n const cm = this.reactive.get('cm', this.id);\n // If the user cannot access the element but the element is present in the page\n // the new url should be an anchor link.\n const element = document.getElementById(cm.anchor);\n if (element) {\n // Marc the element as page item once the event is handled.\n setTimeout(() => {\n this.reactive.dispatch('setPageItem', 'cm', cm.id);\n }, 50);\n return;\n }\n // If the element is not present in the page we need to go to the specific section.\n const course = this.reactive.get('course');\n const section = this.reactive.get('section', cm.sectionid);\n if (!section) {\n return;\n }\n const url = `${course.baseurl}§ion=${section.number}#${cm.anchor}`;\n event.preventDefault();\n window.location = url;\n }\n}\n"],"names":["prefetchTemplate","Component","DndCmItem","create","name","selectors","CM_NAME","CM_COMPLETION","classes","CMHIDDEN","LOCKED","RESTRICTIONS","PAGEITEM","INDENTED","id","this","element","dataset","target","document","getElementById","stateReady","state","configDragDrop","cm","get","course","_refreshCompletion","anchor","URL","window","location","href","hash","replace","url","includes","baseurl","reactive","dispatch","scrollIntoView","block","Config","contextid","courseContextId","contextInstanceId","uservisible","addEventListener","getElement","_activityAnchor","getWatchers","watch","handler","remove","_refreshCm","_refreshPageItem","classList","toggle","visible","innerHTML","DRAGGING","dragging","locked","hascmrestrictions","indent","pageItem","isPageId","type","isEditing","istrackeduser","completionElement","value","completionstate","data","getExporter","cmCompletion","html","js","Templates","renderForPromise","replaceNode","error","event","setTimeout","section","sectionid","number","preventDefault"],"mappings":";;;;;;;;;;uRAiCSA,iBADkB,0DAGNC,kBAAkBC,mBAKnCC,cAESC,KAAO,sBAEPC,UAAY,CACbC,+BACAC,iDAGCC,QAAU,CACXC,SAAU,SACVC,OAAQ,iBACRC,aAAc,eACdC,SAAU,WACVC,SAAU,iBAGTC,GAAKC,KAAKC,QAAQC,QAAQH,eAUvBI,OAAQb,kBACT,IAAIJ,UAAU,CACjBe,QAASG,SAASC,eAAeF,QACjCb,UAAAA,YASRgB,WAAWC,YACFC,eAAeR,KAAKD,UACnBU,GAAKF,MAAME,GAAGC,IAAIV,KAAKD,IACvBY,OAASJ,MAAMI,YAEhBC,mBAAmB,CACpBL,MAAAA,MACAN,QAASQ,WAGPI,OADM,IAAIC,IAAIC,OAAOC,SAASC,MACjBC,KAAKC,QAAQ,IAAK,KAEjCJ,OAAOC,SAASC,MAAQR,GAAGW,KACvBL,OAAOC,SAASC,KAAKI,SAASV,OAAOW,UAAYT,QAAUJ,GAAGI,eAE7DU,SAASC,SAAS,cAAe,KAAMxB,KAAKD,SAC5CE,QAAQwB,eAAe,CAACC,MAAO,YAGpCC,gBAAOC,WAAaD,gBAAOE,iBAAmBF,gBAAOG,mBAAqB9B,KAAKD,UAC1EwB,SAASC,SAAS,cAAe,KAAMxB,KAAKD,IAAI,QAChDE,QAAQwB,eAAe,CAACC,MAAO,YAGnCjB,GAAGsB,kBACCC,iBACDhC,KAAKiC,WAAWjC,KAAKV,UAAUC,SAC/B,QACAS,KAAKkC,iBAUjBC,oBACW,CACH,CAACC,mBAAapC,KAAKD,gBAAesC,QAASrC,KAAKsC,QAChD,CAACF,mBAAapC,KAAKD,gBAAesC,QAASrC,KAAKuC,YAChD,CAACH,mBAAapC,KAAKD,gCAA+BsC,QAASrC,KAAKY,oBAChE,CAACwB,gCAAkCC,QAASrC,KAAKwC,mBAUzDD,iFAAWtC,QAACA,mBAEHA,QAAQwC,UAAUC,OAAO1C,KAAKP,QAAQC,UAAWO,QAAQ0C,cACzDV,WAAWjC,KAAKV,UAAUC,SAASqD,UAAY3C,QAAQZ,UACvDY,QAAQwC,UAAUC,OAAO1C,KAAKP,QAAQoD,mCAAU5C,QAAQ6C,+DACxD7C,QAAQwC,UAAUC,OAAO1C,KAAKP,QAAQE,+BAAQM,QAAQ8C,yDACtD9C,QAAQwC,UAAUC,OAAO1C,KAAKP,QAAQG,2CAAcK,QAAQ+C,gFAC5D/C,QAAQwC,UAAUC,OAAO1C,KAAKP,QAAQK,SAAUG,QAAQgD,aACxDF,OAAS9C,QAAQ8C,OAS1BP,4BAAiBvC,QAACA,mBACTA,QAAQiD,sBAGPC,SAAqC,MAAzBlD,QAAQiD,SAASE,MAAgBnD,QAAQiD,SAASnD,IAAMC,KAAKD,QAC1EE,QAAQwC,UAAUC,OAAO1C,KAAKP,QAAQI,SAAUsD,UACjDA,WAAanD,KAAKuB,SAAS8B,gBACtBpD,QAAQwB,eAAe,CAACC,MAAO,gDAWnBnB,MAACA,MAADN,QAAQA,kBAEzBD,KAAKuB,SAAS8B,YAAcpD,QAAQqD,2BAIlCC,kBAAoBvD,KAAKiC,WAAWjC,KAAKV,UAAUE,kBACrD+D,kBAAkBrD,QAAQsD,OAASvD,QAAQwD,6BAMzCC,KADW1D,KAAKuB,SAASoC,cACTC,aAAarD,MAAON,mBAGhC4D,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAvJpB,mDAuJyDN,yBAC9DO,YAAYV,kBAAmBM,KAAMC,IACjD,MAAOI,aACCA,OASdhC,gBAAgBiC,aACN1D,GAAKT,KAAKuB,SAASb,IAAI,KAAMV,KAAKD,OAGxBK,SAASC,eAAeI,GAAGI,oBAGvCuD,YAAW,UACF7C,SAASC,SAAS,cAAe,KAAMf,GAAGV,MAChD,UAIDY,OAASX,KAAKuB,SAASb,IAAI,UAC3B2D,QAAUrE,KAAKuB,SAASb,IAAI,UAAWD,GAAG6D,eAC3CD,qBAGCjD,cAAST,OAAOW,4BAAmB+C,QAAQE,mBAAU9D,GAAGI,QAC9DsD,MAAMK,iBACNzD,OAAOC,SAAWI"} content/section.min.js.map 0000644 00000023304 15151264135 0011563 0 ustar 00 {"version":3,"file":"section.min.js","sources":["../../../src/local/content/section.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Course section format component.\n *\n * @module core_courseformat/local/content/section\n * @class core_courseformat/local/content/section\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Header from 'core_courseformat/local/content/section/header';\nimport DndSection from 'core_courseformat/local/courseeditor/dndsection';\nimport Templates from 'core/templates';\n\nexport default class extends DndSection {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_section';\n // Default query selectors.\n this.selectors = {\n SECTION_ITEM: `[data-for='section_title']`,\n CM: `[data-for=\"cmitem\"]`,\n SECTIONINFO: `[data-for=\"sectioninfo\"]`,\n SECTIONBADGES: `[data-region=\"sectionbadges\"]`,\n SHOWSECTION: `[data-action=\"sectionShow\"]`,\n HIDESECTION: `[data-action=\"sectionHide\"]`,\n ACTIONTEXT: `.menu-action-text`,\n ICON: `.icon`,\n };\n // Most classes will be loaded later by DndCmItem.\n this.classes = {\n LOCKED: 'editinprogress',\n HASDESCRIPTION: 'description',\n HIDE: 'd-none',\n HIDDEN: 'hidden',\n };\n\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the initial state\n */\n stateReady(state) {\n this.configState(state);\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Section zero and other formats sections may not have a title to drag.\n const sectionItem = this.getElement(this.selectors.SECTION_ITEM);\n if (sectionItem) {\n // Init the inner dragable element.\n const headerComponent = new Header({\n ...this,\n element: sectionItem,\n fullregion: this.element,\n });\n this.configDragDrop(headerComponent);\n }\n }\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `section[${this.id}]:updated`, handler: this._refreshSection},\n ];\n }\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n // If the format uses one section per page sections dropping in the content is ignored.\n if (dropdata?.type === 'section' && this.reactive.sectionReturn != 0) {\n return false;\n }\n return super.validateDropData(dropdata);\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null}\n */\n getLastCm() {\n const cms = this.getElements(this.selectors.CM);\n // DndUpload may add extra elements so :last-child selector cannot be used.\n if (!cms || cms.length === 0) {\n return null;\n }\n return cms[cms.length - 1];\n }\n\n /**\n * Update a content section using the state information.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshSection({element}) {\n // Update classes.\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.element.classList.toggle(this.classes.HIDDEN, !element.visible ?? false);\n this.locked = element.locked;\n // The description box classes depends on the section state.\n const sectioninfo = this.getElement(this.selectors.SECTIONINFO);\n if (sectioninfo) {\n sectioninfo.classList.toggle(this.classes.HASDESCRIPTION, element.hasrestrictions);\n }\n // Update section badges and menus.\n this._updateBadges(element);\n this._updateActionsMenu(element);\n }\n\n /**\n * Update a section badges using the state information.\n *\n * @param {object} section the section state.\n */\n _updateBadges(section) {\n const current = this.getElement(`${this.selectors.SECTIONBADGES} [data-type='iscurrent']`);\n current?.classList.toggle(this.classes.HIDE, !section.current);\n\n const hiddenFromStudents = this.getElement(`${this.selectors.SECTIONBADGES} [data-type='hiddenfromstudents']`);\n hiddenFromStudents?.classList.toggle(this.classes.HIDE, section.visible);\n }\n\n /**\n * Update a section action menus.\n *\n * @param {object} section the section state.\n */\n async _updateActionsMenu(section) {\n let selector;\n let newAction;\n if (section.visible) {\n selector = this.selectors.SHOWSECTION;\n newAction = 'sectionHide';\n } else {\n selector = this.selectors.HIDESECTION;\n newAction = 'sectionShow';\n }\n // Find the affected action.\n const affectedAction = this.getElement(selector);\n if (!affectedAction) {\n return;\n }\n // Change action.\n affectedAction.dataset.action = newAction;\n // Change text.\n const actionText = affectedAction.querySelector(this.selectors.ACTIONTEXT);\n if (affectedAction.dataset?.swapname && actionText) {\n const oldText = actionText?.innerText;\n actionText.innerText = affectedAction.dataset.swapname;\n affectedAction.dataset.swapname = oldText;\n }\n // Change icon.\n const icon = affectedAction.querySelector(this.selectors.ICON);\n if (affectedAction.dataset?.swapicon && icon) {\n const newIcon = affectedAction.dataset.swapicon;\n if (newIcon) {\n const pixHtml = await Templates.renderPix(newIcon, 'core');\n Templates.replaceNode(icon, pixHtml, '');\n }\n }\n }\n}\n"],"names":["DndSection","create","name","selectors","SECTION_ITEM","CM","SECTIONINFO","SECTIONBADGES","SHOWSECTION","HIDESECTION","ACTIONTEXT","ICON","classes","LOCKED","HASDESCRIPTION","HIDE","HIDDEN","id","this","element","dataset","stateReady","state","configState","reactive","isEditing","supportComponents","sectionItem","getElement","headerComponent","Header","fullregion","configDragDrop","getWatchers","watch","handler","_refreshSection","validateDropData","dropdata","type","sectionReturn","super","getLastCm","cms","getElements","length","classList","toggle","DRAGGING","dragging","locked","visible","sectioninfo","hasrestrictions","_updateBadges","_updateActionsMenu","section","current","hiddenFromStudents","selector","newAction","affectedAction","action","actionText","querySelector","swapname","oldText","innerText","icon","swapicon","newIcon","pixHtml","Templates","renderPix","replaceNode"],"mappings":";;;;;;;;kPA4B6BA,oBAKzBC,cAESC,KAAO,uBAEPC,UAAY,CACbC,0CACAC,yBACAC,uCACAC,8CACAC,0CACAC,0CACAC,+BACAC,mBAGCC,QAAU,CACXC,OAAQ,iBACRC,eAAgB,cAChBC,KAAM,SACNC,OAAQ,eAIPC,GAAKC,KAAKC,QAAQC,QAAQH,GAQnCI,WAAWC,eACFC,YAAYD,OAEbJ,KAAKM,SAASC,WAAaP,KAAKM,SAASE,kBAAmB,OAEtDC,YAAcT,KAAKU,WAAWV,KAAKf,UAAUC,iBAC/CuB,YAAa,OAEPE,gBAAkB,IAAIC,gBAAO,IAC5BZ,KACHC,QAASQ,YACTI,WAAYb,KAAKC,eAEhBa,eAAeH,mBAUhCI,oBACW,CACH,CAACC,wBAAkBhB,KAAKD,gBAAekB,QAASjB,KAAKkB,kBAU7DC,iBAAiBC,iBAES,aAAnBA,MAAAA,gBAAAA,SAAUC,OAAqD,GAA/BrB,KAAKM,SAASgB,gBAG1CC,MAAMJ,iBAAiBC,UAQlCI,kBACUC,IAAMzB,KAAK0B,YAAY1B,KAAKf,UAAUE,WAEvCsC,KAAsB,IAAfA,IAAIE,OAGTF,IAAIA,IAAIE,OAAS,GAFb,KAWfT,iFAAgBjB,QAACA,mBAERA,QAAQ2B,UAAUC,OAAO7B,KAAKN,QAAQoC,mCAAU7B,QAAQ8B,+DACxD9B,QAAQ2B,UAAUC,OAAO7B,KAAKN,QAAQC,+BAAQM,QAAQ+B,yDACtD/B,QAAQ2B,UAAUC,OAAO7B,KAAKN,QAAQI,iCAASG,QAAQgC,4DACvDD,OAAS/B,QAAQ+B,aAEhBE,YAAclC,KAAKU,WAAWV,KAAKf,UAAUG,aAC/C8C,aACAA,YAAYN,UAAUC,OAAO7B,KAAKN,QAAQE,eAAgBK,QAAQkC,sBAGjEC,cAAcnC,cACdoC,mBAAmBpC,SAQ5BmC,cAAcE,eACJC,QAAUvC,KAAKU,qBAAcV,KAAKf,UAAUI,2CAClDkD,MAAAA,SAAAA,QAASX,UAAUC,OAAO7B,KAAKN,QAAQG,MAAOyC,QAAQC,eAEhDC,mBAAqBxC,KAAKU,qBAAcV,KAAKf,UAAUI,oDAC7DmD,MAAAA,oBAAAA,mBAAoBZ,UAAUC,OAAO7B,KAAKN,QAAQG,KAAMyC,QAAQL,kCAQ3CK,8DACjBG,SACAC,UACAJ,QAAQL,SACRQ,SAAWzC,KAAKf,UAAUK,YAC1BoD,UAAY,gBAEZD,SAAWzC,KAAKf,UAAUM,YAC1BmD,UAAY,qBAGVC,eAAiB3C,KAAKU,WAAW+B,cAClCE,sBAILA,eAAezC,QAAQ0C,OAASF,gBAE1BG,WAAaF,eAAeG,cAAc9C,KAAKf,UAAUO,6CAC3DmD,eAAezC,gEAAS6C,UAAYF,WAAY,OAC1CG,QAAUH,MAAAA,kBAAAA,WAAYI,UAC5BJ,WAAWI,UAAYN,eAAezC,QAAQ6C,SAC9CJ,eAAezC,QAAQ6C,SAAWC,cAGhCE,KAAOP,eAAeG,cAAc9C,KAAKf,UAAUQ,wCACrDkD,eAAezC,kEAASiD,UAAYD,KAAM,OACpCE,QAAUT,eAAezC,QAAQiD,YACnCC,QAAS,OACHC,cAAgBC,mBAAUC,UAAUH,QAAS,2BACzCI,YAAYN,KAAMG,QAAS"} content/actions.min.js.map 0000644 00000060506 15151264135 0011564 0 ustar 00 {"version":3,"file":"actions.min.js","sources":["../../../src/local/content/actions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Course state actions dispatcher.\n *\n * This module captures all data-dispatch links in the course content and dispatch the proper\n * state mutation, including any confirmation and modal required.\n *\n * @module core_courseformat/local/content/actions\n * @class core_courseformat/local/content/actions\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport ModalFactory from 'core/modal_factory';\nimport ModalEvents from 'core/modal_events';\nimport Templates from 'core/templates';\nimport {prefetchStrings} from 'core/prefetch';\nimport {get_string as getString} from 'core/str';\nimport {getList} from 'core/normalise';\nimport * as CourseEvents from 'core_course/events';\nimport Pending from 'core/pending';\nimport ContentTree from 'core_courseformat/local/courseeditor/contenttree';\n// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.\nimport jQuery from 'jquery';\n\n// Load global strings.\nprefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']);\n\n// Mutations are dispatched by the course content actions.\n// Formats can use this module addActions static method to add custom actions.\n// Direct mutations can be simple strings (mutation) name or functions.\nconst directMutations = {\n sectionHide: 'sectionHide',\n sectionShow: 'sectionShow',\n cmHide: 'cmHide',\n cmShow: 'cmShow',\n cmStealth: 'cmStealth',\n cmMoveRight: 'cmMoveRight',\n cmMoveLeft: 'cmMoveLeft',\n};\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_actions';\n // Default query selectors.\n this.selectors = {\n ACTIONLINK: `[data-action]`,\n // Move modal selectors.\n SECTIONLINK: `[data-for='section']`,\n CMLINK: `[data-for='cm']`,\n SECTIONNODE: `[data-for='sectionnode']`,\n MODALTOGGLER: `[data-toggle='collapse']`,\n ADDSECTION: `[data-action='addSection']`,\n CONTENTTREE: `#destination-selector`,\n ACTIONMENU: `.action-menu`,\n ACTIONMENUTOGGLER: `[data-toggle=\"dropdown\"]`,\n };\n // Component css classes.\n this.classes = {\n DISABLED: `disabled`,\n };\n }\n\n /**\n * Add extra actions to the module.\n *\n * @param {array} actions array of methods to execute\n */\n static addActions(actions) {\n for (const [action, mutationReference] of Object.entries(actions)) {\n if (typeof mutationReference !== 'function' && typeof mutationReference !== 'string') {\n throw new Error(`${action} action must be a mutation name or a function`);\n }\n directMutations[action] = mutationReference;\n }\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the state data.\n *\n */\n stateReady(state) {\n // Delegate dispatch clicks.\n this.addEventListener(\n this.element,\n 'click',\n this._dispatchClick\n );\n // Check section limit.\n this._checkSectionlist({state});\n // Add an Event listener to recalculate limits it if a section HTML is altered.\n this.addEventListener(\n this.element,\n CourseEvents.sectionRefreshed,\n () => this._checkSectionlist({state})\n );\n }\n\n /**\n * Return the component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n // Check section limit.\n {watch: `course.sectionlist:updated`, handler: this._checkSectionlist},\n ];\n }\n\n _dispatchClick(event) {\n const target = event.target.closest(this.selectors.ACTIONLINK);\n if (!target) {\n return;\n }\n if (target.classList.contains(this.classes.DISABLED)) {\n event.preventDefault();\n return;\n }\n\n // Invoke proper method.\n const actionName = target.dataset.action;\n const methodName = this._actionMethodName(actionName);\n\n if (this[methodName] !== undefined) {\n this[methodName](target, event);\n return;\n }\n\n // Check direct mutations or mutations handlers.\n if (directMutations[actionName] !== undefined) {\n if (typeof directMutations[actionName] === 'function') {\n directMutations[actionName](target, event);\n return;\n }\n this._requestMutationAction(target, event, directMutations[actionName]);\n return;\n }\n }\n\n _actionMethodName(name) {\n const requestName = name.charAt(0).toUpperCase() + name.slice(1);\n return `_request${requestName}`;\n }\n\n /**\n * Check the section list and disable some options if needed.\n *\n * @param {Object} detail the update details.\n * @param {Object} detail.state the state object.\n */\n _checkSectionlist({state}) {\n // Disable \"add section\" actions if the course max sections has been exceeded.\n this._setAddSectionLocked(state.course.sectionlist.length > state.course.maxsections);\n }\n\n /**\n * Handle a move section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveSection(target, event) {\n // Check we have an id.\n const sectionId = target.dataset.id;\n if (!sectionId) {\n return;\n }\n const sectionInfo = this.reactive.get('section', sectionId);\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveSectionModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n\n // Add the target section id and title.\n data.sectionid = sectionInfo.id;\n data.sectiontitle = sectionInfo.title;\n\n // Build the modal parameters from the event data.\n const modalParams = {\n title: getString('movecoursesection', 'core'),\n body: Templates.render('core_courseformat/local/content/movesection', data),\n };\n\n // Create the modal.\n const modal = await this._modalBodyRenderedPromise(modalParams);\n\n const modalBody = getList(modal.getBody())[0];\n\n // Disable current element and section zero.\n const currentElement = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-id='${sectionId}']`);\n this._disableLink(currentElement);\n const generalSection = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-number='0']`);\n this._disableLink(generalSection);\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n },\n true\n );\n\n // Capture click.\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for != 'section' || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n this.reactive.dispatch('sectionMove', [sectionId], target.dataset.id);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Handle a move cm request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestMoveCm(target, event) {\n // Check we have an id.\n const cmId = target.dataset.id;\n if (!cmId) {\n return;\n }\n const cmInfo = this.reactive.get('cm', cmId);\n\n event.preventDefault();\n\n const pendingModalReady = new Pending(`courseformat/actions:prepareMoveCmModal`);\n\n // The section edit menu to refocus on end.\n const editTools = this._getClosestActionMenuToogler(target);\n\n // Collect section information from the state.\n const exporter = this.reactive.getExporter();\n const data = exporter.course(this.reactive.state);\n\n // Add the target cm info.\n data.cmid = cmInfo.id;\n data.cmname = cmInfo.name;\n\n // Build the modal parameters from the event data.\n const modalParams = {\n title: getString('movecoursemodule', 'core'),\n body: Templates.render('core_courseformat/local/content/movecm', data),\n };\n\n // Create the modal.\n const modal = await this._modalBodyRenderedPromise(modalParams);\n\n const modalBody = getList(modal.getBody())[0];\n\n // Disable current element.\n let currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);\n this._disableLink(currentElement);\n\n // Setup keyboard navigation.\n new ContentTree(\n modalBody.querySelector(this.selectors.CONTENTTREE),\n {\n SECTION: this.selectors.SECTIONNODE,\n TOGGLER: this.selectors.MODALTOGGLER,\n COLLAPSE: this.selectors.MODALTOGGLER,\n ENTER: this.selectors.SECTIONLINK,\n }\n );\n\n // Open the cm section node if possible (Bootstrap 4 uses jQuery to interact with collapsibles).\n // All jQuery int this code can be replaced when MDL-71979 is integrated.\n const sectionnode = currentElement.closest(this.selectors.SECTIONNODE);\n const toggler = jQuery(sectionnode).find(this.selectors.MODALTOGGLER);\n let collapsibleId = toggler.data('target') ?? toggler.attr('href');\n if (collapsibleId) {\n // We cannot be sure we have # in the id element name.\n collapsibleId = collapsibleId.replace('#', '');\n jQuery(`#${collapsibleId}`).collapse('toggle');\n }\n\n // Capture click.\n modalBody.addEventListener('click', (event) => {\n const target = event.target;\n if (!target.matches('a') || target.dataset.for === undefined || target.dataset.id === undefined) {\n return;\n }\n if (target.getAttribute('aria-disabled')) {\n return;\n }\n event.preventDefault();\n\n // Get draggable data from cm or section to dispatch.\n let targetSectionId;\n let targetCmId;\n if (target.dataset.for == 'cm') {\n const dropData = exporter.cmDraggableData(this.reactive.state, target.dataset.id);\n targetSectionId = dropData.sectionid;\n targetCmId = dropData.nextcmid;\n } else {\n const section = this.reactive.get('section', target.dataset.id);\n targetSectionId = target.dataset.id;\n targetCmId = section?.cmlist[0];\n }\n\n this.reactive.dispatch('cmMove', [cmId], targetSectionId, targetCmId);\n this._destroyModal(modal, editTools);\n });\n\n pendingModalReady.resolve();\n }\n\n /**\n * Handle a create section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestAddSection(target, event) {\n event.preventDefault();\n this.reactive.dispatch('addSection', target.dataset.id ?? 0);\n }\n\n /**\n * Handle a delete section request.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n */\n async _requestDeleteSection(target, event) {\n // Check we have an id.\n const sectionId = target.dataset.id;\n\n if (!sectionId) {\n return;\n }\n const sectionInfo = this.reactive.get('section', sectionId);\n\n event.preventDefault();\n\n const cmList = sectionInfo.cmlist ?? [];\n if (cmList.length || sectionInfo.hassummary || sectionInfo.rawtitle) {\n // We need confirmation if the section has something.\n const modalParams = {\n title: getString('confirm', 'core'),\n body: getString('confirmdeletesection', 'moodle', sectionInfo.title),\n saveButtonText: getString('delete', 'core'),\n type: ModalFactory.types.SAVE_CANCEL,\n };\n\n const modal = await this._modalBodyRenderedPromise(modalParams);\n\n modal.getRoot().on(\n ModalEvents.save,\n e => {\n // Stop the default save button behaviour which is to close the modal.\n e.preventDefault();\n modal.destroy();\n this.reactive.dispatch('sectionDelete', [sectionId]);\n }\n );\n return;\n } else {\n // We don't need confirmation to delete empty sections.\n this.reactive.dispatch('sectionDelete', [sectionId]);\n }\n }\n\n /**\n * Basic mutation action helper.\n *\n * @param {Element} target the dispatch action element\n * @param {Event} event the triggered event\n * @param {string} mutationName the mutation name\n */\n async _requestMutationAction(target, event, mutationName) {\n if (!target.dataset.id) {\n return;\n }\n event.preventDefault();\n this.reactive.dispatch(mutationName, [target.dataset.id]);\n }\n\n /**\n * Disable all add sections actions.\n *\n * @param {boolean} locked the new locked value.\n */\n _setAddSectionLocked(locked) {\n const targets = this.getElements(this.selectors.ADDSECTION);\n targets.forEach(element => {\n element.classList.toggle(this.classes.DISABLED, locked);\n this.setElementLocked(element, locked);\n });\n }\n\n /**\n * Replace an element with a copy with a different tag name.\n *\n * @param {Element} element the original element\n */\n _disableLink(element) {\n if (element) {\n element.style.pointerEvents = 'none';\n element.style.userSelect = 'none';\n element.classList.add(this.classes.DISABLED);\n element.setAttribute('aria-disabled', true);\n element.addEventListener('click', event => event.preventDefault());\n }\n }\n\n /**\n * Render a modal and return a body ready promise.\n *\n * @param {object} modalParams the modal params\n * @return {Promise} the modal body ready promise\n */\n _modalBodyRenderedPromise(modalParams) {\n return new Promise((resolve, reject) => {\n ModalFactory.create(modalParams).then((modal) => {\n modal.setRemoveOnClose(true);\n // Handle body loading event.\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n resolve(modal);\n });\n // Configure some extra modal params.\n if (modalParams.saveButtonText !== undefined) {\n modal.setSaveButtonText(modalParams.saveButtonText);\n }\n modal.show();\n return;\n }).catch(() => {\n reject(`Cannot load modal content`);\n });\n });\n }\n\n /**\n * Hide and later destroy a modal.\n *\n * Behat will fail if we remove the modal while some boostrap collapse is executing.\n *\n * @param {Modal} modal\n * @param {HTMLElement} element the dom element to focus on.\n */\n _destroyModal(modal, element) {\n modal.hide();\n const pendingDestroy = new Pending(`courseformat/actions:destroyModal`);\n if (element) {\n element.focus();\n }\n setTimeout(() =>{\n modal.destroy();\n pendingDestroy.resolve();\n }, 500);\n }\n\n /**\n * Get the closest actions menu toggler to an action element.\n *\n * @param {HTMLElement} element the action link element\n * @returns {HTMLElement|undefined}\n */\n _getClosestActionMenuToogler(element) {\n const actionMenu = element.closest(this.selectors.ACTIONMENU);\n if (!actionMenu) {\n return undefined;\n }\n return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER);\n }\n}\n"],"names":["directMutations","sectionHide","sectionShow","cmHide","cmShow","cmStealth","cmMoveRight","cmMoveLeft","BaseComponent","create","name","selectors","ACTIONLINK","SECTIONLINK","CMLINK","SECTIONNODE","MODALTOGGLER","ADDSECTION","CONTENTTREE","ACTIONMENU","ACTIONMENUTOGGLER","classes","DISABLED","actions","action","mutationReference","Object","entries","Error","stateReady","state","addEventListener","this","element","_dispatchClick","_checkSectionlist","CourseEvents","sectionRefreshed","getWatchers","watch","handler","event","target","closest","classList","contains","preventDefault","actionName","dataset","methodName","_actionMethodName","undefined","_requestMutationAction","requestName","charAt","toUpperCase","slice","_setAddSectionLocked","course","sectionlist","length","maxsections","sectionId","id","sectionInfo","reactive","get","pendingModalReady","Pending","editTools","_getClosestActionMenuToogler","data","getExporter","sectionid","sectiontitle","title","modalParams","body","Templates","render","modal","_modalBodyRenderedPromise","modalBody","getBody","currentElement","querySelector","_disableLink","generalSection","ContentTree","SECTION","TOGGLER","COLLAPSE","matches","for","getAttribute","dispatch","_destroyModal","resolve","cmId","cmInfo","exporter","cmid","cmname","ENTER","sectionnode","toggler","find","collapsibleId","attr","replace","collapse","targetSectionId","targetCmId","dropData","cmDraggableData","nextcmid","section","cmlist","hassummary","rawtitle","saveButtonText","type","ModalFactory","types","SAVE_CANCEL","getRoot","on","ModalEvents","save","e","destroy","mutationName","locked","getElements","forEach","toggle","setElementLocked","style","pointerEvents","userSelect","add","setAttribute","Promise","reject","then","setRemoveOnClose","bodyRendered","setSaveButtonText","show","catch","hide","pendingDestroy","focus","setTimeout","actionMenu"],"mappings":";;;;;;;;;;;ujCAyCgB,OAAQ,CAAC,oBAAqB,mBAAoB,UAAW,iBAKvEA,gBAAkB,CACpBC,YAAa,cACbC,YAAa,cACbC,OAAQ,SACRC,OAAQ,SACRC,UAAW,YACXC,YAAa,cACbC,WAAY,qCAGaC,wBAKzBC,cAESC,KAAO,uBAEPC,UAAY,CACbC,2BAEAC,mCACAC,yBACAC,uCACAC,wCACAC,wCACAC,oCACAC,0BACAC,mDAGCC,QAAU,CACXC,uCASUC,aACT,MAAOC,OAAQC,qBAAsBC,OAAOC,QAAQJ,SAAU,IAC9B,mBAAtBE,mBAAiE,iBAAtBA,wBAC5C,IAAIG,gBAASJ,yDAEvBxB,gBAAgBwB,QAAUC,mBAUlCI,WAAWC,YAEFC,iBACDC,KAAKC,QACL,QACAD,KAAKE,qBAGJC,kBAAkB,CAACL,MAAAA,aAEnBC,iBACDC,KAAKC,QACLG,aAAaC,kBACb,IAAML,KAAKG,kBAAkB,CAACL,MAAAA,UAStCQ,oBACW,CAEH,CAACC,mCAAqCC,QAASR,KAAKG,oBAI5DD,eAAeO,aACLC,OAASD,MAAMC,OAAOC,QAAQX,KAAKrB,UAAUC,gBAC9C8B,iBAGDA,OAAOE,UAAUC,SAASb,KAAKX,QAAQC,sBACvCmB,MAAMK,uBAKJC,WAAaL,OAAOM,QAAQxB,OAC5ByB,WAAajB,KAAKkB,kBAAkBH,oBAEjBI,IAArBnB,KAAKiB,wBAM2BE,IAAhCnD,gBAAgB+C,YAC2B,mBAAhC/C,gBAAgB+C,iBACvB/C,gBAAgB+C,YAAYL,OAAQD,iBAGnCW,uBAAuBV,OAAQD,MAAOzC,gBAAgB+C,yBAVtDE,YAAYP,OAAQD,OAejCS,kBAAkBxC,YACR2C,YAAc3C,KAAK4C,OAAO,GAAGC,cAAgB7C,KAAK8C,MAAM,2BAC5CH,aAStBlB,4BAAkBL,MAACA,iBAEV2B,qBAAqB3B,MAAM4B,OAAOC,YAAYC,OAAS9B,MAAM4B,OAAOG,uCASnDnB,OAAQD,aAExBqB,UAAYpB,OAAOM,QAAQe,OAC5BD,uBAGCE,YAAchC,KAAKiC,SAASC,IAAI,UAAWJ,WAEjDrB,MAAMK,uBAEAqB,kBAAoB,IAAIC,iEAGxBC,UAAYrC,KAAKsC,6BAA6B5B,QAI9C6B,KADWvC,KAAKiC,SAASO,cACTd,OAAO1B,KAAKiC,SAASnC,OAG3CyC,KAAKE,UAAYT,YAAYD,GAC7BQ,KAAKG,aAAeV,YAAYW,YAG1BC,YAAc,CAChBD,OAAO,mBAAU,oBAAqB,QACtCE,KAAMC,mBAAUC,OAAO,8CAA+CR,OAIpES,YAAchD,KAAKiD,0BAA0BL,aAE7CM,WAAY,sBAAQF,MAAMG,WAAW,GAGrCC,eAAiBF,UAAUG,wBAAiBrD,KAAKrB,UAAUE,iCAAwBiD,sBACpFwB,aAAaF,sBACZG,eAAiBL,UAAUG,wBAAiBrD,KAAKrB,UAAUE,uCAC5DyE,aAAaC,oBAGdC,qBACAN,UAAUG,cAAcrD,KAAKrB,UAAUO,aACvC,CACIuE,QAASzD,KAAKrB,UAAUI,YACxB2E,QAAS1D,KAAKrB,UAAUK,aACxB2E,SAAU3D,KAAKrB,UAAUK,eAE7B,GAIJkE,UAAUnD,iBAAiB,SAAUU,cAC3BC,OAASD,MAAMC,OAChBA,OAAOkD,QAAQ,MAA8B,WAAtBlD,OAAOM,QAAQ6C,UAA0C1C,IAAtBT,OAAOM,QAAQe,KAG1ErB,OAAOoD,aAAa,mBAGxBrD,MAAMK,sBACDmB,SAAS8B,SAAS,cAAe,CAACjC,WAAYpB,OAAOM,QAAQe,SAC7DiC,cAAchB,MAAOX,gBAG9BF,kBAAkB8B,+BASDvD,OAAQD,+BAEnByD,KAAOxD,OAAOM,QAAQe,OACvBmC,kBAGCC,OAASnE,KAAKiC,SAASC,IAAI,KAAMgC,MAEvCzD,MAAMK,uBAEAqB,kBAAoB,IAAIC,4DAGxBC,UAAYrC,KAAKsC,6BAA6B5B,QAG9C0D,SAAWpE,KAAKiC,SAASO,cACzBD,KAAO6B,SAAS1C,OAAO1B,KAAKiC,SAASnC,OAG3CyC,KAAK8B,KAAOF,OAAOpC,GACnBQ,KAAK+B,OAASH,OAAOzF,WAGfkE,YAAc,CAChBD,OAAO,mBAAU,mBAAoB,QACrCE,KAAMC,mBAAUC,OAAO,yCAA0CR,OAI/DS,YAAchD,KAAKiD,0BAA0BL,aAE7CM,WAAY,sBAAQF,MAAMG,WAAW,OAGvCC,eAAiBF,UAAUG,wBAAiBrD,KAAKrB,UAAUG,4BAAmBoF,iBAC7EZ,aAAaF,oBAGdI,qBACAN,UAAUG,cAAcrD,KAAKrB,UAAUO,aACvC,CACIuE,QAASzD,KAAKrB,UAAUI,YACxB2E,QAAS1D,KAAKrB,UAAUK,aACxB2E,SAAU3D,KAAKrB,UAAUK,aACzBuF,MAAOvE,KAAKrB,UAAUE,oBAMxB2F,YAAcpB,eAAezC,QAAQX,KAAKrB,UAAUI,aACpD0F,SAAU,mBAAOD,aAAaE,KAAK1E,KAAKrB,UAAUK,kBACpD2F,oCAAgBF,QAAQlC,KAAK,iDAAakC,QAAQG,KAAK,QACvDD,gBAEAA,cAAgBA,cAAcE,QAAQ,IAAK,mCAChCF,gBAAiBG,SAAS,WAIzC5B,UAAUnD,iBAAiB,SAAUU,cAC3BC,OAASD,MAAMC,WAChBA,OAAOkD,QAAQ,WAA+BzC,IAAvBT,OAAOM,QAAQ6C,UAA2C1C,IAAtBT,OAAOM,QAAQe,aAG3ErB,OAAOoD,aAAa,4BAMpBiB,gBACAC,cAJJvE,MAAMK,iBAKoB,MAAtBJ,OAAOM,QAAQ6C,IAAa,OACtBoB,SAAWb,SAASc,gBAAgBlF,KAAKiC,SAASnC,MAAOY,OAAOM,QAAQe,IAC9EgD,gBAAkBE,SAASxC,UAC3BuC,WAAaC,SAASE,aACnB,OACGC,QAAUpF,KAAKiC,SAASC,IAAI,UAAWxB,OAAOM,QAAQe,IAC5DgD,gBAAkBrE,OAAOM,QAAQe,GACjCiD,WAAaI,MAAAA,eAAAA,QAASC,OAAO,QAG5BpD,SAAS8B,SAAS,SAAU,CAACG,MAAOa,gBAAiBC,iBACrDhB,cAAchB,MAAOX,cAG9BF,kBAAkB8B,mCASGvD,OAAQD,8BAC7BA,MAAMK,sBACDmB,SAAS8B,SAAS,wCAAcrD,OAAOM,QAAQe,oDAAM,+BASlCrB,OAAQD,qCAE1BqB,UAAYpB,OAAOM,QAAQe,OAE5BD,uBAGCE,YAAchC,KAAKiC,SAASC,IAAI,UAAWJ,WAEjDrB,MAAMK,iDAESkB,YAAYqD,0DAAU,IAC1BzD,QAAUI,YAAYsD,YAActD,YAAYuD,gBAEjD3C,YAAc,CAChBD,OAAO,mBAAU,UAAW,QAC5BE,MAAM,mBAAU,uBAAwB,SAAUb,YAAYW,OAC9D6C,gBAAgB,mBAAU,SAAU,QACpCC,KAAMC,uBAAaC,MAAMC,aAGvB5C,YAAchD,KAAKiD,0BAA0BL,aAEnDI,MAAM6C,UAAUC,GACZC,sBAAYC,MACZC,IAEIA,EAAEnF,iBACFkC,MAAMkD,eACDjE,SAAS8B,SAAS,gBAAiB,CAACjC,yBAM5CG,SAAS8B,SAAS,gBAAiB,CAACjC,yCAWpBpB,OAAQD,MAAO0F,cACnCzF,OAAOM,QAAQe,KAGpBtB,MAAMK,sBACDmB,SAAS8B,SAASoC,aAAc,CAACzF,OAAOM,QAAQe,MAQzDN,qBAAqB2E,QACDpG,KAAKqG,YAAYrG,KAAKrB,UAAUM,YACxCqH,SAAQrG,UACZA,QAAQW,UAAU2F,OAAOvG,KAAKX,QAAQC,SAAU8G,aAC3CI,iBAAiBvG,QAASmG,WASvC9C,aAAarD,SACLA,UACAA,QAAQwG,MAAMC,cAAgB,OAC9BzG,QAAQwG,MAAME,WAAa,OAC3B1G,QAAQW,UAAUgG,IAAI5G,KAAKX,QAAQC,UACnCW,QAAQ4G,aAAa,iBAAiB,GACtC5G,QAAQF,iBAAiB,SAASU,OAASA,MAAMK,oBAUzDmC,0BAA0BL,oBACf,IAAIkE,SAAQ,CAAC7C,QAAS8C,iCACZtI,OAAOmE,aAAaoE,MAAMhE,QACnCA,MAAMiE,kBAAiB,GAEvBjE,MAAM6C,UAAUC,GAAGC,sBAAYmB,cAAc,KACzCjD,QAAQjB,eAGuB7B,IAA/ByB,YAAY4C,gBACZxC,MAAMmE,kBAAkBvE,YAAY4C,gBAExCxC,MAAMoE,UAEPC,OAAM,KACLN,0CAaZ/C,cAAchB,MAAO/C,SACjB+C,MAAMsE,aACAC,eAAiB,IAAInF,sDACvBnC,SACAA,QAAQuH,QAEZC,YAAW,KACPzE,MAAMkD,UACNqB,eAAetD,YAChB,KASP3B,6BAA6BrC,eACnByH,WAAazH,QAAQU,QAAQX,KAAKrB,UAAUQ,eAC7CuI,kBAGEA,WAAWrE,cAAcrD,KAAKrB,UAAUS"} content/actions.min.js 0000644 00000024661 15151264135 0011012 0 ustar 00 define("core_courseformat/local/content/actions",["exports","core/reactive","core/modal_factory","core/modal_events","core/templates","core/prefetch","core/str","core/normalise","core_course/events","core/pending","core_courseformat/local/courseeditor/contenttree","jquery"],(function(_exports,_reactive,_modal_factory,_modal_events,_templates,_prefetch,_str,_normalise,CourseEvents,_pending,_contenttree,_jquery){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}} /** * Course state actions dispatcher. * * This module captures all data-dispatch links in the course content and dispatch the proper * state mutation, including any confirmation and modal required. * * @module core_courseformat/local/content/actions * @class core_courseformat/local/content/actions * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal_factory=_interopRequireDefault(_modal_factory),_modal_events=_interopRequireDefault(_modal_events),_templates=_interopRequireDefault(_templates),CourseEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CourseEvents),_pending=_interopRequireDefault(_pending),_contenttree=_interopRequireDefault(_contenttree),_jquery=_interopRequireDefault(_jquery),(0,_prefetch.prefetchStrings)("core",["movecoursesection","movecoursemodule","confirm","delete"]);const directMutations={sectionHide:"sectionHide",sectionShow:"sectionShow",cmHide:"cmHide",cmShow:"cmShow",cmStealth:"cmStealth",cmMoveRight:"cmMoveRight",cmMoveLeft:"cmMoveLeft"};class _default extends _reactive.BaseComponent{create(){this.name="content_actions",this.selectors={ACTIONLINK:"[data-action]",SECTIONLINK:"[data-for='section']",CMLINK:"[data-for='cm']",SECTIONNODE:"[data-for='sectionnode']",MODALTOGGLER:"[data-toggle='collapse']",ADDSECTION:"[data-action='addSection']",CONTENTTREE:"#destination-selector",ACTIONMENU:".action-menu",ACTIONMENUTOGGLER:'[data-toggle="dropdown"]'},this.classes={DISABLED:"disabled"}}static addActions(actions){for(const[action,mutationReference]of Object.entries(actions)){if("function"!=typeof mutationReference&&"string"!=typeof mutationReference)throw new Error("".concat(action," action must be a mutation name or a function"));directMutations[action]=mutationReference}}stateReady(state){this.addEventListener(this.element,"click",this._dispatchClick),this._checkSectionlist({state:state}),this.addEventListener(this.element,CourseEvents.sectionRefreshed,(()=>this._checkSectionlist({state:state})))}getWatchers(){return[{watch:"course.sectionlist:updated",handler:this._checkSectionlist}]}_dispatchClick(event){const target=event.target.closest(this.selectors.ACTIONLINK);if(!target)return;if(target.classList.contains(this.classes.DISABLED))return void event.preventDefault();const actionName=target.dataset.action,methodName=this._actionMethodName(actionName);if(void 0===this[methodName])return void 0!==directMutations[actionName]?"function"==typeof directMutations[actionName]?void directMutations[actionName](target,event):void this._requestMutationAction(target,event,directMutations[actionName]):void 0;this[methodName](target,event)}_actionMethodName(name){const requestName=name.charAt(0).toUpperCase()+name.slice(1);return"_request".concat(requestName)}_checkSectionlist(_ref){let{state:state}=_ref;this._setAddSectionLocked(state.course.sectionlist.length>state.course.maxsections)}async _requestMoveSection(target,event){const sectionId=target.dataset.id;if(!sectionId)return;const sectionInfo=this.reactive.get("section",sectionId);event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveSectionModal"),editTools=this._getClosestActionMenuToogler(target),data=this.reactive.getExporter().course(this.reactive.state);data.sectionid=sectionInfo.id,data.sectiontitle=sectionInfo.title;const modalParams={title:(0,_str.get_string)("movecoursesection","core"),body:_templates.default.render("core_courseformat/local/content/movesection",data)},modal=await this._modalBodyRenderedPromise(modalParams),modalBody=(0,_normalise.getList)(modal.getBody())[0],currentElement=modalBody.querySelector("".concat(this.selectors.SECTIONLINK,"[data-id='").concat(sectionId,"']"));this._disableLink(currentElement);const generalSection=modalBody.querySelector("".concat(this.selectors.SECTIONLINK,"[data-number='0']"));this._disableLink(generalSection),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER},!0),modalBody.addEventListener("click",(event=>{const target=event.target;target.matches("a")&&"section"==target.dataset.for&&void 0!==target.dataset.id&&(target.getAttribute("aria-disabled")||(event.preventDefault(),this.reactive.dispatch("sectionMove",[sectionId],target.dataset.id),this._destroyModal(modal,editTools)))})),pendingModalReady.resolve()}async _requestMoveCm(target,event){var _toggler$data;const cmId=target.dataset.id;if(!cmId)return;const cmInfo=this.reactive.get("cm",cmId);event.preventDefault();const pendingModalReady=new _pending.default("courseformat/actions:prepareMoveCmModal"),editTools=this._getClosestActionMenuToogler(target),exporter=this.reactive.getExporter(),data=exporter.course(this.reactive.state);data.cmid=cmInfo.id,data.cmname=cmInfo.name;const modalParams={title:(0,_str.get_string)("movecoursemodule","core"),body:_templates.default.render("core_courseformat/local/content/movecm",data)},modal=await this._modalBodyRenderedPromise(modalParams),modalBody=(0,_normalise.getList)(modal.getBody())[0];let currentElement=modalBody.querySelector("".concat(this.selectors.CMLINK,"[data-id='").concat(cmId,"']"));this._disableLink(currentElement),new _contenttree.default(modalBody.querySelector(this.selectors.CONTENTTREE),{SECTION:this.selectors.SECTIONNODE,TOGGLER:this.selectors.MODALTOGGLER,COLLAPSE:this.selectors.MODALTOGGLER,ENTER:this.selectors.SECTIONLINK});const sectionnode=currentElement.closest(this.selectors.SECTIONNODE),toggler=(0,_jquery.default)(sectionnode).find(this.selectors.MODALTOGGLER);let collapsibleId=null!==(_toggler$data=toggler.data("target"))&&void 0!==_toggler$data?_toggler$data:toggler.attr("href");collapsibleId&&(collapsibleId=collapsibleId.replace("#",""),(0,_jquery.default)("#".concat(collapsibleId)).collapse("toggle")),modalBody.addEventListener("click",(event=>{const target=event.target;if(!target.matches("a")||void 0===target.dataset.for||void 0===target.dataset.id)return;if(target.getAttribute("aria-disabled"))return;let targetSectionId,targetCmId;if(event.preventDefault(),"cm"==target.dataset.for){const dropData=exporter.cmDraggableData(this.reactive.state,target.dataset.id);targetSectionId=dropData.sectionid,targetCmId=dropData.nextcmid}else{const section=this.reactive.get("section",target.dataset.id);targetSectionId=target.dataset.id,targetCmId=null==section?void 0:section.cmlist[0]}this.reactive.dispatch("cmMove",[cmId],targetSectionId,targetCmId),this._destroyModal(modal,editTools)})),pendingModalReady.resolve()}async _requestAddSection(target,event){var _target$dataset$id;event.preventDefault(),this.reactive.dispatch("addSection",null!==(_target$dataset$id=target.dataset.id)&&void 0!==_target$dataset$id?_target$dataset$id:0)}async _requestDeleteSection(target,event){var _sectionInfo$cmlist;const sectionId=target.dataset.id;if(!sectionId)return;const sectionInfo=this.reactive.get("section",sectionId);event.preventDefault();if((null!==(_sectionInfo$cmlist=sectionInfo.cmlist)&&void 0!==_sectionInfo$cmlist?_sectionInfo$cmlist:[]).length||sectionInfo.hassummary||sectionInfo.rawtitle){const modalParams={title:(0,_str.get_string)("confirm","core"),body:(0,_str.get_string)("confirmdeletesection","moodle",sectionInfo.title),saveButtonText:(0,_str.get_string)("delete","core"),type:_modal_factory.default.types.SAVE_CANCEL},modal=await this._modalBodyRenderedPromise(modalParams);modal.getRoot().on(_modal_events.default.save,(e=>{e.preventDefault(),modal.destroy(),this.reactive.dispatch("sectionDelete",[sectionId])}))}else this.reactive.dispatch("sectionDelete",[sectionId])}async _requestMutationAction(target,event,mutationName){target.dataset.id&&(event.preventDefault(),this.reactive.dispatch(mutationName,[target.dataset.id]))}_setAddSectionLocked(locked){this.getElements(this.selectors.ADDSECTION).forEach((element=>{element.classList.toggle(this.classes.DISABLED,locked),this.setElementLocked(element,locked)}))}_disableLink(element){element&&(element.style.pointerEvents="none",element.style.userSelect="none",element.classList.add(this.classes.DISABLED),element.setAttribute("aria-disabled",!0),element.addEventListener("click",(event=>event.preventDefault())))}_modalBodyRenderedPromise(modalParams){return new Promise(((resolve,reject)=>{_modal_factory.default.create(modalParams).then((modal=>{modal.setRemoveOnClose(!0),modal.getRoot().on(_modal_events.default.bodyRendered,(()=>{resolve(modal)})),void 0!==modalParams.saveButtonText&&modal.setSaveButtonText(modalParams.saveButtonText),modal.show()})).catch((()=>{reject("Cannot load modal content")}))}))}_destroyModal(modal,element){modal.hide();const pendingDestroy=new _pending.default("courseformat/actions:destroyModal");element&&element.focus(),setTimeout((()=>{modal.destroy(),pendingDestroy.resolve()}),500)}_getClosestActionMenuToogler(element){const actionMenu=element.closest(this.selectors.ACTIONMENU);if(actionMenu)return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=actions.min.js.map content/section.min.js 0000644 00000010405 15151264135 0011005 0 ustar 00 define("core_courseformat/local/content/section",["exports","core_courseformat/local/content/section/header","core_courseformat/local/courseeditor/dndsection","core/templates"],(function(_exports,_header,_dndsection,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Course section format component. * * @module core_courseformat/local/content/section * @class core_courseformat/local/content/section * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_header=_interopRequireDefault(_header),_dndsection=_interopRequireDefault(_dndsection),_templates=_interopRequireDefault(_templates);class _default extends _dndsection.default{create(){this.name="content_section",this.selectors={SECTION_ITEM:"[data-for='section_title']",CM:'[data-for="cmitem"]',SECTIONINFO:'[data-for="sectioninfo"]',SECTIONBADGES:'[data-region="sectionbadges"]',SHOWSECTION:'[data-action="sectionShow"]',HIDESECTION:'[data-action="sectionHide"]',ACTIONTEXT:".menu-action-text",ICON:".icon"},this.classes={LOCKED:"editinprogress",HASDESCRIPTION:"description",HIDE:"d-none",HIDDEN:"hidden"},this.id=this.element.dataset.id}stateReady(state){if(this.configState(state),this.reactive.isEditing&&this.reactive.supportComponents){const sectionItem=this.getElement(this.selectors.SECTION_ITEM);if(sectionItem){const headerComponent=new _header.default({...this,element:sectionItem,fullregion:this.element});this.configDragDrop(headerComponent)}}}getWatchers(){return[{watch:"section[".concat(this.id,"]:updated"),handler:this._refreshSection}]}validateDropData(dropdata){return("section"!==(null==dropdata?void 0:dropdata.type)||0==this.reactive.sectionReturn)&&super.validateDropData(dropdata)}getLastCm(){const cms=this.getElements(this.selectors.CM);return cms&&0!==cms.length?cms[cms.length-1]:null}_refreshSection(_ref){var _element$dragging,_element$locked,_element$visible;let{element:element}=_ref;this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.element.classList.toggle(this.classes.HIDDEN,null!==(_element$visible=!element.visible)&&void 0!==_element$visible&&_element$visible),this.locked=element.locked;const sectioninfo=this.getElement(this.selectors.SECTIONINFO);sectioninfo&§ioninfo.classList.toggle(this.classes.HASDESCRIPTION,element.hasrestrictions),this._updateBadges(element),this._updateActionsMenu(element)}_updateBadges(section){const current=this.getElement("".concat(this.selectors.SECTIONBADGES," [data-type='iscurrent']"));null==current||current.classList.toggle(this.classes.HIDE,!section.current);const hiddenFromStudents=this.getElement("".concat(this.selectors.SECTIONBADGES," [data-type='hiddenfromstudents']"));null==hiddenFromStudents||hiddenFromStudents.classList.toggle(this.classes.HIDE,section.visible)}async _updateActionsMenu(section){var _affectedAction$datas,_affectedAction$datas2;let selector,newAction;section.visible?(selector=this.selectors.SHOWSECTION,newAction="sectionHide"):(selector=this.selectors.HIDESECTION,newAction="sectionShow");const affectedAction=this.getElement(selector);if(!affectedAction)return;affectedAction.dataset.action=newAction;const actionText=affectedAction.querySelector(this.selectors.ACTIONTEXT);if(null!==(_affectedAction$datas=affectedAction.dataset)&&void 0!==_affectedAction$datas&&_affectedAction$datas.swapname&&actionText){const oldText=null==actionText?void 0:actionText.innerText;actionText.innerText=affectedAction.dataset.swapname,affectedAction.dataset.swapname=oldText}const icon=affectedAction.querySelector(this.selectors.ICON);if(null!==(_affectedAction$datas2=affectedAction.dataset)&&void 0!==_affectedAction$datas2&&_affectedAction$datas2.swapicon&&icon){const newIcon=affectedAction.dataset.swapicon;if(newIcon){const pixHtml=await _templates.default.renderPix(newIcon,"core");_templates.default.replaceNode(icon,pixHtml,"")}}}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=section.min.js.map content/activity_header.min.js.map 0000644 00000006630 15151264135 0013266 0 ustar 00 {"version":3,"file":"activity_header.min.js","sources":["../../../src/local/content/activity_header.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 <http://www.gnu.org/licenses/>.\n\n/**\n * The activity header component.\n *\n * @module core_courseformat/local/content/activity_header\n * @class core_courseformat/local/content/activity_header\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport * as CourseEvents from 'core_course/events';\n\n// Global page selectors.\nconst SELECTORS = {\n ACTIVITY_HEADER: `[data-for='page-activity-header']`,\n};\n\nexport default class Component extends BaseComponent {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'activity_header';\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target optional altentative DOM main element CSS selector\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n const elementselector = (target) ? target : SELECTORS.ACTIVITY_HEADER;\n return new Component({\n element: document.querySelector(elementselector),\n reactive: getCurrentCourseEditor(),\n selectors\n });\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n // Capture completion events.\n this.addEventListener(\n this.element,\n CourseEvents.manualCompletionToggled,\n this._completionHandler\n );\n }\n\n /**\n * Activity manual completion listener.\n *\n * @param {Event} event the custom event\n * @param {object} event.detail the event details\n */\n _completionHandler({detail}) {\n if (detail === undefined) {\n return;\n }\n this.reactive.dispatch('cmCompletion', [detail.cmid], detail.completed);\n }\n}\n"],"names":["SELECTORS","Component","BaseComponent","create","name","target","selectors","elementselector","element","document","querySelector","reactive","stateReady","addEventListener","this","CourseEvents","manualCompletionToggled","_completionHandler","detail","undefined","dispatch","cmid","completed"],"mappings":";;;;;;;;0BA6BMA,oEAIeC,kBAAkBC,wBAKnCC,cAESC,KAAO,8BAUJC,OAAQC,iBACVC,gBAAmBF,QAAmBL,iCACrC,IAAIC,UAAU,CACjBO,QAASC,SAASC,cAAcH,iBAChCI,UAAU,0CACVL,UAAAA,YAORM,kBAESC,iBACDC,KAAKN,QACLO,aAAaC,wBACbF,KAAKG,oBAUbA,6BAAmBC,OAACA,kBACDC,IAAXD,aAGCP,SAASS,SAAS,eAAgB,CAACF,OAAOG,MAAOH,OAAOI"} content/section/cmitem.min.js.map 0000644 00000006750 15151264135 0013047 0 ustar 00 {"version":3,"file":"cmitem.min.js","sources":["../../../../src/local/content/section/cmitem.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Course course module item component.\n *\n * This component is used to control specific course modules interactions like drag and drop.\n *\n * @module core_courseformat/local/content/section/cmitem\n * @class core_courseformat/local/content/section/cmitem\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DndCmItem from 'core_courseformat/local/courseeditor/dndcmitem';\n\nexport default class extends DndCmItem {\n\n /**\n * Constructor hook.\n */\n create() {\n // Optional component name for debugging.\n this.name = 'content_section_cmitem';\n // Default query selectors.\n this.selectors = {\n DRAGICON: `.editing_move`,\n };\n // Most classes will be loaded later by DndCmItem.\n this.classes = {\n LOCKED: 'editinprogress',\n };\n // We need our id to watch specific events.\n this.id = this.element.dataset.id;\n }\n\n /**\n * Initial state ready method.\n */\n stateReady() {\n this.configDragDrop(this.id);\n this.getElement(this.selectors.DRAGICON)?.classList.add(this.classes.DRAGICON);\n }\n\n /**\n * Component watchers.\n *\n * @returns {Array} of watchers\n */\n getWatchers() {\n return [\n {watch: `cm[${this.id}]:deleted`, handler: this.unregister},\n {watch: `cm[${this.id}]:updated`, handler: this._refreshCm},\n ];\n }\n\n /**\n * Update a course index cm using the state information.\n *\n * @param {object} param\n * @param {Object} param.element details the update details.\n */\n _refreshCm({element}) {\n // Update classes.\n this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);\n this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);\n this.locked = element.locked;\n }\n}\n"],"names":["DndCmItem","create","name","selectors","DRAGICON","classes","LOCKED","id","this","element","dataset","stateReady","configDragDrop","getElement","classList","add","getWatchers","watch","handler","unregister","_refreshCm","toggle","DRAGGING","dragging","locked"],"mappings":";;;;;;;;;;0KA4B6BA,mBAKzBC,cAESC,KAAO,8BAEPC,UAAY,CACbC,+BAGCC,QAAU,CACXC,OAAQ,uBAGPC,GAAKC,KAAKC,QAAQC,QAAQH,GAMnCI,uCACSC,eAAeJ,KAAKD,kCACpBM,WAAWL,KAAKL,UAAUC,wDAAWU,UAAUC,IAAIP,KAAKH,QAAQD,UAQzEY,oBACW,CACH,CAACC,mBAAaT,KAAKD,gBAAeW,QAASV,KAAKW,YAChD,CAACF,mBAAaT,KAAKD,gBAAeW,QAASV,KAAKY,aAUxDA,2DAAWX,QAACA,mBAEHA,QAAQK,UAAUO,OAAOb,KAAKH,QAAQiB,mCAAUb,QAAQc,+DACxDd,QAAQK,UAAUO,OAAOb,KAAKH,QAAQC,+BAAQG,QAAQe,yDACtDA,OAASf,QAAQe"} content/section/header.min.js 0000644 00000002133 15151264135 0012234 0 ustar 00 define("core_courseformat/local/content/section/header",["exports","core_courseformat/local/courseeditor/dndsectionitem"],(function(_exports,_dndsectionitem){var obj; /** * Course section header component. * * This component is used to control specific course section interactions like drag and drop. * * @module core_courseformat/local/content/section/header * @class core_courseformat/local/content/section/header * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndsectionitem=(obj=_dndsectionitem)&&obj.__esModule?obj:{default:obj};class _default extends _dndsectionitem.default{create(descriptor){this.name="content_section_header",this.id=descriptor.id,this.section=descriptor.section,this.course=descriptor.course,this.fullregion=descriptor.fullregion}stateReady(state){this.configDragDrop(this.id,state,this.fullregion)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=header.min.js.map content/section/header.min.js.map 0000644 00000004511 15151264135 0013012 0 ustar 00 {"version":3,"file":"header.min.js","sources":["../../../../src/local/content/section/header.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Course section header component.\n *\n * This component is used to control specific course section interactions like drag and drop.\n *\n * @module core_courseformat/local/content/section/header\n * @class core_courseformat/local/content/section/header\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport DndSectionItem from 'core_courseformat/local/courseeditor/dndsectionitem';\n\nexport default class extends DndSectionItem {\n\n /**\n * Constructor hook.\n *\n * @param {Object} descriptor\n */\n create(descriptor) {\n // Optional component name for debugging.\n this.name = 'content_section_header';\n // We need our id to watch specific events.\n\n // Get main info from the descriptor.\n this.id = descriptor.id;\n this.section = descriptor.section;\n this.course = descriptor.course;\n this.fullregion = descriptor.fullregion;\n }\n\n /**\n * Initial state ready method.\n *\n * @param {Object} state the initial state\n */\n stateReady(state) {\n this.configDragDrop(this.id, state, this.fullregion);\n }\n}"],"names":["DndSectionItem","create","descriptor","name","id","section","course","fullregion","stateReady","state","configDragDrop","this"],"mappings":";;;;;;;;;;oLA4B6BA,wBAOzBC,OAAOC,iBAEEC,KAAO,8BAIPC,GAAKF,WAAWE,QAChBC,QAAUH,WAAWG,aACrBC,OAASJ,WAAWI,YACpBC,WAAaL,WAAWK,WAQjCC,WAAWC,YACFC,eAAeC,KAAKP,GAAIK,MAAOE,KAAKJ"} content/section/cmitem.min.js 0000644 00000003336 15151264135 0012270 0 ustar 00 define("core_courseformat/local/content/section/cmitem",["exports","core_courseformat/local/courseeditor/dndcmitem"],(function(_exports,_dndcmitem){var obj; /** * Course course module item component. * * This component is used to control specific course modules interactions like drag and drop. * * @module core_courseformat/local/content/section/cmitem * @class core_courseformat/local/content/section/cmitem * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=(obj=_dndcmitem)&&obj.__esModule?obj:{default:obj};class _default extends _dndcmitem.default{create(){this.name="content_section_cmitem",this.selectors={DRAGICON:".editing_move"},this.classes={LOCKED:"editinprogress"},this.id=this.element.dataset.id}stateReady(){var _this$getElement;this.configDragDrop(this.id),null===(_this$getElement=this.getElement(this.selectors.DRAGICON))||void 0===_this$getElement||_this$getElement.classList.add(this.classes.DRAGICON)}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.unregister},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm}]}_refreshCm(_ref){var _element$dragging,_element$locked;let{element:element}=_ref;this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.locked=element.locked}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=cmitem.min.js.map content/activity_header.min.js 0000644 00000004342 15151264135 0012510 0 ustar 00 define("core_courseformat/local/content/activity_header",["exports","core/reactive","core_courseformat/courseeditor","core_course/events"],(function(_exports,_reactive,_courseeditor,CourseEvents){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)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,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} /** * The activity header component. * * @module core_courseformat/local/content/activity_header * @class core_courseformat/local/content/activity_header * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */(CourseEvents);const SELECTORS_ACTIVITY_HEADER="[data-for='page-activity-header']";class Component extends _reactive.BaseComponent{create(){this.name="activity_header"}static init(target,selectors){const elementselector=target||SELECTORS_ACTIVITY_HEADER;return new Component({element:document.querySelector(elementselector),reactive:(0,_courseeditor.getCurrentCourseEditor)(),selectors:selectors})}stateReady(){this.addEventListener(this.element,CourseEvents.manualCompletionToggled,this._completionHandler)}_completionHandler(_ref){let{detail:detail}=_ref;void 0!==detail&&this.reactive.dispatch("cmCompletion",[detail.cmid],detail.completed)}}return _exports.default=Component,_exports.default})); //# sourceMappingURL=activity_header.min.js.map courseeditor/dndcmitem.min.js.map 0000644 00000012067 15151264135 0013124 0 ustar 00 {"version":3,"file":"dndcmitem.min.js","sources":["../../../src/local/courseeditor/dndcmitem.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Course index cm component.\n *\n * This component is used to control specific course modules interactions like drag and drop\n * in both course index and course content.\n *\n * @module core_courseformat/local/courseeditor/dndcmitem\n * @class core_courseformat/local/courseeditor/dndcmitem\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\n\nexport default class extends BaseComponent {\n\n /**\n * Configure the component drag and drop.\n *\n * @param {number} cmid course module id\n */\n configDragDrop(cmid) {\n\n this.id = cmid;\n\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init element drag and drop.\n this.dragdrop = new DragDrop(this);\n // Save dropzone classes.\n this.classes = this.dragdrop.getClasses();\n }\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n // Drag and drop methods.\n\n /**\n * The element drop start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragStart(dropdata) {\n this.reactive.dispatch('cmDrag', [dropdata.id], true);\n }\n\n /**\n * The element drop end hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragEnd(dropdata) {\n this.reactive.dispatch('cmDrag', [dropdata.id], false);\n }\n\n /**\n * Get the draggable data of this component.\n *\n * @returns {Object} exported course module drop data\n */\n getDraggableData() {\n const exporter = this.reactive.getExporter();\n return exporter.cmDraggableData(this.reactive.state, this.id);\n }\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n return dropdata?.type === 'cm';\n }\n\n /**\n * Display the component dropzone.\n *\n * @param {Object} dropdata the accepted drop data\n */\n showDropZone(dropdata) {\n // If we are the next cmid of the dragged element we accept the drop because otherwise it\n // will get captured by the section. However, we won't trigger any mutation.\n if (dropdata.nextcmid != this.id && dropdata.id != this.id) {\n this.element.classList.add(this.classes.DROPUP);\n }\n }\n\n /**\n * Hide the component dropzone.\n */\n hideDropZone() {\n this.element.classList.remove(this.classes.DROPUP);\n }\n\n /**\n * Drop event handler.\n *\n * @param {Object} dropdata the accepted drop data\n */\n drop(dropdata) {\n // Call the move mutation if necessary.\n if (dropdata.id != this.id && dropdata.nextcmid != this.id) {\n this.reactive.dispatch('cmMove', [dropdata.id], null, this.id);\n }\n }\n\n}\n"],"names":["BaseComponent","configDragDrop","cmid","id","this","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","destroy","undefined","unregister","dragStart","dropdata","dispatch","dragEnd","getDraggableData","getExporter","cmDraggableData","state","validateDropData","type","showDropZone","nextcmid","element","classList","add","DROPUP","hideDropZone","remove","drop"],"mappings":";;;;;;;;;;;;uBA6B6BA,wBAOzBC,eAAeC,WAENC,GAAKD,KAGNE,KAAKC,SAASC,WAAaF,KAAKC,SAASE,yBAEpCC,SAAW,IAAIC,mBAASL,WAExBM,QAAUN,KAAKI,SAASG,cAOrCC,eAC0BC,IAAlBT,KAAKI,eACAA,SAASM,aAWtBC,UAAUC,eACDX,SAASY,SAAS,SAAU,CAACD,SAASb,KAAK,GAQpDe,QAAQF,eACCX,SAASY,SAAS,SAAU,CAACD,SAASb,KAAK,GAQpDgB,0BACqBf,KAAKC,SAASe,cACfC,gBAAgBjB,KAAKC,SAASiB,MAAOlB,KAAKD,IAS9DoB,iBAAiBP,gBACa,QAAnBA,MAAAA,gBAAAA,SAAUQ,MAQrBC,aAAaT,UAGLA,SAASU,UAAYtB,KAAKD,IAAMa,SAASb,IAAMC,KAAKD,SAC/CwB,QAAQC,UAAUC,IAAIzB,KAAKM,QAAQoB,QAOhDC,oBACSJ,QAAQC,UAAUI,OAAO5B,KAAKM,QAAQoB,QAQ/CG,KAAKjB,UAEGA,SAASb,IAAMC,KAAKD,IAAMa,SAASU,UAAYtB,KAAKD,SAC/CE,SAASY,SAAS,SAAU,CAACD,SAASb,IAAK,KAAMC,KAAKD"} courseeditor/exporter.min.js 0000644 00000006615 15151264135 0012256 0 ustar 00 define("core_courseformat/local/courseeditor/exporter",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default= /** * Module to export parts of the state and transform them to be used in templates * and as draggable data. * * @module core_courseformat/local/courseeditor/exporter * @class core_courseformat/local/courseeditor/exporter * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class{constructor(reactive){this.reactive=reactive,this.COMPLETIONS=["incomplete","complete","complete","fail"]}course(state){var _state$course$highlig,_state$course$section;const data={sections:[],editmode:this.reactive.isEditing,highlighted:null!==(_state$course$highlig=state.course.highlighted)&&void 0!==_state$course$highlig?_state$course$highlig:""};return(null!==(_state$course$section=state.course.sectionlist)&&void 0!==_state$course$section?_state$course$section:[]).forEach((sectionid=>{var _state$section$get;const sectioninfo=null!==(_state$section$get=state.section.get(sectionid))&&void 0!==_state$section$get?_state$section$get:{},section=this.section(state,sectioninfo);data.sections.push(section)})),data.hassections=0!=data.sections.length,data}section(state,sectioninfo){var _state$course$highlig2,_sectioninfo$cmlist;const section={...sectioninfo,highlighted:null!==(_state$course$highlig2=state.course.highlighted)&&void 0!==_state$course$highlig2?_state$course$highlig2:"",cms:[]};return(null!==(_sectioninfo$cmlist=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist?_sectioninfo$cmlist:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid),cm=this.cm(state,cminfo);section.cms.push(cm)})),section.hascms=0!=section.cms.length,section}cm(state,cminfo){return{...cminfo,isactive:!1}}cmDraggableData(state,cmid){const cminfo=state.cm.get(cmid);if(!cminfo)return null;let nextcmid;const section=state.section.get(cminfo.sectionid),currentindex=null==section?void 0:section.cmlist.indexOf(cminfo.id);return void 0!==currentindex&&(nextcmid=null==section?void 0:section.cmlist[currentindex+1]),{type:"cm",id:cminfo.id,name:cminfo.name,sectionid:cminfo.sectionid,nextcmid:nextcmid}}sectionDraggableData(state,sectionid){const sectioninfo=state.section.get(sectionid);return sectioninfo?{type:"section",id:sectioninfo.id,name:sectioninfo.name,number:sectioninfo.number}:null}cmCompletion(state,cminfo){const data={statename:"",state:"NaN"};if(void 0!==cminfo.completionstate){var _this$COMPLETIONS$cmi;data.state=cminfo.completionstate,data.hasstate=!0;const statename=null!==(_this$COMPLETIONS$cmi=this.COMPLETIONS[cminfo.completionstate])&&void 0!==_this$COMPLETIONS$cmi?_this$COMPLETIONS$cmi:"NaN";data["is".concat(statename)]=!0}return data}allItemsArray(state){var _state$course$section2;const items=[];return(null!==(_state$course$section2=state.course.sectionlist)&&void 0!==_state$course$section2?_state$course$section2:[]).forEach((sectionid=>{var _sectioninfo$cmlist2;const sectioninfo=state.section.get(sectionid);items.push({type:"section",id:sectioninfo.id,url:sectioninfo.sectionurl});(null!==(_sectioninfo$cmlist2=sectioninfo.cmlist)&&void 0!==_sectioninfo$cmlist2?_sectioninfo$cmlist2:[]).forEach((cmid=>{const cminfo=state.cm.get(cmid);items.push({type:"cm",id:cminfo.id,url:cminfo.url})}))})),items}},_exports.default})); //# sourceMappingURL=exporter.min.js.map courseeditor/courseeditor.min.js 0000644 00000012236 15151264135 0013111 0 ustar 00 define("core_courseformat/local/courseeditor/courseeditor",["exports","core/reactive","core/notification","core_courseformat/local/courseeditor/exporter","core/log","core/ajax","core/sessionstorage"],(function(_exports,_reactive,_notification,_exporter,_log,_ajax,Storage){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj} /** * Main course editor module. * * All formats can register new components on this object to create new reactive * UI components that watch the current course state. * * @module core_courseformat/local/courseeditor/courseeditor * @class core_courseformat/local/courseeditor/courseeditor * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=_interopRequireDefault(_notification),_exporter=_interopRequireDefault(_exporter),_log=_interopRequireDefault(_log),_ajax=_interopRequireDefault(_ajax),Storage=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Storage);class _default extends _reactive.Reactive{constructor(){super(...arguments),_defineProperty(this,"stateKey",1),_defineProperty(this,"sectionReturn",0)}async loadCourse(courseId,serverStateKey){if(this.courseId)throw new Error("Cannot load ".concat(courseId,", course already loaded with id ").concat(this.courseId));let stateData;serverStateKey||(serverStateKey="invalidStateKey_".concat(Date.now())),this._editing=!1,this._supportscomponents=!1,this.courseId=courseId;const storeStateKey=Storage.get("course/".concat(courseId,"/stateKey"));try{this.isEditing||serverStateKey!=storeStateKey||(stateData=JSON.parse(Storage.get("course/".concat(courseId,"/staticState")))),stateData||(stateData=await this.getServerCourseState())}catch(error){return _log.default.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR"),void _log.default.error(error)}if(this.setInitialState(stateData),this.isEditing)this.stateKey=null;else{const newState=JSON.stringify(stateData);var _stateData$course$sta,_stateData,_stateData$course;if(Storage.get("course/".concat(courseId,"/staticState"))!==newState||storeStateKey!==serverStateKey)Storage.set("course/".concat(courseId,"/staticState"),newState),Storage.set("course/".concat(courseId,"/stateKey"),null!==(_stateData$course$sta=null===(_stateData=stateData)||void 0===_stateData||null===(_stateData$course=_stateData.course)||void 0===_stateData$course?void 0:_stateData$course.statekey)&&void 0!==_stateData$course$sta?_stateData$course$sta:serverStateKey);this.stateKey=Storage.get("course/".concat(courseId,"/stateKey"))}}setViewFormat(setup){var _setup$editing,_setup$supportscompon;this._editing=null!==(_setup$editing=setup.editing)&&void 0!==_setup$editing&&_setup$editing,this._supportscomponents=null!==(_setup$supportscompon=setup.supportscomponents)&&void 0!==_setup$supportscompon&&_setup$supportscompon}async getServerCourseState(){const courseState=await _ajax.default.call([{methodname:"core_courseformat_get_state",args:{courseid:this.courseId}}])[0];return{course:{},section:[],cm:[],...JSON.parse(courseState)}}get isEditing(){var _this$_editing;return null!==(_this$_editing=this._editing)&&void 0!==_this$_editing&&_this$_editing}getExporter(){return new _exporter.default(this)}get supportComponents(){var _this$_supportscompon;return null!==(_this$_supportscompon=this._supportscomponents)&&void 0!==_this$_supportscompon&&_this$_supportscompon}getStorageValue(key){if(this.isEditing||!this.stateKey)return!1;const dataJson=Storage.get("course/".concat(this.courseId,"/").concat(key));if(!dataJson)return!1;try{const data=JSON.parse(dataJson);return(null==data?void 0:data.stateKey)===this.stateKey&&data.value}catch(error){return!1}}setStorageValue(key,value){if(this.isEditing)return!1;const data={stateKey:this.stateKey,value:value};return Storage.set("course/".concat(this.courseId,"/").concat(key),JSON.stringify(data))}async dispatch(){try{await super.dispatch(...arguments)}catch(error){_notification.default.exception(error),super.dispatch("unlockAll")}}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=courseeditor.min.js.map courseeditor/contenttree.min.js 0000644 00000006263 15151264135 0012737 0 ustar 00 define("core_courseformat/local/courseeditor/contenttree",["exports","jquery","core/tree","core/normalise"],(function(_exports,_jquery,_tree,_normalise){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Course index keyboard navigation and aria-tree compatibility. * * Node tree and bootstrap collapsibles don't use the same HTML structure. However, * all keybindings and logic is compatible. This class translate the primitive opetations * to a bootstrap collapsible structure. * * @module core_courseformat/local/courseindex/keyboardnav * @class core_courseformat/local/courseindex/keyboardnav * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_tree=_interopRequireDefault(_tree);class _default extends _tree.default{constructor(mainElement,selectors,preventcache){var _selectors$ENTER;super(mainElement),this.selectors={SECTION:selectors.SECTION,TOGGLER:selectors.TOGGLER,COLLAPSE:selectors.COLLAPSE,ENTER:null!==(_selectors$ENTER=selectors.ENTER)&&void 0!==_selectors$ENTER?_selectors$ENTER:selectors.TOGGLER},preventcache&&(this._getVisibleItems=this.getVisibleItems,this.getVisibleItems=()=>(this.refreshVisibleItemsCache(),this._getVisibleItems())),this.treeRoot.on("hidden.bs.collapse shown.bs.collapse",(()=>{this.refreshVisibleItemsCache()})),this.registerEnterCallback(this.enterCallback.bind(this))}getActiveItem(){const activeItem=this.treeRoot.data("activeItem");if(activeItem)return(0,_normalise.getList)(activeItem)[0]}enterCallback(jQueryItem){const item=(0,_normalise.getList)(jQueryItem)[0];if(this.isGroupItem(jQueryItem)){const enter=item.querySelector(this.selectors.ENTER);"#"!==enter.getAttribute("href")&&(window.location.href=enter.getAttribute("href")),enter.click()}else{const link=item.querySelector("a");"#"!==link.getAttribute("href")?window.location.href=link.getAttribute("href"):link.click()}}handleItemClick(event,jQueryItem){event.target.closest(this.selectors.COLLAPSE)?super.handleItemClick(event,jQueryItem):(jQueryItem.focus(),this.isGroupItem(jQueryItem)&&this.expandGroup(jQueryItem))}isGroupCollapsed(jQueryItem){return"false"===(0,_normalise.getList)(jQueryItem)[0].querySelector("[aria-expanded]").getAttribute("aria-expanded")}toggleGroup(item){var _toggler$data;const toggler=item.find(this.selectors.COLLAPSE);let collapsibleId=null!==(_toggler$data=toggler.data("target"))&&void 0!==_toggler$data?_toggler$data:toggler.attr("href");if(!collapsibleId)return;collapsibleId=collapsibleId.replace("#","");(0,_jquery.default)("#".concat(collapsibleId)).length&&(0,_jquery.default)("#".concat(collapsibleId)).collapse("toggle")}expandGroup(item){this.isGroupCollapsed(item)&&this.toggleGroup(item)}collapseGroup(item){this.isGroupCollapsed(item)||this.toggleGroup(item)}expandAllGroups(){(0,_normalise.getList)(this.treeRoot)[0].querySelectorAll(this.selectors.SECTION).forEach((item=>{this.expandGroup((0,_jquery.default)(item))}))}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=contenttree.min.js.map courseeditor/dndsection.min.js.map 0000644 00000015654 15151264135 0013317 0 ustar 00 {"version":3,"file":"dndsection.min.js","sources":["../../../src/local/courseeditor/dndsection.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Course index section component.\n *\n * This component is used to control specific course section interactions like drag and drop\n * in both course index and course content.\n *\n * @module core_courseformat/local/courseeditor/dndsection\n * @class core_courseformat/local/courseeditor/dndsection\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\n\nexport default class extends BaseComponent {\n\n /**\n * Save some values form the state.\n *\n * @param {Object} state the current state\n */\n configState(state) {\n this.id = this.element.dataset.id;\n this.section = state.section.get(this.id);\n this.course = state.course;\n }\n\n /**\n * Register state values and the drag and drop subcomponent.\n *\n * @param {BaseComponent} sectionitem section item component\n */\n configDragDrop(sectionitem) {\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init the inner dragable element.\n this.sectionitem = sectionitem;\n // Init the dropzone.\n this.dragdrop = new DragDrop(this);\n // Save dropzone classes.\n this.classes = this.dragdrop.getClasses();\n }\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.sectionitem !== undefined) {\n this.sectionitem.unregister();\n }\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n /**\n * Get the last CM element of that section.\n *\n * @returns {element|null} the las course module element of the section.\n */\n getLastCm() {\n return null;\n }\n\n // Drag and drop methods.\n\n /**\n * The element drop start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragStart(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], true);\n }\n\n /**\n * The element drop end hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragEnd(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], false);\n }\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n // We accept any course module.\n if (dropdata?.type === 'cm') {\n return true;\n }\n // We accept any section but the section 0 or ourself\n if (dropdata?.type === 'section') {\n const sectionzeroid = this.course.sectionlist[0];\n return dropdata?.id != this.id && dropdata?.id != sectionzeroid && this.id != sectionzeroid;\n }\n return false;\n }\n\n /**\n * Display the component dropzone.\n *\n * @param {Object} dropdata the accepted drop data\n */\n showDropZone(dropdata) {\n if (dropdata.type == 'cm') {\n this.getLastCm()?.classList.add(this.classes.DROPDOWN);\n }\n if (dropdata.type == 'section') {\n // The relative move of section depends on the section number.\n if (this.section.number > dropdata.number) {\n this.element.classList.remove(this.classes.DROPUP);\n this.element.classList.add(this.classes.DROPDOWN);\n } else {\n this.element.classList.add(this.classes.DROPUP);\n this.element.classList.remove(this.classes.DROPDOWN);\n }\n }\n }\n\n /**\n * Hide the component dropzone.\n */\n hideDropZone() {\n this.getLastCm()?.classList.remove(this.classes.DROPDOWN);\n this.element.classList.remove(this.classes.DROPUP);\n this.element.classList.remove(this.classes.DROPDOWN);\n }\n\n /**\n * Drop event handler.\n *\n * @param {Object} dropdata the accepted drop data\n */\n drop(dropdata) {\n // Call the move mutation.\n if (dropdata.type == 'cm') {\n this.reactive.dispatch('cmMove', [dropdata.id], this.id);\n }\n if (dropdata.type == 'section') {\n this.reactive.dispatch('sectionMove', [dropdata.id], this.id);\n }\n }\n}\n"],"names":["BaseComponent","configState","state","id","this","element","dataset","section","get","course","configDragDrop","sectionitem","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","destroy","undefined","unregister","getLastCm","dragStart","dropdata","dispatch","dragEnd","validateDropData","type","sectionzeroid","sectionlist","showDropZone","classList","add","DROPDOWN","number","remove","DROPUP","hideDropZone","drop"],"mappings":";;;;;;;;;;;;uBA6B6BA,wBAOzBC,YAAYC,YACHC,GAAKC,KAAKC,QAAQC,QAAQH,QAC1BI,QAAUL,MAAMK,QAAQC,IAAIJ,KAAKD,SACjCM,OAASP,MAAMO,OAQxBC,eAAeC,aAEPP,KAAKQ,SAASC,WAAaT,KAAKQ,SAASE,yBAEpCH,YAAcA,iBAEdI,SAAW,IAAIC,mBAASZ,WAExBa,QAAUb,KAAKW,SAASG,cAOrCC,eAC6BC,IAArBhB,KAAKO,kBACAA,YAAYU,kBAECD,IAAlBhB,KAAKW,eACAA,SAASM,aAStBC,mBACW,KAUXC,UAAUC,eACDZ,SAASa,SAAS,cAAe,CAACD,SAASrB,KAAK,GAQzDuB,QAAQF,eACCZ,SAASa,SAAS,cAAe,CAACD,SAASrB,KAAK,GASzDwB,iBAAiBH,aAEU,QAAnBA,MAAAA,gBAAAA,SAAUI,aACH,KAGY,aAAnBJ,MAAAA,gBAAAA,SAAUI,MAAoB,OACxBC,cAAgBzB,KAAKK,OAAOqB,YAAY,UACvCN,MAAAA,gBAAAA,SAAUrB,KAAMC,KAAKD,KAAMqB,MAAAA,gBAAAA,SAAUrB,KAAM0B,eAAiBzB,KAAKD,IAAM0B,qBAE3E,EAQXE,aAAaP,8BACY,MAAjBA,SAASI,oCACJN,wDAAaU,UAAUC,IAAI7B,KAAKa,QAAQiB,WAE5B,WAAjBV,SAASI,OAELxB,KAAKG,QAAQ4B,OAASX,SAASW,aAC1B9B,QAAQ2B,UAAUI,OAAOhC,KAAKa,QAAQoB,aACtChC,QAAQ2B,UAAUC,IAAI7B,KAAKa,QAAQiB,iBAEnC7B,QAAQ2B,UAAUC,IAAI7B,KAAKa,QAAQoB,aACnChC,QAAQ2B,UAAUI,OAAOhC,KAAKa,QAAQiB,YAQvDI,kEACShB,0DAAaU,UAAUI,OAAOhC,KAAKa,QAAQiB,eAC3C7B,QAAQ2B,UAAUI,OAAOhC,KAAKa,QAAQoB,aACtChC,QAAQ2B,UAAUI,OAAOhC,KAAKa,QAAQiB,UAQ/CK,KAAKf,UAEoB,MAAjBA,SAASI,WACJhB,SAASa,SAAS,SAAU,CAACD,SAASrB,IAAKC,KAAKD,IAEpC,WAAjBqB,SAASI,WACJhB,SAASa,SAAS,cAAe,CAACD,SAASrB,IAAKC,KAAKD"} courseeditor/dndsection.min.js 0000644 00000005104 15151264135 0012530 0 ustar 00 define("core_courseformat/local/courseeditor/dndsection",["exports","core/reactive"],(function(_exports,_reactive){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; /** * Course index section component. * * This component is used to control specific course section interactions like drag and drop * in both course index and course content. * * @module core_courseformat/local/courseeditor/dndsection * @class core_courseformat/local/courseeditor/dndsection * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class _default extends _reactive.BaseComponent{configState(state){this.id=this.element.dataset.id,this.section=state.section.get(this.id),this.course=state.course}configDragDrop(sectionitem){this.reactive.isEditing&&this.reactive.supportComponents&&(this.sectionitem=sectionitem,this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.sectionitem&&this.sectionitem.unregister(),void 0!==this.dragdrop&&this.dragdrop.unregister()}getLastCm(){return null}dragStart(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!1)}validateDropData(dropdata){if("cm"===(null==dropdata?void 0:dropdata.type))return!0;if("section"===(null==dropdata?void 0:dropdata.type)){const sectionzeroid=this.course.sectionlist[0];return(null==dropdata?void 0:dropdata.id)!=this.id&&(null==dropdata?void 0:dropdata.id)!=sectionzeroid&&this.id!=sectionzeroid}return!1}showDropZone(dropdata){var _this$getLastCm;"cm"==dropdata.type&&(null===(_this$getLastCm=this.getLastCm())||void 0===_this$getLastCm||_this$getLastCm.classList.add(this.classes.DROPDOWN));"section"==dropdata.type&&(this.section.number>dropdata.number?(this.element.classList.remove(this.classes.DROPUP),this.element.classList.add(this.classes.DROPDOWN)):(this.element.classList.add(this.classes.DROPUP),this.element.classList.remove(this.classes.DROPDOWN)))}hideDropZone(){var _this$getLastCm2;null===(_this$getLastCm2=this.getLastCm())||void 0===_this$getLastCm2||_this$getLastCm2.classList.remove(this.classes.DROPDOWN),this.element.classList.remove(this.classes.DROPUP),this.element.classList.remove(this.classes.DROPDOWN)}drop(dropdata){"cm"==dropdata.type&&this.reactive.dispatch("cmMove",[dropdata.id],this.id),"section"==dropdata.type&&this.reactive.dispatch("sectionMove",[dropdata.id],this.id)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=dndsection.min.js.map courseeditor/contenttree.min.js.map 0000644 00000020762 15151264135 0013513 0 ustar 00 {"version":3,"file":"contenttree.min.js","sources":["../../../src/local/courseeditor/contenttree.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Course index keyboard navigation and aria-tree compatibility.\n *\n * Node tree and bootstrap collapsibles don't use the same HTML structure. However,\n * all keybindings and logic is compatible. This class translate the primitive opetations\n * to a bootstrap collapsible structure.\n *\n * @module core_courseformat/local/courseindex/keyboardnav\n * @class core_courseformat/local/courseindex/keyboardnav\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n// The core/tree uses jQuery to expand all nodes.\nimport jQuery from 'jquery';\nimport Tree from 'core/tree';\nimport {getList} from 'core/normalise';\n\nexport default class extends Tree {\n\n /**\n * Setup the core/tree keyboard navigation.\n *\n * @param {Element|undefined} mainElement an alternative main element in case it is not from the parent component\n * @param {Object|undefined} selectors alternative selectors\n * @param {boolean} preventcache if the elements cache must be disabled.\n */\n constructor(mainElement, selectors, preventcache) {\n // Init this value with the parent DOM element.\n super(mainElement);\n\n // Get selectors from parent.\n this.selectors = {\n SECTION: selectors.SECTION,\n TOGGLER: selectors.TOGGLER,\n COLLAPSE: selectors.COLLAPSE,\n ENTER: selectors.ENTER ?? selectors.TOGGLER,\n };\n\n // The core/tree library saves the visible elements cache inside the main tree node.\n // However, in edit mode content can change suddenly so we need to refresh caches when needed.\n if (preventcache) {\n this._getVisibleItems = this.getVisibleItems;\n this.getVisibleItems = () => {\n this.refreshVisibleItemsCache();\n return this._getVisibleItems();\n };\n }\n // All jQuery events can be replaced when MDL-71979 is integrated.\n this.treeRoot.on('hidden.bs.collapse shown.bs.collapse', () => {\n this.refreshVisibleItemsCache();\n });\n // Register a custom callback for pressing enter key.\n this.registerEnterCallback(this.enterCallback.bind(this));\n }\n\n /**\n * Return the current active node.\n *\n * @return {Element|undefined} the active item if any\n */\n getActiveItem() {\n const activeItem = this.treeRoot.data('activeItem');\n if (activeItem) {\n return getList(activeItem)[0];\n }\n return undefined;\n }\n\n /**\n * Handle enter key on a collpasible node.\n *\n * @param {JQuery} jQueryItem the jQuery object\n */\n enterCallback(jQueryItem) {\n const item = getList(jQueryItem)[0];\n if (this.isGroupItem(jQueryItem)) {\n // Group elements is like clicking a topic but without loosing the focus.\n const enter = item.querySelector(this.selectors.ENTER);\n if (enter.getAttribute('href') !== '#') {\n window.location.href = enter.getAttribute('href');\n }\n enter.click();\n } else {\n // Activity links just follow the link href.\n const link = item.querySelector('a');\n if (link.getAttribute('href') !== '#') {\n window.location.href = link.getAttribute('href');\n } else {\n link.click();\n }\n return;\n }\n }\n\n /**\n * Handle an item click.\n *\n * @param {Event} event the click event\n * @param {jQuery} jQueryItem the item clicked\n */\n handleItemClick(event, jQueryItem) {\n const isChevron = event.target.closest(this.selectors.COLLAPSE);\n // Only chevron clicks toogle the sections always.\n if (isChevron) {\n super.handleItemClick(event, jQueryItem);\n return;\n }\n // This is a title or activity name click.\n jQueryItem.focus();\n if (this.isGroupItem(jQueryItem)) {\n this.expandGroup(jQueryItem);\n }\n }\n\n /**\n * Check if a gorup item is collapsed.\n *\n * @param {JQuery} jQueryItem the jQuery object\n * @returns {boolean} if the element is collapsed\n */\n isGroupCollapsed(jQueryItem) {\n const item = getList(jQueryItem)[0];\n const toggler = item.querySelector(`[aria-expanded]`);\n return toggler.getAttribute('aria-expanded') === 'false';\n }\n\n /**\n * Toggle a group item.\n *\n * @param {JQuery} item the jQuery object\n */\n toggleGroup(item) {\n // All jQuery in this segment of code can be replaced when MDL-71979 is integrated.\n const toggler = item.find(this.selectors.COLLAPSE);\n let collapsibleId = toggler.data('target') ?? toggler.attr('href');\n if (!collapsibleId) {\n return;\n }\n collapsibleId = collapsibleId.replace('#', '');\n\n // Bootstrap 4 uses jQuery to interact with collapsibles.\n const collapsible = jQuery(`#${collapsibleId}`);\n if (collapsible.length) {\n jQuery(`#${collapsibleId}`).collapse('toggle');\n }\n }\n\n /**\n * Expand a group item.\n *\n * @param {JQuery} item the jQuery object\n */\n expandGroup(item) {\n if (this.isGroupCollapsed(item)) {\n this.toggleGroup(item);\n }\n }\n\n /**\n * Collpase a group item.\n *\n * @param {JQuery} item the jQuery object\n */\n collapseGroup(item) {\n if (!this.isGroupCollapsed(item)) {\n this.toggleGroup(item);\n }\n }\n\n /**\n * Expand all groups.\n */\n expandAllGroups() {\n const togglers = getList(this.treeRoot)[0].querySelectorAll(this.selectors.SECTION);\n togglers.forEach(item => {\n this.expandGroup(jQuery(item));\n });\n }\n}\n"],"names":["Tree","constructor","mainElement","selectors","preventcache","SECTION","TOGGLER","COLLAPSE","ENTER","_getVisibleItems","this","getVisibleItems","refreshVisibleItemsCache","treeRoot","on","registerEnterCallback","enterCallback","bind","getActiveItem","activeItem","data","jQueryItem","item","isGroupItem","enter","querySelector","getAttribute","window","location","href","click","link","handleItemClick","event","target","closest","focus","expandGroup","isGroupCollapsed","toggleGroup","toggler","find","collapsibleId","attr","replace","length","collapse","collapseGroup","expandAllGroups","querySelectorAll","forEach"],"mappings":";;;;;;;;;;;;wLAiC6BA,cASzBC,YAAYC,YAAaC,UAAWC,yCAE1BF,kBAGDC,UAAY,CACbE,QAASF,UAAUE,QACnBC,QAASH,UAAUG,QACnBC,SAAUJ,UAAUI,SACpBC,+BAAOL,UAAUK,mDAASL,UAAUG,SAKpCF,oBACKK,iBAAmBC,KAAKC,qBACxBA,gBAAkB,UACdC,2BACEF,KAAKD,0BAIfI,SAASC,GAAG,wCAAwC,UAChDF,mCAGJG,sBAAsBL,KAAKM,cAAcC,KAAKP,OAQvDQ,sBACUC,WAAaT,KAAKG,SAASO,KAAK,iBAClCD,kBACO,sBAAQA,YAAY,GAUnCH,cAAcK,kBACJC,MAAO,sBAAQD,YAAY,MAC7BX,KAAKa,YAAYF,YAAa,OAExBG,MAAQF,KAAKG,cAAcf,KAAKP,UAAUK,OACb,MAA/BgB,MAAME,aAAa,UACnBC,OAAOC,SAASC,KAAOL,MAAME,aAAa,SAE9CF,MAAMM,mBAGAC,KAAOT,KAAKG,cAAc,KACE,MAA9BM,KAAKL,aAAa,QAClBC,OAAOC,SAASC,KAAOE,KAAKL,aAAa,QAEzCK,KAAKD,SAYjBE,gBAAgBC,MAAOZ,YACDY,MAAMC,OAAOC,QAAQzB,KAAKP,UAAUI,gBAG5CyB,gBAAgBC,MAAOZ,aAIjCA,WAAWe,QACP1B,KAAKa,YAAYF,kBACZgB,YAAYhB,aAUzBiB,iBAAiBjB,kBAGoC,WAFpC,sBAAQA,YAAY,GACZI,iCACNC,aAAa,iBAQhCa,YAAYjB,8BAEFkB,QAAUlB,KAAKmB,KAAK/B,KAAKP,UAAUI,cACrCmC,oCAAgBF,QAAQpB,KAAK,iDAAaoB,QAAQG,KAAK,YACtDD,qBAGLA,cAAgBA,cAAcE,QAAQ,IAAK,KAGvB,8BAAWF,gBACfG,uCACDH,gBAAiBI,SAAS,UAS7CT,YAAYf,MACJZ,KAAK4B,iBAAiBhB,YACjBiB,YAAYjB,MASzByB,cAAczB,MACLZ,KAAK4B,iBAAiBhB,YAClBiB,YAAYjB,MAOzB0B,mBACqB,sBAAQtC,KAAKG,UAAU,GAAGoC,iBAAiBvC,KAAKP,UAAUE,SAClE6C,SAAQ5B,YACRe,aAAY,mBAAOf"} courseeditor/dndsectionitem.min.js 0000644 00000004131 15151264135 0013406 0 ustar 00 define("core_courseformat/local/courseeditor/dndsectionitem",["exports","core/reactive"],(function(_exports,_reactive){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; /** * Course index section title draggable component. * * This component is used to control specific course section interactions like drag and drop * in both course index and course content. * * @module core_courseformat/local/courseeditor/dndsectionitem * @class core_courseformat/local/courseeditor/dndsectionitem * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class _default extends _reactive.BaseComponent{configDragDrop(sectionid,state,fullregion){this.id=sectionid,void 0===this.section&&(this.section=state.section.get(this.id)),void 0===this.course&&(this.course=state.course),this.section.number>0&&(this.getDraggableData=this._getDraggableData),this.fullregion=fullregion,this.reactive.isEditing&&this.reactive.supportComponents&&(this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.dragdrop&&this.dragdrop.unregister()}dragStart(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("sectionDrag",[dropdata.id],!1)}_getDraggableData(){return this.reactive.getExporter().sectionDraggableData(this.reactive.state,this.id)}validateDropData(dropdata){if("cm"===(null==dropdata?void 0:dropdata.type)){var _this$section;const firstcmid=null===(_this$section=this.section)||void 0===_this$section?void 0:_this$section.cmlist[0];return dropdata.id!==firstcmid}return!1}showDropZone(){this.element.classList.add(this.classes.DROPZONE)}hideDropZone(){this.element.classList.remove(this.classes.DROPZONE)}drop(dropdata){var _this$section2;"cm"==dropdata.type&&this.reactive.dispatch("cmMove",[dropdata.id],this.id,null===(_this$section2=this.section)||void 0===_this$section2?void 0:_this$section2.cmlist[0])}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=dndsectionitem.min.js.map courseeditor/mutations.min.js.map 0000644 00000057215 15151264135 0013207 0 ustar 00 {"version":3,"file":"mutations.min.js","sources":["../../../src/local/courseeditor/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\nimport ajax from 'core/ajax';\n\n/**\n * Default mutation manager\n *\n * @module core_courseformat/local/courseeditor/mutations\n * @class core_courseformat/local/courseeditor/mutations\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n // All course editor mutations for Moodle 4.0 will be located in this file.\n\n /**\n * Private method to call core_courseformat_update_course webservice.\n *\n * @method _callEditWebservice\n * @param {string} action\n * @param {number} courseId\n * @param {array} ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _callEditWebservice(action, courseId, ids, targetSectionId, targetCmId) {\n const args = {\n action,\n courseid: courseId,\n ids,\n };\n if (targetSectionId) {\n args.targetsectionid = targetSectionId;\n }\n if (targetCmId) {\n args.targetcmid = targetCmId;\n }\n let ajaxresult = await ajax.call([{\n methodname: 'core_courseformat_update_course',\n args,\n }])[0];\n return JSON.parse(ajaxresult);\n }\n\n /**\n * Execute a basic section state action.\n * @param {StateManager} stateManager the current state manager\n * @param {string} action the action name\n * @param {array} sectionIds the section ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _sectionBasicAction(stateManager, action, sectionIds, targetSectionId, targetCmId) {\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice(\n action,\n course.id,\n sectionIds,\n targetSectionId,\n targetCmId\n );\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Execute a basic course module state action.\n * @param {StateManager} stateManager the current state manager\n * @param {string} action the action name\n * @param {array} cmIds the cm ids\n * @param {number} targetSectionId optional target section id (for moving actions)\n * @param {number} targetCmId optional target cm id (for moving actions)\n */\n async _cmBasicAction(stateManager, action, cmIds, targetSectionId, targetCmId) {\n const course = stateManager.get('course');\n this.cmLock(stateManager, cmIds, true);\n const updates = await this._callEditWebservice(\n action,\n course.id,\n cmIds,\n targetSectionId,\n targetCmId\n );\n stateManager.processUpdates(updates);\n this.cmLock(stateManager, cmIds, false);\n }\n\n /**\n * Mutation module initialize.\n *\n * The reactive instance will execute this method when addMutations or setMutation is invoked.\n *\n * @param {StateManager} stateManager the state manager\n */\n init(stateManager) {\n // Add a method to prepare the fields when some update is comming from the server.\n stateManager.addUpdateTypes({\n prepareFields: this._prepareFields,\n });\n }\n\n /**\n * Add default values to state elements.\n *\n * This method is called every time a webservice returns a update state message.\n *\n * @param {Object} stateManager the state manager\n * @param {String} updateName the state element to update\n * @param {Object} fields the new data\n * @returns {Object} final fields data\n */\n _prepareFields(stateManager, updateName, fields) {\n // Any update should unlock the element.\n fields.locked = false;\n return fields;\n }\n\n /**\n * Hides sections.\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n */\n async sectionHide(stateManager, sectionIds) {\n await this._sectionBasicAction(stateManager, 'section_hide', sectionIds);\n }\n\n /**\n * Show sections.\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n */\n async sectionShow(stateManager, sectionIds) {\n await this._sectionBasicAction(stateManager, 'section_show', sectionIds);\n }\n\n /**\n * Show cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmShow(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_show', cmIds);\n }\n\n /**\n * Hide cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmHide(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_hide', cmIds);\n }\n\n /**\n * Stealth cms.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmStealth(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_stealth', cmIds);\n }\n\n /**\n * Move course modules to specific course location.\n *\n * Note that one of targetSectionId or targetCmId should be provided in order to identify the\n * new location:\n * - targetCmId: the activities will be located avobe the target cm. The targetSectionId\n * value will be ignored in this case.\n * - targetSectionId: the activities will be appended to the section. In this case\n * targetSectionId should not be present.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmids the list of cm ids to move\n * @param {number} targetSectionId the target section id\n * @param {number} targetCmId the target course module id\n */\n async cmMove(stateManager, cmids, targetSectionId, targetCmId) {\n if (!targetSectionId && !targetCmId) {\n throw new Error(`Mutation cmMove requires targetSectionId or targetCmId`);\n }\n const course = stateManager.get('course');\n this.cmLock(stateManager, cmids, true);\n const updates = await this._callEditWebservice('cm_move', course.id, cmids, targetSectionId, targetCmId);\n stateManager.processUpdates(updates);\n this.cmLock(stateManager, cmids, false);\n }\n\n /**\n * Move course modules to specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids to move\n * @param {number} targetSectionId the target section id\n */\n async sectionMove(stateManager, sectionIds, targetSectionId) {\n if (!targetSectionId) {\n throw new Error(`Mutation sectionMove requires targetSectionId`);\n }\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice('section_move', course.id, sectionIds, targetSectionId);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Add a new section to a specific course location.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {number} targetSectionId optional the target section id\n */\n async addSection(stateManager, targetSectionId) {\n if (!targetSectionId) {\n targetSectionId = 0;\n }\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_add', course.id, [], targetSectionId);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Delete sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of course modules ids\n */\n async sectionDelete(stateManager, sectionIds) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_delete', course.id, sectionIds);\n stateManager.processUpdates(updates);\n }\n\n /**\n * Mark or unmark course modules as dragging.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {bool} dragValue the new dragging value\n */\n cmDrag(stateManager, cmIds, dragValue) {\n this.setPageItem(stateManager);\n this._setElementsValue(stateManager, 'cm', cmIds, 'dragging', dragValue);\n }\n\n /**\n * Mark or unmark course sections as dragging.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n * @param {bool} dragValue the new dragging value\n */\n sectionDrag(stateManager, sectionIds, dragValue) {\n this.setPageItem(stateManager);\n this._setElementsValue(stateManager, 'section', sectionIds, 'dragging', dragValue);\n }\n\n /**\n * Mark or unmark course modules as complete.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {bool} complete the new completion value\n */\n cmCompletion(stateManager, cmIds, complete) {\n const newValue = (complete) ? 1 : 0;\n this._setElementsValue(stateManager, 'cm', cmIds, 'completionstate', newValue);\n }\n\n /**\n * Move cms to the right: indent = 1.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmMoveRight(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_moveright', cmIds);\n }\n\n /**\n * Move cms to the left: indent = 0.\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of cm ids\n */\n async cmMoveLeft(stateManager, cmIds) {\n await this._cmBasicAction(stateManager, 'cm_moveleft', cmIds);\n }\n\n /**\n * Lock or unlock course modules.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} cmIds the list of course modules ids\n * @param {bool} lockValue the new locked value\n */\n cmLock(stateManager, cmIds, lockValue) {\n this._setElementsValue(stateManager, 'cm', cmIds, 'locked', lockValue);\n }\n\n /**\n * Lock or unlock course sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n * @param {bool} lockValue the new locked value\n */\n sectionLock(stateManager, sectionIds, lockValue) {\n this._setElementsValue(stateManager, 'section', sectionIds, 'locked', lockValue);\n }\n\n _setElementsValue(stateManager, name, ids, fieldName, newValue) {\n stateManager.setReadOnly(false);\n ids.forEach((id) => {\n const element = stateManager.get(name, id);\n if (element) {\n element[fieldName] = newValue;\n }\n });\n stateManager.setReadOnly(true);\n }\n\n /**\n * Set the page current item.\n *\n * Only one element of the course state can be the page item at a time.\n *\n * There are several actions that can alter the page current item. For example, when the user is in an activity\n * page, the page item is always the activity one. However, in a course page, when the user scrolls to an element,\n * this element get the page item.\n *\n * If the page item is static means that it is not meant to change. This is important because\n * static page items has some special logic. For example, if a cm is the static page item\n * and it is inside a collapsed section, the course index will expand the section to make it visible.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {String|undefined} type the element type (section or cm). Undefined will remove the current page item.\n * @param {Number|undefined} id the element id\n * @param {boolean|undefined} isStatic if the page item is static\n */\n setPageItem(stateManager, type, id, isStatic) {\n let newPageItem;\n if (type !== undefined) {\n newPageItem = stateManager.get(type, id);\n if (!newPageItem) {\n return;\n }\n }\n stateManager.setReadOnly(false);\n // Remove the current page item.\n const course = stateManager.get('course');\n course.pageItem = null;\n // Save the new page item.\n if (newPageItem) {\n course.pageItem = {\n id,\n type,\n sectionId: (type == 'section') ? newPageItem.id : newPageItem.sectionid,\n isStatic,\n };\n }\n stateManager.setReadOnly(true);\n }\n\n /**\n * Unlock all course elements.\n *\n * @param {StateManager} stateManager the current state manager\n */\n unlockAll(stateManager) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.section.forEach((section) => {\n section.locked = false;\n });\n state.cm.forEach((cm) => {\n cm.locked = false;\n });\n stateManager.setReadOnly(true);\n }\n\n /**\n * Update the course index collapsed attribute of some sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the affected section ids\n * @param {boolean} collapsed the new collapsed value\n */\n async sectionIndexCollapsed(stateManager, sectionIds, collapsed) {\n const collapsedIds = this._updateStateSectionPreference(stateManager, 'indexcollapsed', sectionIds, collapsed);\n if (!collapsedIds) {\n return;\n }\n const course = stateManager.get('course');\n await this._callEditWebservice('section_index_collapsed', course.id, collapsedIds);\n }\n\n /**\n * Update the course content collapsed attribute of some sections.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the affected section ids\n * @param {boolean} collapsed the new collapsed value\n */\n async sectionContentCollapsed(stateManager, sectionIds, collapsed) {\n const collapsedIds = this._updateStateSectionPreference(stateManager, 'contentcollapsed', sectionIds, collapsed);\n if (!collapsedIds) {\n return;\n }\n const course = stateManager.get('course');\n await this._callEditWebservice('section_content_collapsed', course.id, collapsedIds);\n }\n\n /**\n * Private batch update for a section preference attribute.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {string} preferenceName the preference name\n * @param {array} sectionIds the affected section ids\n * @param {boolean} preferenceValue the new preferenceValue value\n * @return {Number[]|null} sections ids with the preference value true or null if no update is required\n */\n _updateStateSectionPreference(stateManager, preferenceName, sectionIds, preferenceValue) {\n stateManager.setReadOnly(false);\n const affectedSections = new Set();\n // Check if we need to update preferences.\n sectionIds.forEach(sectionId => {\n const section = stateManager.get('section', sectionId);\n if (section === undefined) {\n return null;\n }\n const newValue = preferenceValue ?? section[preferenceName];\n if (section[preferenceName] != newValue) {\n section[preferenceName] = newValue;\n affectedSections.add(section.id);\n }\n });\n stateManager.setReadOnly(true);\n if (affectedSections.size == 0) {\n return null;\n }\n // Get all collapsed section ids.\n const collapsedSectionIds = [];\n const state = stateManager.state;\n state.section.forEach(section => {\n if (section[preferenceName]) {\n collapsedSectionIds.push(section.id);\n }\n });\n return collapsedSectionIds;\n }\n\n /**\n * Get updated state data related to some cm ids.\n *\n * @method cmState\n * @param {StateManager} stateManager the current state\n * @param {array} cmids the list of cm ids to update\n */\n async cmState(stateManager, cmids) {\n this.cmLock(stateManager, cmids, true);\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('cm_state', course.id, cmids);\n stateManager.processUpdates(updates);\n this.cmLock(stateManager, cmids, false);\n }\n\n /**\n * Get updated state data related to some section ids.\n *\n * @method sectionState\n * @param {StateManager} stateManager the current state\n * @param {array} sectionIds the list of section ids to update\n */\n async sectionState(stateManager, sectionIds) {\n this.sectionLock(stateManager, sectionIds, true);\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('section_state', course.id, sectionIds);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n }\n\n /**\n * Get the full updated state data of the course.\n *\n * @param {StateManager} stateManager the current state\n */\n async courseState(stateManager) {\n const course = stateManager.get('course');\n const updates = await this._callEditWebservice('course_state', course.id);\n stateManager.processUpdates(updates);\n }\n\n}\n"],"names":["action","courseId","ids","targetSectionId","targetCmId","args","courseid","targetsectionid","targetcmid","ajaxresult","ajax","call","methodname","JSON","parse","stateManager","sectionIds","course","get","sectionLock","updates","this","_callEditWebservice","id","processUpdates","cmIds","cmLock","init","addUpdateTypes","prepareFields","_prepareFields","updateName","fields","locked","_sectionBasicAction","_cmBasicAction","cmids","Error","cmDrag","dragValue","setPageItem","_setElementsValue","sectionDrag","cmCompletion","complete","newValue","lockValue","name","fieldName","setReadOnly","forEach","element","type","isStatic","newPageItem","undefined","pageItem","sectionId","sectionid","unlockAll","state","section","cm","collapsed","collapsedIds","_updateStateSectionPreference","preferenceName","preferenceValue","affectedSections","Set","add","size","collapsedSectionIds","push"],"mappings":";;;;;;;;iMAuC8BA,OAAQC,SAAUC,IAAKC,gBAAiBC,kBACxDC,KAAO,CACTL,OAAAA,OACAM,SAAUL,SACVC,IAAAA,KAEAC,kBACAE,KAAKE,gBAAkBJ,iBAEvBC,aACAC,KAAKG,WAAaJ,gBAElBK,iBAAmBC,cAAKC,KAAK,CAAC,CAC9BC,WAAY,kCACZP,KAAAA,QACA,UACGQ,KAAKC,MAAML,sCAWIM,aAAcf,OAAQgB,WAAYb,gBAAiBC,kBACnEa,OAASF,aAAaG,IAAI,eAC3BC,YAAYJ,aAAcC,YAAY,SACrCI,cAAgBC,KAAKC,oBACvBtB,OACAiB,OAAOM,GACPP,WACAb,gBACAC,YAEJW,aAAaS,eAAeJ,cACvBD,YAAYJ,aAAcC,YAAY,wBAW1BD,aAAcf,OAAQyB,MAAOtB,gBAAiBC,kBACzDa,OAASF,aAAaG,IAAI,eAC3BQ,OAAOX,aAAcU,OAAO,SAC3BL,cAAgBC,KAAKC,oBACvBtB,OACAiB,OAAOM,GACPE,MACAtB,gBACAC,YAEJW,aAAaS,eAAeJ,cACvBM,OAAOX,aAAcU,OAAO,GAUrCE,KAAKZ,cAEDA,aAAaa,eAAe,CACxBC,cAAeR,KAAKS,iBAc5BA,eAAef,aAAcgB,WAAYC,eAErCA,OAAOC,QAAS,EACTD,yBAQOjB,aAAcC,kBACtBK,KAAKa,oBAAoBnB,aAAc,eAAgBC,8BAQ/CD,aAAcC,kBACtBK,KAAKa,oBAAoBnB,aAAc,eAAgBC,yBAQpDD,aAAcU,aACjBJ,KAAKc,eAAepB,aAAc,UAAWU,oBAQ1CV,aAAcU,aACjBJ,KAAKc,eAAepB,aAAc,UAAWU,uBAQvCV,aAAcU,aACpBJ,KAAKc,eAAepB,aAAc,aAAcU,oBAkB7CV,aAAcqB,MAAOjC,gBAAiBC,gBAC1CD,kBAAoBC,iBACf,IAAIiC,sEAERpB,OAASF,aAAaG,IAAI,eAC3BQ,OAAOX,aAAcqB,OAAO,SAC3BhB,cAAgBC,KAAKC,oBAAoB,UAAWL,OAAOM,GAAIa,MAAOjC,gBAAiBC,YAC7FW,aAAaS,eAAeJ,cACvBM,OAAOX,aAAcqB,OAAO,qBAUnBrB,aAAcC,WAAYb,qBACnCA,sBACK,IAAIkC,6DAERpB,OAASF,aAAaG,IAAI,eAC3BC,YAAYJ,aAAcC,YAAY,SACrCI,cAAgBC,KAAKC,oBAAoB,eAAgBL,OAAOM,GAAIP,WAAYb,iBACtFY,aAAaS,eAAeJ,cACvBD,YAAYJ,aAAcC,YAAY,oBAS9BD,aAAcZ,iBACtBA,kBACDA,gBAAkB,SAEhBc,OAASF,aAAaG,IAAI,UAC1BE,cAAgBC,KAAKC,oBAAoB,cAAeL,OAAOM,GAAI,GAAIpB,iBAC7EY,aAAaS,eAAeJ,6BASZL,aAAcC,kBACxBC,OAASF,aAAaG,IAAI,UAC1BE,cAAgBC,KAAKC,oBAAoB,iBAAkBL,OAAOM,GAAIP,YAC5ED,aAAaS,eAAeJ,SAUhCkB,OAAOvB,aAAcU,MAAOc,gBACnBC,YAAYzB,mBACZ0B,kBAAkB1B,aAAc,KAAMU,MAAO,WAAYc,WAUlEG,YAAY3B,aAAcC,WAAYuB,gBAC7BC,YAAYzB,mBACZ0B,kBAAkB1B,aAAc,UAAWC,WAAY,WAAYuB,WAU5EI,aAAa5B,aAAcU,MAAOmB,gBACxBC,SAAYD,SAAY,EAAI,OAC7BH,kBAAkB1B,aAAc,KAAMU,MAAO,kBAAmBoB,4BAQvD9B,aAAcU,aACtBJ,KAAKc,eAAepB,aAAc,eAAgBU,wBAQ3CV,aAAcU,aACrBJ,KAAKc,eAAepB,aAAc,cAAeU,OAU3DC,OAAOX,aAAcU,MAAOqB,gBACnBL,kBAAkB1B,aAAc,KAAMU,MAAO,SAAUqB,WAUhE3B,YAAYJ,aAAcC,WAAY8B,gBAC7BL,kBAAkB1B,aAAc,UAAWC,WAAY,SAAU8B,WAG1EL,kBAAkB1B,aAAcgC,KAAM7C,IAAK8C,UAAWH,UAClD9B,aAAakC,aAAY,GACzB/C,IAAIgD,SAAS3B,WACH4B,QAAUpC,aAAaG,IAAI6B,KAAMxB,IACnC4B,UACAA,QAAQH,WAAaH,aAG7B9B,aAAakC,aAAY,GAqB7BT,YAAYzB,aAAcqC,KAAM7B,GAAI8B,cAC5BC,oBACSC,IAATH,OACAE,YAAcvC,aAAaG,IAAIkC,KAAM7B,KAChC+B,oBAITvC,aAAakC,aAAY,SAEnBhC,OAASF,aAAaG,IAAI,UAChCD,OAAOuC,SAAW,KAEdF,cACArC,OAAOuC,SAAW,CACdjC,GAAAA,GACA6B,KAAAA,KACAK,UAAoB,WAARL,KAAqBE,YAAY/B,GAAK+B,YAAYI,UAC9DL,SAAAA,WAGRtC,aAAakC,aAAY,GAQ7BU,UAAU5C,oBACA6C,MAAQ7C,aAAa6C,MAC3B7C,aAAakC,aAAY,GACzBW,MAAMC,QAAQX,SAASW,UACnBA,QAAQ5B,QAAS,KAErB2B,MAAME,GAAGZ,SAASY,KACdA,GAAG7B,QAAS,KAEhBlB,aAAakC,aAAY,+BAUDlC,aAAcC,WAAY+C,iBAC5CC,aAAe3C,KAAK4C,8BAA8BlD,aAAc,iBAAkBC,WAAY+C,eAC/FC,0BAGC/C,OAASF,aAAaG,IAAI,gBAC1BG,KAAKC,oBAAoB,0BAA2BL,OAAOM,GAAIyC,4CAU3CjD,aAAcC,WAAY+C,iBAC9CC,aAAe3C,KAAK4C,8BAA8BlD,aAAc,mBAAoBC,WAAY+C,eACjGC,0BAGC/C,OAASF,aAAaG,IAAI,gBAC1BG,KAAKC,oBAAoB,4BAA6BL,OAAOM,GAAIyC,cAY3EC,8BAA8BlD,aAAcmD,eAAgBlD,WAAYmD,iBACpEpD,aAAakC,aAAY,SACnBmB,iBAAmB,IAAIC,OAE7BrD,WAAWkC,SAAQO,kBACTI,QAAU9C,aAAaG,IAAI,UAAWuC,mBAC5BF,IAAZM,eACO,WAELhB,SAAWsB,MAAAA,gBAAAA,gBAAmBN,QAAQK,gBACxCL,QAAQK,iBAAmBrB,WAC3BgB,QAAQK,gBAAkBrB,SAC1BuB,iBAAiBE,IAAIT,QAAQtC,QAGrCR,aAAakC,aAAY,GACI,GAAzBmB,iBAAiBG,YACV,WAGLC,oBAAsB,UACdzD,aAAa6C,MACrBC,QAAQX,SAAQW,UACdA,QAAQK,iBACRM,oBAAoBC,KAAKZ,QAAQtC,OAGlCiD,kCAUGzD,aAAcqB,YACnBV,OAAOX,aAAcqB,OAAO,SAC3BnB,OAASF,aAAaG,IAAI,UAC1BE,cAAgBC,KAAKC,oBAAoB,WAAYL,OAAOM,GAAIa,OACtErB,aAAaS,eAAeJ,cACvBM,OAAOX,aAAcqB,OAAO,sBAUlBrB,aAAcC,iBACxBG,YAAYJ,aAAcC,YAAY,SACrCC,OAASF,aAAaG,IAAI,UAC1BE,cAAgBC,KAAKC,oBAAoB,gBAAiBL,OAAOM,GAAIP,YAC3ED,aAAaS,eAAeJ,cACvBD,YAAYJ,aAAcC,YAAY,qBAQ7BD,oBACRE,OAASF,aAAaG,IAAI,UAC1BE,cAAgBC,KAAKC,oBAAoB,eAAgBL,OAAOM,IACtER,aAAaS,eAAeJ"} courseeditor/courseeditor.min.js.map 0000644 00000025104 15151264135 0013663 0 ustar 00 {"version":3,"file":"courseeditor.min.js","sources":["../../../src/local/courseeditor/courseeditor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\nimport {Reactive} from 'core/reactive';\nimport notification from 'core/notification';\nimport Exporter from 'core_courseformat/local/courseeditor/exporter';\nimport log from 'core/log';\nimport ajax from 'core/ajax';\nimport * as Storage from 'core/sessionstorage';\n\n/**\n * Main course editor module.\n *\n * All formats can register new components on this object to create new reactive\n * UI components that watch the current course state.\n *\n * @module core_courseformat/local/courseeditor/courseeditor\n * @class core_courseformat/local/courseeditor/courseeditor\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class extends Reactive {\n\n /**\n * The current state cache key\n *\n * The state cache is considered dirty if the state changes from the last page or\n * if the page has editing mode on.\n *\n * @attribute stateKey\n * @type number|null\n * @default 1\n * @package\n */\n stateKey = 1;\n\n /**\n * The current page section return\n * @attribute sectionReturn\n * @type number\n * @default 0\n */\n sectionReturn = 0;\n\n /**\n * Set up the course editor when the page is ready.\n *\n * The course can only be loaded once per instance. Otherwise an error is thrown.\n *\n * The backend can inform the module of the current state key. This key changes every time some\n * update in the course affect the current user state. Some examples are:\n * - The course content has been edited\n * - The user marks some activity as completed\n * - The user collapses or uncollapses a section (it is stored as a user preference)\n *\n * @param {number} courseId course id\n * @param {string} serverStateKey the current backend course cache reference\n */\n async loadCourse(courseId, serverStateKey) {\n\n if (this.courseId) {\n throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);\n }\n\n if (!serverStateKey) {\n // The server state key is not provided, we use a invalid statekey to force reloading.\n serverStateKey = `invalidStateKey_${Date.now()}`;\n }\n\n // Default view format setup.\n this._editing = false;\n this._supportscomponents = false;\n\n this.courseId = courseId;\n\n let stateData;\n\n const storeStateKey = Storage.get(`course/${courseId}/stateKey`);\n try {\n // Check if the backend state key is the same we have in our session storage.\n if (!this.isEditing && serverStateKey == storeStateKey) {\n stateData = JSON.parse(Storage.get(`course/${courseId}/staticState`));\n }\n if (!stateData) {\n stateData = await this.getServerCourseState();\n }\n\n } catch (error) {\n log.error(\"EXCEPTION RAISED WHILE INIT COURSE EDITOR\");\n log.error(error);\n return;\n }\n\n this.setInitialState(stateData);\n\n // In editing mode, the session cache is considered dirty always.\n if (this.isEditing) {\n this.stateKey = null;\n } else {\n // Check if the last state is the same as the cached one.\n const newState = JSON.stringify(stateData);\n const previousState = Storage.get(`course/${courseId}/staticState`);\n if (previousState !== newState || storeStateKey !== serverStateKey) {\n Storage.set(`course/${courseId}/staticState`, newState);\n Storage.set(`course/${courseId}/stateKey`, stateData?.course?.statekey ?? serverStateKey);\n }\n this.stateKey = Storage.get(`course/${courseId}/stateKey`);\n }\n }\n\n /**\n * Setup the current view settings\n *\n * @param {Object} setup format, page and course settings\n * @param {boolean} setup.editing if the page is in edit mode\n * @param {boolean} setup.supportscomponents if the format supports components for content\n * @param {string} setup.cacherev the backend cached state revision\n */\n setViewFormat(setup) {\n this._editing = setup.editing ?? false;\n this._supportscomponents = setup.supportscomponents ?? false;\n }\n\n /**\n * Load the current course state from the server.\n *\n * @returns {Object} the current course state\n */\n async getServerCourseState() {\n const courseState = await ajax.call([{\n methodname: 'core_courseformat_get_state',\n args: {\n courseid: this.courseId,\n }\n }])[0];\n\n const stateData = JSON.parse(courseState);\n\n return {\n course: {},\n section: [],\n cm: [],\n ...stateData,\n };\n }\n\n /**\n * Return the current edit mode.\n *\n * Components should use this method to check if edit mode is active.\n *\n * @return {boolean} if edit is enabled\n */\n get isEditing() {\n return this._editing ?? false;\n }\n\n /**\n * Return a data exporter to transform state part into mustache contexts.\n *\n * @return {Exporter} the exporter class\n */\n getExporter() {\n return new Exporter(this);\n }\n\n /**\n * Return if the current course support components to refresh the content.\n *\n * @returns {boolean} if the current content support components\n */\n get supportComponents() {\n return this._supportscomponents ?? false;\n }\n\n /**\n * Get a value from the course editor static storage if any.\n *\n * The course editor static storage uses the sessionStorage to store values from the\n * components. This is used to prevent unnecesary template loadings on every page. However,\n * the storage does not work if no sessionStorage can be used (in debug mode for example),\n * if the page is in editing mode or if the initial state change from the last page.\n *\n * @param {string} key the key to get\n * @return {boolean|string} the storage value or false if cannot be loaded\n */\n getStorageValue(key) {\n if (this.isEditing || !this.stateKey) {\n return false;\n }\n const dataJson = Storage.get(`course/${this.courseId}/${key}`);\n if (!dataJson) {\n return false;\n }\n // Check the stateKey.\n try {\n const data = JSON.parse(dataJson);\n if (data?.stateKey !== this.stateKey) {\n return false;\n }\n return data.value;\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Stores a value into the course editor static storage if available\n *\n * @param {String} key the key to store\n * @param {*} value the value to store (must be compatible with JSON,stringify)\n * @returns {boolean} true if the value is stored\n */\n setStorageValue(key, value) {\n // Values cannot be stored on edit mode.\n if (this.isEditing) {\n return false;\n }\n const data = {\n stateKey: this.stateKey,\n value,\n };\n return Storage.set(`course/${this.courseId}/${key}`, JSON.stringify(data));\n }\n\n /**\n * Dispatch a change in the state.\n *\n * Usually reactive modules throw an error directly to the components when something\n * goes wrong. However, course editor can directly display a notification.\n *\n * @method dispatch\n * @param {mixed} args any number of params the mutation needs.\n */\n async dispatch(...args) {\n try {\n await super.dispatch(...args);\n } catch (error) {\n // Display error modal.\n notification.exception(error);\n // Force unlock all elements.\n super.dispatch('unlockAll');\n }\n }\n}\n"],"names":["Reactive","courseId","serverStateKey","this","Error","stateData","Date","now","_editing","_supportscomponents","storeStateKey","Storage","get","isEditing","JSON","parse","getServerCourseState","error","setInitialState","stateKey","newState","stringify","set","_stateData","course","_stateData$course","statekey","setViewFormat","setup","editing","supportscomponents","courseState","ajax","call","methodname","args","courseid","section","cm","getExporter","Exporter","supportComponents","getStorageValue","key","dataJson","data","value","setStorageValue","super","dispatch","exception"],"mappings":";;;;;;;;;;;g7BAiC6BA,qFAad,wCAQK,oBAgBCC,SAAUC,mBAEnBC,KAAKF,eACC,IAAIG,4BAAqBH,oDAA2CE,KAAKF,eAc/EI,UAXCH,iBAEDA,yCAAoCI,KAAKC,aAIxCC,UAAW,OACXC,qBAAsB,OAEtBR,SAAWA,eAIVS,cAAgBC,QAAQC,qBAAcX,2BAGnCE,KAAKU,WAAaX,gBAAkBQ,gBACrCL,UAAYS,KAAKC,MAAMJ,QAAQC,qBAAcX,4BAE5CI,YACDA,gBAAkBF,KAAKa,wBAG7B,MAAOC,2BACDA,MAAM,+DACNA,MAAMA,eAITC,gBAAgBb,WAGjBF,KAAKU,eACAM,SAAW,SACb,OAEGC,SAAWN,KAAKO,UAAUhB,qEACVM,QAAQC,qBAAcX,4BACtBmB,UAAYV,gBAAkBR,eAChDS,QAAQW,qBAAcrB,yBAAwBmB,UAC9CT,QAAQW,qBAAcrB,uEAAqBI,2DAAAkB,WAAWC,2CAAXC,kBAAmBC,gEAAYxB,qBAEzEiB,SAAWR,QAAQC,qBAAcX,wBAY9C0B,cAAcC,qDACLpB,gCAAWoB,MAAMC,uDACjBpB,kDAAsBmB,MAAME,8GAS3BC,kBAAoBC,cAAKC,KAAK,CAAC,CACjCC,WAAY,8BACZC,KAAM,CACFC,SAAUjC,KAAKF,aAEnB,SAIG,CACHuB,OAAQ,GACRa,QAAS,GACTC,GAAI,MALUxB,KAAKC,MAAMgB,cAiB7BlB,iEACOV,KAAKK,mDAQhB+B,qBACW,IAAIC,kBAASrC,MAQpBsC,uFACOtC,KAAKM,4EAchBiC,gBAAgBC,QACRxC,KAAKU,YAAcV,KAAKgB,gBACjB,QAELyB,SAAWjC,QAAQC,qBAAcT,KAAKF,qBAAY0C,UACnDC,gBACM,YAIDC,KAAO/B,KAAKC,MAAM6B,iBACpBC,MAAAA,YAAAA,KAAM1B,YAAahB,KAAKgB,UAGrB0B,KAAKC,MACd,MAAO7B,cACE,GAWf8B,gBAAgBJ,IAAKG,UAEb3C,KAAKU,iBACE,QAELgC,KAAO,CACT1B,SAAUhB,KAAKgB,SACf2B,MAAAA,cAEGnC,QAAQW,qBAAcnB,KAAKF,qBAAY0C,KAAO7B,KAAKO,UAAUwB,kCAc1DG,MAAMC,uBACd,MAAOhC,6BAEQiC,UAAUjC,aAEjBgC,SAAS"} courseeditor/mutations.min.js 0000644 00000016201 15151264135 0012421 0 ustar 00 define("core_courseformat/local/courseeditor/mutations",["exports","core/ajax"],(function(_exports,_ajax){var obj; /** * Default mutation manager * * @module core_courseformat/local/courseeditor/mutations * @class core_courseformat/local/courseeditor/mutations * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};return _exports.default=class{async _callEditWebservice(action,courseId,ids,targetSectionId,targetCmId){const args={action:action,courseid:courseId,ids:ids};targetSectionId&&(args.targetsectionid=targetSectionId),targetCmId&&(args.targetcmid=targetCmId);let ajaxresult=await _ajax.default.call([{methodname:"core_courseformat_update_course",args:args}])[0];return JSON.parse(ajaxresult)}async _sectionBasicAction(stateManager,action,sectionIds,targetSectionId,targetCmId){const course=stateManager.get("course");this.sectionLock(stateManager,sectionIds,!0);const updates=await this._callEditWebservice(action,course.id,sectionIds,targetSectionId,targetCmId);stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)}async _cmBasicAction(stateManager,action,cmIds,targetSectionId,targetCmId){const course=stateManager.get("course");this.cmLock(stateManager,cmIds,!0);const updates=await this._callEditWebservice(action,course.id,cmIds,targetSectionId,targetCmId);stateManager.processUpdates(updates),this.cmLock(stateManager,cmIds,!1)}init(stateManager){stateManager.addUpdateTypes({prepareFields:this._prepareFields})}_prepareFields(stateManager,updateName,fields){return fields.locked=!1,fields}async sectionHide(stateManager,sectionIds){await this._sectionBasicAction(stateManager,"section_hide",sectionIds)}async sectionShow(stateManager,sectionIds){await this._sectionBasicAction(stateManager,"section_show",sectionIds)}async cmShow(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_show",cmIds)}async cmHide(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_hide",cmIds)}async cmStealth(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_stealth",cmIds)}async cmMove(stateManager,cmids,targetSectionId,targetCmId){if(!targetSectionId&&!targetCmId)throw new Error("Mutation cmMove requires targetSectionId or targetCmId");const course=stateManager.get("course");this.cmLock(stateManager,cmids,!0);const updates=await this._callEditWebservice("cm_move",course.id,cmids,targetSectionId,targetCmId);stateManager.processUpdates(updates),this.cmLock(stateManager,cmids,!1)}async sectionMove(stateManager,sectionIds,targetSectionId){if(!targetSectionId)throw new Error("Mutation sectionMove requires targetSectionId");const course=stateManager.get("course");this.sectionLock(stateManager,sectionIds,!0);const updates=await this._callEditWebservice("section_move",course.id,sectionIds,targetSectionId);stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)}async addSection(stateManager,targetSectionId){targetSectionId||(targetSectionId=0);const course=stateManager.get("course"),updates=await this._callEditWebservice("section_add",course.id,[],targetSectionId);stateManager.processUpdates(updates)}async sectionDelete(stateManager,sectionIds){const course=stateManager.get("course"),updates=await this._callEditWebservice("section_delete",course.id,sectionIds);stateManager.processUpdates(updates)}cmDrag(stateManager,cmIds,dragValue){this.setPageItem(stateManager),this._setElementsValue(stateManager,"cm",cmIds,"dragging",dragValue)}sectionDrag(stateManager,sectionIds,dragValue){this.setPageItem(stateManager),this._setElementsValue(stateManager,"section",sectionIds,"dragging",dragValue)}cmCompletion(stateManager,cmIds,complete){const newValue=complete?1:0;this._setElementsValue(stateManager,"cm",cmIds,"completionstate",newValue)}async cmMoveRight(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_moveright",cmIds)}async cmMoveLeft(stateManager,cmIds){await this._cmBasicAction(stateManager,"cm_moveleft",cmIds)}cmLock(stateManager,cmIds,lockValue){this._setElementsValue(stateManager,"cm",cmIds,"locked",lockValue)}sectionLock(stateManager,sectionIds,lockValue){this._setElementsValue(stateManager,"section",sectionIds,"locked",lockValue)}_setElementsValue(stateManager,name,ids,fieldName,newValue){stateManager.setReadOnly(!1),ids.forEach((id=>{const element=stateManager.get(name,id);element&&(element[fieldName]=newValue)})),stateManager.setReadOnly(!0)}setPageItem(stateManager,type,id,isStatic){let newPageItem;if(void 0!==type&&(newPageItem=stateManager.get(type,id),!newPageItem))return;stateManager.setReadOnly(!1);const course=stateManager.get("course");course.pageItem=null,newPageItem&&(course.pageItem={id:id,type:type,sectionId:"section"==type?newPageItem.id:newPageItem.sectionid,isStatic:isStatic}),stateManager.setReadOnly(!0)}unlockAll(stateManager){const state=stateManager.state;stateManager.setReadOnly(!1),state.section.forEach((section=>{section.locked=!1})),state.cm.forEach((cm=>{cm.locked=!1})),stateManager.setReadOnly(!0)}async sectionIndexCollapsed(stateManager,sectionIds,collapsed){const collapsedIds=this._updateStateSectionPreference(stateManager,"indexcollapsed",sectionIds,collapsed);if(!collapsedIds)return;const course=stateManager.get("course");await this._callEditWebservice("section_index_collapsed",course.id,collapsedIds)}async sectionContentCollapsed(stateManager,sectionIds,collapsed){const collapsedIds=this._updateStateSectionPreference(stateManager,"contentcollapsed",sectionIds,collapsed);if(!collapsedIds)return;const course=stateManager.get("course");await this._callEditWebservice("section_content_collapsed",course.id,collapsedIds)}_updateStateSectionPreference(stateManager,preferenceName,sectionIds,preferenceValue){stateManager.setReadOnly(!1);const affectedSections=new Set;if(sectionIds.forEach((sectionId=>{const section=stateManager.get("section",sectionId);if(void 0===section)return null;const newValue=null!=preferenceValue?preferenceValue:section[preferenceName];section[preferenceName]!=newValue&&(section[preferenceName]=newValue,affectedSections.add(section.id))})),stateManager.setReadOnly(!0),0==affectedSections.size)return null;const collapsedSectionIds=[];return stateManager.state.section.forEach((section=>{section[preferenceName]&&collapsedSectionIds.push(section.id)})),collapsedSectionIds}async cmState(stateManager,cmids){this.cmLock(stateManager,cmids,!0);const course=stateManager.get("course"),updates=await this._callEditWebservice("cm_state",course.id,cmids);stateManager.processUpdates(updates),this.cmLock(stateManager,cmids,!1)}async sectionState(stateManager,sectionIds){this.sectionLock(stateManager,sectionIds,!0);const course=stateManager.get("course"),updates=await this._callEditWebservice("section_state",course.id,sectionIds);stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)}async courseState(stateManager){const course=stateManager.get("course"),updates=await this._callEditWebservice("course_state",course.id);stateManager.processUpdates(updates)}},_exports.default})); //# sourceMappingURL=mutations.min.js.map courseeditor/exporter.min.js.map 0000644 00000021345 15151264135 0013027 0 ustar 00 {"version":3,"file":"exporter.min.js","sources":["../../../src/local/courseeditor/exporter.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module to export parts of the state and transform them to be used in templates\n * and as draggable data.\n *\n * @module core_courseformat/local/courseeditor/exporter\n * @class core_courseformat/local/courseeditor/exporter\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n /**\n * Class constructor.\n *\n * @param {CourseEditor} reactive the course editor object\n */\n constructor(reactive) {\n this.reactive = reactive;\n\n // Completions states are defined in lib/completionlib.php. There are 4 different completion\n // state values, however, the course index uses the same state for complete and complete_pass.\n // This is the reason why completed appears twice in the array.\n this.COMPLETIONS = ['incomplete', 'complete', 'complete', 'fail'];\n }\n\n /**\n * Generate the course export data from the state.\n *\n * @param {Object} state the current state.\n * @returns {Object}\n */\n course(state) {\n // Collect section information from the state.\n const data = {\n sections: [],\n editmode: this.reactive.isEditing,\n highlighted: state.course.highlighted ?? '',\n };\n const sectionlist = state.course.sectionlist ?? [];\n sectionlist.forEach(sectionid => {\n const sectioninfo = state.section.get(sectionid) ?? {};\n const section = this.section(state, sectioninfo);\n data.sections.push(section);\n });\n data.hassections = (data.sections.length != 0);\n\n return data;\n }\n\n /**\n * Generate a section export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} sectioninfo the section state data.\n * @returns {Object}\n */\n section(state, sectioninfo) {\n const section = {\n ...sectioninfo,\n highlighted: state.course.highlighted ?? '',\n cms: [],\n };\n const cmlist = sectioninfo.cmlist ?? [];\n cmlist.forEach(cmid => {\n const cminfo = state.cm.get(cmid);\n const cm = this.cm(state, cminfo);\n section.cms.push(cm);\n });\n section.hascms = (section.cms.length != 0);\n\n return section;\n }\n\n /**\n * Generate a cm export data from the state.\n *\n * @param {Object} state the current state.\n * @param {Object} cminfo the course module state data.\n * @returns {Object}\n */\n cm(state, cminfo) {\n const cm = {\n ...cminfo,\n isactive: false,\n };\n return cm;\n }\n\n /**\n * Generate a dragable cm data structure.\n *\n * This method is used by any draggable course module element to generate drop data\n * for its reactive/dragdrop instance.\n *\n * @param {*} state the state object\n * @param {*} cmid the cours emodule id\n * @returns {Object|null}\n */\n cmDraggableData(state, cmid) {\n const cminfo = state.cm.get(cmid);\n if (!cminfo) {\n return null;\n }\n\n // Drop an activity over the next activity is the same as doing anything.\n let nextcmid;\n const section = state.section.get(cminfo.sectionid);\n const currentindex = section?.cmlist.indexOf(cminfo.id);\n if (currentindex !== undefined) {\n nextcmid = section?.cmlist[currentindex + 1];\n }\n\n return {\n type: 'cm',\n id: cminfo.id,\n name: cminfo.name,\n sectionid: cminfo.sectionid,\n nextcmid,\n };\n }\n\n /**\n * Generate a dragable cm data structure.\n *\n * This method is used by any draggable section element to generate drop data\n * for its reactive/dragdrop instance.\n *\n * @param {*} state the state object\n * @param {*} sectionid the cours section id\n * @returns {Object|null}\n */\n sectionDraggableData(state, sectionid) {\n const sectioninfo = state.section.get(sectionid);\n if (!sectioninfo) {\n return null;\n }\n return {\n type: 'section',\n id: sectioninfo.id,\n name: sectioninfo.name,\n number: sectioninfo.number,\n };\n }\n\n /**\n * Generate a compoetion export data from the cm element.\n *\n * @param {Object} state the current state.\n * @param {Object} cminfo the course module state data.\n * @returns {Object}\n */\n cmCompletion(state, cminfo) {\n const data = {\n statename: '',\n state: 'NaN',\n };\n if (cminfo.completionstate !== undefined) {\n data.state = cminfo.completionstate;\n data.hasstate = true;\n const statename = this.COMPLETIONS[cminfo.completionstate] ?? 'NaN';\n data[`is${statename}`] = true;\n }\n return data;\n }\n\n /**\n * Return a sorted list of all sections and cms items in the state.\n *\n * @param {Object} state the current state.\n * @returns {Array} all sections and cms items in the state.\n */\n allItemsArray(state) {\n const items = [];\n const sectionlist = state.course.sectionlist ?? [];\n // Add sections.\n sectionlist.forEach(sectionid => {\n const sectioninfo = state.section.get(sectionid);\n items.push({type: 'section', id: sectioninfo.id, url: sectioninfo.sectionurl});\n // Add cms.\n const cmlist = sectioninfo.cmlist ?? [];\n cmlist.forEach(cmid => {\n const cminfo = state.cm.get(cmid);\n items.push({type: 'cm', id: cminfo.id, url: cminfo.url});\n });\n });\n return items;\n }\n}\n"],"names":["constructor","reactive","COMPLETIONS","course","state","data","sections","editmode","this","isEditing","highlighted","sectionlist","forEach","sectionid","sectioninfo","section","get","push","hassections","length","cms","cmlist","cmid","cminfo","cm","hascms","isactive","cmDraggableData","nextcmid","currentindex","indexOf","id","undefined","type","name","sectionDraggableData","number","cmCompletion","statename","completionstate","hasstate","allItemsArray","items","url","sectionurl"],"mappings":";;;;;;;;;;MA+BIA,YAAYC,eACHA,SAAWA,cAKXC,YAAc,CAAC,aAAc,WAAY,WAAY,QAS9DC,OAAOC,6DAEGC,KAAO,CACTC,SAAU,GACVC,SAAUC,KAAKP,SAASQ,UACxBC,0CAAaN,MAAMD,OAAOO,mEAAe,yCAEzBN,MAAMD,OAAOQ,mEAAe,IACpCC,SAAQC,yCACVC,uCAAcV,MAAMW,QAAQC,IAAIH,4DAAc,GAC9CE,QAAUP,KAAKO,QAAQX,MAAOU,aACpCT,KAAKC,SAASW,KAAKF,YAEvBV,KAAKa,YAAuC,GAAxBb,KAAKC,SAASa,OAE3Bd,KAUXU,QAAQX,MAAOU,kEACLC,QAAU,IACTD,YACHJ,2CAAaN,MAAMD,OAAOO,qEAAe,GACzCU,IAAK,uCAEMN,YAAYO,0DAAU,IAC9BT,SAAQU,aACLC,OAASnB,MAAMoB,GAAGR,IAAIM,MACtBE,GAAKhB,KAAKgB,GAAGpB,MAAOmB,QAC1BR,QAAQK,IAAIH,KAAKO,OAErBT,QAAQU,OAAgC,GAAtBV,QAAQK,IAAID,OAEvBJ,QAUXS,GAAGpB,MAAOmB,cACK,IACJA,OACHG,UAAU,GAelBC,gBAAgBvB,MAAOkB,YACbC,OAASnB,MAAMoB,GAAGR,IAAIM,UACvBC,cACM,SAIPK,eACEb,QAAUX,MAAMW,QAAQC,IAAIO,OAAOV,WACnCgB,aAAed,MAAAA,eAAAA,QAASM,OAAOS,QAAQP,OAAOQ,gBAC/BC,IAAjBH,eACAD,SAAWb,MAAAA,eAAAA,QAASM,OAAOQ,aAAe,IAGvC,CACHI,KAAM,KACNF,GAAIR,OAAOQ,GACXG,KAAMX,OAAOW,KACbrB,UAAWU,OAAOV,UAClBe,SAAAA,UAcRO,qBAAqB/B,MAAOS,iBAClBC,YAAcV,MAAMW,QAAQC,IAAIH,kBACjCC,YAGE,CACHmB,KAAM,UACNF,GAAIjB,YAAYiB,GAChBG,KAAMpB,YAAYoB,KAClBE,OAAQtB,YAAYsB,QANb,KAiBfC,aAAajC,MAAOmB,cACVlB,KAAO,CACTiC,UAAW,GACXlC,MAAO,eAEoB4B,IAA3BT,OAAOgB,gBAA+B,2BACtClC,KAAKD,MAAQmB,OAAOgB,gBACpBlC,KAAKmC,UAAW,QACVF,wCAAY9B,KAAKN,YAAYqB,OAAOgB,wEAAoB,MAC9DlC,iBAAUiC,aAAe,SAEtBjC,KASXoC,cAAcrC,wCACJsC,MAAQ,yCACMtC,MAAMD,OAAOQ,qEAAe,IAEpCC,SAAQC,2CACVC,YAAcV,MAAMW,QAAQC,IAAIH,WACtC6B,MAAMzB,KAAK,CAACgB,KAAM,UAAWF,GAAIjB,YAAYiB,GAAIY,IAAK7B,YAAY8B,2CAEnD9B,YAAYO,4DAAU,IAC9BT,SAAQU,aACLC,OAASnB,MAAMoB,GAAGR,IAAIM,MAC5BoB,MAAMzB,KAAK,CAACgB,KAAM,KAAMF,GAAIR,OAAOQ,GAAIY,IAAKpB,OAAOoB,YAGpDD"} courseeditor/dndsectionitem.min.js.map 0000644 00000013531 15151264135 0014166 0 ustar 00 {"version":3,"file":"dndsectionitem.min.js","sources":["../../../src/local/courseeditor/dndsectionitem.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Course index section title draggable component.\n *\n * This component is used to control specific course section interactions like drag and drop\n * in both course index and course content.\n *\n * @module core_courseformat/local/courseeditor/dndsectionitem\n * @class core_courseformat/local/courseeditor/dndsectionitem\n * @copyright 2021 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent, DragDrop} from 'core/reactive';\n\nexport default class extends BaseComponent {\n\n /**\n * Initial state ready method.\n *\n * @param {number} sectionid the section id\n * @param {Object} state the initial state\n * @param {Element} fullregion the complete section region to mark as dragged\n */\n configDragDrop(sectionid, state, fullregion) {\n\n this.id = sectionid;\n if (this.section === undefined) {\n this.section = state.section.get(this.id);\n }\n if (this.course === undefined) {\n this.course = state.course;\n }\n\n // Prevent topic zero from being draggable.\n if (this.section.number > 0) {\n this.getDraggableData = this._getDraggableData;\n }\n\n this.fullregion = fullregion;\n\n // Drag and drop is only available for components compatible course formats.\n if (this.reactive.isEditing && this.reactive.supportComponents) {\n // Init the dropzone.\n this.dragdrop = new DragDrop(this);\n // Save dropzone classes.\n this.classes = this.dragdrop.getClasses();\n }\n }\n\n /**\n * Remove all subcomponents dependencies.\n */\n destroy() {\n if (this.dragdrop !== undefined) {\n this.dragdrop.unregister();\n }\n }\n\n // Drag and drop methods.\n\n /**\n * The element drop start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragStart(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], true);\n }\n\n /**\n * The element end start hook.\n *\n * @param {Object} dropdata the dropdata\n */\n dragEnd(dropdata) {\n this.reactive.dispatch('sectionDrag', [dropdata.id], false);\n }\n\n /**\n * Get the draggable data of this component.\n *\n * @returns {Object} exported course module drop data\n */\n _getDraggableData() {\n const exporter = this.reactive.getExporter();\n return exporter.sectionDraggableData(this.reactive.state, this.id);\n }\n\n /**\n * Validate if the drop data can be dropped over the component.\n *\n * @param {Object} dropdata the exported drop data.\n * @returns {boolean}\n */\n validateDropData(dropdata) {\n // Course module validation.\n if (dropdata?.type === 'cm') {\n // The first section element is already there so we can ignore it.\n const firstcmid = this.section?.cmlist[0];\n return dropdata.id !== firstcmid;\n }\n return false;\n }\n\n /**\n * Display the component dropzone.\n */\n showDropZone() {\n this.element.classList.add(this.classes.DROPZONE);\n }\n\n /**\n * Hide the component dropzone.\n */\n hideDropZone() {\n this.element.classList.remove(this.classes.DROPZONE);\n }\n\n /**\n * Drop event handler.\n *\n * @param {Object} dropdata the accepted drop data\n */\n drop(dropdata) {\n // Call the move mutation.\n if (dropdata.type == 'cm') {\n this.reactive.dispatch('cmMove', [dropdata.id], this.id, this.section?.cmlist[0]);\n }\n }\n}\n"],"names":["BaseComponent","configDragDrop","sectionid","state","fullregion","id","undefined","this","section","get","course","number","getDraggableData","_getDraggableData","reactive","isEditing","supportComponents","dragdrop","DragDrop","classes","getClasses","destroy","unregister","dragStart","dropdata","dispatch","dragEnd","getExporter","sectionDraggableData","validateDropData","type","firstcmid","_this$section","cmlist","showDropZone","element","classList","add","DROPZONE","hideDropZone","remove","drop","_this$section2"],"mappings":";;;;;;;;;;;;uBA6B6BA,wBASzBC,eAAeC,UAAWC,MAAOC,iBAExBC,GAAKH,eACWI,IAAjBC,KAAKC,eACAA,QAAUL,MAAMK,QAAQC,IAAIF,KAAKF,UAEtBC,IAAhBC,KAAKG,cACAA,OAASP,MAAMO,QAIpBH,KAAKC,QAAQG,OAAS,SACjBC,iBAAmBL,KAAKM,wBAG5BT,WAAaA,WAGdG,KAAKO,SAASC,WAAaR,KAAKO,SAASE,yBAEpCC,SAAW,IAAIC,mBAASX,WAExBY,QAAUZ,KAAKU,SAASG,cAOrCC,eAC0Bf,IAAlBC,KAAKU,eACAA,SAASK,aAWtBC,UAAUC,eACDV,SAASW,SAAS,cAAe,CAACD,SAASnB,KAAK,GAQzDqB,QAAQF,eACCV,SAASW,SAAS,cAAe,CAACD,SAASnB,KAAK,GAQzDQ,2BACqBN,KAAKO,SAASa,cACfC,qBAAqBrB,KAAKO,SAASX,MAAOI,KAAKF,IASnEwB,iBAAiBL,aAEU,QAAnBA,MAAAA,gBAAAA,SAAUM,MAAe,yBAEnBC,gCAAYxB,KAAKC,wCAALwB,cAAcC,OAAO,UAChCT,SAASnB,KAAO0B,iBAEpB,EAMXG,oBACSC,QAAQC,UAAUC,IAAI9B,KAAKY,QAAQmB,UAM5CC,oBACSJ,QAAQC,UAAUI,OAAOjC,KAAKY,QAAQmB,UAQ/CG,KAAKjB,6BAEoB,MAAjBA,SAASM,WACJhB,SAASW,SAAS,SAAU,CAACD,SAASnB,IAAKE,KAAKF,0BAAIE,KAAKC,yCAALkC,eAAcT,OAAO"} courseeditor/dndcmitem.min.js 0000644 00000003166 15151264135 0012350 0 ustar 00 define("core_courseformat/local/courseeditor/dndcmitem",["exports","core/reactive"],(function(_exports,_reactive){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; /** * Course index cm component. * * This component is used to control specific course modules interactions like drag and drop * in both course index and course content. * * @module core_courseformat/local/courseeditor/dndcmitem * @class core_courseformat/local/courseeditor/dndcmitem * @copyright 2021 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class _default extends _reactive.BaseComponent{configDragDrop(cmid){this.id=cmid,this.reactive.isEditing&&this.reactive.supportComponents&&(this.dragdrop=new _reactive.DragDrop(this),this.classes=this.dragdrop.getClasses())}destroy(){void 0!==this.dragdrop&&this.dragdrop.unregister()}dragStart(dropdata){this.reactive.dispatch("cmDrag",[dropdata.id],!0)}dragEnd(dropdata){this.reactive.dispatch("cmDrag",[dropdata.id],!1)}getDraggableData(){return this.reactive.getExporter().cmDraggableData(this.reactive.state,this.id)}validateDropData(dropdata){return"cm"===(null==dropdata?void 0:dropdata.type)}showDropZone(dropdata){dropdata.nextcmid!=this.id&&dropdata.id!=this.id&&this.element.classList.add(this.classes.DROPUP)}hideDropZone(){this.element.classList.remove(this.classes.DROPUP)}drop(dropdata){dropdata.id!=this.id&&dropdata.nextcmid!=this.id&&this.reactive.dispatch("cmMove",[dropdata.id],null,this.id)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=dndcmitem.min.js.map entities/payable.php 0000644 00000003545 15151774324 0010536 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The payable class. * * @package core_payment * @copyright 2020 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_payment\local\entities; /** * The payable class. * * @copyright 2020 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class payable { private $amount; private $currency; private $accountid; public function __construct(float $amount, string $currency, int $accountid) { $this->amount = $amount; $this->currency = $currency; $this->accountid = $accountid; } /** * Get the amount of the payable cost. * * @return float */ public function get_amount(): float { return $this->amount; } /** * Get the currency of the payable cost. * * @return string */ public function get_currency(): string { return $this->currency; } /** * Get the id of the payment account the cost is payable to. * * @return int */ public function get_account_id(): int { return $this->accountid; } } callback/service_provider.php 0000644 00000005231 15151774324 0012375 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the \core_payment\local\local\callback\service_provider interface. * * Plugins should implement this if they use payment subsystem. * * @package core_payment * @copyright 2020 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_payment\local\callback; /** * The service_provider interface for plugins to provide callbacks which are needed by the payment subsystem. * * @copyright 2020 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface service_provider { /** * Callback function that returns the cost of the given item in the specified payment area, * along with the accountid that payments are paid to. * * @param string $paymentarea Payment area * @param int $itemid An identifier that is known to the plugin * @return \core_payment\local\entities\payable */ public static function get_payable(string $paymentarea, int $itemid): \core_payment\local\entities\payable; /** * Callback function that returns the URL of the page the user should be redirected to in the case of a successful payment. * * @param string $paymentarea Payment area * @param int $itemid An identifier that is known to the plugin * @return \moodle_url */ public static function get_success_url(string $paymentarea, int $itemid): \moodle_url; /** * Callback function that delivers what the user paid for to them. * * @param string $paymentarea Payment area * @param int $itemid An identifier that is known to the plugin * @param int $paymentid payment id as inserted into the 'payments' table, if needed for reference * @param int $userid The userid the order is going to deliver to * * @return bool Whether successful or not */ public static function deliver_order(string $paymentarea, int $itemid, int $paymentid, int $userid): bool; } library/autoloader.php 0000644 00000013760 15152217247 0011075 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * H5P autoloader management class. * * @package core_h5p * @copyright 2019 Sara Arjona <sara@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_h5p\local\library; /** * H5P autoloader management class. * * @package core_h5p * @copyright 2019 Sara Arjona <sara@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class autoloader { /** * Returns the list of plugins that can work as H5P library handlers (have class PLUGINNAME\local\library\handler) * @return array with the format: pluginname => class */ public static function get_all_handlers(): array { $handlers = []; $plugins = \core_component::get_plugin_list_with_class('h5plib', 'local\library\handler') + \core_component::get_plugin_list_with_class('h5plib', 'local_library_handler'); // Allow plugins to have the class either with namespace or without (useful for unittest). foreach ($plugins as $pname => $class) { $handlers[$pname] = $class; } return $handlers; } /** * Returns the default H5P library handler class. * * @return string|null H5P library handler class */ public static function get_default_handler(): ?string { $default = null; $handlers = self::get_all_handlers(); if (!empty($handlers)) { // The default handler will be the first value in the list. $default = array_shift($handlers); } return $default; } /** * Returns the default H5P library handler. * * @return string|null H5P library handler */ public static function get_default_handler_library(): ?string { $default = null; $handlers = self::get_all_handlers(); if (!empty($handlers)) { // The default handler will be the first in the list. $keys = array_keys($handlers); $default = array_shift($keys); } return $default; } /** * Returns the current H5P library handler class. * * @return string H5P library handler class * @throws \moodle_exception */ public static function get_handler_classname(): string { global $CFG; $handlers = self::get_all_handlers(); if (!empty($CFG->h5plibraryhandler)) { if (isset($handlers[$CFG->h5plibraryhandler])) { return $handlers[$CFG->h5plibraryhandler]; } } // If no handler has been defined, return the default one. $defaulthandler = self::get_default_handler(); if (empty($defaulthandler)) { // If there is no default handler, throw an exception. throw new \moodle_exception('noh5plibhandlerdefined', 'core_h5p'); } return $defaulthandler; } /** * Get the current version of the H5P core library. * * @return string */ public static function get_h5p_version(): string { return component_class_callback(self::get_handler_classname(), 'get_h5p_version', []); } /** * Get a URL for the current H5P Core Library. * * @param string $filepath The path within the h5p root * @param array $params these params override current params or add new * @return null|moodle_url */ public static function get_h5p_core_library_url(?string $filepath = null, ?array $params = null): ?\moodle_url { return component_class_callback(self::get_handler_classname(), 'get_h5p_core_library_url', [$filepath, $params]); } /** * Get a URL for the current H5P Editor Library. * * @param string $filepath The path within the h5p root. * @param array $params These params override current params or add new. * @return null|\moodle_url The moodle_url instance to a file in the H5P Editor library. */ public static function get_h5p_editor_library_url(?string $filepath = null, ?array $params = null): ?\moodle_url { return component_class_callback(self::get_handler_classname(), 'get_h5p_editor_library_url', [$filepath, $params]); } /** * Get the base path for the current H5P Editor Library. * * @param string $filepath The path within the h5p root. * @return string Path to a file in the H5P Editor library. */ public static function get_h5p_editor_library_base(?string $filepath = null): string { return component_class_callback(self::get_handler_classname(), 'get_h5p_editor_library_base', [$filepath]); } /** * Returns a localized string, if it exists in the h5plib plugin and the value it's different from the English version. * * @param string $identifier The key identifier for the localized string * @param string $language Language to get the localized string. * @return string|null The localized string or null if it doesn't exist in this H5P library plugin. */ public static function get_h5p_string(string $identifier, string $language): ?string { return component_class_callback(self::get_handler_classname(), 'get_h5p_string', [$identifier, $language]); } /** * Register the H5P autoloader. */ public static function register(): void { component_class_callback(self::get_handler_classname(), 'register', []); } } library/handler.php 0000644 00000016155 15152217247 0010354 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Base class for library handlers. * * @package core_h5p * @copyright 2019 Sara Arjona <sara@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_h5p\local\library; defined('MOODLE_INTERNAL') || die(); /** * Base class for library handlers. * * If a new H5P libraries handler plugin has to be created, it has to define class * PLUGINNAME\local\library\handler that extends \core_h5p\local\library\handler. * * @package core_h5p * @copyright 2019 Sara Arjona <sara@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class handler { /** * Get the current version of the H5P core library. * * @return string */ abstract public static function get_h5p_version(): string; /** * Get the base path for the H5P Libraries. * * @return null|string */ public static function get_h5p_library_base(): ?string { $h5pversion = static::get_h5p_version(); return "/h5p/h5plib/v{$h5pversion}/joubel"; } /** * Get the base path for the current H5P Core Library. * * @param string $filepath The path within the H5P root * @return null|string */ public static function get_h5p_core_library_base(?string $filepath = null): ?string { return static::get_h5p_library_base() . "/core/{$filepath}"; } /** * Get the base path for the current H5P Editor Library. * * @param null|string $filepath The path within the H5P root. * @return string Path to a file in the H5P Editor library. */ public static function get_h5p_editor_library_base(?string $filepath = null): string { return static::get_h5p_library_base() . "/editor/{$filepath}"; } /** * Register the H5P autoloader. */ public static function register(): void { // Prepend H5P libraries in order to guarantee they are loaded first. Plugins using same libraries will need to use a // different namespace if they want to use a different version. spl_autoload_register([static::class, 'autoload'], true, true); } /** * SPL Autoloading function for H5P. * * @param string $classname The name of the class to load */ public static function autoload($classname): void { global $CFG; $classes = static::get_class_list(); if (isset($classes[$classname])) { if (file_exists($CFG->dirroot . static::get_h5p_core_library_base($classes[$classname]))) { require_once($CFG->dirroot . static::get_h5p_core_library_base($classes[$classname])); } else { require_once($CFG->dirroot . static::get_h5p_editor_library_base($classes[$classname])); } } } /** * Get a URL for the current H5P Core Library. * * @param string $filepath The path within the h5p root * @param array $params these params override current params or add new * @return null|\moodle_url */ public static function get_h5p_core_library_url(?string $filepath = null, ?array $params = null): ?\moodle_url { return new \moodle_url(static::get_h5p_core_library_base($filepath), $params); } /** * Get a URL for the current H5P Editor Library. * * @param string $filepath The path within the h5p root. * @param array $params These params override current params or add new. * @return null|\moodle_url The moodle_url to a file in the H5P Editor library. */ public static function get_h5p_editor_library_url(?string $filepath = null, ?array $params = null): ?\moodle_url { return new \moodle_url(static::get_h5p_editor_library_base($filepath), $params); } /** * Returns a localized string, if it exists in the h5plib plugin and the value it's different from the English version. * * @param string $identifier The key identifier for the localized string * @param string $language Language to get the localized string. * @return string|null The localized string or null if it doesn't exist in this H5P library plugin. */ public static function get_h5p_string(string $identifier, string $language): ?string { $value = null; $h5pversion = static::get_h5p_version(); $component = 'h5plib_v' . $h5pversion; // Composed code languages, such as 'Spanish, Mexican' are different in H5P and Moodle: // - In H5P, they use '-' to separate language from the country. For instance: es-mx. // - However, in Moodle, they have '_' instead of '-'. For instance: es_mx. $language = str_replace('-', '_', $language); if (get_string_manager()->string_exists($identifier, $component)) { $defaultmoodlelang = 'en'; // In Moodle, all the English strings always will exist because they have to be declared in order to let users // to translate them. That's why, this method will only replace existing key if the value is different from // the English version and the current language is not English. $string = new \lang_string($identifier, $component); if ($language === $defaultmoodlelang || $string->out($language) !== $string->out($defaultmoodlelang)) { $value = $string->out($language); } } return $value; } /** * Return the list of classes with their location within the joubel directory. * * @return array */ protected static function get_class_list(): array { return [ 'Moodle\H5PCore' => 'h5p.classes.php', 'Moodle\H5PFrameworkInterface' => 'h5p.classes.php', 'Moodle\H5PContentValidator' => 'h5p.classes.php', 'Moodle\H5PValidator' => 'h5p.classes.php', 'Moodle\H5PStorage' => 'h5p.classes.php', 'Moodle\H5PDevelopment' => 'h5p-development.class.php', 'Moodle\H5PFileStorage' => 'h5p-file-storage.interface.php', 'Moodle\H5PDefaultStorage' => 'h5p-default-storage.class.php', 'Moodle\H5PMetadata' => 'h5p-metadata.class.php', 'Moodle\H5peditor' => 'h5peditor.class.php', 'Moodle\H5peditorStorage' => 'h5peditor-storage.interface.php', 'Moodle\H5PEditorAjaxInterface' => 'h5peditor-ajax.interface.php', 'Moodle\H5PEditorAjax' => 'h5peditor-ajax.class.php', 'Moodle\H5peditorFile' => 'h5peditor-file.class.php', ]; } } entities/group.php 0000644 00000020741 15152250155 0010242 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_group\reportbuilder\local\entities; use context_course; use context_helper; use html_writer; use lang_string; use moodle_url; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{date, text}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Group entity * * @package core_group * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'context' => 'gctx', 'groups' => 'g', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('group', 'core_group'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $contextalias = $this->get_table_alias('context'); $groupsalias = $this->get_table_alias('groups'); // Name column. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsalias}.name, {$groupsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->set_callback(static function($name, stdClass $group): string { if ($name === null) { return ''; } context_helper::preload_from_record($group); $context = context_course::instance($group->courseid); return format_string($group->name, true, ['context' => $context]); }); // ID number column. $columns[] = (new column( 'idnumber', new lang_string('idnumber'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsalias}.idnumber") ->set_is_sortable(true); // Description column. $descriptionfieldsql = "{$groupsalias}.description"; if ($DB->get_dbfamily() === 'oracle') { $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); } $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_field($descriptionfieldsql, 'description') ->add_fields("{$groupsalias}.descriptionformat, {$groupsalias}.id, {$groupsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(false) ->set_callback(static function(?string $description, stdClass $group): string { global $CFG; if ($description === null) { return ''; } require_once("{$CFG->libdir}/filelib.php"); context_helper::preload_from_record($group); $context = context_course::instance($group->courseid); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'group', 'description', $group->id); return format_text($description, $group->descriptionformat, ['context' => $context]); }); // Enrolment key column. $columns[] = (new column( 'enrolmentkey', new lang_string('enrolmentkey', 'core_group'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsalias}.enrolmentkey") ->set_is_sortable(true); // Picture column. $columns[] = (new column( 'picture', new lang_string('picture'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_fields("{$groupsalias}.picture, {$groupsalias}.id, {$contextalias}.id AS contextid") ->set_is_sortable(false) // It doesn't make sense to offer integer aggregation methods for this column. ->set_disabled_aggregation(['avg', 'max', 'min', 'sum']) ->set_callback(static function ($picture, stdClass $group): string { if (empty($group->picture)) { return ''; } $pictureurl = moodle_url::make_pluginfile_url($group->contextid, 'group', 'icon', $group->id, '/', 'f2'); $pictureurl->param('rev', $group->picture); return html_writer::img($pictureurl, ''); }); // Time created column. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupsalias}.timecreated") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Time modified column. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupsalias}.timemodified") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $groupsalias = $this->get_table_alias('groups'); // Name filter. $filters[] = (new filter( text::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$groupsalias}.name" )) ->add_joins($this->get_joins()); // ID number filter. $filters[] = (new filter( text::class, 'idnumber', new lang_string('idnumber'), $this->get_entity_name(), "{$groupsalias}.idnumber" )) ->add_joins($this->get_joins()); // Time created filter. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$groupsalias}.timecreated" )) ->add_joins($this->get_joins()); return $filters; } } entities/grouping.php 0000644 00000016276 15152250155 0010750 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_group\reportbuilder\local\entities; use context_course; use context_helper; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{date, text}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Group member entity * * @package core_group * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class grouping extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'context' => 'ggctx', 'groupings' => 'gg', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('grouping', 'core_group'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $contextalias = $this->get_table_alias('context'); $groupingsalias = $this->get_table_alias('groupings'); // Name column. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupingsalias}.name, {$groupingsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->set_callback(static function($name, stdClass $grouping): string { if ($name === null) { return ''; } context_helper::preload_from_record($grouping); $context = context_course::instance($grouping->courseid); return format_string($grouping->name, true, ['context' => $context]); }); // ID number column. $columns[] = (new column( 'idnumber', new lang_string('idnumber'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupingsalias}.idnumber") ->set_is_sortable(true); // Description column. $descriptionfieldsql = "{$groupingsalias}.description"; if ($DB->get_dbfamily() === 'oracle') { $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); } $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_field($descriptionfieldsql, 'description') ->add_fields("{$groupingsalias}.descriptionformat, {$groupingsalias}.id, {$groupingsalias}.courseid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(false) ->set_callback(static function(?string $description, stdClass $grouping): string { global $CFG; if ($description === null) { return ''; } require_once("{$CFG->libdir}/filelib.php"); context_helper::preload_from_record($grouping); $context = context_course::instance($grouping->courseid); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'grouping', 'description', $grouping->id); return format_text($description, $grouping->descriptionformat, ['context' => $context]); }); // Time created column. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupingsalias}.timecreated") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Time modified column. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupingsalias}.timemodified") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $groupingsalias = $this->get_table_alias('groupings'); // Name filter. $filters[] = (new filter( text::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$groupingsalias}.name" )) ->add_joins($this->get_joins()); // ID number filter. $filters[] = (new filter( text::class, 'idnumber', new lang_string('idnumber'), $this->get_entity_name(), "{$groupingsalias}.idnumber" )) ->add_joins($this->get_joins()); // Time created filter. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'core_reportbuilder'), $this->get_entity_name(), "{$groupingsalias}.timecreated" )) ->add_joins($this->get_joins()); return $filters; } } entities/group_member.php 0000644 00000007404 15152250155 0011572 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_group\reportbuilder\local\entities; use core_reportbuilder\local\filters\date; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Group member entity * * @package core_group * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class group_member extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'groups_members' => 'gm', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('groupmember', 'core_group'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $groupsmembersalias = $this->get_table_alias('groups_members'); // Time added column. $columns[] = (new column( 'timeadded', new lang_string('timeadded', 'core_reportbuilder'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$groupsmembersalias}.timeadded") ->set_is_sortable(true) ->set_callback([format::class, 'userdate']); // Component column. $columns[] = (new column( 'component', new lang_string('plugin', 'core'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$groupsmembersalias}.component") ->set_is_sortable(true); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $groupsmembersalias = $this->get_table_alias('groups_members'); // Time added filter. $filters[] = (new filter( date::class, 'timeadded', new lang_string('timeadded', 'core_reportbuilder'), $this->get_entity_name(), "{$groupsmembersalias}.timeadded" )) ->add_joins($this->get_joins()); return $filters; } } activitychooser/selectors.js 0000644 00000007611 15152262713 0012335 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Define all of the selectors we will be using on the grading interface. * * @module core_course/local/chooser/selectors * @copyright 2019 Mathew May <mathew.solutions> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * A small helper function to build queryable data selectors. * @method getDataSelector * @param {String} name * @param {String} value * @return {string} */ const getDataSelector = (name, value) => { return `[data-${name}="${value}"]`; }; export default { regions: { chooser: getDataSelector('region', 'chooser-container'), getSectionChooserOptions: containerid => `${containerid} ${getDataSelector('region', 'chooser-options-container')}`, chooserOption: { container: getDataSelector('region', 'chooser-option-container'), actions: getDataSelector('region', 'chooser-option-actions-container'), info: getDataSelector('region', 'chooser-option-info-container'), }, chooserSummary: { container: getDataSelector('region', 'chooser-option-summary-container'), content: getDataSelector('region', 'chooser-option-summary-content-container'), header: getDataSelector('region', 'summary-header'), actions: getDataSelector('region', 'chooser-option-summary-actions-container'), }, carousel: getDataSelector('region', 'carousel'), help: getDataSelector('region', 'help'), modules: getDataSelector('region', 'modules'), favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'), defaultTabNav: getDataSelector('region', 'default-tab-nav'), activityTabNav: getDataSelector('region', 'activity-tab-nav'), favouriteTab: getDataSelector('region', 'favourites'), recommendedTab: getDataSelector('region', 'recommended'), defaultTab: getDataSelector('region', 'default'), activityTab: getDataSelector('region', 'activity'), resourceTab: getDataSelector('region', 'resources'), getModuleSelector: modname => `[role="menuitem"][data-modname="${modname}"]`, searchResults: getDataSelector('region', 'search-results-container'), searchResultItems: getDataSelector('region', 'search-result-items-container'), }, actions: { optionActions: { showSummary: getDataSelector('action', 'show-option-summary'), manageFavourite: getDataSelector('action', 'manage-module-favourite'), }, addChooser: getDataSelector('action', 'add-chooser-option'), closeOption: getDataSelector('action', 'close-chooser-option-summary'), hide: getDataSelector('action', 'hide'), search: getDataSelector('action', 'search'), clearSearch: getDataSelector('action', 'clearsearch'), }, render: { favourites: getDataSelector('render', 'favourites-area'), }, elements: { section: '.section', sectionmodchooser: 'button.section-modchooser-link', sitemenu: '.block_site_main_menu', sitetopic: 'div.sitetopic', tab: 'a[data-toggle="tab"]', activetab: 'a[data-toggle="tab"][aria-selected="true"]', visibletabs: 'a[data-toggle="tab"]:not(.d-none)' }, }; activitychooser/repository.js 0000644 00000006227 15152262713 0012553 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * * @module core_course/repository * @copyright 2019 Mathew May <mathew.solutions> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import ajax from 'core/ajax'; /** * Fetch all the information on modules we'll need in the activity chooser. * * @method activityModules * @param {Number} courseid What course to fetch the modules for * @return {object} jQuery promise */ export const activityModules = (courseid) => { const request = { methodname: 'core_course_get_course_content_items', args: { courseid: courseid, }, }; return ajax.call([request])[0]; }; /** * Given a module name, module ID & the current course we want to specify that the module * is a users' favourite. * * @method favouriteModule * @param {String} modName Frankenstyle name of the component to add favourite * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names * @return {object} jQuery promise */ export const favouriteModule = (modName, modID) => { const request = { methodname: 'core_course_add_content_item_to_user_favourites', args: { componentname: modName, contentitemid: modID, }, }; return ajax.call([request])[0]; }; /** * Given a module name, module ID & the current course we want to specify that the module * is no longer a users' favourite. * * @method unfavouriteModule * @param {String} modName Frankenstyle name of the component to add favourite * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names * @return {object} jQuery promise */ export const unfavouriteModule = (modName, modID) => { const request = { methodname: 'core_course_remove_content_item_from_user_favourites', args: { componentname: modName, contentitemid: modID, }, }; return ajax.call([request])[0]; }; /** * Fetch all the information on modules we'll need in the activity chooser. * * @method fetchFooterData * @param {Number} courseid What course to fetch the data for * @param {Number} sectionid What section to fetch the data for * @return {object} jQuery promise */ export const fetchFooterData = (courseid, sectionid) => { const request = { methodname: 'core_course_get_activity_chooser_footer', args: { courseid: courseid, sectionid: sectionid, }, }; return ajax.call([request])[0]; }; activitychooser/dialogue.js 0000644 00000055064 15152262713 0012130 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A type of dialogue used as for choosing options. * * @module core_course/local/chooser/dialogue * @copyright 2019 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import * as ModalEvents from 'core/modal_events'; import selectors from 'core_course/local/activitychooser/selectors'; import * as Templates from 'core/templates'; import {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes'; import {addIconToContainer} from 'core/loadingicon'; import * as Repository from 'core_course/local/activitychooser/repository'; import Notification from 'core/notification'; import {debounce} from 'core/utils'; const getPlugin = pluginName => import(pluginName); /** * Given an event from the main module 'page' navigate to it's help section via a carousel. * * @method showModuleHelp * @param {jQuery} carousel Our initialized carousel to manipulate * @param {Object} moduleData Data of the module to carousel to * @param {jQuery} modal We need to figure out if the current modal has a footer. */ const showModuleHelp = (carousel, moduleData, modal = null) => { // If we have a real footer then we need to change temporarily. if (modal !== null && moduleData.showFooter === true) { modal.setFooter(Templates.render('core_course/local/activitychooser/footer_partial', moduleData)); } const help = carousel.find(selectors.regions.help)[0]; help.innerHTML = ''; help.classList.add('m-auto'); // Add a spinner. const spinnerPromise = addIconToContainer(help); // Used later... let transitionPromiseResolver = null; const transitionPromise = new Promise(resolve => { transitionPromiseResolver = resolve; }); // Build up the html & js ready to place into the help section. const contentPromise = Templates.renderForPromise('core_course/local/activitychooser/help', moduleData); // Wait for the content to be ready, and for the transition to be complet. Promise.all([contentPromise, spinnerPromise, transitionPromise]) .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js)) .then(() => { help.querySelector(selectors.regions.chooserSummary.header).focus(); return help; }) .catch(Notification.exception); // Move to the next slide, and resolve the transition promise when it's done. carousel.one('slid.bs.carousel', () => { transitionPromiseResolver(); }); // Trigger the transition between 'pages'. carousel.carousel('next'); }; /** * Given a user wants to change the favourite state of a module we either add or remove the status. * We also propergate this change across our map of modals. * * @method manageFavouriteState * @param {HTMLElement} modalBody The DOM node of the modal to manipulate * @param {HTMLElement} caller * @param {Function} partialFavourite Partially applied function we need to manage favourite status */ const manageFavouriteState = async(modalBody, caller, partialFavourite) => { const isFavourite = caller.dataset.favourited; const id = caller.dataset.id; const name = caller.dataset.name; const internal = caller.dataset.internal; // Switch on fave or not. if (isFavourite === 'true') { await Repository.unfavouriteModule(name, id); partialFavourite(internal, false, modalBody); } else { await Repository.favouriteModule(name, id); partialFavourite(internal, true, modalBody); } }; /** * Register chooser related event listeners. * * @method registerListenerEvents * @param {Promise} modal Our modal that we are working with * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object} * @param {Function} partialFavourite Partially applied function we need to manage favourite status * @param {Object} footerData Our base footer object. */ const registerListenerEvents = (modal, mappedModules, partialFavourite, footerData) => { const bodyClickListener = async(e) => { if (e.target.closest(selectors.actions.optionActions.showSummary)) { const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel)); const module = e.target.closest(selectors.regions.chooserOption.container); const moduleName = module.dataset.modname; const moduleData = mappedModules.get(moduleName); // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer. moduleData.showFooter = modal.hasFooterContent(); showModuleHelp(carousel, moduleData, modal); } if (e.target.closest(selectors.actions.optionActions.manageFavourite)) { const caller = e.target.closest(selectors.actions.optionActions.manageFavourite); await manageFavouriteState(modal.getBody()[0], caller, partialFavourite); const activeSectionId = modal.getBody()[0].querySelector(selectors.elements.activetab).getAttribute("href"); const sectionChooserOptions = modal.getBody()[0] .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId)); const firstChooserOption = sectionChooserOptions .querySelector(selectors.regions.chooserOption.container); toggleFocusableChooserOption(firstChooserOption, true); initChooserOptionsKeyboardNavigation(modal.getBody()[0], mappedModules, sectionChooserOptions, modal); } // From the help screen go back to the module overview. if (e.target.matches(selectors.actions.closeOption)) { const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel)); // Trigger the transition between 'pages'. carousel.carousel('prev'); carousel.on('slid.bs.carousel', () => { const allModules = modal.getBody()[0].querySelector(selectors.regions.modules); const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname)); caller.focus(); }); } // The "clear search" button is triggered. if (e.target.closest(selectors.actions.clearSearch)) { // Clear the entered search query in the search bar and hide the search results container. const searchInput = modal.getBody()[0].querySelector(selectors.actions.search); searchInput.value = ""; searchInput.focus(); toggleSearchResultsView(modal, mappedModules, searchInput.value); } }; // We essentially have two types of footer. // A fake one that is handled within the template for chooser_help and then all of the stuff for // modal.footer. We need to ensure we know exactly what type of footer we are using so we know what we // need to manage. The below code handles a real footer going to a mnet carousel item. const footerClickListener = async(e) => { if (footerData.footer === true) { const footerjs = await getPlugin(footerData.customfooterjs); await footerjs.footerClickListener(e, footerData, modal); } }; modal.getBodyPromise() // The return value of getBodyPromise is a jquery object containing the body NodeElement. .then(body => body[0]) // Set up the carousel. .then(body => { $(body.querySelector(selectors.regions.carousel)) .carousel({ interval: false, pause: true, keyboard: false }); return body; }) // Add the listener for clicks on the body. .then(body => { body.addEventListener('click', bodyClickListener); return body; }) // Add a listener for an input change in the activity chooser's search bar. .then(body => { const searchInput = body.querySelector(selectors.actions.search); // The search input is triggered. searchInput.addEventListener('input', debounce(() => { // Display the search results. toggleSearchResultsView(modal, mappedModules, searchInput.value); }, 300)); return body; }) // Register event listeners related to the keyboard navigation controls. .then(body => { // Get the active chooser options section. const activeSectionId = body.querySelector(selectors.elements.activetab).getAttribute("href"); const sectionChooserOptions = body.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId)); const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container); toggleFocusableChooserOption(firstChooserOption, true); initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal); return body; }) .catch(); modal.getFooterPromise() // The return value of getBodyPromise is a jquery object containing the body NodeElement. .then(footer => footer[0]) // Add the listener for clicks on the footer. .then(footer => { footer.addEventListener('click', footerClickListener); return footer; }) .catch(); }; /** * Initialise the keyboard navigation controls for the chooser options. * * @method initChooserOptionsKeyboardNavigation * @param {HTMLElement} body Our modal that we are working with * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object} * @param {HTMLElement} chooserOptionsContainer The section that contains the chooser items * @param {Object} modal Our created modal for the section */ const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer, modal = null) => { const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container); Array.from(chooserOptions).forEach((element) => { return element.addEventListener('keydown', (e) => { // Check for enter/ space triggers for showing the help. if (e.keyCode === enter || e.keyCode === space) { if (e.target.matches(selectors.actions.optionActions.showSummary)) { e.preventDefault(); const module = e.target.closest(selectors.regions.chooserOption.container); const moduleName = module.dataset.modname; const moduleData = mappedModules.get(moduleName); const carousel = $(body.querySelector(selectors.regions.carousel)); carousel.carousel({ interval: false, pause: true, keyboard: false }); // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer. moduleData.showFooter = modal.hasFooterContent(); showModuleHelp(carousel, moduleData, modal); } } // Next. if (e.keyCode === arrowRight) { e.preventDefault(); const currentOption = e.target.closest(selectors.regions.chooserOption.container); const nextOption = currentOption.nextElementSibling; const firstOption = chooserOptionsContainer.firstElementChild; const toFocusOption = clickErrorHandler(nextOption, firstOption); focusChooserOption(toFocusOption, currentOption); } // Previous. if (e.keyCode === arrowLeft) { e.preventDefault(); const currentOption = e.target.closest(selectors.regions.chooserOption.container); const previousOption = currentOption.previousElementSibling; const lastOption = chooserOptionsContainer.lastElementChild; const toFocusOption = clickErrorHandler(previousOption, lastOption); focusChooserOption(toFocusOption, currentOption); } if (e.keyCode === home) { e.preventDefault(); const currentOption = e.target.closest(selectors.regions.chooserOption.container); const firstOption = chooserOptionsContainer.firstElementChild; focusChooserOption(firstOption, currentOption); } if (e.keyCode === end) { e.preventDefault(); const currentOption = e.target.closest(selectors.regions.chooserOption.container); const lastOption = chooserOptionsContainer.lastElementChild; focusChooserOption(lastOption, currentOption); } }); }); }; /** * Focus on a chooser option element and remove the previous chooser element from the focus order * * @method focusChooserOption * @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus * @param {HTMLElement|null} previousChooserOption The previous focused option element */ const focusChooserOption = (currentChooserOption, previousChooserOption = null) => { if (previousChooserOption !== null) { toggleFocusableChooserOption(previousChooserOption, false); } toggleFocusableChooserOption(currentChooserOption, true); currentChooserOption.focus(); }; /** * Add or remove a chooser option from the focus order. * * @method toggleFocusableChooserOption * @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order * @param {Boolean} isFocusable Whether the chooser element is focusable or not */ const toggleFocusableChooserOption = (chooserOption, isFocusable) => { const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser); const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary); const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite); if (isFocusable) { // Set tabindex to 0 to add current chooser option element to the focus order. chooserOption.tabIndex = 0; chooserOptionLink.tabIndex = 0; chooserOptionHelp.tabIndex = 0; chooserOptionFavourite.tabIndex = 0; } else { // Set tabindex to -1 to remove the previous chooser option element from the focus order. chooserOption.tabIndex = -1; chooserOptionLink.tabIndex = -1; chooserOptionHelp.tabIndex = -1; chooserOptionFavourite.tabIndex = -1; } }; /** * Small error handling function to make sure the navigated to object exists * * @method clickErrorHandler * @param {HTMLElement} item What we want to check exists * @param {HTMLElement} fallback If we dont match anything fallback the focus * @return {HTMLElement} */ const clickErrorHandler = (item, fallback) => { if (item !== null) { return item; } else { return fallback; } }; /** * Render the search results in a defined container * * @method renderSearchResults * @param {HTMLElement} searchResultsContainer The container where the data should be rendered * @param {Object} searchResultsData Data containing the module items that satisfy the search criteria */ const renderSearchResults = async(searchResultsContainer, searchResultsData) => { const templateData = { 'searchresultsnumber': searchResultsData.length, 'searchresults': searchResultsData }; // Build up the html & js ready to place into the help section. const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/search_results', templateData); await Templates.replaceNodeContents(searchResultsContainer, html, js); }; /** * Toggle (display/hide) the search results depending on the value of the search query * * @method toggleSearchResultsView * @param {Object} modal Our created modal for the section * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object} * @param {String} searchQuery The search query */ const toggleSearchResultsView = async(modal, mappedModules, searchQuery) => { const modalBody = modal.getBody()[0]; const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults); const chooserContainer = modalBody.querySelector(selectors.regions.chooser); const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch); if (searchQuery.length > 0) { // Search query is present. const searchResultsData = searchModules(mappedModules, searchQuery); await renderSearchResults(searchResultsContainer, searchResultsData); const searchResultItemsContainer = searchResultsContainer.querySelector(selectors.regions.searchResultItems); const firstSearchResultItem = searchResultItemsContainer.querySelector(selectors.regions.chooserOption.container); if (firstSearchResultItem) { // Set the first result item to be focusable. toggleFocusableChooserOption(firstSearchResultItem, true); // Register keyboard events on the created search result items. initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal); } // Display the "clear" search button in the activity chooser search bar. clearSearchButton.classList.remove('d-none'); // Hide the default chooser options container. chooserContainer.setAttribute('hidden', 'hidden'); // Display the search results container. searchResultsContainer.removeAttribute('hidden'); } else { // Search query is not present. // Hide the "clear" search button in the activity chooser search bar. clearSearchButton.classList.add('d-none'); // Hide the search results container. searchResultsContainer.setAttribute('hidden', 'hidden'); // Display the default chooser options container. chooserContainer.removeAttribute('hidden'); } }; /** * Return the list of modules which have a name or description that matches the given search term. * * @method searchModules * @param {Array} modules List of available modules * @param {String} searchTerm The search term to match * @return {Array} */ const searchModules = (modules, searchTerm) => { if (searchTerm === '') { return modules; } searchTerm = searchTerm.toLowerCase(); const searchResults = []; modules.forEach((activity) => { const activityName = activity.title.toLowerCase(); const activityDesc = activity.help.toLowerCase(); if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) { searchResults.push(activity); } }); return searchResults; }; /** * Set up our tabindex information across the chooser. * * @method setupKeyboardAccessibility * @param {Promise} modal Our created modal for the section * @param {Map} mappedModules A map of all of the built module information */ const setupKeyboardAccessibility = (modal, mappedModules) => { modal.getModal()[0].tabIndex = -1; modal.getBodyPromise().then(body => { $(selectors.elements.tab).on('shown.bs.tab', (e) => { const activeSectionId = e.target.getAttribute("href"); const activeSectionChooserOptions = body[0] .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId)); const firstChooserOption = activeSectionChooserOptions .querySelector(selectors.regions.chooserOption.container); const prevActiveSectionId = e.relatedTarget.getAttribute("href"); const prevActiveSectionChooserOptions = body[0] .querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId)); // Disable the focus of every chooser option in the previous active section. disableFocusAllChooserOptions(prevActiveSectionChooserOptions); // Enable the focus of the first chooser option in the current active section. toggleFocusableChooserOption(firstChooserOption, true); initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions, modal); }); return; }).catch(Notification.exception); }; /** * Disable the focus of all chooser options in a specific container (section). * * @method disableFocusAllChooserOptions * @param {HTMLElement} sectionChooserOptions The section that contains the chooser items */ const disableFocusAllChooserOptions = (sectionChooserOptions) => { const allChooserOptions = sectionChooserOptions.querySelectorAll(selectors.regions.chooserOption.container); allChooserOptions.forEach((chooserOption) => { toggleFocusableChooserOption(chooserOption, false); }); }; /** * Display the module chooser. * * @method displayChooser * @param {Promise} modalPromise Our created modal for the section * @param {Array} sectionModules An array of all of the built module information * @param {Function} partialFavourite Partially applied function we need to manage favourite status * @param {Object} footerData Our base footer object. */ export const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => { // Make a map so we can quickly fetch a specific module's object for either rendering or searching. const mappedModules = new Map(); sectionModules.forEach((module) => { mappedModules.set(module.componentname + '_' + module.link, module); }); // Register event listeners. modalPromise.then(modal => { registerListenerEvents(modal, mappedModules, partialFavourite, footerData); // We want to focus on the first chooser option element as soon as the modal is opened. setupKeyboardAccessibility(modal, mappedModules); // We want to focus on the action select when the dialog is closed. modal.getRoot().on(ModalEvents.hidden, () => { modal.destroy(); }); return modal; }).catch(); }; entities/base_test.php 0000644 00000024132 15152276235 0011064 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Unit tests for base entity * * @package core_reportbuilder * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace core_reportbuilder\local\entities; use advanced_testcase; use coding_exception; use lang_string; use core_reportbuilder\local\filters\text; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; defined('MOODLE_INTERNAL') || die(); /** * Unit tests for base entity * * @package core_reportbuilder * @covers \core_reportbuilder\local\entities\base * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class base_test extends advanced_testcase { /** * Test entity table alias */ public function test_get_table_alias(): void { $entity = new base_test_entity(); $this->assertEquals('m', $entity->get_table_alias('mytable')); } /** * Test for invalid get table alias */ public function test_get_table_alias_invalid(): void { $entity = new base_test_entity(); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Coding error detected, it must be fixed by a programmer: ' . 'Invalid table name (nonexistingalias)'); $entity->get_table_alias('nonexistingalias'); } /** * Test setting table alias */ public function test_set_table_alias(): void { $entity = new base_test_entity(); $entity->set_table_alias('mytable', 'newalias'); $this->assertEquals('newalias', $entity->get_table_alias('mytable')); } /** * Test invalid entity set table alias */ public function test_set_table_alias_invalid(): void { $entity = new base_test_entity(); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Coding error detected, it must be fixed by a programmer: Invalid table name (nonexistent)'); $entity->set_table_alias('nonexistent', 'newalias'); } /** * Test setting multiple table aliases */ public function test_set_table_aliases(): void { $entity = new base_test_entity(); $entity->set_table_aliases([ 'mytable' => 'newalias', 'myothertable' => 'newalias2', ]); $this->assertEquals('newalias', $entity->get_table_alias('mytable')); $this->assertEquals('newalias2', $entity->get_table_alias('myothertable')); } /** * Test setting multiple table aliases, containing an invalid table */ public function test_set_table_aliases_invalid(): void { $entity = new base_test_entity(); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Coding error detected, it must be fixed by a programmer: Invalid table name (nonexistent)'); $entity->set_table_aliases([ 'mytable' => 'newalias', 'nonexistent' => 'newalias2', ]); } /** * Test entity name */ public function test_set_entity_name(): void { $entity = new base_test_entity(); $this->assertEquals('base_test_entity', $entity->get_entity_name()); $entity->set_entity_name('newentityname'); $this->assertEquals('newentityname', $entity->get_entity_name()); } /** * Test invalid entity name */ public function test_set_entity_name_invalid(): void { $entity = new base_test_entity(); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Entity name must be comprised of alphanumeric character, underscore or dash'); $entity->set_entity_name(''); } /** * Test entity title */ public function test_set_entity_title(): void { $entity = new base_test_entity(); $this->assertEquals(new lang_string('yes'), $entity->get_entity_title()); $newtitle = new lang_string('fullname'); $entity->set_entity_title($newtitle); $this->assertEquals($newtitle, $entity->get_entity_title()); } /** * Test adding single join */ public function test_add_join(): void { $entity = new base_test_entity(); $tablejoin = "JOIN {course} c2 ON c2.id = c1.id"; $entity->add_join($tablejoin); $this->assertEquals([$tablejoin], $entity->get_joins()); } /** * Test adding multiple joins */ public function test_add_joins(): void { $entity = new base_test_entity(); $tablejoins = [ "JOIN {course} c2 ON c2.id = c1.id", "JOIN {course} c3 ON c3.id = c1.id", ]; $entity->add_joins($tablejoins); $this->assertEquals($tablejoins, $entity->get_joins()); } /** * Test adding duplicate joins */ public function test_add_duplicate_joins(): void { $entity = new base_test_entity(); $tablejoins = [ "JOIN {course} c2 ON c2.id = c1.id", "JOIN {course} c3 ON c3.id = c1.id", ]; $entity ->add_joins($tablejoins) ->add_joins($tablejoins); $this->assertEquals($tablejoins, $entity->get_joins()); } /** * Test getting column */ public function test_get_column(): void { $entity = (new base_test_entity())->initialise(); $column = $entity->get_column('test'); $this->assertEquals('base_test_entity:test', $column->get_unique_identifier()); } /** * Test for invalid get column */ public function test_get_column_invalid(): void { $entity = (new base_test_entity())->initialise(); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Coding error detected, it must be fixed by a programmer: ' . 'Invalid column name (nonexistingcolumn)'); $entity->get_column('nonexistingcolumn'); } /** * Test getting columns */ public function test_get_columns(): void { $entity = (new base_test_entity())->initialise(); $columns = $entity->get_columns(); $this->assertCount(1, $columns); $this->assertContainsOnlyInstancesOf(column::class, $columns); } /** * Test getting filter */ public function test_get_filter(): void { $entity = (new base_test_entity())->initialise(); $filter = $entity->get_filter('test'); $this->assertEquals('base_test_entity:test', $filter->get_unique_identifier()); } /** * Test for invalid get filter */ public function test_get_filter_invalid(): void { $entity = (new base_test_entity())->initialise(); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Coding error detected, it must be fixed by a programmer: ' . 'Invalid filter name (nonexistingfilter)'); $entity->get_filter('nonexistingfilter'); } /** * Test getting filters */ public function test_get_filters(): void { $entity = (new base_test_entity())->initialise(); $filters = $entity->get_filters(); $this->assertCount(1, $filters); $this->assertContainsOnlyInstancesOf(filter::class, $filters); } /** * Test getting condition */ public function test_get_condition(): void { $entity = (new base_test_entity())->initialise(); $condition = $entity->get_condition('test'); $this->assertEquals('base_test_entity:test', $condition->get_unique_identifier()); } /** * Test for invalid get condition */ public function test_get_condition_invalid(): void { $entity = (new base_test_entity())->initialise(); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Coding error detected, it must be fixed by a programmer: ' . 'Invalid condition name (nonexistingcondition)'); $entity->get_condition('nonexistingcondition'); } /** * Test getting conditions */ public function test_get_conditions(): void { $entity = (new base_test_entity())->initialise(); $conditions = $entity->get_conditions(); $this->assertCount(1, $conditions); $this->assertContainsOnlyInstancesOf(filter::class, $conditions); } } /** * Simple implementation of the base entity */ class base_test_entity extends base { /** * Table aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'mytable' => 'm', 'myothertable' => 'o', ]; } /** * Entity title * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('yes'); } /** * Initialise entity * * @return base */ public function initialise(): base { $column = (new column( 'test', new lang_string('no'), $this->get_entity_name() )) ->add_field('no'); $filter = (new filter( text::class, 'test', new lang_string('no'), $this->get_entity_name(), )) ->set_field_sql('no'); return $this ->add_column($column) ->add_filter($filter) ->add_condition($filter); } } entities/user_test.php 0000644 00000007113 15152276235 0011130 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\entities; use advanced_testcase; /** * Unit tests for user entity * * @package core_reportbuilder * @covers \core_reportbuilder\local\entities\user * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_test extends advanced_testcase { /** * Test getting user identity column */ public function test_get_identity_column(): void { $this->resetAfterTest(); $this->getDataGenerator()->create_custom_profile_field(['datatype' => 'text', 'name' => 'Hi', 'shortname' => 'hi']); $user = new user(); $user->initialise(); $columnusername = $user->get_identity_column('username'); $this->assertEquals('user:username', $columnusername->get_unique_identifier()); $columnprofilefield = $user->get_identity_column('profile_field_hi'); $this->assertEquals('user:profilefield_hi', $columnprofilefield->get_unique_identifier()); } /** * Test getting user identity filter */ public function test_get_identity_filter(): void { $this->resetAfterTest(); $this->getDataGenerator()->create_custom_profile_field(['datatype' => 'text', 'name' => 'Hi', 'shortname' => 'hi']); $user = new user(); $user->initialise(); $filterusername = $user->get_identity_filter('username'); $this->assertEquals('user:username', $filterusername->get_unique_identifier()); $filterprofilefield = $user->get_identity_filter('profile_field_hi'); $this->assertEquals('user:profilefield_hi', $filterprofilefield->get_unique_identifier()); } /** * Data provider for {@see test_get_name_fields_select} * * @return array */ public function get_name_fields_select_provider(): array { return [ ['firstname lastname', ['firstname', 'lastname']], ['firstname middlename lastname', ['firstname', 'middlename', 'lastname']], ['alternatename lastname firstname', ['alternatename', 'lastname', 'firstname']], ]; } /** * Tests the helper method for selecting all of a users' name fields * * @param string $fullnamedisplay * @param string[] $expecteduserfields * * @dataProvider get_name_fields_select_provider */ public function test_get_name_fields_select(string $fullnamedisplay, array $expecteduserfields): void { global $DB; $this->resetAfterTest(true); set_config('alternativefullnameformat', $fullnamedisplay); $fields = user::get_name_fields_select('u'); $user = $DB->get_record_sql("SELECT {$fields} FROM {user} u WHERE username = :username", ['username' => 'admin']); // Ensure we received back all name fields. $this->assertEquals($expecteduserfields, array_keys((array) $user)); } } filters/autocomplete_test.php 0000644 00000006255 15152276235 0012505 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\filters; use advanced_testcase; use core_reportbuilder\local\report\filter; /** * Unit tests for course selector filter * * @package core_reportbuilder * @covers \core_reportbuilder\local\filters\base * @covers \core_reportbuilder\local\filters\autocomplete * @copyright 2022 Nathan Nguyen <nathannguyen@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class autocomplete_test extends advanced_testcase { /** * Data provider for {@see test_get_sql_filter} * * @return array */ public function get_sql_filter_provider(): array { return [ [[], ["Course 1 full name", "Course 2 full name", "Course 3 full name", "PHPUnit test site"]], [["course1", "course3"], ["Course 1 full name", "Course 3 full name"]], [["course1"], ["Course 1 full name"]], ]; } /** * Test getting filter SQL * * @param array $shortnames list of course short name * @param array $expected list of course full name * * @dataProvider get_sql_filter_provider */ public function test_get_sql_filter(array $shortnames, array $expected): void { global $DB; $this->resetAfterTest(); // Create courses as values for autocompletion. $course1 = $this->getDataGenerator()->create_course([ 'fullname' => "Course 1 full name", 'shortname' => 'course1', ]); $course2 = $this->getDataGenerator()->create_course([ 'fullname' => "Course 2 full name", 'shortname' => 'course2', ]); $course3 = $this->getDataGenerator()->create_course([ 'fullname' => "Course 3 full name", 'shortname' => 'course3', ]); $filter = (new filter( autocomplete::class, 'test', new \lang_string('course'), 'testentity', 'shortname' ))->set_options([ $course1->shortname => $course1->fullname, $course2->shortname => $course2->fullname, $course3->shortname => $course3->fullname, ]); [$select, $params] = text::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_values' => $shortnames, ]); $fullnames = $DB->get_fieldset_select('course', 'fullname', $select, $params); $this->assertEqualsCanonicalizing($expected, $fullnames); } } filters/date_test.php 0000644 00000024111 15152276235 0010710 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\filters; use advanced_testcase; use lang_string; use core_reportbuilder\local\report\filter; /** * Unit tests for date report filter * * @package core_reportbuilder * @covers \core_reportbuilder\local\filters\base * @covers \core_reportbuilder\local\filters\date * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class date_test extends advanced_testcase { /** * Data provider for {@see test_get_sql_filter_simple} * * @return array */ public function get_sql_filter_simple_provider(): array { return [ [date::DATE_ANY, true], [date::DATE_NOT_EMPTY, true], [date::DATE_EMPTY, false], ]; } /** * Test getting filter SQL * * @param int $operator * @param bool $expectuser * * @dataProvider get_sql_filter_simple_provider */ public function test_get_sql_filter_simple(int $operator, bool $expectuser): void { global $DB; $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user([ 'timecreated' => 12345, ]); $filter = new filter( date::class, 'test', new lang_string('yes'), 'testentity', 'timecreated' ); // Create instance of our filter, passing given operator. [$select, $params] = date::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => $operator, ]); $usernames = $DB->get_fieldset_select('user', 'username', $select, $params); if ($expectuser) { $this->assertContains($user->username, $usernames); } else { $this->assertNotContains($user->username, $usernames); } } /** * Test getting filter SQL while specifying a date range */ public function test_get_sql_filter_date_range(): void { global $DB; $this->resetAfterTest(); $userone = $this->getDataGenerator()->create_user(['timecreated' => 50]); $usertwo = $this->getDataGenerator()->create_user(['timecreated' => 100]); $filter = new filter( date::class, 'test', new lang_string('yes'), 'testentity', 'timecreated' ); // Create instance of our date range filter. [$select, $params] = date::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => date::DATE_RANGE, $filter->get_unique_identifier() . '_from' => 80, $filter->get_unique_identifier() . '_to' => 120, ]); // The only matching user should be our first test user. $usernames = $DB->get_fieldset_select('user', 'username', $select, $params); $this->assertEquals([$usertwo->username], $usernames); } /** * Data provider for {@see test_get_sql_filter_current_week} * * @return array */ public function get_sql_filter_current_week_provider(): array { return array_map(static function(int $day): array { return [$day]; }, range(0, 6)); } /** * Test getting filter SQL for the current week. Note that relative dates are hard to test, here we are asserting that * the current time is always within the current week regardless of calendar configuration/preferences * * @param int $startweekday * * @dataProvider get_sql_filter_current_week_provider */ public function test_get_sql_filter_current_week(int $startweekday): void { global $DB; $this->resetAfterTest(); set_config('calendar_startwday', $startweekday); $user = $this->getDataGenerator()->create_user(['timecreated' => time()]); $filter = new filter( date::class, 'test', new lang_string('yes'), 'testentity', 'timecreated' ); [$select, $params] = date::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => date::DATE_CURRENT, $filter->get_unique_identifier() . '_unit' => date::DATE_UNIT_WEEK, ]); $matchingusers = $DB->get_fieldset_select('user', 'username', $select, $params); $this->assertContains($user->username, $matchingusers); } /** * Data provider for {@see test_get_sql_filter_current_week_no_match} * * @return array */ public function get_sql_filter_current_week_no_match_provider(): array { $data = []; // For each day, create provider data for -/+ 8 days. foreach (range(0, 6) as $day) { $data = array_merge($data, [ [$day, '-8 day'], [$day, '+8 day'], ]); } return $data; } /** * Test getting filter SQL for the current week excludes dates that don't match (outside week time range) * * @param int $startweekday * @param string $timecreated Relative time suitable for passing to {@see strtotime} * * @dataProvider get_sql_filter_current_week_no_match_provider */ public function test_get_sql_filter_current_week_no_match(int $startweekday, string $timecreated): void { global $DB; $this->resetAfterTest(); set_config('calendar_startwday', $startweekday); $usertimecreated = strtotime($timecreated); $user = $this->getDataGenerator()->create_user(['timecreated' => $usertimecreated]); $filter = new filter( date::class, 'test', new lang_string('yes'), 'testentity', 'timecreated' ); [$select, $params] = date::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => date::DATE_CURRENT, $filter->get_unique_identifier() . '_unit' => date::DATE_UNIT_WEEK, ]); $matchingusers = $DB->get_fieldset_select('user', 'username', $select, $params); $this->assertNotContains($user->username, $matchingusers); } /** * Data provider for {@see test_get_sql_filter_relative} * * @return array */ public function get_sql_filter_relative_provider(): array { return [ 'Last day' => [date::DATE_LAST, 1, date::DATE_UNIT_DAY, '-6 hour'], 'Last week' => [date::DATE_LAST, 1, date::DATE_UNIT_WEEK, '-3 day'], 'Last month' => [date::DATE_LAST, 1, date::DATE_UNIT_MONTH, '-3 week'], 'Last year' => [date::DATE_LAST, 1, date::DATE_UNIT_YEAR, '-6 month'], 'Last two days' => [date::DATE_LAST, 2, date::DATE_UNIT_DAY, '-25 hour'], 'Last two weeks' => [date::DATE_LAST, 2, date::DATE_UNIT_WEEK, '-10 day'], 'Last two months' => [date::DATE_LAST, 2, date::DATE_UNIT_MONTH, '-7 week'], 'Last two years' => [date::DATE_LAST, 2, date::DATE_UNIT_YEAR, '-15 month'], // Current week is tested separately. 'Current day' => [date::DATE_CURRENT, null, date::DATE_UNIT_DAY], 'Current month' => [date::DATE_CURRENT, null, date::DATE_UNIT_MONTH], 'Current year' => [date::DATE_CURRENT, null, date::DATE_UNIT_YEAR], 'Next day' => [date::DATE_NEXT, 1, date::DATE_UNIT_DAY, '+6 hour'], 'Next week' => [date::DATE_NEXT, 1, date::DATE_UNIT_WEEK, '+3 day'], 'Next month' => [date::DATE_NEXT, 1, date::DATE_UNIT_MONTH, '+3 week'], 'Next year' => [date::DATE_NEXT, 1, date::DATE_UNIT_YEAR, '+6 month'], 'Next two days' => [date::DATE_NEXT, 2, date::DATE_UNIT_DAY, '+25 hour'], 'Next two weeks' => [date::DATE_NEXT, 2, date::DATE_UNIT_WEEK, '+10 day'], 'Next two months' => [date::DATE_NEXT, 2, date::DATE_UNIT_MONTH, '+7 week'], 'Next two years' => [date::DATE_NEXT, 2, date::DATE_UNIT_YEAR, '+15 month'], 'In the past' => [date::DATE_PAST, null, null, '-3 hour'], 'In the future' => [date::DATE_FUTURE, null, null, '+3 hour'], ]; } /** * Unit tests for filtering relative dates * * @param int $operator * @param int|null $unitvalue * @param int|null $unit * @param string|null $timecreated Relative time suitable for passing to {@see strtotime} (or null for current time) * * @dataProvider get_sql_filter_relative_provider */ public function test_get_sql_filter_relative(int $operator, ?int $unitvalue, ?int $unit, ?string $timecreated = null): void { global $DB; $this->resetAfterTest(); $usertimecreated = ($timecreated !== null ? strtotime($timecreated) : time()); $user = $this->getDataGenerator()->create_user(['timecreated' => $usertimecreated]); $filter = new filter( date::class, 'test', new lang_string('yes'), 'testentity', 'timecreated' ); [$select, $params] = date::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => $operator, $filter->get_unique_identifier() . '_value' => $unitvalue, $filter->get_unique_identifier() . '_unit' => $unit, ]); $matchingusers = $DB->get_fieldset_select('user', 'username', $select, $params); $this->assertContains($user->username, $matchingusers); } } filters/boolean_select_test.php 0000644 00000005171 15152276235 0012756 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\filters; use advanced_testcase; use lang_string; use core_reportbuilder\local\report\filter; /** * Unit tests for boolean report filter * * @package core_reportbuilder * @covers \core_reportbuilder\local\filters\base * @covers \core_reportbuilder\local\filters\boolean_select * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class boolean_select_test extends advanced_testcase { /** * Data provider for {@see test_get_sql_filter_simple} * * @return array */ public function get_sql_filter_simple_provider(): array { return [ [boolean_select::ANY_VALUE, true], [boolean_select::CHECKED, true], [boolean_select::NOT_CHECKED, false], ]; } /** * Test getting filter SQL * * @param int $operator * @param bool $expectuser * * @dataProvider get_sql_filter_simple_provider */ public function test_get_sql_filter_simple(int $operator, bool $expectuser): void { global $DB; $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user([ 'suspended' => 1, ]); $filter = new filter( boolean_select::class, 'test', new lang_string('yes'), 'testentity', 'suspended' ); // Create instance of our filter, passing given operator. [$select, $params] = boolean_select::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => $operator, ]); $usernames = $DB->get_fieldset_select('user', 'username', $select, $params); if ($expectuser) { $this->assertContains($user->username, $usernames); } else { $this->assertNotContains($user->username, $usernames); } } } filters/user_test.php 0000644 00000007035 15152276235 0010757 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\filters; use advanced_testcase; use lang_string; use core_reportbuilder\local\report\filter; /** * Unit tests for user report filter * * @package core_reportbuilder * @covers \core_reportbuilder\local\filters\base * @covers \core_reportbuilder\local\filters\user * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_test extends advanced_testcase { /** * Data provider for {@see test_get_sql_filter} * * @return array */ public function get_sql_filter_simple(): array { return [ [user::USER_ANY, ['admin', 'guest', 'user01', 'user02']], [user::USER_CURRENT, ['user01']], ]; } /** * Test getting filter SQL * * @param int $operator * @param string[] $expectedusernames * * @dataProvider get_sql_filter_simple */ public function test_get_sql_filter(int $operator, array $expectedusernames): void { global $DB; $this->resetAfterTest(); $user01 = $this->getDataGenerator()->create_user(['username' => 'user01']); $user02 = $this->getDataGenerator()->create_user(['username' => 'user02']); $this->setUser($user01); $filter = new filter( user::class, 'test', new lang_string('yes'), 'testentity', 'id' ); // Create instance of our filter, passing given operator. [$select, $params] = user::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => $operator, ]); $usernames = $DB->get_fieldset_select('user', 'username', $select, $params); $this->assertEqualsCanonicalizing($expectedusernames, $usernames); } /** * Test getting filter SQL using specific user selection operator/value */ public function test_get_sql_filter_select_user(): void { global $DB; $this->resetAfterTest(); $user01 = $this->getDataGenerator()->create_user(['username' => 'user01']); $user02 = $this->getDataGenerator()->create_user(['username' => 'user02']); $filter = new filter( user::class, 'test', new lang_string('yes'), 'testentity', 'id' ); // Create instance of our filter, passing given operator/value matching second user. [$select, $params] = user::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => user::USER_SELECT, $filter->get_unique_identifier() . '_value' => [$user02->id], ]); $usernames = $DB->get_fieldset_select('user', 'username', $select, $params); $this->assertEquals([$user02->username], $usernames); } } filters/select_test.php 0000644 00000006313 15152276235 0011256 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\filters; use advanced_testcase; use lang_string; use core_reportbuilder\local\report\filter; /** * Unit tests for select report filter * * @package core_reportbuilder * @covers \core_reportbuilder\local\filters\base * @covers \core_reportbuilder\local\filters\select * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class select_test extends advanced_testcase { /** * Data provider for {@see test_get_sql_filter_simple} * * @return array */ public function get_sql_filter_simple_provider(): array { return [ [select::ANY_VALUE, null, true], [select::EQUAL_TO, 'starwars', true], [select::EQUAL_TO, 'mandalorian', false], [select::NOT_EQUAL_TO, 'starwars', false], [select::NOT_EQUAL_TO, 'mandalorian', true], ]; } /** * Test getting filter SQL * * @param int $operator * @param string|null $value * @param bool $expectmatch * * @dataProvider get_sql_filter_simple_provider */ public function test_get_sql_filter_simple(int $operator, ?string $value, bool $expectmatch): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course([ 'fullname' => "May the course be with you", 'shortname' => 'starwars', ]); $course2 = $this->getDataGenerator()->create_course([ 'fullname' => "This is the course", 'shortname' => 'mandalorian', ]); $filter = (new filter( select::class, 'test', new lang_string('course'), 'testentity', 'shortname' ))->set_options([ $course1->shortname => $course1->fullname, $course2->shortname => $course2->fullname, ]); // Create instance of our filter, passing given operator. [$select, $params] = select::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => $operator, $filter->get_unique_identifier() . '_value' => $value, ]); $fullnames = $DB->get_fieldset_select('course', 'fullname', $select, $params); if ($expectmatch) { $this->assertContains($course1->fullname, $fullnames); } else { $this->assertNotContains($course1->fullname, $fullnames); } } } filters/tags_test.php 0000644 00000007366 15152276235 0010746 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\filters; use advanced_testcase; use lang_string; use core_reportbuilder\local\report\filter; /** * Unit tests for tags report filter * * @package core_reportbuilder * @covers \core_reportbuilder\local\filters\base * @covers \core_reportbuilder\local\filters\tags * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tags_test extends advanced_testcase { /** * Data provider for {@see test_get_sql_filter} * * @return array[] */ public function get_sql_filter_provider(): array { return [ 'Any value' => [tags::ANY_VALUE, null, ['course01', 'course01', 'course02', 'course03']], 'Not empty' => [tags::NOT_EMPTY, null, ['course01', 'course01', 'course02']], 'Empty' => [tags::EMPTY, null, ['course03']], 'Equal to unselected' => [tags::EQUAL_TO, null, ['course01', 'course01', 'course02', 'course03']], 'Equal to selected tag' => [tags::EQUAL_TO, 'cat', ['course01']], 'Not equal to unselected' => [tags::NOT_EQUAL_TO, null, ['course01', 'course01', 'course02', 'course03']], 'Not equal to selected tag' => [tags::NOT_EQUAL_TO, 'fish', ['course01', 'course01', 'course03']], ]; } /** * Test getting filter SQL * * @param int $operator * @param string|null $tagname * @param array $expectedcoursenames * * @dataProvider get_sql_filter_provider */ public function test_get_sql_filter(int $operator, ?string $tagname, array $expectedcoursenames): void { global $DB; $this->resetAfterTest(); $this->getDataGenerator()->create_course(['fullname' => 'course01', 'tags' => ['cat', 'dog']]); $this->getDataGenerator()->create_course(['fullname' => 'course02', 'tags' => ['fish']]); $this->getDataGenerator()->create_course(['fullname' => 'course03']); $filter = (new filter( tags::class, 'tags', new lang_string('tags'), 'testentity', 't.id' )); // Create instance of our filter, passing ID of the tag if specified. if ($tagname !== null) { $tagid = $DB->get_field('tag', 'id', ['name' => $tagname], MUST_EXIST); $value = [$tagid]; } else { $value = null; } [$select, $params] = tags::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => $operator, $filter->get_unique_identifier() . '_value' => $value, ]); $sql = 'SELECT c.fullname FROM {course} c LEFT JOIN {tag_instance} ti ON ti.itemid = c.id LEFT JOIN {tag} t ON t.id = ti.tagid WHERE c.id != ' . SITEID; if ($select) { $sql .= " AND {$select}"; } $courses = $DB->get_fieldset_sql($sql, $params); $this->assertEqualsCanonicalizing($expectedcoursenames, $courses); } } filters/number_test.php 0000644 00000011552 15152276235 0011270 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\filters; use advanced_testcase; use lang_string; use core_reportbuilder\local\report\filter; /** * Unit tests for number report filter * * @package core_reportbuilder * @covers \core_reportbuilder\local\filters\base * @covers \core_reportbuilder\local\filters\number * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class number_test extends advanced_testcase { /** * Data provider for {@see test_get_sql_filter_simple} * * @return array[] */ public function get_sql_filter_simple_provider(): array { return [ [number::ANY_VALUE, null, null, true], [number::IS_NOT_EMPTY, null, null, true], [number::IS_EMPTY, null, null, false], [number::LESS_THAN, 1, null, false], [number::LESS_THAN, 123, null, false], [number::LESS_THAN, 124, null, true], [number::GREATER_THAN, 1, null, true], [number::GREATER_THAN, 123, null, false], [number::GREATER_THAN, 124, null, false], [number::EQUAL_TO, 123, null, true], [number::EQUAL_TO, 124, null, false], [number::EQUAL_OR_LESS_THAN, 124, null, true], [number::EQUAL_OR_LESS_THAN, 123, null, true], [number::EQUAL_OR_LESS_THAN, 122, null, false], [number::EQUAL_OR_GREATER_THAN, 122, null, true], [number::EQUAL_OR_GREATER_THAN, 123, null, true], [number::EQUAL_OR_GREATER_THAN, 124, null, false], [number::RANGE, 122, 124, true], [number::RANGE, 124, 125, false], [number::RANGE, 122, 123, true], [number::RANGE, 123, 124, true], ]; } /** * Test getting filter SQL * * @param int $operator * @param int|null $value1 * @param int|null $value2 * @param bool $expectmatch * * @dataProvider get_sql_filter_simple_provider */ public function test_get_sql_filter_simple(int $operator, ?int $value1, ?int $value2, bool $expectmatch): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course([ 'timecreated' => 123, ]); $filter = new filter( number::class, 'test', new lang_string('course'), 'testentity', 'timecreated' ); // Create instance of our filter, passing given operator. [$select, $params] = number::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_value1' => $value1, $filter->get_unique_identifier() . '_value2' => $value2, $filter->get_unique_identifier() . '_operator' => $operator, ]); $fullnames = $DB->get_fieldset_select('course', 'fullname', $select, $params); if ($expectmatch) { $this->assertContains($course->fullname, $fullnames); } else { $this->assertNotContains($course->fullname, $fullnames); } } /** * Data provider for {@see test_get_sql_filter_invalid} * * @return array[] */ public function get_sql_filter_invalid_provider(): array { return [ [number::LESS_THAN], [number::GREATER_THAN], [number::EQUAL_TO], [number::EQUAL_OR_LESS_THAN], [number::EQUAL_OR_GREATER_THAN], [number::RANGE], ]; } /** * Test getting filter SQL for operators that require values * * @param int $operator * * @dataProvider get_sql_filter_invalid_provider */ public function test_get_sql_filter_invalid(int $operator): void { $filter = new filter( number::class, 'test', new lang_string('course'), 'testentity', 'timecreated' ); [$select, $params] = number::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => $operator, ]); $this->assertEquals('', $select); $this->assertEquals([], $params); } } filters/course_selector_test.php 0000644 00000005271 15152276235 0013201 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\filters; use advanced_testcase; use core_reportbuilder\local\report\filter; /** * Unit tests for course selector filter * * @package core_reportbuilder * @covers \core_reportbuilder\local\filters\base * @covers \core_reportbuilder\local\filters\course_selector * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_selector_test extends advanced_testcase { /** * Test getting filter SQL */ public function test_get_sql_filter(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course([ 'fullname' => "Time travel", ]); $course2 = $this->getDataGenerator()->create_course([ 'fullname' => "Quantum computing", ]); $course3 = $this->getDataGenerator()->create_course([ 'fullname' => "Space travel", ]); $filter = new filter( course_selector::class, 'test', new \lang_string('course'), 'testentity', 'id' ); // Create instance of our filter, passing given courses ids. [$select, $params] = text::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_values' => [$course1->id, $course3->id], ]); $fullnames = $DB->get_fieldset_select('course', 'fullname', $select, $params); $this->assertEqualsCanonicalizing(['Time travel', 'Space travel'], $fullnames); // Test without passing any course id. [$select, $params] = text::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_values' => [], ]); $fullnames = $DB->get_fieldset_select('course', 'fullname', $select, $params); $this->assertEqualsCanonicalizing(['Time travel', 'Quantum computing', 'Space travel', 'PHPUnit test site'], $fullnames); } } filters/duration_test.php 0000644 00000011371 15152276235 0011624 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\filters; use advanced_testcase; use lang_string; use core_reportbuilder\local\report\filter; /** * Unit tests for duration report filter * * @package core_reportbuilder * @covers \core_reportbuilder\local\filters\base * @covers \core_reportbuilder\local\filters\duration * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class duration_test extends advanced_testcase { /** * Data provider for {@see test_get_sql_filter} * * @return array */ public function get_sql_filter_provider(): array { return [ 'Any duration' => [duration::DURATION_ANY, true], // Maximum operator. 'Maximum seconds non-match' => [duration::DURATION_MAXIMUM, false, HOURSECS, 1], 'Maximum seconds match' => [duration::DURATION_MAXIMUM, true, HOURSECS * 3, 1], 'Maximum minutes non-match' => [duration::DURATION_MAXIMUM, false, 60, MINSECS], 'Maximum minutes match' => [duration::DURATION_MAXIMUM, true, 150, MINSECS], 'Maximum hours non-match (float)' => [duration::DURATION_MAXIMUM, false, 0.5, HOURSECS], 'Maximum hours non-match' => [duration::DURATION_MAXIMUM, false, 1, HOURSECS], 'Maximum hours match (float)' => [duration::DURATION_MAXIMUM, true, 2.5, HOURSECS], 'Maximum hours match' => [duration::DURATION_MAXIMUM, true, 3, HOURSECS], // Minimum operator. 'Minimum seconds match' => [duration::DURATION_MINIMUM, true, HOURSECS, 1], 'Minimum seconds non-match' => [duration::DURATION_MINIMUM, false, HOURSECS * 3, 1], 'Minimum minutes match' => [duration::DURATION_MINIMUM, true, 60, MINSECS], 'Minimum minutes non-match' => [duration::DURATION_MINIMUM, false, 150, MINSECS], 'Minimum hours match (float)' => [duration::DURATION_MINIMUM, true, 0.5, HOURSECS], 'Minimum hours match' => [duration::DURATION_MINIMUM, true, 1, HOURSECS], 'Minimum hours non-match (float)' => [duration::DURATION_MINIMUM, false, 2.5, HOURSECS], 'Minimum hours non-match' => [duration::DURATION_MINIMUM, false, 3, HOURSECS], ]; } /** * Test getting filter SQL * * @param int $operator * @param bool $expectuser * @param float $value * @param int $unit * * @dataProvider get_sql_filter_provider */ public function test_get_sql_filter(int $operator, bool $expectuser, float $value = 0, int $unit = MINSECS): void { global $DB; $this->resetAfterTest(); // We are going to enrol our student from now, with a duration of two hours (timeend is two hours later). $timestart = time(); $timeend = $timestart + (HOURSECS * 2); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_and_enrol($course, 'student', null, 'manual', $timestart, $timeend); $filter = new filter( duration::class, 'test', new lang_string('yes'), 'testentity', 'timeend - timestart' ); // Create instance of our filter, passing given values. [$select, $params] = duration::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => $operator, $filter->get_unique_identifier() . '_value' => $value, $filter->get_unique_identifier() . '_unit' => $unit, ]); $useridfield = $DB->get_field_select('user_enrolments', 'userid', $select, $params); if ($expectuser) { $this->assertEquals($useridfield, $user->id); } else { $this->assertFalse($useridfield); } } } filters/category_test.php 0000644 00000011021 15152276235 0011604 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\filters; use advanced_testcase; use lang_string; use core_reportbuilder\local\helpers\database; use core_reportbuilder\local\report\filter; /** * Unit tests for course category report filter * * @package core_reportbuilder * @covers \core_reportbuilder\local\filters\category * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class category_test extends advanced_testcase { /** * Data provider for {@see test_get_sql_filter} * * @return array */ public function get_sql_filter_provider(): array { return [ ['One', false, ['One']], ['One', true, ['One', 'Two', 'Three']], ['Two', true, ['Two', 'Three']], ['Three', true, ['Three']], [null, false, ['Category 1', 'One', 'Two', 'Three']], ]; } /** * Test getting filter SQL * * @param string|null $categoryname * @param bool $subcategories * @param string[] $expectedcategories * * @dataProvider get_sql_filter_provider */ public function test_get_sql_filter(?string $categoryname, bool $subcategories, array $expectedcategories): void { global $DB; $this->resetAfterTest(); $category1 = $this->getDataGenerator()->create_category(['name' => 'One']); $category2 = $this->getDataGenerator()->create_category(['name' => 'Two', 'parent' => $category1->id]); $category3 = $this->getDataGenerator()->create_category(['name' => 'Three', 'parent' => $category2->id]); if ($categoryname !== null) { $categoryid = $DB->get_field('course_categories', 'id', ['name' => $categoryname], MUST_EXIST); } else { $categoryid = null; } $filter = new filter( category::class, 'test', new lang_string('yes'), 'testentity', 'id' ); // Create instance of our filter, passing given operator. [$select, $params] = category::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_value' => $categoryid, $filter->get_unique_identifier() . '_subcategories' => $subcategories, ]); $categories = $DB->get_fieldset_select('course_categories', 'name', $select, $params); $this->assertEqualsCanonicalizing($expectedcategories, $categories); } /** * Test getting filter SQL with parameters */ public function test_get_sql_filter_parameters(): void { global $DB; $this->resetAfterTest(); $category1 = $this->getDataGenerator()->create_category(['name' => 'One']); $category2 = $this->getDataGenerator()->create_category(['name' => 'Two', 'parent' => $category1->id]); $category3 = $this->getDataGenerator()->create_category(['name' => 'Three']); // Rather convoluted filter SQL, but enough to demonstrate usage of a parameter that gets used twice in the query. $paramzero = database::generate_param_name(); $filter = new filter( category::class, 'test', new lang_string('yes'), 'testentity', "id + :{$paramzero}", [$paramzero => 0] ); // When including sub-categories, the filter SQL is included twice (for the category itself, plus to find descendents). [$select, $params] = category::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_value' => $category1->id, $filter->get_unique_identifier() . '_subcategories' => true, ]); $categories = $DB->get_fieldset_select('course_categories', 'id', $select, $params); $this->assertEqualsCanonicalizing([$category1->id, $category2->id], $categories); } } filters/text_test.php 0000644 00000012151 15152276235 0010760 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\filters; use advanced_testcase; use lang_string; use core_reportbuilder\local\report\filter; /** * Unit tests for text report filter * * @package core_reportbuilder * @covers \core_reportbuilder\local\filters\base * @covers \core_reportbuilder\local\filters\text * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class text_test extends advanced_testcase { /** * Data provider for {@see test_get_sql_filter_simple} * * @return array */ public function get_sql_filter_simple_provider(): array { return [ [text::ANY_VALUE, null, true], [text::CONTAINS, 'looking', true], [text::CONTAINS, 'sky', false], [text::DOES_NOT_CONTAIN, 'sky', true], [text::DOES_NOT_CONTAIN, 'looking', false], [text::IS_EQUAL_TO, "Hello, is it me you're looking for?", true], [text::IS_EQUAL_TO, 'I can see it in your eyes', false], [text::IS_NOT_EQUAL_TO, "Hello, is it me you're looking for?", false], [text::IS_NOT_EQUAL_TO, 'I can see it in your eyes', true], [text::STARTS_WITH, 'Hello', true], [text::STARTS_WITH, 'sunlight', false], [text::ENDS_WITH, 'looking for?', true], [text::ENDS_WITH, 'your heart', false], ]; } /** * Test getting filter SQL * * @param int $operator * @param string|null $value * @param bool $expectmatch * * @dataProvider get_sql_filter_simple_provider */ public function test_get_sql_filter_simple(int $operator, ?string $value, bool $expectmatch): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course([ 'fullname' => "Hello, is it me you're looking for?", ]); $filter = new filter( text::class, 'test', new lang_string('course'), 'testentity', 'fullname' ); // Create instance of our filter, passing given operator. [$select, $params] = text::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => $operator, $filter->get_unique_identifier() . '_value' => $value, ]); $fullnames = $DB->get_fieldset_select('course', 'fullname', $select, $params); if ($expectmatch) { $this->assertContains($course->fullname, $fullnames); } else { $this->assertNotContains($course->fullname, $fullnames); } } /** * Data provider for {@see test_get_sql_filter_empty} * * @return array */ public function get_sql_filter_empty_provider(): array { return [ [text::IS_EMPTY, null, true], [text::IS_EMPTY, '', true], [text::IS_EMPTY, 'hola', false], [text::IS_NOT_EMPTY, null, false], [text::IS_NOT_EMPTY, '', false], [text::IS_NOT_EMPTY, 'hola', true], ]; } /** * Test getting filter SQL using the {@see text::IS_EMPTY} and {@see text::IS_NOT_EMPTY} operators * * @param int $operator * @param string|null $profilefieldvalue * @param bool $expectmatch * * @dataProvider get_sql_filter_empty_provider */ public function test_get_sql_filter_empty(int $operator, ?string $profilefieldvalue, bool $expectmatch): void { global $DB; $this->resetAfterTest(); // We are using the user.moodlenetprofile field because it is nullable. $user = $this->getDataGenerator()->create_user([ 'moodlenetprofile' => $profilefieldvalue, ]); $filter = new filter( text::class, 'test', new lang_string('user'), 'testentity', 'moodlenetprofile' ); // Create instance of our filter, passing given operator. [$select, $params] = text::create($filter)->get_sql_filter([ $filter->get_unique_identifier() . '_operator' => $operator, ]); $usernames = $DB->get_fieldset_select('user', 'username', $select, $params); if ($expectmatch) { $this->assertContains($user->username, $usernames); } else { $this->assertNotContains($user->username, $usernames); } } } aggregation/groupconcat_test.php 0000644 00000017544 15152276235 0013152 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\aggregation; use core_badges_generator; use core_badges\reportbuilder\datasource\badges; use core_reportbuilder_testcase; use core_reportbuilder_generator; use core_user\reportbuilder\datasource\users; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** * Unit tests for group concatenation aggregation * * @package core_reportbuilder * @covers \core_reportbuilder\local\aggregation\base * @covers \core_reportbuilder\local\aggregation\groupconcat * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class groupconcat_test extends core_reportbuilder_testcase { /** * Test aggregation when applied to column */ public function test_column_aggregation(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Banana']); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Apple']); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Banana']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastname', 'aggregation' => groupconcat::get_class_name(), ]); // Assert lastname column was aggregated, and sorted predictably. $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_lastname' => 'User', ], [ 'c0_firstname' => 'Bob', 'c1_lastname' => 'Apple, Banana, Banana', ], ], $content); } /** * Test aggregation when applied to column with multiple fields */ public function test_column_aggregation_multiple_fields(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(['firstname' => 'Adam', 'lastname' => 'Apple']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithlink', 'aggregation' => groupconcat::get_class_name(), ]); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(1, $content); // Ensure users are sorted predictably (Adam -> Admin). [$userone, $usertwo] = explode(', ', reset($content[0])); $this->assertStringContainsString(fullname($user, true), $userone); $this->assertStringContainsString(fullname(get_admin(), true), $usertwo); } /** * Test aggregation when applied to column with callback */ public function test_column_aggregation_with_callback(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'confirmed' => 1]); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'confirmed' => 0]); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'confirmed' => 1]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:confirmed', 'aggregation' => groupconcat::get_class_name(), ]); // Assert confirmed column was aggregated, and sorted predictably with callback applied. $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_confirmed' => 'Yes', ], [ 'c0_firstname' => 'Bob', 'c1_confirmed' => 'No, Yes, Yes', ], ], $content); } /** * Test aggregation when applied to column with callback that expects/handles null values */ public function test_datasource_aggregate_column_callback_with_null(): void { $this->resetAfterTest(); $this->setAdminUser(); $userone = $this->getDataGenerator()->create_user(['description' => 'First user']); $usertwo = $this->getDataGenerator()->create_user(['description' => 'Second user']); /** @var core_badges_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_badges'); // Create course badge, issue to both users. $badgeone = $generator->create_badge(['name' => 'First badge']); $badgeone->issue($userone->id, true); $badgeone->issue($usertwo->id, true); // Create second badge, without issuing to anyone. $badgetwo = $generator->create_badge(['name' => 'Second badge']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Badges', 'source' => badges::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'badge:name', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:description', 'aggregation' => groupconcat::get_class_name(), ]); // Assert description column was aggregated, with callbacks accounting for null values. $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_name' => $badgeone->name, 'c1_description' => "{$userone->description}, {$usertwo->description}", ], [ 'c0_name' => $badgetwo->name, 'c1_description' => '', ], ], $content); } } aggregation/max_test.php 0000644 00000005255 15152276235 0011407 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\aggregation; use core_reportbuilder_testcase; use core_reportbuilder_generator; use core_user\reportbuilder\datasource\users; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** * Unit tests for max aggregation * * @package core_reportbuilder * @covers \core_reportbuilder\local\aggregation\base * @covers \core_reportbuilder\local\aggregation\max * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class max_test extends core_reportbuilder_testcase { /** * Test aggregation when applied to column */ public function test_column_aggregation(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 1]); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 0]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column( ['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:suspended', 'aggregation' => max::get_class_name()] ); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_suspended' => 'No', ], [ 'c0_firstname' => 'Bob', 'c1_suspended' => 'Yes', ], ], $content); } } aggregation/sum_test.php 0000644 00000010765 15152276235 0011430 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\aggregation; use core_reportbuilder_testcase; use core_reportbuilder_generator; use core_reportbuilder\manager; use core_reportbuilder\local\report\column; use core_user\reportbuilder\datasource\users; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** * Unit tests for sum aggregation * * @package core_reportbuilder * @covers \core_reportbuilder\local\aggregation\base * @covers \core_reportbuilder\local\aggregation\sum * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sum_test extends core_reportbuilder_testcase { /** * Test aggregation when applied to column */ public function test_column_aggregation(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 1]); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 1]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:suspended', 'aggregation' => sum::get_class_name() ]); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_suspended' => 0, ], [ 'c0_firstname' => 'Bob', 'c1_suspended' => 2, ], ], $content); } /** * Test aggregation when applied to column with callback */ public function test_column_aggregation_with_callback(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 1]); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 1]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column( ['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:suspended', 'aggregation' => sum::get_class_name()] ); // Set callback to format the column (hack column definition to ensure callbacks are executed). $instance = manager::get_report_from_persistent($report); $instance->get_column('user:suspended') ->set_type(column::TYPE_INTEGER) ->set_callback(static function(int $value): string { return "{$value} suspended"; }); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_suspended' => '0 suspended', ], [ 'c0_firstname' => 'Bob', 'c1_suspended' => '2 suspended', ], ], $content); } } aggregation/avg_test.php 0000644 00000010731 15152276235 0011372 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\aggregation; use core_reportbuilder_testcase; use core_reportbuilder_generator; use core_reportbuilder\manager; use core_reportbuilder\local\report\column; use core_user\reportbuilder\datasource\users; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** * Unit tests for avg aggregation * * @package core_reportbuilder * @covers \core_reportbuilder\local\aggregation\avg * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class avg_test extends core_reportbuilder_testcase { /** * Test aggregation when applied to column */ public function test_column_aggregation(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 1]); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 0]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column( ['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:suspended', 'aggregation' => avg::get_class_name()] ); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_suspended' => '0.0', ], [ 'c0_firstname' => 'Bob', 'c1_suspended' => '0.5', ], ], $content); } /** * Test aggregation when applied to column with callback */ public function test_column_aggregation_with_callback(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 1]); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 0]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column( ['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:suspended', 'aggregation' => avg::get_class_name()] ); // Set callback to format the column (hack column definition to ensure callbacks are executed). $instance = manager::get_report_from_persistent($report); $instance->get_column('user:suspended') ->set_type(column::TYPE_FLOAT) ->set_callback(static function(float $value): string { return number_format($value, 1) . ' suspended'; }); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_suspended' => '0.0 suspended', ], [ 'c0_firstname' => 'Bob', 'c1_suspended' => '0.5 suspended', ], ], $content); } } aggregation/groupconcatdistinct_test.php 0000644 00000014162 15152276235 0014705 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\aggregation; use core_reportbuilder_testcase; use core_reportbuilder_generator; use core_reportbuilder\local\report\column; use core_user\reportbuilder\datasource\users; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** * Unit tests for group concatenation distinct aggregation * * @package core_reportbuilder * @covers \core_reportbuilder\local\aggregation\base * @covers \core_reportbuilder\local\aggregation\groupconcatdistinct * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class groupconcatdistinct_test extends core_reportbuilder_testcase { /** * Test setup, we need to skip these tests on non-supported databases */ public function setUp(): void { global $DB; if (!groupconcatdistinct::compatible(column::TYPE_TEXT)) { $this->markTestSkipped('Distinct group concatenation not supported in ' . $DB->get_dbfamily()); } } /** * Test aggregation when applied to column */ public function test_column_aggregation(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Banana']); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Apple']); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Banana']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastname', 'aggregation' => groupconcatdistinct::get_class_name(), ]); // Assert lastname column was aggregated, and sorted predictably. $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_lastname' => 'User', ], [ 'c0_firstname' => 'Bob', 'c1_lastname' => 'Apple, Banana', ], ], $content); } /** * Test aggregation when applied to column with multiple fields */ public function test_column_aggregation_multiple_fields(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(['firstname' => 'Adam', 'lastname' => 'Apple']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullnamewithlink', 'aggregation' => groupconcatdistinct::get_class_name(), ]); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(1, $content); // Ensure users are sorted predictably (Adam -> Admin). [$userone, $usertwo] = explode(', ', reset($content[0])); $this->assertStringContainsString(fullname($user, true), $userone); $this->assertStringContainsString(fullname(get_admin(), true), $usertwo); } /** * Test aggregation when applied to column with callback */ public function test_column_aggregation_with_callback(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'confirmed' => 1]); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'confirmed' => 0]); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'confirmed' => 1]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:confirmed', 'aggregation' => groupconcatdistinct::get_class_name(), ]); // Assert confirmed column was aggregated, and sorted predictably with callback applied. $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_confirmed' => 'Yes', ], [ 'c0_firstname' => 'Bob', 'c1_confirmed' => 'No, Yes', ], ], $content); } } aggregation/min_test.php 0000644 00000005254 15152276235 0011404 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\aggregation; use core_reportbuilder_testcase; use core_reportbuilder_generator; use core_user\reportbuilder\datasource\users; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** * Unit tests for min aggregation * * @package core_reportbuilder * @covers \core_reportbuilder\local\aggregation\base * @covers \core_reportbuilder\local\aggregation\min * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class min_test extends core_reportbuilder_testcase { /** * Test aggregation when applied to column */ public function test_column_aggregation(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 1]); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 0]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:suspended', 'aggregation' => min::get_class_name()] ); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_suspended' => 'No', ], [ 'c0_firstname' => 'Bob', 'c1_suspended' => 'No', ], ], $content); } } aggregation/percent_test.php 0000644 00000005275 15152276235 0012264 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\aggregation; use core_reportbuilder_testcase; use core_reportbuilder_generator; use core_user\reportbuilder\datasource\users; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** * Unit tests for sum aggregation * * @package core_reportbuilder * @covers \core_reportbuilder\local\aggregation\base * @covers \core_reportbuilder\local\aggregation\percent * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class percent_test extends core_reportbuilder_testcase { /** * Test aggregation when applied to column */ public function test_column_aggregation(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 1]); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'suspended' => 0]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:suspended', 'aggregation' => percent::get_class_name()] ); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_suspended' => '0.0%', ], [ 'c0_firstname' => 'Bob', 'c1_suspended' => '50.0%', ], ], $content); } } aggregation/count_test.php 0000644 00000005426 15152276235 0011752 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\aggregation; use core_reportbuilder_testcase; use core_reportbuilder_generator; use core_user\reportbuilder\datasource\users; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** * Unit tests for count aggregation * * @package core_reportbuilder * @covers \core_reportbuilder\local\aggregation\base * @covers \core_reportbuilder\local\aggregation\count * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class count_test extends core_reportbuilder_testcase { /** * Test aggregation when applied to column */ public function test_column_aggregation(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Apple']); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Banana']); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Banana']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastname', 'aggregation' => count::get_class_name()] ); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_lastname' => 1, ], [ 'c0_firstname' => 'Bob', 'c1_lastname' => 3, ], ], $content); } } aggregation/countdistinct_test.php 0000644 00000007727 15152276235 0013522 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\aggregation; use core_reportbuilder_testcase; use core_reportbuilder_generator; use core_user\reportbuilder\datasource\users; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** * Unit tests for count distinct aggregation * * @package core_reportbuilder * @covers \core_reportbuilder\local\aggregation\base * @covers \core_reportbuilder\local\aggregation\countdistinct * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class countdistinct_test extends core_reportbuilder_testcase { /** * Test aggregation when applied to column */ public function test_column_aggregation(): void { $this->resetAfterTest(); // Test subjects. $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Apple']); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Banana']); $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Banana']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // First column, sorted. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname', 'sortenabled' => 1]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:lastname', 'aggregation' => countdistinct::get_class_name(), ]); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_lastname' => 1, ], [ 'c0_firstname' => 'Bob', 'c1_lastname' => 2, ], ], $content); } /** * Test aggregation when applied to column with multiple fields */ public function test_column_aggregation_multiple_fields(): void { $this->resetAfterTest(); // Create a user with the same firstname as existing admin. $this->getDataGenerator()->create_user(['firstname' => 'Admin', 'lastname' => 'Test']); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // This is the column we'll aggregate. $generator->create_column([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname', 'aggregation' => countdistinct::get_class_name(), ]); $content = $this->get_custom_report_content($report->get('id')); $this->assertCount(1, $content); // There are two distinct fullnames ("Admin User" & "Admin Test"). $countdistinct = reset($content[0]); $this->assertEquals(2, $countdistinct); } } report/base_test.php 0000644 00000013153 15152276235 0010554 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\report; use advanced_testcase; use context_system; use core_reportbuilder\system_report_available; use core_reportbuilder\system_report_factory; /** * Unit tests for report base class * * @package core_reportbuilder * @covers \core_reportbuilder\local\report\base * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class base_test extends advanced_testcase { /** * Load required class */ public static function setUpBeforeClass(): void { global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_available.php"); } /** * Test for add_base_condition_simple */ public function test_add_base_condition_simple(): void { $this->resetAfterTest(); $systemreport = system_report_factory::create(system_report_available::class, context_system::instance()); $systemreport->add_base_condition_simple('username', 'admin'); [$where, $params] = $systemreport->get_base_condition(); $this->assertStringMatchesFormat('username = :%a', $where); $this->assertEqualsCanonicalizing(['admin'], $params); } /** * Test for add_base_condition_simple null */ public function test_add_base_condition_simple_null(): void { $this->resetAfterTest(); $systemreport = system_report_factory::create(system_report_available::class, context_system::instance()); $systemreport->add_base_condition_simple('username', null); [$where, $params] = $systemreport->get_base_condition(); $this->assertEquals('username IS NULL', $where); $this->assertEmpty($params); } /** * Test for get_filter_instances */ public function test_get_filter_instances(): void { $this->resetAfterTest(); $systemreport = system_report_factory::create(system_report_available::class, context_system::instance(), '', '', 0, ['withfilters' => true]); $filters = $systemreport->get_filter_instances(); $this->assertCount(1, $filters); $this->assertInstanceOf(\core_reportbuilder\local\filters\text::class, reset($filters)); } /** * Test for set_downloadable */ public function test_set_downloadable(): void { $this->resetAfterTest(); $systemreport = system_report_factory::create(system_report_available::class, context_system::instance()); $systemreport->set_downloadable(true, 'testfilename'); $this->assertTrue($systemreport->is_downloadable()); $this->assertEquals('testfilename', $systemreport->get_downloadfilename()); $systemreport->set_downloadable(false, 'anothertestfilename'); $this->assertFalse($systemreport->is_downloadable()); $this->assertEquals('anothertestfilename', $systemreport->get_downloadfilename()); } /** * Test for get_context */ public function test_get_context(): void { $this->resetAfterTest(); $systemreport = system_report_factory::create(system_report_available::class, context_system::instance()); $this->assertEquals(context_system::instance(), $systemreport->get_context()); $course = $this->getDataGenerator()->create_course(); $contextcourse = \context_course::instance($course->id); $systemreport2 = system_report_factory::create(system_report_available::class, $contextcourse); $this->assertEquals($contextcourse, $systemreport2->get_context()); } /** * Test for get_column */ public function test_get_column(): void { $this->resetAfterTest(); $systemreport = system_report_factory::create(system_report_available::class, context_system::instance()); $column = $systemreport->get_column('user:username'); $this->assertInstanceOf(column::class, $column); $column = $systemreport->get_column('user:nonexistingcolumn'); $this->assertNull($column); } /** * Test for get_filter */ public function test_get_filter(): void { $this->resetAfterTest(); $systemreport = system_report_factory::create(system_report_available::class, context_system::instance(), '', '', 0, ['withfilters' => true]); $filter = $systemreport->get_filter('user:username'); $this->assertInstanceOf(filter::class, $filter); $filter = $systemreport->get_filter('user:nonexistingfilter'); $this->assertNull($filter); } /** * Test for get_report_persistent */ public function test_get_report_persistent(): void { $this->resetAfterTest(); $systemreport = system_report_factory::create(system_report_available::class, context_system::instance()); $persistent = $systemreport->get_report_persistent(); $this->assertEquals(system_report_available::class, $persistent->get('source')); } } report/filter_test.php 0000644 00000016413 15152276235 0011131 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\report; use advanced_testcase; use lang_string; use moodle_exception; use core_reportbuilder\local\filters\text; /** * Unit tests for a report filter * * @package core_reportbuilder * @covers \core_reportbuilder\local\report\filter * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class filter_test extends advanced_testcase { /** * Test getting filter class */ public function test_get_filter_class(): void { $filter = $this->create_filter('username'); $this->assertEquals(text::class, $filter->get_filter_class()); } /** * Test specifying invalid filter class */ public function test_invalid_filter_class(): void { $this->expectException(moodle_exception::class); $this->expectExceptionMessage('Invalid filter (sillyclass)'); new filter('sillyclass', 'username', new lang_string('username'), 'filter_testcase'); } /** * Test getting name */ public function test_get_name(): void { $filter = $this->create_filter('username'); $this->assertEquals('username', $filter->get_name()); } /** * Test getting header */ public function test_get_header(): void { $filter = $this->create_filter('username'); $this->assertEquals('Username', $filter->get_header()); } /** * Test setting header */ public function test_set_header(): void { $filter = $this->create_filter('username') ->set_header(new lang_string('firstname')); $this->assertEquals('First name', $filter->get_header()); } /** * Test getting entity name */ public function test_get_entity_name(): void { $filter = $this->create_filter('username'); $this->assertEquals('filter_testcase', $filter->get_entity_name()); } /** * Test getting unique identifier */ public function test_get_unique_identifier(): void { $filter = $this->create_filter('username'); $this->assertEquals('filter_testcase:username', $filter->get_unique_identifier()); } /** * Test getting field SQL */ public function test_get_field_sql(): void { $filter = $this->create_filter('username', 'u.username'); $this->assertEquals('u.username', $filter->get_field_sql()); } /** * Test getting field params */ public function test_get_field_params(): void { $filter = $this->create_filter('username', 'u.username = :foo', ['foo' => 'bar']); $this->assertEquals(['foo' => 'bar'], $filter->get_field_params()); } /** * Test getting field SQL and params, while providing index for uniqueness */ public function test_get_field_sql_and_params(): void { $filter = $this->create_filter('username', 'u.username = :username AND u.idnumber = :idnumber', ['username' => 'test', 'idnumber' => 'bar']); [$sql, $params] = $filter->get_field_sql_and_params(1); $this->assertEquals('u.username = :username_1 AND u.idnumber = :idnumber_1', $sql); $this->assertEquals(['username_1' => 'test', 'idnumber_1' => 'bar'], $params); } /** * Test adding single join */ public function test_add_join(): void { $filter = $this->create_filter('username', 'u.username'); $this->assertEquals([], $filter->get_joins()); $filter->add_join('JOIN {user} u ON u.id = table.userid'); $this->assertEquals(['JOIN {user} u ON u.id = table.userid'], $filter->get_joins()); } /** * Test adding multiple joins */ public function test_add_joins(): void { $tablejoins = [ "JOIN {course} c2 ON c2.id = c1.id", "JOIN {course} c3 ON c3.id = c1.id", ]; $filter = $this->create_filter('username', 'u.username') ->add_joins($tablejoins); $this->assertEquals($tablejoins, $filter->get_joins()); } /** * Test is available */ public function test_is_available(): void { $filter = $this->create_filter('username', 'u.username'); $this->assertTrue($filter->get_is_available()); $filter->set_is_available(true); $this->assertTrue($filter->get_is_available()); } /** * Test setting filter options */ public function test_set_options(): void { $filter = $this->create_filter('username', 'u.username') ->set_options([1, 2, 3]); $this->assertEquals([1, 2, 3], $filter->get_options()); } /** * Test setting filter options via callback */ public function test_set_options_callback(): void { $filter = $this->create_filter('username', 'u.username') ->set_options_callback(static function() { return 10 * 5; }); $this->assertEquals(50, $filter->get_options()); } /** * Test restricting filter operators */ public function test_limited_operators(): void { $filter = $this->create_filter('username', 'u.username') ->set_limited_operators([ text::IS_EQUAL_TO, text::IS_NOT_EQUAL_TO, ]); $limitedoperators = $filter->restrict_limited_operators([ text::CONTAINS => 'Contains', text::DOES_NOT_CONTAIN => 'Does not contain', text::IS_EQUAL_TO => 'Is equal to', text::IS_NOT_EQUAL_TO => 'Is not equal to', ]); $this->assertEquals([ text::IS_EQUAL_TO => 'Is equal to', text::IS_NOT_EQUAL_TO => 'Is not equal to', ], $limitedoperators); } /** * Test not restricting filter operators */ public function test_unlimited_operators(): void { $filter = $this->create_filter('username', 'u.username'); $operators = [ text::CONTAINS => 'Contains', text::DOES_NOT_CONTAIN => 'Does not contain', ]; // If no operator limit has been set for the filter, then all available operators should be present. $this->assertEquals($operators, $filter->restrict_limited_operators($operators)); } /** * Helper method to create a filter instance * * @param string $name * @param string $fieldsql * @param array $fieldparams * @return filter */ private function create_filter(string $name, string $fieldsql = '', array $fieldparams = []): filter { return new filter(text::class, $name, new lang_string($name), 'filter_testcase', $fieldsql, $fieldparams); } } report/action_test.php 0000644 00000011212 15152276235 0011111 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\report; use advanced_testcase; use lang_string; use moodle_url; use pix_icon; use stdClass; /** * Unit tests for a report action * * @package core_reportbuilder * @covers \core_reportbuilder\local\report\action * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class action_test extends advanced_testcase { /** * Test adding a callback that returns true */ public function test_add_callback_true(): void { $action = $this->create_action() ->add_callback(static function(stdClass $row): bool { return true; }); $this->assertNotNull($action->get_action_link(new stdClass())); } /** * Test adding a callback that returns false */ public function test_add_callback_false(): void { $action = $this->create_action() ->add_callback(static function(stdClass $row): bool { return false; }); $this->assertNull($action->get_action_link(new stdClass())); } /** * Data provider for {@see test_action_title} * * @return array[] */ public function action_title_provider(): array { $title = new lang_string('yes'); return [ 'Specified via constructor' => ['', [], $title], 'Specified via pix icon' => [(string) $title], 'Specified via attributes' => ['', ['title' => $title]], 'Specified via attributes placeholder' => ['', ['title' => ':title'], null, ['title' => $title]], ]; } /** * Test action title is correct * * @param string $pixiconalt * @param array $attributes * @param lang_string|null $title * @param array $row * * @dataProvider action_title_provider */ public function test_action_title( string $pixiconalt, array $attributes = [], ?lang_string $title = null, array $row = [] ): void { $action = new action( new moodle_url('#'), new pix_icon('t/edit', $pixiconalt), $attributes, false, $title ); // Assert correct title appears inside action link, after the icon. $actionlink = $action->get_action_link((object) $row); $this->assertEquals('Yes', $actionlink->text); } /** * Test that action link URL parameters have placeholders replaced */ public function test_get_action_link_url_parameters(): void { $action = $this->create_action(['id' => ':id', 'action' => 'edit']); $actionlink = $action->get_action_link((object) ['id' => 42]); // This is the action URL we expect. $expectedactionurl = (new moodle_url('/', ['id' => 42, 'action' => 'edit']))->out(false); $this->assertEquals($expectedactionurl, $actionlink->url->out(false)); } /** * Test that action link attributes have placeholders replaced */ public function test_get_action_link_attributes(): void { $action = $this->create_action([], ['data-id' => ':id', 'data-action' => 'edit']); $actionlink = $action->get_action_link((object) ['id' => 42]); // We expect each of these attributes to exist. $expectedattributes = [ 'data-id' => 42, 'data-action' => 'edit', ]; foreach ($expectedattributes as $key => $value) { $this->assertEquals($value, $actionlink->attributes[$key]); } } /** * Helper method to create an action instance * * @param array $urlparams * @param array $attributes * @return action */ private function create_action(array $urlparams = [], array $attributes = []): action { return new action( new moodle_url('/', $urlparams), new pix_icon('t/edit', get_string('edit')), $attributes ); } } report/column_test.php 0000644 00000041301 15152276235 0011133 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\report; use advanced_testcase; use coding_exception; use lang_string; use stdClass; use core_reportbuilder\local\helpers\database; /** * Unit tests for a report column * * @package core_reportbuilder * @covers \core_reportbuilder\local\report\column * @copyright 2020 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class column_test extends advanced_testcase { /** * Test column name getter/setter */ public function test_name(): void { $column = $this->create_column('test'); $this->assertEquals('test', $column->get_name()); $this->assertEquals('another', $column ->set_name('another') ->get_name() ); } /** * Test column title getter/setter */ public function test_title(): void { $column = $this->create_column('test', new lang_string('show')); $this->assertEquals('Show', $column->get_title()); $this->assertFalse($column->has_custom_title()); $this->assertEquals('Hide', $column ->set_title(new lang_string('hide')) ->get_title() ); $this->assertTrue($column->has_custom_title()); // Column titles can also be empty. $this->assertEmpty($column ->set_title(null) ->get_title()); } /** * Test entity name getter */ public function test_get_entity_name(): void { $column = $this->create_column('test', null, 'entityname'); $this->assertEquals('entityname', $column->get_entity_name()); } /** * Test getting unique identifier */ public function test_get_unique_identifier(): void { $column = $this->create_column('test', null, 'entityname'); $this->assertEquals('entityname:test', $column->get_unique_identifier()); } /** * Test column type getter/setter */ public function test_type(): void { $column = $this->create_column('test'); $this->assertEquals(column::TYPE_INTEGER, $column ->set_type(column::TYPE_INTEGER) ->get_type()); } /** * Test column default type */ public function test_type_default(): void { $column = $this->create_column('test'); $this->assertEquals(column::TYPE_TEXT, $column->get_type()); } /** * Test column type with invalid value */ public function test_type_invalid(): void { $column = $this->create_column('test'); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Invalid column type'); $column->set_type(-1); } /** * Test adding single join */ public function test_add_join(): void { $column = $this->create_column('test'); $this->assertEquals([], $column->get_joins()); $column->add_join('JOIN {user} u ON u.id = table.userid'); $this->assertEquals(['JOIN {user} u ON u.id = table.userid'], $column->get_joins()); } /** * Test adding multiple joins */ public function test_add_joins(): void { $tablejoins = [ "JOIN {course} c2 ON c2.id = c1.id", "JOIN {course} c3 ON c3.id = c1.id", ]; $column = $this->create_column('test') ->add_joins($tablejoins); $this->assertEquals($tablejoins, $column->get_joins()); } /** * Data provider for {@see test_add_field} * * @return array */ public function add_field_provider(): array { return [ ['foo', '', ['foo AS c1_foo']], ['foo', 'bar', ['foo AS c1_bar']], ['t.foo', '', ['t.foo AS c1_foo']], ['t.foo', 'bar', ['t.foo AS c1_bar']], ]; } /** * Test adding single field, and retrieving it * * @param string $sql * @param string $alias * @param array $expectedselect * * @dataProvider add_field_provider */ public function test_add_field(string $sql, string $alias, array $expectedselect): void { $column = $this->create_column('test') ->set_index(1) ->add_field($sql, $alias); $this->assertEquals($expectedselect, $column->get_fields()); } /** * Test adding params to field, and retrieving them */ public function test_add_field_with_params(): void { [$param0, $param1] = database::generate_param_names(2); $column = $this->create_column('test') ->set_index(1) ->add_field(":{$param0}", 'foo', [$param0 => 'foo']) ->add_field(":{$param1}", 'bar', [$param1 => 'bar']); // Select will look like the following: "p<index>_rbparam<counter>", where index is the column index and counter is // a static value of the report helper class. $fields = $column->get_fields(); $this->assertCount(2, $fields); preg_match('/:(?<paramname>p1_rbparam[\d]+) AS c1_foo/', $fields[0], $matches); $this->assertArrayHasKey('paramname', $matches); $fieldparam0 = $matches['paramname']; preg_match('/:(?<paramname>p1_rbparam[\d]+) AS c1_bar/', $fields[1], $matches); $this->assertArrayHasKey('paramname', $matches); $fieldparam1 = $matches['paramname']; // Ensure column parameters have been renamed appropriately. $this->assertEquals([ $fieldparam0 => 'foo', $fieldparam1 => 'bar', ], $column->get_params()); } /** * Test adding field with alias as part of SQL throws an exception */ public function test_add_field_alias_in_sql(): void { $column = $this->create_column('test') ->set_index(1); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Column alias must be passed as a separate argument'); $column->add_field('foo AS bar'); } /** * Test adding field with complex SQL without an alias throws an exception */ public function test_add_field_complex_without_alias(): void { global $DB; $column = $this->create_column('test') ->set_index(1); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Complex columns must have an alias'); $column->add_field($DB->sql_concat('foo', 'bar')); } /** * Data provider for {@see test_add_fields} * * @return array */ public function add_fields_provider(): array { return [ ['t.foo', ['t.foo AS c1_foo']], ['t.foo bar', ['t.foo AS c1_bar']], ['t.foo AS bar', ['t.foo AS c1_bar']], ['t.foo1, t.foo2 bar, t.foo3 AS baz', ['t.foo1 AS c1_foo1', 't.foo2 AS c1_bar', 't.foo3 AS c1_baz']], ]; } /** * Test adding fields to a column, and retrieving them * * @param string $sql * @param array $expectedselect * * @dataProvider add_fields_provider */ public function test_add_fields(string $sql, array $expectedselect): void { $column = $this->create_column('test') ->set_index(1) ->add_fields($sql); $this->assertEquals($expectedselect, $column->get_fields()); } /** * Test column alias */ public function test_column_alias(): void { $column = $this->create_column('test') ->set_index(1) ->add_fields('t.foo, t.bar'); $this->assertEquals('c1_foo', $column->get_column_alias()); } /** * Test column alias with a field containing an alias */ public function test_column_alias_with_field_alias(): void { $column = $this->create_column('test') ->set_index(1) ->add_field('COALESCE(t.foo, t.bar)', 'lionel'); $this->assertEquals('c1_lionel', $column->get_column_alias()); } /** * Test alias of column without any fields throws exception */ public function test_column_alias_no_fields(): void { $column = $this->create_column('test'); $this->expectException(coding_exception::class); $this->expectExceptionMessage('Column ' . $column->get_unique_identifier() . ' contains no fields'); $column->add_field($column->get_column_alias()); } /** * Test setting column group by SQL */ public function test_set_groupby_sql(): void { $column = $this->create_column('test') ->set_index(1) ->add_field('COALESCE(t.foo, t.bar)', 'lionel') ->set_groupby_sql('t.id'); $this->assertEquals(['t.id'], $column->get_groupby_sql()); } /** * Test getting default column group by SQL */ public function test_get_groupby_sql(): void { global $DB; $column = $this->create_column('test') ->set_index(1) ->add_fields('t.foo, t.bar'); // The behaviour of this method differs due to DB limitations. $usealias = in_array($DB->get_dbfamily(), ['mysql', 'postgres']); if ($usealias) { $expected = ['c1_foo', 'c1_bar']; } else { $expected = ['t.foo', 't.bar']; } $this->assertEquals($expected, $column->get_groupby_sql()); } /** * Data provider for {@see test_get_default_value} and {@see test_format_value} * * @return array[] */ public function column_type_provider(): array { return [ [column::TYPE_INTEGER, 42], [column::TYPE_TEXT, 'Hello'], [column::TYPE_TIMESTAMP, HOURSECS], [column::TYPE_BOOLEAN, 1, true], [column::TYPE_FLOAT, 1.23], [column::TYPE_LONGTEXT, 'Amigos'], ]; } /** * Test default value is returned from selected values, with correct type * * @param int $columntype * @param mixed $value * @param mixed|null $expected Expected value, or null to indicate it should be identical to value * * @dataProvider column_type_provider */ public function test_get_default_value(int $columntype, $value, $expected = null): void { $defaultvalue = column::get_default_value([ 'value' => $value, 'foo' => 'bar', ], $columntype); $this->assertSame($expected ?? $value, $defaultvalue); } /** * Test that column value is returned correctly, with correct type * * @param int $columntype * @param mixed $value * @param mixed|null $expected Expected value, or null to indicate it should be identical to value * * @dataProvider column_type_provider */ public function test_format_value(int $columntype, $value, $expected = null): void { $column = $this->create_column('test') ->set_index(1) ->set_type($columntype) ->add_field('t.foo'); $this->assertSame($expected ?? $value, $column->format_value([ 'c1_foo' => $value, ])); } /** * Test that column value with callback is returned */ public function test_format_value_callback(): void { $column = $this->create_column('test') ->set_index(1) ->add_field('t.foo') ->set_type(column::TYPE_INTEGER) ->add_callback(static function(int $value, stdClass $values) { return $value * 2; }); $this->assertEquals(84, $column->format_value([ 'c1_bar' => 10, 'c1_foo' => 42, ])); } /** * Test that column value with callback (using all fields) is returned */ public function test_format_value_callback_fields(): void { $column = $this->create_column('test') ->set_index(1) ->add_fields('t.foo, t.baz') ->set_type(column::TYPE_INTEGER) ->add_callback(static function(int $value, stdClass $values) { return $values->foo + $values->baz; }); $this->assertEquals(60, $column->format_value([ 'c1_bar' => 10, 'c1_foo' => 42, 'c1_baz' => 18, ])); } /** * Test that column value with callback (using arguments) is returned */ public function test_format_value_callback_arguments(): void { $column = $this->create_column('test') ->set_index(1) ->add_field('t.foo') ->set_type(column::TYPE_INTEGER) ->add_callback(static function(int $value, stdClass $values, int $argument) { return $value - $argument; }, 10); $this->assertEquals(32, $column->format_value([ 'c1_bar' => 10, 'c1_foo' => 42, ])); } /** * Test adding multiple callbacks to a column */ public function test_add_multiple_callback(): void { $column = $this->create_column('test') ->set_index(1) ->add_field('t.foo') ->set_type(column::TYPE_TEXT) ->add_callback(static function(string $value): string { return strrev($value); }) ->add_callback(static function(string $value): string { return strtoupper($value); }); $this->assertEquals('LIONEL', $column->format_value([ 'c1_foo' => 'lenoil', ])); } /** * Test that setting column callback overwrites previous callbacks */ public function test_set_callback(): void { $column = $this->create_column('test') ->set_index(1) ->add_field('t.foo') ->set_type(column::TYPE_TEXT) ->add_callback(static function(string $value): string { return strrev($value); }) ->set_callback(static function(string $value): string { return strtoupper($value); }); $this->assertEquals('LENOIL', $column->format_value([ 'c1_foo' => 'lenoil', ])); } /** * Test is sortable */ public function test_is_sortable(): void { $column = $this->create_column('test'); $this->assertFalse($column->get_is_sortable()); $column->set_is_sortable(true); $this->assertTrue($column->get_is_sortable()); } /** * Test retrieving sort fields */ public function test_get_sortfields(): void { $column = $this->create_column('test') ->set_index(1) ->add_fields('t.foo, t.bar, t.baz') ->set_is_sortable(true, ['t.baz', 't.bar']); $this->assertEquals(['c1_baz', 'c1_bar'], $column->get_sort_fields()); } /** * Test retrieving sort fields when an aliased field is set as sortable */ public function test_get_sortfields_with_field_alias(): void { $column = $this->create_column('test') ->set_index(1) ->add_field('t.foo') ->add_field('COALESCE(t.foo, t.bar)', 'lionel') ->set_is_sortable(true, ['lionel']); $this->assertEquals(['c1_lionel'], $column->get_sort_fields()); } /** * Test retrieving sort fields when an unknown field is set as sortable */ public function test_get_sortfields_unknown_field(): void { $column = $this->create_column('test') ->set_index(1) ->add_fields('t.foo') ->set_is_sortable(true, ['t.baz']); $this->assertEquals(['t.baz'], $column->get_sort_fields()); } /** * Test is available */ public function test_is_available(): void { $column = $this->create_column('test'); $this->assertTrue($column->get_is_available()); $column->set_is_available(true); $this->assertTrue($column->get_is_available()); } /** * Helper method to create a column instance * * @param string $name * @param lang_string|null $title * @param string $entityname * @return column */ private function create_column(string $name, ?lang_string $title = null, string $entityname = 'column_testcase'): column { return new column($name, $title, $entityname); } } helpers/custom_fields_test.php 0000644 00000025552 15152276235 0012637 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\helpers; use core_customfield_generator; use core_reportbuilder_generator; use core_reportbuilder_testcase; use core_reportbuilder\local\entities\course; use core_reportbuilder\local\filters\boolean_select; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\filters\text; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use core_course\reportbuilder\datasource\courses; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** * Unit tests for custom fields helper * * @package core_reportbuilder * @covers \core_reportbuilder\local\helpers\custom_fields * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class custom_fields_test extends core_reportbuilder_testcase { /** * Generate custom fields, one of each type * * @return custom_fields */ private function generate_customfields(): custom_fields { /** @var core_customfield_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_customfield'); $category = $generator->create_category([ 'component' => 'core_course', 'area' => 'course', 'itemid' => 0, 'contextid' => \context_system::instance()->id ]); $generator->create_field( ['categoryid' => $category->get('id'), 'type' => 'text', 'name' => 'Text', 'shortname' => 'text']); $generator->create_field( ['categoryid' => $category->get('id'), 'type' => 'checkbox', 'name' => 'Checkbox', 'shortname' => 'checkbox']); $generator->create_field( ['categoryid' => $category->get('id'), 'type' => 'date', 'name' => 'Date', 'shortname' => 'date']); $generator->create_field( ['categoryid' => $category->get('id'), 'type' => 'select', 'name' => 'Select', 'shortname' => 'select', 'configdata' => ['options' => "Cat\nDog", 'defaultvalue' => 'Cat']]); $courseentity = new course(); $coursealias = $courseentity->get_table_alias('course'); // Create an instance of the customfields helper. return new custom_fields($coursealias . '.id', $courseentity->get_entity_name(), 'core_course', 'course'); } /** * Test for get_columns */ public function test_get_columns(): void { $this->resetAfterTest(); $customfields = $this->generate_customfields(); $columns = $customfields->get_columns(); $this->assertCount(4, $columns); $this->assertContainsOnlyInstancesOf(column::class, $columns); [$column0, $column1, $column2, $column3] = $columns; $this->assertEqualsCanonicalizing(['Text', 'Checkbox', 'Date', 'Select'], [$column0->get_title(), $column1->get_title(), $column2->get_title(), $column3->get_title()]); $this->assertEquals(column::TYPE_TEXT, $column0->get_type()); $this->assertEquals('course', $column0->get_entity_name()); $this->assertStringStartsWith('LEFT JOIN {customfield_data}', $column0->get_joins()[0]); // Column of type TEXT is sortable. $this->assertTrue($column0->get_is_sortable()); } /** * Test for add_join */ public function test_add_join(): void { $this->resetAfterTest(); $customfields = $this->generate_customfields(); $columns = $customfields->get_columns(); $this->assertCount(1, ($columns[0])->get_joins()); $customfields->add_join('JOIN {test} t ON t.id = id'); $columns = $customfields->get_columns(); $this->assertCount(2, ($columns[0])->get_joins()); } /** * Test for add_joins */ public function test_add_joins(): void { $this->resetAfterTest(); $customfields = $this->generate_customfields(); $columns = $customfields->get_columns(); $this->assertCount(1, ($columns[0])->get_joins()); $customfields->add_joins(['JOIN {test} t ON t.id = id', 'JOIN {test2} t2 ON t2.id = id']); $columns = $customfields->get_columns(); $this->assertCount(3, ($columns[0])->get_joins()); } /** * Test for get_filters */ public function test_get_filters(): void { $this->resetAfterTest(); $customfields = $this->generate_customfields(); $filters = $customfields->get_filters(); $this->assertCount(4, $filters); $this->assertContainsOnlyInstancesOf(filter::class, $filters); [$filter0, $filter1, $filter2, $filter3] = $filters; $this->assertEqualsCanonicalizing(['Text', 'Checkbox', 'Date', 'Select'], [$filter0->get_header(), $filter1->get_header(), $filter2->get_header(), $filter3->get_header()]); } /** * Test that adding custom field columns to a report returns expected values */ public function test_custom_report_content(): void { $this->resetAfterTest(); $this->generate_customfields(); $course = $this->getDataGenerator()->create_course(['customfields' => [ ['shortname' => 'text', 'value' => 'Hello'], ['shortname' => 'checkbox', 'value' => true], ['shortname' => 'date', 'value' => 1669852800], ['shortname' => 'select', 'value' => 2], ]]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Courses', 'source' => courses::class, 'default' => 0]); // Add user profile field columns to the report. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:customfield_text']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:customfield_checkbox']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:customfield_date']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:customfield_select']); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ $course->fullname, 'Hello', 'Yes', userdate(1669852800), 'Dog' ], array_values($content[0])); } /** * Data provider for {@see test_custom_report_filter} * * @return array[] */ public function custom_report_filter_provider(): array { return [ 'Filter by text custom field' => ['course:customfield_text', [ 'course:customfield_text_operator' => text::IS_EQUAL_TO, 'course:customfield_text_value' => 'Hello', ], true], 'Filter by text custom field (no match)' => ['course:customfield_text', [ 'course:customfield_text_operator' => text::IS_EQUAL_TO, 'course:customfield_text_value' => 'Goodbye', ], false], 'Filter by checkbox custom field' => ['course:customfield_checkbox', [ 'course:customfield_checkbox_operator' => boolean_select::CHECKED, ], true], 'Filter by checkbox custom field (no match)' => ['course:customfield_checkbox', [ 'course:customfield_checkbox_operator' => boolean_select::NOT_CHECKED, ], false], 'Filter by date custom field' => ['course:customfield_date', [ 'course:customfield_date_operator' => date::DATE_RANGE, 'course:customfield_date_from' => 1622502000, ], true], 'Filter by date custom field (no match)' => ['course:customfield_date', [ 'course:customfield_date_operator' => date::DATE_RANGE, 'course:customfield_date_to' => 1622502000, ], false], 'Filter by select custom field' => ['course:customfield_select', [ 'course:customfield_select_operator' => select::EQUAL_TO, 'course:customfield_select_value' => 2, ], true], 'Filter by select custom field (no match)' => ['course:customfield_select', [ 'course:customfield_select_operator' => select::EQUAL_TO, 'course:customfield_select_value' => 1, ], false], ]; } /** * Test filtering report by custom fields * * @param string $filtername * @param array $filtervalues * @param bool $expectmatch * * @dataProvider custom_report_filter_provider */ public function test_custom_report_filter(string $filtername, array $filtervalues, bool $expectmatch): void { $this->resetAfterTest(); $this->generate_customfields(); $course = $this->getDataGenerator()->create_course(['customfields' => [ ['shortname' => 'text', 'value' => 'Hello'], ['shortname' => 'checkbox', 'value' => true], ['shortname' => 'date', 'value' => 1669852800], ['shortname' => 'select', 'value' => 2], ]]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single column, and given filter. $report = $generator->create_report(['name' => 'Users', 'source' => courses::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'course:fullname']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); if ($expectmatch) { $this->assertCount(1, $content); $this->assertEquals($course->fullname, reset($content[0])); } else { $this->assertEmpty($content); } } } helpers/user_filter_manager_test.php 0000644 00000016053 15152276235 0014010 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\helpers; use advanced_testcase; /** * Unit tests for the user filter helper * * @package core_reportbuilder * @covers \core_reportbuilder\local\helpers\user_filter_manager * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_filter_manager_test extends advanced_testcase { /** * Helper method to return all user preferences for filters - based on the current storage backend using the same * * @return array */ private function get_filter_preferences(): array { return array_filter(get_user_preferences(), static function(string $key): bool { return strpos($key, 'reportbuilder-report-') === 0; }, ARRAY_FILTER_USE_KEY); } /** * Data provider for {@see test_get} * * @return array */ public function get_provider(): array { return [ 'Small value' => ['foo'], 'Large value' => [str_repeat('A', 4000)], 'Empty value' => [''], ]; } /** * Test getting filter values * * @param string $value * * @dataProvider get_provider */ public function test_get(string $value): void { $this->resetAfterTest(); $values = [ 'entity:filter_name' => $value, ]; user_filter_manager::set(5, $values); // Make sure we get the same value back. $this->assertEquals($values, user_filter_manager::get(5)); } /** * Test getting filter values that once spanned multiple chunks */ public function test_get_large_to_small(): void { $this->resetAfterTest(); // Set a large initial filter value. user_filter_manager::set(5, [ 'longvalue' => str_repeat('ABCD', 1000), ]); // Sanity check, there should be 4 (because 4000 characters plus some JSON encoding requires that many chunks). $preferences = $this->get_filter_preferences(); $this->assertCount(4, $preferences); $values = [ 'longvalue' => 'ABCD', ]; user_filter_manager::set(5, $values); // Make sure we get the same value back. $this->assertEquals($values, user_filter_manager::get(5)); // Everything should now fit in a single filter preference. $preferences = $this->get_filter_preferences(); $this->assertCount(1, $preferences); } /** * Test getting filter values that haven't been set */ public function test_get_empty(): void { $this->assertEquals([], user_filter_manager::get(5)); } /** * Data provider for {@see test_reset_all} * * @return array */ public function reset_all_provider(): array { return [ 'Small value' => ['foo'], 'Large value' => [str_repeat('A', 4000)], 'Empty value' => [''], ]; } /** * Test resetting all filter values * * @param string $value * * @dataProvider reset_all_provider */ public function test_reset_all(string $value): void { $this->resetAfterTest(); user_filter_manager::set(5, [ 'entity:filter_name' => $value ]); $reset = user_filter_manager::reset_all(5); $this->assertTrue($reset); // We should get an empty array back. $this->assertEquals([], user_filter_manager::get(5)); // All filter preferences should be removed. $this->assertEmpty($this->get_filter_preferences()); } /** * Test resetting single filter values */ public function test_reset_single(): void { $this->resetAfterTest(); user_filter_manager::set(5, [ 'entity:filter_name' => 'foo', 'entity:filter_value' => 'bar', 'entity:other_name' => 'baz', 'entity:other_value' => 'bax', ]); $reset = user_filter_manager::reset_single(5, 'entity:other'); $this->assertTrue($reset); $this->assertEquals([ 'entity:filter_name' => 'foo', 'entity:filter_value' => 'bar', ], user_filter_manager::get(5)); } /** * Test merging filter values */ public function test_merge(): void { $this->resetAfterTest(); $values = [ 'entity:filter_name' => 'foo', 'entity:filter_value' => 'bar', 'entity:filter2_name' => 'tree', 'entity:filter2_value' => 'house', ]; // Make sure we get the same value back. user_filter_manager::set(5, $values); $this->assertEqualsCanonicalizing($values, user_filter_manager::get(5)); user_filter_manager::merge(5, [ 'entity:filter_name' => 'twotimesfoo', 'entity:filter_value' => 'twotimesbar', ]); // Make sure that both values have been changed and the other values have not been modified. $expected = [ 'entity:filter_name' => 'twotimesfoo', 'entity:filter_value' => 'twotimesbar', 'entity:filter2_name' => 'tree', 'entity:filter2_value' => 'house', ]; $this->assertEqualsCanonicalizing($expected, user_filter_manager::get(5)); } /** * Test to get all filters from a given user */ public function test_get_all_for_user(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $filtervalues1 = [ 'entity:filter_name' => 'foo', 'entity:filter_value' => 'bar', 'entity:other_name' => 'baz', 'entity:other_value' => 'bax', ]; user_filter_manager::set(5, $filtervalues1); $filtervalues2 = [ 'entity:filter_name' => 'blue', 'entity:filter_value' => 'red', ]; user_filter_manager::set(9, $filtervalues2); $this->setAdminUser(); $values = user_filter_manager::get_all_for_user((int)$user->id); $this->assertEqualsCanonicalizing([$filtervalues1, $filtervalues2], [reset($values), end($values)]); // Check for a user with no filters. $user2 = $this->getDataGenerator()->create_user(); $values = user_filter_manager::get_all_for_user((int)$user2->id); $this->assertEmpty($values); } } helpers/database_test.php 0000644 00000012342 15152276235 0011534 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\helpers; use advanced_testcase; use coding_exception; use core_user; /** * Unit tests for the database helper class * * @package core_reportbuilder * @covers \core_reportbuilder\local\helpers\database * @copyright 2020 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class database_test extends advanced_testcase { /** * Test generating alias */ public function test_generate_alias(): void { $this->assertMatchesRegularExpression('/^rbalias(\d+)$/', database::generate_alias()); } /** * Test generating multiple aliases */ public function test_generate_aliases(): void { $aliases = database::generate_aliases(3); $this->assertCount(3, $aliases); [$aliasone, $aliastwo, $aliasthree] = $aliases; // Ensure they are different. $this->assertNotEquals($aliasone, $aliastwo); $this->assertNotEquals($aliasone, $aliasthree); $this->assertNotEquals($aliastwo, $aliasthree); } /** * Test generating parameter name */ public function test_generate_param_name(): void { $this->assertMatchesRegularExpression('/^rbparam(\d+)$/', database::generate_param_name()); } /** * Test generating multiple parameter names */ public function test_generate_param_names(): void { $params = database::generate_param_names(3); $this->assertCount(3, $params); [$paramone, $paramtwo, $paramthree] = $params; // Ensure they are different. $this->assertNotEquals($paramone, $paramtwo); $this->assertNotEquals($paramone, $paramthree); $this->assertNotEquals($paramtwo, $paramthree); } /** * Test parameter validation */ public function test_validate_params(): void { [$paramone, $paramtwo] = database::generate_param_names(2); $params = [ $paramone => 1, $paramtwo => 2, ]; $this->assertTrue(database::validate_params($params)); } /** * Test parameter validation for invalid parameters */ public function test_validate_params_invalid(): void { $params = [ database::generate_param_name() => 1, 'invalidfoo' => 2, 'invalidbar' => 4, ]; $this->expectException(coding_exception::class); $this->expectExceptionMessage('Invalid parameter names (invalidfoo, invalidbar)'); database::validate_params($params); } /** * Generate aliases and parameters and confirm they can be used within a query */ public function test_generated_data_in_query(): void { global $DB; // Unique aliases. [ $usertablealias, $userfieldalias, ] = database::generate_aliases(2); // Unique parameters. [ $paramuserid, $paramuserdeleted, ] = database::generate_param_names(2); // Simple query to retrieve the admin user. $sql = "SELECT {$usertablealias}.id AS {$userfieldalias} FROM {user} {$usertablealias} WHERE {$usertablealias}.id = :{$paramuserid} AND {$usertablealias}.deleted = :{$paramuserdeleted}"; $admin = core_user::get_user_by_username('admin'); $params = [ $paramuserid => $admin->id, $paramuserdeleted => 0, ]; $record = $DB->get_record_sql($sql, $params); $this->assertEquals($admin->id, $record->{$userfieldalias}); } /** * Test replacement of parameter names within SQL statements */ public function test_sql_replace_parameter_names(): void { global $DB; // Predefine parameter names, to ensure they don't overwrite each other. [$param0, $param1, $param10] = ['rbparam0', 'rbparam1', 'rbparam10']; $sql = "SELECT :{$param0} AS field0, :{$param1} AS field1, :{$param10} AS field10" . $DB->sql_null_from_clause(); $sql = database::sql_replace_parameter_names($sql, [$param0, $param1, $param10], static function(string $param): string { return "prefix_{$param}"; }); $record = $DB->get_record_sql($sql, [ "prefix_{$param0}" => 'Zero', "prefix_{$param1}" => 'One', "prefix_{$param10}" => 'Ten', ]); $this->assertEquals((object) [ 'field0' => 'Zero', 'field1' => 'One', 'field10' => 'Ten', ], $record); } } helpers/audience_test.php 0000644 00000026555 15152276235 0011560 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\helpers; use advanced_testcase; use context_system; use core_reportbuilder_generator; use core_reportbuilder\reportbuilder\audience\manual; use core_user\reportbuilder\datasource\users; /** * Unit tests for audience helper * * @package core_reportbuilder * @covers \core_reportbuilder\local\helpers\audience * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class audience_test extends advanced_testcase { /** * Test reports list is empty for a normal user without any audience records configured */ public function test_reports_list_no_access(): void { $this->resetAfterTest(); $reports = audience::user_reports_list(); $this->assertEmpty($reports); } /** * Test get_base_records() */ public function test_get_base_records(): void { $this->resetAfterTest(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Report with no audiences. $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $baserecords = audience::get_base_records($report->get('id')); $this->assertEmpty($baserecords); // Create a couple of manual audience types. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $audience1 = $generator->create_audience([ 'reportid' => $report->get('id'), 'classname' => manual::class, 'configdata' => ['users' => [$user1->id, $user2->id]], ]); $user3 = $this->getDataGenerator()->create_user(); $audience2 = $generator->create_audience([ 'reportid' => $report->get('id'), 'classname' => manual::class, 'configdata' => ['users' => [$user3->id]], ]); $baserecords = audience::get_base_records($report->get('id')); $this->assertCount(2, $baserecords); $this->assertContainsOnlyInstancesOf(manual::class, $baserecords); // Set invalid classname of first audience, should be excluded in subsequent request. $audience1->get_persistent()->set('classname', '\invalid')->save(); $baserecords = audience::get_base_records($report->get('id')); $this->assertCount(1, $baserecords); $baserecord = reset($baserecords); $this->assertInstanceOf(manual::class, $baserecord); $this->assertEquals($audience2->get_persistent()->get('id'), $baserecord->get_persistent()->get('id')); } /** * Test get_allowed_reports() */ public function test_get_allowed_reports(): void { $this->resetAfterTest(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); self::setUser($user1); // No reports. $reports = audience::get_allowed_reports(); $this->assertEmpty($reports); $report1 = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $report2 = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $report3 = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); // Reports with no audiences set. $reports = audience::get_allowed_reports(); $this->assertEmpty($reports); $generator->create_audience([ 'reportid' => $report1->get('id'), 'classname' => manual::class, 'configdata' => ['users' => [$user1->id, $user2->id]], ]); $generator->create_audience([ 'reportid' => $report2->get('id'), 'classname' => manual::class, 'configdata' => ['users' => [$user2->id]], ]); $generator->create_audience([ 'reportid' => $report3->get('id'), 'classname' => manual::class, 'configdata' => ['users' => [$user1->id]], ]); // Purge cache, to ensure allowed reports are re-calculated. audience::purge_caches(); $reports = audience::get_allowed_reports(); $this->assertEqualsCanonicalizing([$report1->get('id'), $report3->get('id')], $reports); // User2 can access report1 and report2. $reports = audience::get_allowed_reports((int) $user2->id); $this->assertEqualsCanonicalizing([$report1->get('id'), $report2->get('id')], $reports); } /** * Test user_reports_list() */ public function test_user_reports_list(): void { $this->resetAfterTest(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); self::setUser($user1); $reports = audience::user_reports_list(); $this->assertEmpty($reports); $report1 = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $report2 = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $report3 = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $generator->create_audience([ 'reportid' => $report1->get('id'), 'classname' => manual::class, 'configdata' => ['users' => [$user1->id, $user2->id]], ]); $generator->create_audience([ 'reportid' => $report2->get('id'), 'classname' => manual::class, 'configdata' => ['users' => [$user2->id]], ]); $generator->create_audience([ 'reportid' => $report3->get('id'), 'classname' => manual::class, 'configdata' => ['users' => [$user1->id]], ]); // Purge cache, to ensure allowed reports are re-calculated. audience::purge_caches(); // User1 can access report1 and report3. $reports = audience::user_reports_list(); $this->assertEqualsCanonicalizing([$report1->get('id'), $report3->get('id')], $reports); // User2 can access report1 and report2. $reports = audience::user_reports_list((int) $user2->id); $this->assertEqualsCanonicalizing([$report1->get('id'), $report2->get('id')], $reports); // User3 can not access any report. $reports = audience::user_reports_list((int) $user3->id); $this->assertEmpty($reports); } /** * Test retrieving full list of reports that user can access */ public function test_user_reports_list_access_sql(): void { global $DB; $this->resetAfterTest(); $userone = $this->getDataGenerator()->create_user(); $usertwo = $this->getDataGenerator()->create_user(); $userthree = $this->getDataGenerator()->create_user(); $userfour = $this->getDataGenerator()->create_user(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Manager role gives users one and two capability to create own reports. $managerrole = $DB->get_field('role', 'id', ['shortname' => 'manager']); role_assign($managerrole, $userone->id, context_system::instance()); role_assign($managerrole, $usertwo->id, context_system::instance()); // Admin creates a report, no audience. $this->setAdminUser(); $useradminreport = $generator->create_report(['name' => 'Admin report', 'source' => users::class]); // User one creates a report, adds users two and three to audience. $this->setUser($userone); $useronereport = $generator->create_report(['name' => 'User one report', 'source' => users::class]); $generator->create_audience(['reportid' => $useronereport->get('id'), 'classname' => manual::class, 'configdata' => [ 'users' => [$usertwo->id, $userthree->id], ]]); // User two creates a report, no audience. $this->setUser($usertwo); $usertworeport = $generator->create_report(['name' => 'User two report', 'source' => users::class]); // Admin user sees all reports. $this->setAdminUser(); [$where, $params] = audience::user_reports_list_access_sql('r'); $reports = $DB->get_fieldset_sql("SELECT r.id FROM {reportbuilder_report} r WHERE {$where}", $params); $this->assertEqualsCanonicalizing([ $useradminreport->get('id'), $useronereport->get('id'), $usertworeport->get('id'), ], $reports); // User one sees only the report they created. [$where, $params] = audience::user_reports_list_access_sql('r', (int) $userone->id); $reports = $DB->get_fieldset_sql("SELECT r.id FROM {reportbuilder_report} r WHERE {$where}", $params); $this->assertEquals([$useronereport->get('id')], $reports); // User two see the report they created and the one they are in the audience of. [$where, $params] = audience::user_reports_list_access_sql('r', (int) $usertwo->id); $reports = $DB->get_fieldset_sql("SELECT r.id FROM {reportbuilder_report} r WHERE {$where}", $params); $this->assertEqualsCanonicalizing([$useronereport->get('id'), $usertworeport->get('id')], $reports); // User three sees the report they are in the audience of. [$where, $params] = audience::user_reports_list_access_sql('r', (int) $userthree->id); $reports = $DB->get_fieldset_sql("SELECT r.id FROM {reportbuilder_report} r WHERE {$where}", $params); $this->assertEquals([$useronereport->get('id')], $reports); // User four sees no reports. [$where, $params] = audience::user_reports_list_access_sql('r', (int) $userfour->id); $reports = $DB->get_fieldset_sql("SELECT r.id FROM {reportbuilder_report} r WHERE {$where}", $params); $this->assertEmpty($reports); } } helpers/user_profile_fields_test.php 0000644 00000035365 15152276235 0014026 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\helpers; use core_reportbuilder_generator; use core_reportbuilder_testcase; use core_reportbuilder\local\entities\user; use core_reportbuilder\local\filters\boolean_select; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\filters\text; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use core_user\reportbuilder\datasource\users; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** * Unit tests for user profile fields helper * * @package core_reportbuilder * @covers \core_reportbuilder\local\helpers\user_profile_fields * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_profile_fields_test extends core_reportbuilder_testcase { /** * Generate custom profile fields, one of each type * * @return user_profile_fields */ private function generate_userprofilefields(): user_profile_fields { $this->getDataGenerator()->create_custom_profile_field([ 'shortname' => 'checkbox', 'name' => 'Checkbox field', 'datatype' => 'checkbox']); $this->getDataGenerator()->create_custom_profile_field([ 'shortname' => 'datetime', 'name' => 'Date field', 'datatype' => 'datetime', 'param2' => 2022, 'param3' => 0]); $this->getDataGenerator()->create_custom_profile_field([ 'shortname' => 'menu', 'name' => 'Menu field', 'datatype' => 'menu', 'param1' => "Cat\nDog"]); $this->getDataGenerator()->create_custom_profile_field([ 'shortname' => 'Social', 'name' => 'msn', 'datatype' => 'social', 'param1' => 'msn']); $this->getDataGenerator()->create_custom_profile_field([ 'shortname' => 'text', 'name' => 'Text field', 'datatype' => 'text']); $this->getDataGenerator()->create_custom_profile_field([ 'shortname' => 'textarea', 'name' => 'Textarea field', 'datatype' => 'textarea']); $userentity = new user(); $useralias = $userentity->get_table_alias('user'); // Create an instance of the userprofilefield helper. return new user_profile_fields("$useralias.id", $userentity->get_entity_name()); } /** * Test for get_columns */ public function test_get_columns(): void { $this->resetAfterTest(); $userentity = new user(); $useralias = $userentity->get_table_alias('user'); // Get pre-existing user profile fields. $initialuserprofilefields = new user_profile_fields("$useralias.id", $userentity->get_entity_name()); $initialcolumns = $initialuserprofilefields->get_columns(); $initialcolumntitles = array_map(static function(column $column): string { return $column->get_title(); }, $initialcolumns); $initialcolumntypes = array_map(static function(column $column): int { return $column->get_type(); }, $initialcolumns); // Add new custom profile fields. $userprofilefields = $this->generate_userprofilefields(); $columns = $userprofilefields->get_columns(); // Columns count should be equal to start + 6. $this->assertCount(count($initialcolumns) + 6, $columns); $this->assertContainsOnlyInstancesOf(column::class, $columns); // Assert column titles. $columntitles = array_map(static function(column $column): string { return $column->get_title(); }, $columns); $expectedcolumntitles = array_merge($initialcolumntitles, [ 'Checkbox field', 'Date field', 'Menu field', 'MSN ID', 'Text field', 'Textarea field', ]); $this->assertEquals($expectedcolumntitles, $columntitles); // Assert column types. $columntypes = array_map(static function(column $column): int { return $column->get_type(); }, $columns); $expectedcolumntypes = array_merge($initialcolumntypes, [ column::TYPE_BOOLEAN, column::TYPE_TIMESTAMP, column::TYPE_TEXT, column::TYPE_TEXT, column::TYPE_TEXT, column::TYPE_LONGTEXT, ]); $this->assertEquals($expectedcolumntypes, $columntypes); } /** * Test for add_join */ public function test_add_join(): void { $this->resetAfterTest(); $userprofilefields = $this->generate_userprofilefields(); $columns = $userprofilefields->get_columns(); $this->assertCount(1, ($columns[0])->get_joins()); $userprofilefields->add_join('JOIN {test} t ON t.id = id'); $columns = $userprofilefields->get_columns(); $this->assertCount(2, ($columns[0])->get_joins()); } /** * Test for add_joins */ public function test_add_joins(): void { $this->resetAfterTest(); $userprofilefields = $this->generate_userprofilefields(); $columns = $userprofilefields->get_columns(); $this->assertCount(1, ($columns[0])->get_joins()); $userprofilefields->add_joins(['JOIN {test} t ON t.id = id', 'JOIN {test2} t2 ON t2.id = id']); $columns = $userprofilefields->get_columns(); $this->assertCount(3, ($columns[0])->get_joins()); } /** * Test for get_filters */ public function test_get_filters(): void { $this->resetAfterTest(); $userentity = new user(); $useralias = $userentity->get_table_alias('user'); // Get pre-existing user profile fields. $initialuserprofilefields = new user_profile_fields("$useralias.id", $userentity->get_entity_name()); $initialfilters = $initialuserprofilefields->get_filters(); $initialfilterheaders = array_map(static function(filter $filter): string { return $filter->get_header(); }, $initialfilters); // Add new custom profile fields. $userprofilefields = $this->generate_userprofilefields(); $filters = $userprofilefields->get_filters(); // Filters count should be equal to start + 6. $this->assertCount(count($initialfilters) + 6, $filters); $this->assertContainsOnlyInstancesOf(filter::class, $filters); // Assert filter headers. $filterheaders = array_map(static function(filter $filter): string { return $filter->get_header(); }, $filters); $expectedfilterheaders = array_merge($initialfilterheaders, [ 'Checkbox field', 'Date field', 'Menu field', 'MSN ID', 'Text field', 'Textarea field', ]); $this->assertEquals($expectedfilterheaders, $filterheaders); } /** * Test that adding user profile field columns to a report returns expected values */ public function test_custom_report_content(): void { $this->resetAfterTest(); $userprofilefields = $this->generate_userprofilefields(); // Create test subject with user profile fields content. $user = $this->getDataGenerator()->create_user([ 'firstname' => 'Zebedee', 'profile_field_checkbox' => true, 'profile_field_datetime' => '2021-12-09', 'profile_field_menu' => 'Cat', 'profile_field_Social' => 12345, 'profile_field_text' => 'Hello', 'profile_field_textarea' => 'Goodbye', ]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); // Add user profile field columns to the report. $firstname = $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:profilefield_checkbox']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:profilefield_datetime']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:profilefield_menu']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:profilefield_social']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:profilefield_text']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:profilefield_textarea']); // Sort the report, Admin -> Zebedee for consistency. report::toggle_report_column_sorting($report->get('id'), $firstname->get('id'), true); $content = $this->get_custom_report_content($report->get('id')); $this->assertEquals([ [ 'c0_firstname' => 'Admin', 'c1_data' => '', 'c2_data' => 'Not set', 'c3_data' => '', 'c4_data' => '', 'c5_data' => '', 'c6_data' => '', ], [ 'c0_firstname' => 'Zebedee', 'c1_data' => 'Yes', 'c2_data' => '9 December 2021', 'c3_data' => 'Cat', 'c4_data' => '12345', 'c5_data' => 'Hello', 'c6_data' => '<div class="no-overflow">Goodbye</div>', ], ], $content); } /** * Data provider for {@see test_custom_report_filter} * * @return array[] */ public function custom_report_filter_provider(): array { return [ 'Filter by checkbox profile field' => ['user:profilefield_checkbox', [ 'user:profilefield_checkbox_operator' => boolean_select::CHECKED, ], 'testuser'], 'Filter by checkbox profile field (empty)' => ['user:profilefield_checkbox', [ 'user:profilefield_checkbox_operator' => boolean_select::NOT_CHECKED, ], 'admin'], 'Filter by datetime profile field' => ['user:profilefield_datetime', [ 'user:profilefield_datetime_operator' => date::DATE_RANGE, 'user:profilefield_datetime_from' => 1622502000, ], 'testuser'], 'Filter by datetime profile field (empty)' => ['user:profilefield_datetime', [ 'user:profilefield_datetime_operator' => date::DATE_EMPTY, ], 'admin'], 'Filter by menu profile field' => ['user:profilefield_menu', [ 'user:profilefield_menu_operator' => select::EQUAL_TO, 'user:profilefield_menu_value' => 'Dog', ], 'testuser'], 'Filter by menu profile field (empty)' => ['user:profilefield_menu', [ 'user:profilefield_menu_operator' => select::NOT_EQUAL_TO, 'user:profilefield_menu_value' => 'Dog', ], 'admin'], 'Filter by social profile field' => ['user:profilefield_social', [ 'user:profilefield_social_operator' => text::IS_EQUAL_TO, 'user:profilefield_social_value' => '12345', ], 'testuser'], 'Filter by social profile field (empty)' => ['user:profilefield_social', [ 'user:profilefield_social_operator' => text::IS_EMPTY, ], 'admin'], 'Filter by text profile field' => ['user:profilefield_text', [ 'user:profilefield_text_operator' => text::IS_EQUAL_TO, 'user:profilefield_text_value' => 'Hello', ], 'testuser'], 'Filter by text profile field (empty)' => ['user:profilefield_text', [ 'user:profilefield_text_operator' => text::IS_NOT_EQUAL_TO, 'user:profilefield_text_value' => 'Hello', ], 'admin'], 'Filter by textarea profile field' => ['user:profilefield_textarea', [ 'user:profilefield_textarea_operator' => text::IS_EQUAL_TO, 'user:profilefield_textarea_value' => 'Goodbye', ], 'testuser'], 'Filter by textarea profile field (empty)' => ['user:profilefield_textarea', [ 'user:profilefield_textarea_operator' => text::DOES_NOT_CONTAIN, 'user:profilefield_textarea_value' => 'Goodbye', ], 'admin'], ]; } /** * Test filtering report by custom profile fields * * @param string $filtername * @param array $filtervalues * @param string $expectmatchuser * * @dataProvider custom_report_filter_provider */ public function test_custom_report_filter(string $filtername, array $filtervalues, string $expectmatchuser): void { $this->resetAfterTest(); $userprofilefields = $this->generate_userprofilefields(); // Create test subject with user profile fields content. $user = $this->getDataGenerator()->create_user([ 'username' => 'testuser', 'profile_field_checkbox' => true, 'profile_field_datetime' => '2021-12-09', 'profile_field_menu' => 'Dog', 'profile_field_Social' => '12345', 'profile_field_text' => 'Hello', 'profile_field_textarea' => 'Goodbye', ]); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create report containing single column, and given filter. $report = $generator->create_report(['name' => 'Users', 'source' => users::class, 'default' => 0]); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:username']); // Add filter, set it's values. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filtername]); $content = $this->get_custom_report_content($report->get('id'), 0, $filtervalues); $this->assertCount(1, $content); $this->assertEquals($expectmatchuser, reset($content[0])); } } helpers/schedule_test.php 0000644 00000040203 15152276235 0011561 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\helpers; use advanced_testcase; use invalid_parameter_exception; use core_cohort\reportbuilder\audience\cohortmember; use core_reportbuilder_generator; use core_reportbuilder\local\models\schedule as model; use core_reportbuilder\reportbuilder\audience\manual; use core_user\reportbuilder\datasource\users; /** * Unit tests for the schedule helper class * * @package core_reportbuilder * @covers \core_reportbuilder\local\helpers\schedule * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class schedule_test extends advanced_testcase { /** * Test create schedule */ public function test_create_schedule(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $timescheduled = time() + DAYSECS; $schedule = schedule::create_schedule((object) [ 'name' => 'My schedule', 'reportid' => $report->get('id'), 'format' => 'csv', 'subject' => 'Hello', 'message' => 'Hola', 'timescheduled' => $timescheduled, ]); $this->assertEquals('My schedule', $schedule->get('name')); $this->assertEquals($report->get('id'), $schedule->get('reportid')); $this->assertEquals('csv', $schedule->get('format')); $this->assertEquals('Hello', $schedule->get('subject')); $this->assertEquals('Hola', $schedule->get('message')); $this->assertEquals($timescheduled, $schedule->get('timescheduled')); $this->assertEquals($timescheduled, $schedule->get('timenextsend')); } /** * Test update schedule */ public function test_update_schedule(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $schedule = $generator->create_schedule(['reportid' => $report->get('id'), 'name' => 'My schedule']); // Update some record properties. $record = $schedule->to_record(); $record->name = 'My updated schedule'; $record->timescheduled = 1861340400; // 25/12/2028 07:00 UTC. $schedule = schedule::update_schedule($record); $this->assertEquals($record->name, $schedule->get('name')); $this->assertEquals($record->timescheduled, $schedule->get('timescheduled')); } /** * Test update invalid schedule */ public function test_update_schedule_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid schedule'); schedule::update_schedule((object) ['id' => 42, 'reportid' => $report->get('id')]); } /** * Test toggle schedule */ public function test_toggle_schedule(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $schedule = $generator->create_schedule(['reportid' => $report->get('id'), 'name' => 'My schedule']); // Disable the schedule. schedule::toggle_schedule($report->get('id'), $schedule->get('id'), false); $schedule = $schedule->read(); $this->assertFalse($schedule->get('enabled')); // Enable the schedule. schedule::toggle_schedule($report->get('id'), $schedule->get('id'), true); $schedule = $schedule->read(); $this->assertTrue($schedule->get('enabled')); } /** * Test toggle invalid schedule */ public function test_toggle_schedule_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid schedule'); schedule::toggle_schedule($report->get('id'), 42, true); } /** * Test delete schedule */ public function test_delete_schedule(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $schedule = $generator->create_schedule(['reportid' => $report->get('id'), 'name' => 'My schedule']); $scheduleid = $schedule->get('id'); schedule::delete_schedule($report->get('id'), $scheduleid); $this->assertFalse($schedule::record_exists($scheduleid)); } /** * Test delete invalid schedule */ public function test_delete_schedule_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid schedule'); schedule::delete_schedule($report->get('id'), 42); } /** * Test getting schedule report users (those in matching audience) */ public function test_get_schedule_report_users(): void { $this->resetAfterTest(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); // Create cohort, with some members. $cohort = $this->getDataGenerator()->create_cohort(); $cohortuserone = $this->getDataGenerator()->create_user(['firstname' => 'Zoe', 'lastname' => 'Zebra']); cohort_add_member($cohort->id, $cohortuserone->id); $cohortusertwo = $this->getDataGenerator()->create_user(['firstname' => 'Henrietta', 'lastname' => 'Hamster']); cohort_add_member($cohort->id, $cohortusertwo->id); // Create a third user, to be added manually. $manualuserone = $this->getDataGenerator()->create_user(['firstname' => 'Bob', 'lastname' => 'Badger']); $audiencecohort = $generator->create_audience([ 'reportid' => $report->get('id'), 'classname' => cohortmember::class, 'configdata' => ['cohorts' => [$cohort->id]], ]); $audiencemanual = $generator->create_audience([ 'reportid' => $report->get('id'), 'classname' => manual::class, 'configdata' => ['users' => [$manualuserone->id]], ]); // Now create our schedule. $schedule = $generator->create_schedule([ 'reportid' => $report->get('id'), 'name' => 'My schedule', 'audiences' => json_encode([ $audiencecohort->get_persistent()->get('id'), $audiencemanual->get_persistent()->get('id'), ]), ]); $users = schedule::get_schedule_report_users($schedule); $this->assertEquals([ 'Bob', 'Henrietta', 'Zoe', ], array_column($users, 'firstname')); } /** * Test getting schedule report row count */ public function test_get_schedule_report_count(): void { $this->resetAfterTest(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $schedule = $generator->create_schedule(['reportid' => $report->get('id'), 'name' => 'My schedule']); // There is only one row in the report (the only user on the site). $count = schedule::get_schedule_report_count($schedule); $this->assertEquals(1, $count); } /** * Data provider for {@see test_get_schedule_report_file} * * @return string[] */ public function get_schedule_report_file_format(): array { return [ ['csv'], ['excel'], ['html'], ['json'], ['ods'], ['pdf'], ]; } /** * Test getting schedule report exported file, in each supported format * * @param string $format * * @dataProvider get_schedule_report_file_format */ public function test_get_schedule_report_file(string $format): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $schedule = $generator->create_schedule(['reportid' => $report->get('id'), 'name' => 'My schedule', 'format' => $format]); // There is only one row in the report (the only user on the site). $file = schedule::get_schedule_report_file($schedule); $this->assertGreaterThan(64, $file->get_filesize()); } /** * Data provider for {@see test_should_send_schedule} * * @return array[] */ public function should_send_schedule_provider(): array { $time = time(); // We just need large offsets for dates in the past/future. $yesterday = $time - DAYSECS; $tomorrow = $time + DAYSECS; return [ 'Disabled' => [[ 'enabled' => false, ], false], 'Time scheduled in the past' => [[ 'recurrence' => model::RECURRENCE_NONE, 'timescheduled' => $yesterday, ], true], 'Time scheduled in the past, already sent prior to schedule' => [[ 'recurrence' => model::RECURRENCE_NONE, 'timescheduled' => $yesterday, 'timelastsent' => $yesterday - HOURSECS, ], true], 'Time scheduled in the past, already sent on schedule' => [[ 'recurrence' => model::RECURRENCE_NONE, 'timescheduled' => $yesterday, 'timelastsent' => $yesterday, ], false], 'Time scheduled in the future' => [[ 'recurrence' => model::RECURRENCE_NONE, 'timescheduled' => $tomorrow, ], false], 'Time scheduled in the future, already sent prior to schedule' => [[ 'recurrence' => model::RECURRENCE_NONE, 'timelastsent' => $yesterday, 'timescheduled' => $tomorrow, ], false], 'Next send in the past' => [[ 'recurrence' => model::RECURRENCE_DAILY, 'timescheduled' => $yesterday, 'timenextsend' => $yesterday, ], true], 'Next send in the future' => [[ 'recurrence' => model::RECURRENCE_DAILY, 'timescheduled' => $yesterday, 'timenextsend' => $tomorrow, ], false], ]; } /** * Test for whether a schedule should be sent * * @param array $properties * @param bool $expected * * @dataProvider should_send_schedule_provider */ public function test_should_send_schedule(array $properties, bool $expected): void { $this->resetAfterTest(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $schedule = $generator->create_schedule(['reportid' => $report->get('id'), 'name' => 'My schedule'] + $properties); // If "Time next send" is specified, then override calculated value. if (array_key_exists('timenextsend', $properties)) { $schedule->set('timenextsend', $properties['timenextsend']); } $this->assertEquals($expected, schedule::should_send_schedule($schedule)); } /** * Data provider for {@see test_calculate_next_send_time} * * @return array[] */ public function calculate_next_send_time_provider(): array { $timescheduled = 1635865200; // Tue Nov 02 2021 15:00:00 GMT+0000. $timenow = 1639846800; // Sat Dec 18 2021 17:00:00 GMT+0000. return [ 'No recurrence' => [model::RECURRENCE_NONE, $timescheduled, $timenow, $timescheduled], 'Recurrence, time scheduled in future' => [model::RECURRENCE_DAILY, $timenow + DAYSECS, $timenow, $timenow + DAYSECS], // Sun Dec 19 2021 15:00:00 GMT+0000. 'Daily recurrence' => [model::RECURRENCE_DAILY, $timescheduled, $timenow, 1639926000], // Mon Dec 20 2021 15:00:00 GMT+0000. 'Weekday recurrence' => [model::RECURRENCE_WEEKDAYS, $timescheduled, $timenow, 1640012400], // Tue Dec 21 2021 15:00:00 GMT+0000. 'Weekly recurrence' => [model::RECURRENCE_WEEKLY, $timescheduled, $timenow, 1640098800], // Sun Jan 02 2022 15:00:00 GMT+0000. 'Monthy recurrence' => [model::RECURRENCE_MONTHLY, $timescheduled, $timenow, 1641135600], // Wed Nov 02 2022 15:00:00 GMT+0000. 'Annual recurrence' => [model::RECURRENCE_ANNUALLY, $timescheduled, $timenow, 1667401200], ]; } /** * Test for calculating next schedule send time * * @param int $recurrence * @param int $timescheduled * @param int $timenow * @param int $expected * * @dataProvider calculate_next_send_time_provider */ public function test_calculate_next_send_time(int $recurrence, int $timescheduled, int $timenow, int $expected): void { $this->resetAfterTest(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $schedule = $generator->create_schedule([ 'reportid' => $report->get('id'), 'name' => 'My schedule', 'recurrence' => $recurrence, 'timescheduled' => $timescheduled, 'timenow' => $timenow, ]); $this->assertEquals($expected, schedule::calculate_next_send_time($schedule, $timenow)); } } helpers/report_test.php 0000644 00000063731 15152276235 0011313 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\helpers; use advanced_testcase; use core_reportbuilder_generator; use invalid_parameter_exception; use core_reportbuilder\local\models\column; use core_reportbuilder\local\models\filter; use core_user\reportbuilder\datasource\users; /** * Unit tests for the report helper class * * @package core_reportbuilder * @covers \core_reportbuilder\local\helpers\report * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_test extends advanced_testcase { /** * Test deleting report */ public function test_delete_report(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Create Report1 and add some elements. $report1 = $generator->create_report(['name' => 'My report 1', 'source' => users::class, 'default' => false]); $column1 = $generator->create_column(['reportid' => $report1->get('id'), 'uniqueidentifier' => 'user:email']); $filter1 = $generator->create_filter(['reportid' => $report1->get('id'), 'uniqueidentifier' => 'user:email']); $condition1 = $generator->create_condition(['reportid' => $report1->get('id'), 'uniqueidentifier' => 'user:email']); // Create Report2 and add some elements. $report2 = $generator->create_report(['name' => 'My report 2', 'source' => users::class, 'default' => false]); $column2 = $generator->create_column(['reportid' => $report2->get('id'), 'uniqueidentifier' => 'user:email']); $filter2 = $generator->create_filter(['reportid' => $report2->get('id'), 'uniqueidentifier' => 'user:email']); $condition2 = $generator->create_condition(['reportid' => $report2->get('id'), 'uniqueidentifier' => 'user:email']); // Delete Report1. report::delete_report($report1->get('id')); // Make sure Report1, and all it's elements are deleted. $this->assertFalse($report1::record_exists($report1->get('id'))); $this->assertFalse($column1::record_exists($column1->get('id'))); $this->assertFalse($filter1::record_exists($filter1->get('id'))); $this->assertFalse($condition1::record_exists($condition1->get('id'))); // Make sure Report2, and all it's elements still exist. $this->assertTrue($report2::record_exists($report2->get('id'))); $this->assertTrue($column2::record_exists($column2->get('id'))); $this->assertTrue($filter2::record_exists($filter2->get('id'))); $this->assertTrue($condition2::record_exists($condition2->get('id'))); } /** * Testing adding report column */ public function test_add_report_column(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); // Add first column. $columnfullname = report::add_report_column($report->get('id'), 'user:fullname'); $this->assertTrue(column::record_exists($columnfullname->get('id'))); $this->assertEquals($report->get('id'), $columnfullname->get('reportid')); $this->assertEquals('user:fullname', $columnfullname->get('uniqueidentifier')); $this->assertEquals(1, $columnfullname->get('columnorder')); $this->assertEquals(1, $columnfullname->get('sortorder')); // Add second column. $columnemail = report::add_report_column($report->get('id'), 'user:email'); $this->assertTrue(column::record_exists($columnemail->get('id'))); $this->assertEquals($report->get('id'), $columnemail->get('reportid')); $this->assertEquals('user:email', $columnemail->get('uniqueidentifier')); $this->assertEquals(2, $columnemail->get('columnorder')); $this->assertEquals(2, $columnemail->get('sortorder')); } /** * Test adding invalid report column */ public function test_add_report_column_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid column'); report::add_report_column($report->get('id'), 'user:invalid'); } /** * Testing deleting report column */ public function test_delete_report_column(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); // Add two columns. $columnfullname = $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); // Delete the first column. $result = report::delete_report_column($report->get('id'), $columnfullname->get('id')); $this->assertTrue($result); // Assert report columns. $columns = column::get_records(['reportid' => $report->get('id')]); $this->assertCount(1, $columns); $column = reset($columns); $this->assertEquals('user:email', $column->get('uniqueidentifier')); $this->assertEquals(1, $column->get('columnorder')); } /** * Testing deleting invalid report column */ public function test_delete_report_column_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid column'); report::delete_report_column($report->get('id'), 42); } /** * Testing re-ordering report column */ public function test_reorder_report_column(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); // Add four columns. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:country']); $columncity = $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:city']); // Move the city column to second position. $result = report::reorder_report_column($report->get('id'), $columncity->get('id'), 2); $this->assertTrue($result); // Assert report columns order. $columns = column::get_records(['reportid' => $report->get('id')], 'columnorder'); $columnidentifiers = array_map(static function(column $column): string { return $column->get('uniqueidentifier'); }, $columns); $this->assertEquals([ 'user:fullname', 'user:city', 'user:email', 'user:country', ], $columnidentifiers); } /** * Testing re-ordering invalid report column */ public function test_reorder_report_column_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid column'); report::reorder_report_column($report->get('id'), 42, 1); } /** * Testing re-ordering report column sorting */ public function test_reorder_report_column_sorting(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); // Add four columns. $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:country']); $columncity = $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:city']); // Move the city column to second position. $result = report::reorder_report_column_sorting($report->get('id'), $columncity->get('id'), 2); $this->assertTrue($result); // Assert report columns order. $columns = column::get_records(['reportid' => $report->get('id')], 'sortorder'); $columnidentifiers = array_map(static function(column $column): string { return $column->get('uniqueidentifier'); }, $columns); $this->assertEquals([ 'user:fullname', 'user:city', 'user:email', 'user:country', ], $columnidentifiers); } /** * Testing re-ordering invalid report column sorting */ public function test_reorder_report_column_sorting_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid column'); report::reorder_report_column_sorting($report->get('id'), 42, 1); } /** * Test toggling of report column sorting */ public function test_toggle_report_column_sorting(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $column = $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); // Toggle sort descending. $result = report::toggle_report_column_sorting($report->get('id'), $column->get('id'), true, SORT_DESC); $this->assertTrue($result); // Confirm column was updated. $columnupdated = new column($column->get('id')); $this->assertTrue($columnupdated->get('sortenabled')); $this->assertEquals(SORT_DESC, $columnupdated->get('sortdirection')); } /** * Test toggling of report column sorting with invalid column */ public function test_toggle_report_column_sorting_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid column'); report::toggle_report_column_sorting($report->get('id'), 42, false); } /** * Test adding report condition */ public function test_add_report_condition(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); // Add first condition. $conditionfullname = report::add_report_condition($report->get('id'), 'user:fullname'); $this->assertTrue(filter::record_exists_select('id = :id AND iscondition = 1', ['id' => $conditionfullname->get('id')])); $this->assertEquals($report->get('id'), $conditionfullname->get('reportid')); $this->assertEquals('user:fullname', $conditionfullname->get('uniqueidentifier')); $this->assertEquals(1, $conditionfullname->get('filterorder')); // Add second condition. $conditionemail = report::add_report_condition($report->get('id'), 'user:email'); $this->assertTrue(filter::record_exists_select('id = :id AND iscondition = 1', ['id' => $conditionemail->get('id')])); $this->assertEquals($report->get('id'), $conditionemail->get('reportid')); $this->assertEquals('user:email', $conditionemail->get('uniqueidentifier')); $this->assertEquals(2, $conditionemail->get('filterorder')); } /** * Test adding invalid report condition */ public function test_add_report_condition_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid condition'); report::add_report_condition($report->get('id'), 'user:invalid'); } /** * Test deleting report condition */ public function test_delete_report_condition(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); // Add two conditions. $conditionfullname = $generator->create_condition([ 'reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname', ]); $generator->create_condition(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); // Delete the first condition. $result = report::delete_report_condition($report->get('id'), $conditionfullname->get('id')); $this->assertTrue($result); // Assert report conditions. $conditions = filter::get_condition_records($report->get('id')); $this->assertCount(1, $conditions); $condition = reset($conditions); $this->assertEquals('user:email', $condition->get('uniqueidentifier')); $this->assertEquals(1, $condition->get('filterorder')); } /** * Test deleting invalid report condition */ public function test_delete_report_condition_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid condition'); report::delete_report_condition($report->get('id'), 42); } /** * Test re-ordering report condition */ public function test_reorder_report_condition(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); // Add four conditions. $generator->create_condition(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); $generator->create_condition(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); $generator->create_condition(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:country']); $conditioncity = $generator->create_condition(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:city']); // Move the city condition to second position. $result = report::reorder_report_condition($report->get('id'), $conditioncity->get('id'), 2); $this->assertTrue($result); // Assert report conditions order. $conditions = filter::get_condition_records($report->get('id'), 'filterorder'); $conditionidentifiers = array_map(static function(filter $condition): string { return $condition->get('uniqueidentifier'); }, $conditions); $this->assertEquals([ 'user:fullname', 'user:city', 'user:email', 'user:country', ], $conditionidentifiers); } /** * Test re-ordering invalid report condition */ public function test_reorder_report_condition_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid condition'); report::reorder_report_condition($report->get('id'), 42, 1); } /** * Test adding report filter */ public function test_add_report_filter(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); // Add first filter. $filterfullname = report::add_report_filter($report->get('id'), 'user:fullname'); $this->assertTrue(filter::record_exists_select('id = :id AND iscondition = 0', ['id' => $filterfullname->get('id')])); $this->assertEquals($report->get('id'), $filterfullname->get('reportid')); $this->assertEquals('user:fullname', $filterfullname->get('uniqueidentifier')); $this->assertEquals(1, $filterfullname->get('filterorder')); // Add second filter. $filteremail = report::add_report_filter($report->get('id'), 'user:email'); $this->assertTrue(filter::record_exists_select('id = :id AND iscondition = 0', ['id' => $filteremail->get('id')])); $this->assertEquals($report->get('id'), $filteremail->get('reportid')); $this->assertEquals('user:email', $filteremail->get('uniqueidentifier')); $this->assertEquals(2, $filteremail->get('filterorder')); } /** * Test adding invalid report filter */ public function test_add_report_filter_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid filter'); report::add_report_filter($report->get('id'), 'user:invalid'); } /** * Test deleting report filter */ public function test_delete_report_filter(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); // Add two filters. $filterfullname = $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); // Delete the first filter. $result = report::delete_report_filter($report->get('id'), $filterfullname->get('id')); $this->assertTrue($result); // Assert report filters. $filters = filter::get_filter_records($report->get('id')); $this->assertCount(1, $filters); $filter = reset($filters); $this->assertEquals('user:email', $filter->get('uniqueidentifier')); $this->assertEquals(1, $filter->get('filterorder')); } /** * Test deleting invalid report filter */ public function test_delete_report_filter_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid filter'); report::delete_report_filter($report->get('id'), 42); } /** * Test re-ordering report filter */ public function test_reorder_report_filter(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); // Add four filters. $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']); $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:country']); $filtercity = $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:city']); // Move the city filter to second position. $result = report::reorder_report_filter($report->get('id'), $filtercity->get('id'), 2); $this->assertTrue($result); // Assert report filters order. $filters = filter::get_filter_records($report->get('id'), 'filterorder'); $filteridentifiers = array_map(static function(filter $filter): string { return $filter->get('uniqueidentifier'); }, $filters); $this->assertEquals([ 'user:fullname', 'user:city', 'user:email', 'user:country', ], $filteridentifiers); } /** * Test re-ordering invalid report filter */ public function test_reorder_report_filter_invalid(): void { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $this->expectException(invalid_parameter_exception::class); $this->expectExceptionMessage('Invalid filter'); report::reorder_report_filter($report->get('id'), 42, 1); } } helpers/format_test.php 0000644 00000004100 15152276235 0011251 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\helpers; use advanced_testcase; use stdClass; /** * Unit tests for the format helper * * @package core_reportbuilder * @covers \core_reportbuilder\local\helpers\format * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class format_test extends advanced_testcase { /** * Test userdate method */ public function test_userdate(): void { $now = time(); $userdate = format::userdate($now, new stdClass()); $this->assertEquals(userdate($now), $userdate); } /** * Data provider for {@see test_boolean_as_text} * * @return array */ public function boolean_as_text_provider(): array { return [ [false, get_string('no')], [true, get_string('yes')], ]; } /** * Test boolean as text * * @param bool $value * @param string $expected * * @dataProvider boolean_as_text_provider */ public function test_boolean_as_text(bool $value, string $expected): void { $this->assertEquals($expected, format::boolean_as_text($value)); } /** * Test percentage formatting of a float */ public function test_percent(): void { $this->assertEquals('33.3%', format::percent(1 / 3 * 100)); } } models/audience_test.php 0000644 00000012511 15152276235 0011364 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\models; use advanced_testcase; use core\persistent; use core_reportbuilder\event\audience_created; use core_reportbuilder\event\audience_deleted; use core_reportbuilder\event\audience_updated; use core_reportbuilder_generator; use core_user\reportbuilder\datasource\users; /** * Unit tests for the audience model * * @package core_reportbuilder * @covers \core_reportbuilder\local\models\audience * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class audience_test extends advanced_testcase { /** * Tests for audience_created event * * @return persistent[] * * @covers \core_reportbuilder\event\audience_created */ public function test_audience_created_event(): array { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); // Catch the events. $sink = $this->redirectEvents(); $audience = $generator->create_audience(['reportid' => $report->get('id'), 'configdata' => []]) ->get_persistent(); $events = $sink->get_events(); $sink->close(); // Validate the event. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf(audience_created::class, $event); $this->assertEquals(audience::TABLE, $event->objecttable); $this->assertEquals($audience->get('id'), $event->objectid); $this->assertEquals($report->get('id'), $event->other['reportid']); $this->assertEquals($report->get_context()->id, $event->contextid); return [$report, $audience]; } /** * Tests for audience_updated event * * @param persistent[] $persistents * @return persistent[] * * @depends test_audience_created_event * @covers \core_reportbuilder\event\audience_updated */ public function test_audience_updated_event(array $persistents): array { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Re-create the persistents. [$report, $audience] = $persistents; $report = new report($DB->insert_record(report::TABLE, $report->to_record())); $audience = new audience($DB->insert_record(audience::TABLE, $audience->to_record())); // Catch the events. $sink = $this->redirectEvents(); $audience->set('heading', 'Hello')->update(); $events = $sink->get_events(); $sink->close(); // Validate the event. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf(audience_updated::class, $event); $this->assertEquals(audience::TABLE, $event->objecttable); $this->assertEquals($audience->get('id'), $event->objectid); $this->assertEquals($report->get('id'), $event->other['reportid']); $this->assertEquals($report->get_context()->id, $event->contextid); return [$report, $audience]; } /** * Tests for audience_deleted event * * @param persistent[] $persistents * * @depends test_audience_updated_event * @covers \core_reportbuilder\event\audience_deleted */ public function test_audience_deleted_event(array $persistents): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Re-create the persistents (remembering audience ID which is removed from persistent upon deletion). [$report, $audience] = $persistents; $report = new report($DB->insert_record(report::TABLE, $report->to_record())); $audienceid = $DB->insert_record(audience::TABLE, $audience->to_record()); $audience = new audience($audienceid); // Catch the events. $sink = $this->redirectEvents(); $audience->delete(); $events = $sink->get_events(); $sink->close(); // Validate the event. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf(audience_deleted::class, $event); $this->assertEquals(audience::TABLE, $event->objecttable); $this->assertEquals($audienceid, $event->objectid); $this->assertEquals($report->get('id'), $event->other['reportid']); $this->assertEquals($report->get_context()->id, $event->contextid); } } models/schedule_test.php 0000644 00000012466 15152276235 0011414 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\models; use advanced_testcase; use core\persistent; use core_reportbuilder\event\schedule_created; use core_reportbuilder\event\schedule_deleted; use core_reportbuilder\event\schedule_updated; use core_reportbuilder_generator; use core_user\reportbuilder\datasource\users; /** * Unit tests for the schedule model * * @package core_reportbuilder * @covers \core_reportbuilder\local\models\schedule * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class schedule_test extends advanced_testcase { /** * Tests for schedule_created event * * @return persistent[] * * @covers \core_reportbuilder\event\schedule_created */ public function test_schedule_created_event(): array { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); // Catch the events. $sink = $this->redirectEvents(); $schedule = $generator->create_schedule(['reportid' => $report->get('id'), 'name' => 'My schedule']); $events = $sink->get_events(); $sink->close(); // Validate the event. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf(schedule_created::class, $event); $this->assertEquals(schedule::TABLE, $event->objecttable); $this->assertEquals($schedule->get('id'), $event->objectid); $this->assertEquals($report->get('id'), $event->other['reportid']); $this->assertEquals($report->get_context()->id, $event->contextid); return [$report, $schedule]; } /** * Tests for schedule_updated event * * @param persistent[] $persistents * @return persistent[] * * @depends test_schedule_created_event * @covers \core_reportbuilder\event\schedule_updated */ public function test_schedule_updated_event(array $persistents): array { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Re-create the persistents. [$report, $schedule] = $persistents; $report = new report($DB->insert_record(report::TABLE, $report->to_record())); $schedule = new schedule($DB->insert_record(schedule::TABLE, $schedule->to_record())); // Catch the events. $sink = $this->redirectEvents(); $schedule->set('name', 'My new schedule')->update(); $events = $sink->get_events(); $sink->close(); // Validate the event. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf(schedule_updated::class, $event); $this->assertEquals(schedule::TABLE, $event->objecttable); $this->assertEquals($schedule->get('id'), $event->objectid); $this->assertEquals($report->get('id'), $event->other['reportid']); $this->assertEquals($report->get_context()->id, $event->contextid); return [$report, $schedule]; } /** * Tests for schedule_deleted event * * @param persistent[] $persistents * * @depends test_schedule_updated_event * @covers \core_reportbuilder\event\schedule_deleted */ public function test_schedule_deleted_event(array $persistents): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Re-create the persistents (remembering schedule ID which is removed from persistent upon deletion). [$report, $schedule] = $persistents; $report = new report($DB->insert_record(report::TABLE, $report->to_record())); $scheduleid = $DB->insert_record(schedule::TABLE, $schedule->to_record()); $schedule = new schedule($scheduleid); // Catch the events. $sink = $this->redirectEvents(); $schedule->delete(); $events = $sink->get_events(); $sink->close(); // Validate the event. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf(schedule_deleted::class, $event); $this->assertEquals(schedule::TABLE, $event->objecttable); $this->assertEquals($scheduleid, $event->objectid); $this->assertEquals($report->get('id'), $event->other['reportid']); $this->assertEquals($report->get_context()->id, $event->contextid); } } models/report_test.php 0000644 00000011770 15152276235 0011130 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_reportbuilder\local\models; use advanced_testcase; use core_reportbuilder\event\report_created; use core_reportbuilder\event\report_deleted; use core_reportbuilder\event\report_updated; use core_reportbuilder_generator; use core_user\reportbuilder\datasource\users; /** * Unit tests for the report model * * @package core_reportbuilder * @covers \core_reportbuilder\local\models\report * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_test extends advanced_testcase { /** * Tests for report_created event * * @return report * * @covers \core_reportbuilder\event\report_created */ public function test_report_created_event(): report { $this->resetAfterTest(); $this->setAdminUser(); /** @var core_reportbuilder_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); // Catch the events. $sink = $this->redirectEvents(); $report = $generator->create_report([ 'name' => 'My report', 'source' => users::class, 'default' => false, ]); $events = $sink->get_events(); $sink->close(); // Validate the event. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf(report_created::class, $event); $this->assertEquals(report::TABLE, $event->objecttable); $this->assertEquals($report->get('id'), $event->objectid); $this->assertEquals($report->get('name'), $event->other['name']); $this->assertEquals($report->get('source'), $event->other['source']); $this->assertEquals($report->get_context()->id, $event->contextid); return $report; } /** * Tests for report_updated event * * @param report $report * @return report * * @depends test_report_created_event * @covers \core_reportbuilder\event\report_updated */ public function test_report_updated_event(report $report): report { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Re-create the persistent. $report = new report($DB->insert_record(report::TABLE, $report->to_record())); // Catch the events. $sink = $this->redirectEvents(); $report->set('name', 'New report name')->update(); $events = $sink->get_events(); $sink->close(); // Validate the event. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf(report_updated::class, $event); $this->assertEquals(report::TABLE, $event->objecttable); $this->assertEquals($report->get('id'), $event->objectid); $this->assertEquals('New report name', $event->other['name']); $this->assertEquals($report->get('source'), $event->other['source']); $this->assertEquals($report->get_context()->id, $event->contextid); return $report; } /** * Tests for report_deleted event * * @param report $report * * @depends test_report_updated_event * @covers \core_reportbuilder\event\report_deleted */ public function test_report_deleted_event(report $report): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Re-create the persistent (remembering report ID which is removed from persistent upon deletion). $reportid = $DB->insert_record(report::TABLE, $report->to_record()); $report = new report($reportid); // Catch the events. $sink = $this->redirectEvents(); $report->delete(); $events = $sink->get_events(); $sink->close(); // Validate the event. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf(report_deleted::class, $event); $this->assertEquals(report::TABLE, $event->objecttable); $this->assertEquals($reportid, $event->objectid); $this->assertEquals($report->get('name'), $event->other['name']); $this->assertEquals($report->get('source'), $event->other['source']); $this->assertEquals($report->get_context()->id, $event->contextid); } } target/selector.php 0000644 00000006720 15152360013 0010364 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Selector target. * * @package tool_usertours * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\target; defined('MOODLE_INTERNAL') || die(); use tool_usertours\step; /** * Selector target. * * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class selector extends base { /** * Convert the target value to a valid CSS selector for use in the * output configuration. * * @return string */ public function convert_to_css() { return $this->step->get_targetvalue(); } /** * Convert the step target to a friendly name for use in the UI. * * @return string */ public function get_displayname() { return get_string('selectordisplayname', 'tool_usertours', $this->step->get_targetvalue()); } /** * Get the default title. * * @return string */ public function get_default_title() { return get_string('selector_defaulttitle', 'tool_usertours'); } /** * Get the default content. * * @return string */ public function get_default_content() { return get_string('selector_defaultcontent', 'tool_usertours'); } /** * Add the target type configuration to the form. * * @param MoodleQuickForm $mform The form to add configuration to. * @return $this */ public static function add_config_to_form(\MoodleQuickForm $mform) { $mform->addElement('text', 'targetvalue_selector', get_string('cssselector', 'tool_usertours')); $mform->setType('targetvalue_selector', PARAM_RAW); $mform->addHelpButton('targetvalue_selector', 'target_selector_targetvalue', 'tool_usertours'); } /** * Add the disabledIf values. * * @param MoodleQuickForm $mform The form to add configuration to. */ public static function add_disabled_constraints_to_form(\MoodleQuickForm $mform) { $mform->hideIf('targetvalue_selector', 'targettype', 'noteq', \tool_usertours\target::get_target_constant_for_class(get_class())); } /** * Prepare data to submit to the form. * * @param object $data The data being passed to the form */ public function prepare_data_for_form($data) { $data->targetvalue_selector = $this->step->get_targetvalue(); } /** * Fetch the targetvalue from the form for this target type. * * @param stdClass $data The data submitted in the form * @return string */ public function get_value_from_form($data) { return $data->targetvalue_selector; } } target/base.php 0000644 00000006701 15152360013 0007455 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Target base. * * @package tool_usertours * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\target; defined('MOODLE_INTERNAL') || die(); use tool_usertours\step; /** * Target base. * * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class base { /** * @var step $step The step being targetted. */ protected $step; /** * @var array $forcedsettings The settings forced by this type. */ protected static $forcedsettings = []; /** * Create the target type. * * @param step $step The step being targetted. */ public function __construct(step $step) { $this->step = $step; } /** * Convert the target value to a valid CSS selector for use in the * output configuration. * * @return string */ abstract public function convert_to_css(); /** * Convert the step target to a friendly name for use in the UI. * * @return string */ abstract public function get_displayname(); /** * Add the target type configuration to the form. * * @param MoodleQuickForm $mform The form to add configuration to. */ public static function add_config_to_form(\MoodleQuickForm $mform) { } /** * Add the disabledIf values. * * @param MoodleQuickForm $mform The form to add configuration to. */ public static function add_disabled_constraints_to_form(\MoodleQuickForm $mform) { } /** * Prepare data to submit to the form. * * @param object $data The data being passed to the form */ abstract public function prepare_data_for_form($data); /** * Whether the specified step setting is forced by this target type. * * @param string $key The name of the key to check. * @return boolean */ public function is_setting_forced($key) { return isset(static::$forcedsettings[$key]); } /** * The value of the forced setting. * * @param string $key The name of the key to check. * @return mixed */ public function get_forced_setting_value($key) { if ($this->is_setting_forced($key)) { return static::$forcedsettings[$key]; } return null; } /** * Fetch the targetvalue from the form for this target type. * * @param stdClass $data The data submitted in the form * @return string */ abstract public function get_value_from_form($data); } target/block.php 0000644 00000007430 15152360013 0007635 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Block target. * * @package tool_usertours * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\target; defined('MOODLE_INTERNAL') || die(); use tool_usertours\step; /** * Block target. * * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class block extends base { /** * Convert the target value to a valid CSS selector for use in the * output configuration. * * @return string */ public function convert_to_css() { // The block has the following CSS class selector style: // .block-region .block_[name] . return sprintf('.block-region .block_%s', $this->step->get_targetvalue()); } /** * Convert the step target to a friendly name for use in the UI. * * @return string */ public function get_displayname() { return get_string('block_named', 'tool_usertours', $this->get_block_name()); } /** * Get the translated name of the block. * * @return string */ protected function get_block_name() { return get_string('pluginname', self::get_frankenstyle($this->step->get_targetvalue())); } /** * Get the frankenstyle name of the block. * * @param string $block The block name. * @return The frankenstyle block name. */ protected static function get_frankenstyle($block) { return sprintf('block_%s', $block); } /** * Add the target type configuration to the form. * * @param MoodleQuickForm $mform The form to add configuration to. * @return $this */ public static function add_config_to_form(\MoodleQuickForm $mform) { global $PAGE; $blocks = []; foreach ($PAGE->blocks->get_installed_blocks() as $block) { $blocks[$block->name] = get_string('pluginname', 'block_' . $block->name); } \core_collator::asort($blocks); $mform->addElement('select', 'targetvalue_block', get_string('block', 'tool_usertours'), $blocks); } /** * Add the disabledIf values. * * @param MoodleQuickForm $mform The form to add configuration to. */ public static function add_disabled_constraints_to_form(\MoodleQuickForm $mform) { $mform->hideIf('targetvalue_block', 'targettype', 'noteq', \tool_usertours\target::get_target_constant_for_class(get_class())); } /** * Prepare data to submit to the form. * * @param object $data The data being passed to the form */ public function prepare_data_for_form($data) { $data->targetvalue_block = $this->step->get_targetvalue(); } /** * Fetch the targetvalue from the form for this target type. * * @param stdClass $data The data submitted in the form * @return string */ public function get_value_from_form($data) { return $data->targetvalue_block; } } target/unattached.php 0000644 00000006357 15152360013 0010672 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A step designed to be orphaned. * * @package tool_usertours * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\target; defined('MOODLE_INTERNAL') || die(); use tool_usertours\step; /** * A step designed to be orphaned. * * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class unattached extends base { /** * @var array $forcedsettings The settings forced by this type. */ protected static $forcedsettings = [ 'placement' => 'top', 'orphan' => true, 'reflex' => false, ]; /** * Convert the target value to a valid CSS selector for use in the * output configuration. * * @return string */ public function convert_to_css() { return ''; } /** * Convert the step target to a friendly name for use in the UI. * * @return string */ public function get_displayname() { return get_string('target_unattached', 'tool_usertours'); } /** * Add the target type configuration to the form. * * @param MoodleQuickForm $mform The form to add configuration to. * @return $this */ public static function add_config_to_form(\MoodleQuickForm $mform) { // There is no relevant value here. $mform->addElement('hidden', 'targetvalue_unattached', ''); $mform->setType('targetvalue_unattached', PARAM_TEXT); } /** * Add the disabledIf values. * * @param MoodleQuickForm $mform The form to add configuration to. */ public static function add_disabled_constraints_to_form(\MoodleQuickForm $mform) { $myvalue = \tool_usertours\target::get_target_constant_for_class(get_class()); foreach (array_keys(self::$forcedsettings) as $settingname) { $mform->hideIf($settingname, 'targettype', 'eq', $myvalue); } } /** * Prepare data to submit to the form. * * @param object $data The data being passed to the form */ public function prepare_data_for_form($data) { $data->targetvalue_unattached = ''; } /** * Fetch the targetvalue from the form for this target type. * * @param stdClass $data The data submitted in the form * @return string */ public function get_value_from_form($data) { return ''; } } table/step_list.php 0000644 00000011170 15152360013 0010346 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Table to show the list of steps in a tour. * * @package tool_usertours * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\table; defined('MOODLE_INTERNAL') || die(); use tool_usertours\helper; use tool_usertours\tour; use tool_usertours\step; require_once($CFG->libdir . '/tablelib.php'); /** * Table to show the list of steps in a tour. * * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class step_list extends \flexible_table { /** * @var int $tourid The id of the tour. */ protected $tourid; /** * Construct the table for the specified tour ID. * * @param int $tourid The id of the tour. */ public function __construct($tourid) { parent::__construct('steps'); $this->tourid = $tourid; $baseurl = new \moodle_url('/tool/usertours/configure.php', array( 'id' => $tourid, )); $this->define_baseurl($baseurl); // Column definition. $this->define_columns(array( 'title', 'content', 'target', 'actions', )); $this->define_headers(array( get_string('title', 'tool_usertours'), get_string('content', 'tool_usertours'), get_string('target', 'tool_usertours'), get_string('actions', 'tool_usertours'), )); $this->set_attribute('class', 'admintable generaltable steptable'); $this->setup(); } /** * Format the current row's title column. * * @param step $step The step for this row. * @return string */ protected function col_title(step $step) { global $OUTPUT; return $OUTPUT->render(helper::render_stepname_inplace_editable($step)); } /** * Format the current row's content column. * * @param step $step The step for this row. * @return string */ protected function col_content(step $step) { $content = $step->get_content(); $systemcontext = \context_system::instance(); $content = file_rewrite_pluginfile_urls($content, 'pluginfile.php', $systemcontext->id, 'tool_usertours', 'stepcontent', $step->get_id()); $content = helper::get_string_from_input($content); $content = step::get_step_image_from_input($content); return format_text($content, $step->get_contentformat()); } /** * Format the current row's target column. * * @param step $step The step for this row. * @return string */ protected function col_target(step $step) { return $step->get_target()->get_displayname(); } /** * Format the current row's actions column. * * @param step $step The step for this row. * @return string */ protected function col_actions(step $step) { $actions = []; if ($step->is_first_step()) { $actions[] = helper::get_filler_icon(); } else { $actions[] = helper::format_icon_link($step->get_moveup_link(), 't/up', get_string('movestepup', 'tool_usertours')); } if ($step->is_last_step()) { $actions[] = helper::get_filler_icon(); } else { $actions[] = helper::format_icon_link($step->get_movedown_link(), 't/down', get_string('movestepdown', 'tool_usertours')); } $actions[] = helper::format_icon_link($step->get_edit_link(), 't/edit', get_string('edit')); $actions[] = helper::format_icon_link($step->get_delete_link(), 't/delete', get_string('delete'), 'moodle', [ 'data-action' => 'delete', 'data-id' => $step->get_id(), ]); return implode(' ', $actions); } } table/tour_list.php 0000644 00000011513 15152360013 0010365 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Table to show the list of tours. * * @package tool_usertours * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\table; defined('MOODLE_INTERNAL') || die(); use tool_usertours\helper; use tool_usertours\tour; require_once($CFG->libdir . '/tablelib.php'); /** * Table to show the list of tours. * * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tour_list extends \flexible_table { /** * Construct the tour table. */ public function __construct() { parent::__construct('tours'); $baseurl = new \moodle_url('/tool/usertours/configure.php'); $this->define_baseurl($baseurl); // Column definition. $this->define_columns(array( 'name', 'description', 'appliesto', 'enabled', 'actions', )); $this->define_headers(array( get_string('name', 'tool_usertours'), get_string('description', 'tool_usertours'), get_string('appliesto', 'tool_usertours'), get_string('enabled', 'tool_usertours'), get_string('actions', 'tool_usertours'), )); $this->set_attribute('class', 'admintable generaltable'); $this->setup(); $this->tourcount = helper::count_tours(); } /** * Format the current row's name column. * * @param tour $tour The tour for this row. * @return string */ protected function col_name(tour $tour) { global $OUTPUT; return $OUTPUT->render(helper::render_tourname_inplace_editable($tour)); } /** * Format the current row's description column. * * @param tour $tour The tour for this row. * @return string */ protected function col_description(tour $tour) { global $OUTPUT; return $OUTPUT->render(helper::render_tourdescription_inplace_editable($tour)); } /** * Format the current row's appliesto column. * * @param tour $tour The tour for this row. * @return string */ protected function col_appliesto(tour $tour) { return $tour->get_pathmatch(); } /** * Format the current row's enabled column. * * @param tour $tour The tour for this row. * @return string */ protected function col_enabled(tour $tour) { global $OUTPUT; return $OUTPUT->render(helper::render_tourenabled_inplace_editable($tour)); } /** * Format the current row's actions column. * * @param tour $tour The tour for this row. * @return string */ protected function col_actions(tour $tour) { $actions = []; if ($tour->is_first_tour()) { $actions[] = helper::get_filler_icon(); } else { $actions[] = helper::format_icon_link($tour->get_moveup_link(), 't/up', get_string('movetourup', 'tool_usertours')); } if ($tour->is_last_tour($this->tourcount)) { $actions[] = helper::get_filler_icon(); } else { $actions[] = helper::format_icon_link($tour->get_movedown_link(), 't/down', get_string('movetourdown', 'tool_usertours')); } $actions[] = helper::format_icon_link($tour->get_view_link(), 't/viewdetails', get_string('view')); $actions[] = helper::format_icon_link($tour->get_edit_link(), 't/edit', get_string('edit')); $actions[] = helper::format_icon_link($tour->get_duplicate_link(), 't/copy', get_string('duplicate')); $actions[] = helper::format_icon_link($tour->get_export_link(), 't/export', get_string('exporttour', 'tool_usertours'), 'tool_usertours'); $actions[] = helper::format_icon_link($tour->get_delete_link(), 't/delete', get_string('delete'), null, [ 'data-action' => 'delete', 'data-id' => $tour->get_id(), ]); return implode(' ', $actions); } } clientside_filter/cssselector.php 0000644 00000006745 15152360013 0013306 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Selector filter. * * @package tool_usertours * @copyright 2020 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\clientside_filter; use stdClass; use tool_usertours\tour; /** * Course filter. * * @copyright 2020 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cssselector extends clientside_filter { /** * The name of the filter. * * @return string */ public static function get_filter_name() { return 'cssselector'; } /** * Overrides the base add form element with a selector text box. * * @param \MoodleQuickForm $mform */ public static function add_filter_to_form(\MoodleQuickForm &$mform) { $filtername = self::get_filter_name(); $key = "filter_{$filtername}"; $mform->addElement('text', $key, get_string($key, 'tool_usertours')); $mform->setType($key, PARAM_RAW); $mform->addHelpButton($key, $key, 'tool_usertours'); } /** * Prepare the filter values for the form. * * @param tour $tour The tour to prepare values from * @param stdClass $data The data value * @return stdClass */ public static function prepare_filter_values_for_form(tour $tour, \stdClass $data) { $filtername = static::get_filter_name(); $key = "filter_{$filtername}"; $values = $tour->get_filter_values($filtername); if (empty($values)) { $values = [""]; } $data->$key = $values[0]; return $data; } /** * Save the filter values from the form to the tour. * * @param tour $tour The tour to save values to * @param stdClass $data The data submitted in the form */ public static function save_filter_values_from_form(tour $tour, \stdClass $data) { $filtername = static::get_filter_name(); $key = "filter_{$filtername}"; $newvalue = [$data->$key]; if (empty($data->$key)) { $newvalue = []; } $tour->set_filter_values($filtername, $newvalue); } /** * Returns the filter values needed for client side filtering. * * @param tour $tour The tour to find the filter values for * @return stdClass */ public static function get_client_side_values(tour $tour): stdClass { $filtername = static::get_filter_name(); $filtervalues = $tour->get_filter_values($filtername); // Filter values might not exist for tours that were created before this filter existed. if (!$filtervalues) { return new stdClass; } return (object) $filtervalues; } } clientside_filter/clientside_filter.php 0000644 00000003302 15152360013 0014427 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Clientside filter base. * * @package tool_usertours * @copyright 2020 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\clientside_filter; defined('MOODLE_INTERNAL') || die(); use stdClass; use tool_usertours\local\filter\base; use tool_usertours\tour; /** * Clientside filter base. * * @copyright 2020 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class clientside_filter extends base { /** * Returns the filter values needed for client side filtering. * * @param tour $tour The tour to find the filter values for * @return stdClass */ public static function get_client_side_values(tour $tour): stdClass { $data = (object) []; if (is_a(static::class, clientside_filter::class, true)) { $data->filterdata = $tour->get_filter_values(static::get_filter_name()); } return $data; } } filter/course.php 0000644 00000007223 15152360013 0010042 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Course filter. * * @package tool_usertours * @copyright 2017 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\filter; defined('MOODLE_INTERNAL') || die(); use tool_usertours\tour; use context; /** * Course filter. * * @copyright 2017 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course extends base { /** * The name of the filter. * * @return string */ public static function get_filter_name() { return 'course'; } /** * Overrides the base add form element with a course selector. * * @param \MoodleQuickForm $mform */ public static function add_filter_to_form(\MoodleQuickForm &$mform) { $options = ['multiple' => true]; $filtername = self::get_filter_name(); $key = "filter_{$filtername}"; $mform->addElement('course', $key, get_string($key, 'tool_usertours'), $options); $mform->setDefault($key, '0'); $mform->addHelpButton($key, $key, 'tool_usertours'); } /** * Check whether the filter matches the specified tour and/or context. * * @param tour $tour The tour to check * @param context $context The context to check * @return boolean */ public static function filter_matches(tour $tour, context $context) { global $COURSE; $values = $tour->get_filter_values(self::get_filter_name()); if (empty($values) || empty($values[0])) { // There are no values configured, meaning all. return true; } if (empty($COURSE->id)) { return false; } return in_array($COURSE->id, $values); } /** * Overrides the base prepare the filter values for the form with an integer value. * * @param tour $tour The tour to prepare values from * @param stdClass $data The data value * @return stdClass */ public static function prepare_filter_values_for_form(tour $tour, \stdClass $data) { $filtername = static::get_filter_name(); $key = "filter_{$filtername}"; $values = $tour->get_filter_values($filtername); if (empty($values)) { $values = 0; } $data->$key = $values; return $data; } /** * Overrides the base save the filter values from the form to the tour. * * @param tour $tour The tour to save values to * @param stdClass $data The data submitted in the form */ public static function save_filter_values_from_form(tour $tour, \stdClass $data) { $filtername = static::get_filter_name(); $key = "filter_{$filtername}"; $newvalue = $data->$key; if (empty($data->$key)) { $newvalue = []; } $tour->set_filter_values($filtername, $newvalue); } } filter/role.php 0000644 00000011033 15152360013 0007475 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Theme filter. * * @package tool_usertours * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\filter; defined('MOODLE_INTERNAL') || die(); use tool_usertours\tour; use context; /** * Theme filter. * * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class role extends base { /** * The Site Admin pseudo-role. * * @var ROLE_SITEADMIN int */ const ROLE_SITEADMIN = -1; /** * The name of the filter. * * @return string */ public static function get_filter_name() { return 'role'; } /** * Retrieve the list of available filter options. * * @return array An array whose keys are the valid options * And whose values are the values to display */ public static function get_filter_options() { $allroles = role_get_names(null, ROLENAME_ALIAS); $roles = []; foreach ($allroles as $role) { if ($role->archetype === 'guest') { // No point in including the 'guest' role as it isn't possible to show tours to a guest. continue; } $roles[$role->shortname] = $role->localname; } // Add the Site Administrator pseudo-role. $roles[self::ROLE_SITEADMIN] = get_string('administrator', 'core'); // Sort alphabetically too. \core_collator::asort($roles); return $roles; } /** * Check whether the filter matches the specified tour and/or context. * * @param tour $tour The tour to check * @param context $context The context to check * @return boolean */ public static function filter_matches(tour $tour, context $context) { global $USER; $values = $tour->get_filter_values(self::get_filter_name()); if (empty($values)) { // There are no values configured. // No values means all. return true; } // Presence within the array is sufficient. Ignore any value. $values = array_flip($values); if (isset($values[self::ROLE_SITEADMIN]) && is_siteadmin()) { // This tour has been restricted to a role including site admin, and this user is a site admin. return true; } // Use a request cache to save on DB queries. // We may be checking multiple tours and they'll all be for the same userid, and contextid $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'tool_usertours', 'filter_role'); // Get all of the roles used in this context, including special roles such as user, and frontpageuser. $cachekey = "{$USER->id}_{$context->id}"; $userroles = $cache->get($cachekey); if ($userroles === false) { $userroles = get_user_roles_with_special($context); $cache->set($cachekey, $userroles); } // Some special roles do not include the shortname. // Therefore we must fetch all roles too. Thankfully these don't actually change based on context. // They do require a DB call, so let's cache it. $cachekey = "allroles"; $allroles = $cache->get($cachekey); if ($allroles === false) { $allroles = get_all_roles(); $cache->set($cachekey, $allroles); } // Now we can check whether any of the user roles are in the list of allowed roles for this filter. foreach ($userroles as $role) { $shortname = $allroles[$role->roleid]->shortname; if (isset($values[$shortname])) { return true; } } return false; } } filter/theme.php 0000644 00000006277 15152360013 0007654 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Theme filter. * * @package tool_usertours * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\filter; defined('MOODLE_INTERNAL') || die(); use tool_usertours\tour; use context; /** * Theme filter. * * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class theme extends base { /** * The name of the filter. * * @return string */ public static function get_filter_name() { return 'theme'; } /** * Retrieve the list of available filter options. * * @return array An array whose keys are the valid options * And whose values are the values to display */ public static function get_filter_options() { $manager = \core_plugin_manager::instance(); $themes = $manager->get_installed_plugins('theme'); $options = []; foreach (array_keys($themes) as $themename) { try { $theme = \theme_config::load($themename); } catch (Exception $e) { // Bad theme, just skip it for now. continue; } if ($themename !== $theme->name) { // Obsoleted or broken theme, just skip for now. continue; } if ($theme->hidefromselector) { // The theme doesn't want to be shown in the theme selector and as theme // designer mode is switched off we will respect that decision. continue; } $options[$theme->name] = get_string('pluginname', "theme_{$theme->name}"); } return $options; } /** * Check whether the filter matches the specified tour and/or context. * * @param tour $tour The tour to check * @param context $context The context to check * @return boolean */ public static function filter_matches(tour $tour, context $context) { global $PAGE; $values = $tour->get_filter_values('theme'); if (empty($values)) { // There are no values configured. // No values means all. return true; } // Presence within the array is sufficient. Ignore any value. $values = array_flip($values); return isset($values[$PAGE->theme->name]); } } filter/accessdate.php 0000644 00000017161 15152360013 0010643 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Access Date filter. * * @package tool_usertours * @copyright 2019 Tom Dickman <tomdickman@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\filter; defined('MOODLE_INTERNAL') || die(); use context; use tool_usertours\tour; /** * Access date filter. Used to determine if USER should see a tour based on a particular access date. * * @copyright 2019 Tom Dickman <tomdickman@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class accessdate extends base { /** * Access date filtering constant for setting base date as account creation date. */ const FILTER_ACCOUNT_CREATION = 'tool_usertours_accountcreation'; /** * Access date filtering constant for setting base date as account first login date. */ const FILTER_FIRST_LOGIN = 'tool_usertours_firstlogin'; /** * Access date filtering constant for setting base date as account last login date. */ const FILTER_LAST_LOGIN = 'tool_usertours_lastlogin'; /** * Default this filter to not be enabled. */ const FILTER_ENABLED_DEFAULT = 0; /** * The name of the filter. * * @return string */ public static function get_filter_name() { return 'accessdate'; } /** * Retrieve the list of available filter options. * * @return array An array whose keys are the valid options * And whose values are the values to display * @throws \coding_exception */ public static function get_filter_options() { return array( self::FILTER_ACCOUNT_CREATION => get_string('filter_date_account_creation', 'tool_usertours'), self::FILTER_FIRST_LOGIN => get_string('filter_date_first_login', 'tool_usertours'), self::FILTER_LAST_LOGIN => get_string('filter_date_last_login', 'tool_usertours'), ); } /** * Add the form elements for the filter to the supplied form. * * @param \MoodleQuickForm $mform The form to add filter settings to. * * @throws \coding_exception */ public static function add_filter_to_form(\MoodleQuickForm &$mform) { $filtername = static::get_filter_name(); $key = "filter_{$filtername}"; $range = "{$key}_range"; $enabled = "{$key}_enabled"; $mform->addElement('advcheckbox', $enabled, get_string($key, 'tool_usertours'), get_string('filter_accessdate_enabled', 'tool_usertours'), null, array(0, 1)); $mform->addHelpButton($enabled, $enabled, 'tool_usertours'); $mform->addElement('select', $key, ' ', self::get_filter_options()); $mform->setDefault($key, self::FILTER_ACCOUNT_CREATION); $mform->hideIf($key, $enabled, 'notchecked'); $mform->addElement('duration', $range, null, [ 'optional' => false, 'defaultunit' => DAYSECS, ]); $mform->setDefault($range, 90 * DAYSECS); $mform->hideIf($range, $enabled, 'notchecked'); } /** * Prepare the filter values for the form. * * @param tour $tour The tour to prepare values from * @param stdClass $data The data value * @return stdClass */ public static function prepare_filter_values_for_form(tour $tour, \stdClass $data) { $filtername = static::get_filter_name(); $key = "filter_{$filtername}"; $range = "{$key}_range"; $enabled = "{$key}_enabled"; $values = $tour->get_filter_values($filtername); // Prepare the advanced checkbox value and prepare filter values based on previously set values. if (!empty($values)) { $data->$enabled = $values->$enabled ? $values->$enabled : self::FILTER_ENABLED_DEFAULT; if ($data->$enabled) { if (isset($values->$key)) { $data->$key = $values->$key; } if (isset($values->$range)) { $data->$range = $values->$range; } } } else { $data->$enabled = self::FILTER_ENABLED_DEFAULT; } return $data; } /** * Save the filter values from the form to the tour. * * @param tour $tour The tour to save values to * @param \stdClass $data The data submitted in the form */ public static function save_filter_values_from_form(tour $tour, \stdClass $data) { $filtername = static::get_filter_name(); $key = "filter_{$filtername}"; $range = "{$key}_range"; $enabled = "{$key}_enabled"; $savedata = []; $savedata[$key] = $data->$key; $savedata[$range] = $data->$range; $savedata[$enabled] = $data->$enabled; $tour->set_filter_values($filtername, $savedata); } /** * Check whether the filter matches the specified tour and/or context. * * @param tour $tour The tour to check * @param context $context The context to check * @return boolean */ public static function filter_matches(tour $tour, context $context) { global $USER; $filtername = static::get_filter_name(); $key = "filter_{$filtername}"; $range = "{$key}_range"; $enabled = "{$key}_enabled"; // Default behaviour is to match filter. $result = true; $values = (array) $tour->get_filter_values(self::get_filter_name()); // If the access date filter is not enabled, end here. if (empty($values[$enabled])) { return $result; } if (!empty($values[$key])) { switch ($values[$key]) { case (self::FILTER_ACCOUNT_CREATION): $filterbasedate = (int) $USER->timecreated; break; case (self::FILTER_FIRST_LOGIN): $filterbasedate = (int) $USER->firstaccess; break; case (self::FILTER_LAST_LOGIN): $filterbasedate = (int) $USER->lastlogin; break; default: // Use account creation as default. $filterbasedate = (int) $USER->timecreated; break; } // If the base date has no value because a user hasn't accessed Moodle yet, default to account creation. if (empty($filterbasedate)) { $filterbasedate = (int) $USER->timecreated; } if (!empty($values[$range])) { $filterrange = (int) $values[$range]; } else { $filterrange = 90 * DAYSECS; } // If we're outside the set range from the set base date, filter out tour. if ((time() > ($filterbasedate + $filterrange))) { $result = false; } } return $result; } } filter/category.php 0000644 00000006042 15152360013 0010355 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Category filter. * * @package tool_usertours * @copyright 2017 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\filter; defined('MOODLE_INTERNAL') || die(); use tool_usertours\tour; use context; /** * Category filter. * * @copyright 2017 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class category extends base { /** * The name of the filter. * * @return string */ public static function get_filter_name() { return 'category'; } /** * Retrieve the list of available filter options. * * @return array An array whose keys are the valid options * And whose values are the values to display */ public static function get_filter_options() { $options = \core_course_category::make_categories_list(); return $options; } /** * Check whether the filter matches the specified tour and/or context. * * @param tour $tour The tour to check * @param context $context The context to check * @return boolean */ public static function filter_matches(tour $tour, context $context) { $values = $tour->get_filter_values(self::get_filter_name()); if (empty($values) || empty($values[0])) { // There are no values configured, meaning all. return true; } if ($context->contextlevel < CONTEXT_COURSECAT) { return false; } return self::check_contexts($context, $values); } /** * Recursive function allows checking of parent categories. * * @param context $context * @param array $values * @return boolean */ private static function check_contexts(context $context, $values) { if ($context->contextlevel > CONTEXT_COURSECAT) { return self::check_contexts($context->get_parent_context(), $values); } else if ($context->contextlevel == CONTEXT_COURSECAT) { if (in_array($context->instanceid, $values)) { return true; } else { return self::check_contexts($context->get_parent_context(), $values); } } else { return false; } } } filter/base.php 0000644 00000007701 15152360013 0007455 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Filter base. * * @package tool_usertours * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\filter; defined('MOODLE_INTERNAL') || die(); use tool_usertours\tour; use context; /** * Filter base. * * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class base { /** * Any Value. */ const ANYVALUE = '__ANYVALUE__'; /** * The name of the filter. * * @return string */ public static function get_filter_name() { throw new \coding_exception('get_filter_name() must be defined'); } /** * Retrieve the list of available filter options. * * @return array An array whose keys are the valid options */ public static function get_filter_options() { return []; } /** * Check whether the filter matches the specified tour and/or context. * * @param tour $tour The tour to check * @param context $context The context to check * @return boolean */ public static function filter_matches(tour $tour, context $context) { return true; } /** * Add the form elements for the filter to the supplied form. * * @param MoodleQuickForm $mform The form to add filter settings to. */ public static function add_filter_to_form(\MoodleQuickForm &$mform) { $options = [ static::ANYVALUE => get_string('all'), ]; $options += static::get_filter_options(); $filtername = static::get_filter_name(); $key = "filter_{$filtername}"; $mform->addElement('select', $key, get_string($key, 'tool_usertours'), $options, [ 'multiple' => true, ]); $mform->setDefault($key, static::ANYVALUE); $mform->addHelpButton($key, $key, 'tool_usertours'); } /** * Prepare the filter values for the form. * * @param tour $tour The tour to prepare values from * @param stdClass $data The data value * @return stdClass */ public static function prepare_filter_values_for_form(tour $tour, \stdClass $data) { $filtername = static::get_filter_name(); $key = "filter_{$filtername}"; $values = $tour->get_filter_values($filtername); if (empty($values)) { $values = static::ANYVALUE; } $data->$key = $values; return $data; } /** * Save the filter values from the form to the tour. * * @param tour $tour The tour to save values to * @param stdClass $data The data submitted in the form */ public static function save_filter_values_from_form(tour $tour, \stdClass $data) { $filtername = static::get_filter_name(); $key = "filter_{$filtername}"; $newvalue = $data->$key; foreach ($data->$key as $value) { if ($value === static::ANYVALUE) { $newvalue = []; break; } } $tour->set_filter_values($filtername, $newvalue); } } filter/courseformat.php 0000644 00000004756 15152360013 0011263 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Course format filter. * * @package tool_usertours * @copyright 2017 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\filter; defined('MOODLE_INTERNAL') || die(); use tool_usertours\tour; use context; /** * Course format filter. * * @copyright 2017 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class courseformat extends base { /** * The name of the filter. * * @return string */ public static function get_filter_name() { return 'courseformat'; } /** * Retrieve the list of available filter options. * * @return array An array whose keys are the valid options * And whose values are the values to display */ public static function get_filter_options() { $options = []; $courseformats = get_sorted_course_formats(true); foreach ($courseformats as $courseformat) { $options[$courseformat] = get_string('pluginname', "format_$courseformat"); } return $options; } /** * Check whether the filter matches the specified tour and/or context. * * @param tour $tour The tour to check * @param context $context The context to check * @return boolean */ public static function filter_matches(tour $tour, context $context) { global $COURSE; $values = $tour->get_filter_values('courseformat'); if (empty($values)) { // There are no values configured, meaning all. return true; } if (empty($COURSE->format)) { return false; } return in_array($COURSE->format, $values); } } forms/editstep.php 0000644 00000021420 15152360013 0010217 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Form for editing steps. * * @package tool_usertours * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\forms; use stdClass; use tool_usertours\helper; use tool_usertours\step; defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); require_once($CFG->libdir . '/formslib.php'); /** * Form for editing steps. * * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class editstep extends \moodleform { /** * @var tool_usertours\step $step */ protected $step; /** * @var int Display the step's content by using Moodle language string. */ private const CONTENTTYPE_LANGSTRING = 0; /** * @var int Display the step's content by entering it manually. */ private const CONTENTTYPE_MANUAL = 1; /** * Create the edit step form. * * @param string $target The target of the form. * @param step $step The step being editted. */ public function __construct($target, \tool_usertours\step $step) { $this->step = $step; parent::__construct($target); } /** * Form definition. */ public function definition() { global $CFG; $mform = $this->_form; $mform->addElement('header', 'heading_target', get_string('target_heading', 'tool_usertours')); $types = []; foreach (\tool_usertours\target::get_target_types() as $value => $type) { $types[$value] = get_string('target_' . $type, 'tool_usertours'); } $mform->addElement('select', 'targettype', get_string('targettype', 'tool_usertours'), $types); $mform->addHelpButton('targettype', 'targettype', 'tool_usertours'); // The target configuration. foreach (\tool_usertours\target::get_target_types() as $value => $type) { $targetclass = \tool_usertours\target::get_classname($type); $targetclass::add_config_to_form($mform); } // Content of the step. $mform->addElement('header', 'heading_content', get_string('content_heading', 'tool_usertours')); $mform->addElement('textarea', 'title', get_string('title', 'tool_usertours')); $mform->addRule('title', get_string('required'), 'required', null, 'client'); $mform->setType('title', PARAM_TEXT); $mform->addHelpButton('title', 'title', 'tool_usertours'); // Content type. $typeoptions = [ static::CONTENTTYPE_LANGSTRING => get_string('content_type_langstring', 'tool_usertours'), static::CONTENTTYPE_MANUAL => get_string('content_type_manual', 'tool_usertours') ]; $mform->addElement('select', 'contenttype', get_string('content_type', 'tool_usertours'), $typeoptions); $mform->addHelpButton('contenttype', 'content_type', 'tool_usertours'); $mform->setDefault('contenttype', static::CONTENTTYPE_MANUAL); // Language identifier. $mform->addElement('textarea', 'contentlangstring', get_string('moodle_language_identifier', 'tool_usertours')); $mform->setType('contentlangstring', PARAM_TEXT); $mform->hideIf('contentlangstring', 'contenttype', 'eq', static::CONTENTTYPE_MANUAL); $editoroptions = [ 'subdirs' => 1, 'maxbytes' => $CFG->maxbytes, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'changeformat' => 1, 'trusttext' => true ]; $objs = $mform->createElement('editor', 'content', get_string('content', 'tool_usertours'), null, $editoroptions); // TODO: MDL-68540 We need to add the editor to a group element because editor element will not work with hideIf. $mform->addElement('group', 'contenthtmlgrp', get_string('content', 'tool_usertours'), [$objs], ' ', false); $mform->addHelpButton('contenthtmlgrp', 'content', 'tool_usertours'); $mform->hideIf('contenthtmlgrp', 'contenttype', 'eq', static::CONTENTTYPE_LANGSTRING); // Add the step configuration. $mform->addElement('header', 'heading_options', get_string('options_heading', 'tool_usertours')); // All step configuration is defined in the step. $this->step->add_config_to_form($mform); // And apply any form constraints. foreach (\tool_usertours\target::get_target_types() as $value => $type) { $targetclass = \tool_usertours\target::get_classname($type); $targetclass::add_disabled_constraints_to_form($mform); } $this->add_action_buttons(); } /** * Validate the database on the submitted content type. * * @param array $data array of ("fieldname"=>value) of submitted data * @param array $files array of uploaded files "element_name"=>tmp_file_path * @return array of "element_name"=>"error_description" if there are errors, * or an empty array if everything is OK (true allowed for backwards compatibility too). */ public function validation($data, $files): array { $errors = parent::validation($data, $files); if ($data['contenttype'] == static::CONTENTTYPE_LANGSTRING) { if (!isset($data['contentlangstring']) || trim($data['contentlangstring']) == '') { $errors['contentlangstring'] = get_string('required'); } else { $splitted = explode(',', trim($data['contentlangstring']), 2); $langid = $splitted[0]; $langcomponent = $splitted[1]; if (!get_string_manager()->string_exists($langid, $langcomponent)) { $errors['contentlangstring'] = get_string('invalid_lang_id', 'tool_usertours'); } } } // Validate manually entered text content. Validation logic derived from \MoodleQuickForm_Rule_Required::validate() // without the checking of the "strictformsrequired" admin setting. if ($data['contenttype'] == static::CONTENTTYPE_MANUAL) { $value = $data['content']['text'] ?? ''; // All tags except img, canvas and hr, plus all forms of whitespaces. $stripvalues = [ '#</?(?!img|canvas|hr).*?>#im', '#(\xc2\xa0|\s| )#', ]; $value = preg_replace($stripvalues, '', (string)$value); if (empty($value)) { $errors['contenthtmlgrp'] = get_string('required'); } } return $errors; } /** * Load in existing data as form defaults. Usually new entry defaults are stored directly in * form definition (new entry form); this function is used to load in data where values * already exist and data is being edited (edit entry form). * * @param stdClass|array $data object or array of default values */ public function set_data($data): void { $data = (object) $data; if (!isset($data->contenttype)) { if (!empty($data->content['text']) && helper::is_language_string_from_input($data->content['text'])) { $data->contenttype = static::CONTENTTYPE_LANGSTRING; $data->contentlangstring = $data->content['text']; // Empty the editor content. $data->content = ['text' => '']; } else { $data->contenttype = static::CONTENTTYPE_MANUAL; } } parent::set_data($data); } /** * Return submitted data if properly submitted or returns NULL if validation fails or * if there is no submitted data. * * @return object|null submitted data; NULL if not valid or not submitted or cancelled */ public function get_data(): ?object { $data = parent::get_data(); if ($data) { if ($data->contenttype == static::CONTENTTYPE_LANGSTRING) { $data->content = [ 'text' => $data->contentlangstring, 'format' => FORMAT_MOODLE, ]; } } return $data; } } forms/edittour.php 0000644 00000007143 15152360013 0010243 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Form for editing tours. * * @package tool_usertours * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\forms; defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); require_once($CFG->libdir . '/formslib.php'); use \tool_usertours\helper; /** * Form for editing tours. * * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class edittour extends \moodleform { /** * @var tool_usertours\tour $tour */ protected $tour; /** * Create the edit tour form. * * @param tour $tour The tour being editted. */ public function __construct(\tool_usertours\tour $tour) { $this->tour = $tour; parent::__construct($tour->get_edit_link()); } /** * Form definition. */ public function definition() { $mform = $this->_form; // ID of existing tour. $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); // Name of the tour. $mform->addElement('text', 'name', get_string('name', 'tool_usertours')); $mform->addRule('name', get_string('required'), 'required', null, 'client'); $mform->setType('name', PARAM_TEXT); $mform->addHelpButton('name', 'name', 'tool_usertours'); // Admin-only descriptions. $mform->addElement('textarea', 'description', get_string('description', 'tool_usertours')); $mform->setType('description', PARAM_RAW); $mform->addHelpButton('description', 'description', 'tool_usertours'); // Application. $mform->addElement('text', 'pathmatch', get_string('pathmatch', 'tool_usertours')); $mform->setType('pathmatch', PARAM_RAW); $mform->addHelpButton('pathmatch', 'pathmatch', 'tool_usertours'); $mform->addElement('checkbox', 'enabled', get_string('tourisenabled', 'tool_usertours')); $mform->addElement('text', 'endtourlabel', get_string('endtourlabel', 'tool_usertours')); $mform->setType('endtourlabel', PARAM_TEXT); $mform->addHelpButton('endtourlabel', 'endtourlabel', 'tool_usertours'); $mform->addElement('checkbox', 'displaystepnumbers', get_string('displaystepnumbers', 'tool_usertours')); $mform->addHelpButton('displaystepnumbers', 'displaystepnumbers', 'tool_usertours'); // Configuration. $this->tour->add_config_to_form($mform); // Filters. $mform->addElement('header', 'filters', get_string('filter_header', 'tool_usertours')); $mform->addElement('static', 'filterhelp', '', get_string('filter_help', 'tool_usertours')); foreach (helper::get_all_filters() as $filterclass) { $filterclass::add_filter_to_form($mform); } $this->add_action_buttons(); } } forms/importtour.php 0000644 00000003320 15152360013 0010621 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Form for editing tours. * * @package tool_usertours * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_usertours\local\forms; defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); require_once($CFG->libdir . '/formslib.php'); /** * Form for importing tours. * * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class importtour extends \moodleform { /** * Create the import tour form. */ public function __construct() { parent::__construct(\tool_usertours\helper::get_import_tour_link()); } /** * Form definition. */ public function definition() { $mform = $this->_form; $mform->addElement('filepicker', 'tourconfig', get_string('tourconfig', 'tool_usertours')); $mform->addRule('tourconfig', null, 'required'); $this->add_action_buttons(); } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | ���֧ߧ֧�ѧ�ڧ� ����ѧߧڧ��: 0.82 |
proxy
|
phpinfo
|
���ѧ����ۧܧ�