Merge branch 'MDL-76188-master' of https://github.com/ferranrecio/moodle

This commit is contained in:
Paul Holden 2023-01-03 09:34:28 +00:00
commit 42926069da
19 changed files with 704 additions and 46 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -60,7 +60,7 @@ define(
// Meanwhile, we filter the migrated actions.
const componentActions = [
'moveSection', 'moveCm', 'addSection', 'deleteSection', 'sectionHide', 'sectionShow',
'cmHide', 'cmShow', 'cmStealth',
'cmHide', 'cmShow', 'cmStealth', 'sectionHighlight', 'sectionUnhighlight',
];
// The course reactive instance.

View File

@ -6,6 +6,6 @@ define("core_courseformat/local/content/section",["exports","core_courseformat/l
* @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&&sectioninfo.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}));
*/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",CURRENT:"current"},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,_element$current;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.element.classList.toggle(this.classes.CURRENT,null!==(_element$current=element.current)&&void 0!==_element$current&&_element$current),this.locked=element.locked;const sectioninfo=this.getElement(this.selectors.SECTIONINFO);sectioninfo&&sectioninfo.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

File diff suppressed because one or more lines are too long

View File

@ -51,6 +51,7 @@ export default class extends DndSection {
HASDESCRIPTION: 'description',
HIDE: 'd-none',
HIDDEN: 'hidden',
CURRENT: 'current',
};
// We need our id to watch specific events.
@ -130,6 +131,7 @@ export default class extends DndSection {
this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);
this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);
this.element.classList.toggle(this.classes.HIDDEN, !element.visible ?? false);
this.element.classList.toggle(this.classes.CURRENT, element.current ?? false);
this.locked = element.locked;
// The description box classes depends on the section state.
const sectioninfo = this.getElement(this.selectors.SECTIONINFO);

View File

@ -286,6 +286,15 @@ abstract class base {
return $this->courseid;
}
/**
* Returns the course context.
*
* @return context_course the course context
*/
final public function get_context(): context_course {
return context_course::instance($this->courseid);
}
/**
* Returns a record from course database table plus additional fields
* that course format defines

View File

@ -0,0 +1,3 @@
define("format_topics/mutations",["exports","core_courseformat/courseeditor","core_courseformat/local/courseeditor/mutations","core_courseformat/local/content/actions"],(function(_exports,_courseeditor,_mutations,_actions){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_mutations=_interopRequireDefault(_mutations),_actions=_interopRequireDefault(_actions);class TopicsMutations extends _mutations.default{constructor(){super(...arguments),_defineProperty(this,"sectionHighlight",(async function(stateManager,sectionIds){const course=stateManager.get("course");this.sectionLock(stateManager,sectionIds,!0);const updates=await this._callEditWebservice("section_highlight",course.id,sectionIds);stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)})),_defineProperty(this,"sectionUnhighlight",(async function(stateManager,sectionIds){const course=stateManager.get("course");this.sectionLock(stateManager,sectionIds,!0);const updates=await this._callEditWebservice("section_unhighlight",course.id,sectionIds);stateManager.processUpdates(updates),this.sectionLock(stateManager,sectionIds,!1)}))}}_exports.init=()=>{(0,_courseeditor.getCurrentCourseEditor)().addMutations(new TopicsMutations),_actions.default.addActions({sectionHighlight:"sectionHighlight",sectionUnhighlight:"sectionUnhighlight"})}}));
//# sourceMappingURL=mutations.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"mutations.min.js","sources":["../src/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\n/**\n * Format topics mutations.\n *\n * An instance of this class will be used to add custom mutations to the course editor.\n * To make sure the addMutations method find the proper functions, all functions must\n * be declared as class attributes, not a simple methods. The reason is because many\n * plugins can add extra mutations to the course editor.\n *\n * @module format_topics/mutations\n * @copyright 2022 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getCurrentCourseEditor} from 'core_courseformat/courseeditor';\nimport DefaultMutations from 'core_courseformat/local/courseeditor/mutations';\nimport CourseActions from 'core_courseformat/local/content/actions';\n\nclass TopicsMutations extends DefaultMutations {\n\n /**\n * Highlight sections.\n *\n * It is important to note this mutation method is declared as a class attribute,\n * See the class jsdoc for more details on why.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n */\n sectionHighlight = async function(stateManager, sectionIds) {\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice('section_highlight', course.id, sectionIds);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n };\n\n /**\n * Unhighlight sections.\n *\n * It is important to note this mutation method is declared as a class attribute,\n * See the class jsdoc for more details on why.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {array} sectionIds the list of section ids\n */\n sectionUnhighlight = async function(stateManager, sectionIds) {\n const course = stateManager.get('course');\n this.sectionLock(stateManager, sectionIds, true);\n const updates = await this._callEditWebservice('section_unhighlight', course.id, sectionIds);\n stateManager.processUpdates(updates);\n this.sectionLock(stateManager, sectionIds, false);\n };\n}\n\nexport const init = () => {\n const courseEditor = getCurrentCourseEditor();\n // Some plugin (activity or block) may have their own mutations already registered.\n // This is why we use addMutations instead of setMutations here.\n courseEditor.addMutations(new TopicsMutations());\n // Add direct mutation content actions.\n CourseActions.addActions({\n sectionHighlight: 'sectionHighlight',\n sectionUnhighlight: 'sectionUnhighlight',\n });\n};\n"],"names":["TopicsMutations","DefaultMutations","async","stateManager","sectionIds","course","get","sectionLock","updates","this","_callEditWebservice","id","processUpdates","addMutations","addActions","sectionHighlight","sectionUnhighlight"],"mappings":"goBAgCMA,wBAAwBC,8FAWPC,eAAeC,aAAcC,kBACtCC,OAASF,aAAaG,IAAI,eAC3BC,YAAYJ,aAAcC,YAAY,SACrCI,cAAgBC,KAAKC,oBAAoB,oBAAqBL,OAAOM,GAAIP,YAC/ED,aAAaS,eAAeJ,cACvBD,YAAYJ,aAAcC,YAAY,iDAY1BF,eAAeC,aAAcC,kBACxCC,OAASF,aAAaG,IAAI,eAC3BC,YAAYJ,aAAcC,YAAY,SACrCI,cAAgBC,KAAKC,oBAAoB,sBAAuBL,OAAOM,GAAIP,YACjFD,aAAaS,eAAeJ,cACvBD,YAAYJ,aAAcC,YAAY,qBAI/B,MACK,0CAGRS,aAAa,IAAIb,kCAEhBc,WAAW,CACrBC,iBAAkB,mBAClBC,mBAAoB"}

View File

@ -0,0 +1,10 @@
define("format_topics/section",["exports","core/reactive","core_courseformat/courseeditor","core/templates"],(function(_exports,_reactive,_courseeditor,_templates){var obj;
/**
* Format topics section extra logic component.
*
* @module format_topics/mutations
* @copyright 2022 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.init=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};class HighlightSection extends _reactive.BaseComponent{create(){this.name="format_topics_section",this.selectors={SECTION:"[data-for='section']",SETMARKER:'[data-action="sectionHighlight"]',REMOVEMARKER:'[data-action="sectionUnhighlight"]',ACTIONTEXT:".menu-action-text",ICON:".icon"},this.classes={HIDE:"d-none"},this.formatActions={HIGHLIGHT:"sectionHighlight",UNHIGHLIGHT:"sectionUnhighlight"}}getWatchers(){return[{watch:"section.current:updated",handler:this._refreshHighlight}]}async _refreshHighlight(_ref){var _affectedAction$datas,_affectedAction$datas2;let selector,newAction,{element:element}=_ref;element.current?(selector=this.selectors.SETMARKER,newAction=this.formatActions.UNHIGHLIGHT):(selector=this.selectors.REMOVEMARKER,newAction=this.formatActions.HIGHLIGHT);const affectedAction=this.getElement("".concat(this.selectors.SECTION," ").concat(selector),element.id);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,"")}}}}_exports.init=()=>{const courseEditor=(0,_courseeditor.getCurrentCourseEditor)();courseEditor.supportComponents&&courseEditor.isEditing&&new HighlightSection({element:document.getElementById("region-main"),reactive:courseEditor})}}));
//# sourceMappingURL=section.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,80 @@
// 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/>.
/**
* Format topics mutations.
*
* An instance of this class will be used to add custom mutations to the course editor.
* To make sure the addMutations method find the proper functions, all functions must
* be declared as class attributes, not a simple methods. The reason is because many
* plugins can add extra mutations to the course editor.
*
* @module format_topics/mutations
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
import DefaultMutations from 'core_courseformat/local/courseeditor/mutations';
import CourseActions from 'core_courseformat/local/content/actions';
class TopicsMutations extends DefaultMutations {
/**
* Highlight sections.
*
* It is important to note this mutation method is declared as a class attribute,
* See the class jsdoc for more details on why.
*
* @param {StateManager} stateManager the current state manager
* @param {array} sectionIds the list of section ids
*/
sectionHighlight = async function(stateManager, sectionIds) {
const course = stateManager.get('course');
this.sectionLock(stateManager, sectionIds, true);
const updates = await this._callEditWebservice('section_highlight', course.id, sectionIds);
stateManager.processUpdates(updates);
this.sectionLock(stateManager, sectionIds, false);
};
/**
* Unhighlight sections.
*
* It is important to note this mutation method is declared as a class attribute,
* See the class jsdoc for more details on why.
*
* @param {StateManager} stateManager the current state manager
* @param {array} sectionIds the list of section ids
*/
sectionUnhighlight = async function(stateManager, sectionIds) {
const course = stateManager.get('course');
this.sectionLock(stateManager, sectionIds, true);
const updates = await this._callEditWebservice('section_unhighlight', course.id, sectionIds);
stateManager.processUpdates(updates);
this.sectionLock(stateManager, sectionIds, false);
};
}
export const init = () => {
const courseEditor = getCurrentCourseEditor();
// Some plugin (activity or block) may have their own mutations already registered.
// This is why we use addMutations instead of setMutations here.
courseEditor.addMutations(new TopicsMutations());
// Add direct mutation content actions.
CourseActions.addActions({
sectionHighlight: 'sectionHighlight',
sectionUnhighlight: 'sectionUnhighlight',
});
};

View File

@ -0,0 +1,115 @@
// 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/>.
/**
* Format topics section extra logic component.
*
* @module format_topics/mutations
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {BaseComponent} from 'core/reactive';
import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
import Templates from 'core/templates';
class HighlightSection extends BaseComponent {
/**
* Constructor hook.
*/
create() {
// Optional component name for debugging.
this.name = 'format_topics_section';
// Default query selectors.
this.selectors = {
SECTION: `[data-for='section']`,
SETMARKER: `[data-action="sectionHighlight"]`,
REMOVEMARKER: `[data-action="sectionUnhighlight"]`,
ACTIONTEXT: `.menu-action-text`,
ICON: `.icon`,
};
// Default classes to toggle on refresh.
this.classes = {
HIDE: 'd-none',
};
// The topics format section specific actions.
this.formatActions = {
HIGHLIGHT: 'sectionHighlight',
UNHIGHLIGHT: 'sectionUnhighlight',
};
}
/**
* Component watchers.
*
* @returns {Array} of watchers
*/
getWatchers() {
return [
{watch: `section.current:updated`, handler: this._refreshHighlight},
];
}
/**
* Update a content section using the state information.
*
* @param {object} param
* @param {Object} param.element details the update details.
*/
async _refreshHighlight({element}) {
let selector;
let newAction;
if (element.current) {
selector = this.selectors.SETMARKER;
newAction = this.formatActions.UNHIGHLIGHT;
} else {
selector = this.selectors.REMOVEMARKER;
newAction = this.formatActions.HIGHLIGHT;
}
// Find the affected action.
const affectedAction = this.getElement(`${this.selectors.SECTION} ${selector}`, element.id);
if (!affectedAction) {
return;
}
// Change action, text and icon.
affectedAction.dataset.action = newAction;
const actionText = affectedAction.querySelector(this.selectors.ACTIONTEXT);
if (affectedAction.dataset?.swapname && actionText) {
const oldText = actionText?.innerText;
actionText.innerText = affectedAction.dataset.swapname;
affectedAction.dataset.swapname = oldText;
}
const icon = affectedAction.querySelector(this.selectors.ICON);
if (affectedAction.dataset?.swapicon && icon) {
const newIcon = affectedAction.dataset.swapicon;
if (newIcon) {
const pixHtml = await Templates.renderPix(newIcon, 'core');
Templates.replaceNode(icon, pixHtml, '');
}
}
}
}
export const init = () => {
// Add component to the section.
const courseEditor = getCurrentCourseEditor();
if (courseEditor.supportComponents && courseEditor.isEditing) {
new HighlightSection({
element: document.getElementById('region-main'),
reactive: courseEditor,
});
}
};

View File

@ -0,0 +1,117 @@
<?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/>.
namespace format_topics\courseformat;
use core_courseformat\stateupdates;
use core_courseformat\stateactions as stateactions_base;
use core\event\course_module_updated;
use cm_info;
use section_info;
use stdClass;
use course_modinfo;
use moodle_exception;
use context_module;
use context_course;
/**
* Contains the core course state actions specific to topics format.
*
* @package format_topics
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class stateactions extends stateactions_base {
/**
* Highlight course section.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids section ids (only ther first one will be highlighted)
* @param int $targetsectionid not used
* @param int $targetcmid not used
*/
public function section_highlight(
stateupdates $updates,
stdClass $course,
array $ids = [],
?int $targetsectionid = null,
?int $targetcmid = null
): void {
global $DB;
$this->validate_sections($course, $ids, __FUNCTION__);
$coursecontext = context_course::instance($course->id);
require_capability('moodle/course:setcurrentsection', $coursecontext);
// Get the previous marked section.
$modinfo = get_fast_modinfo($course);
$previousmarker = $DB->get_field("course", "marker", ['id' => $course->id]);
$section = $modinfo->get_section_info_by_id(reset($ids), MUST_EXIST);
if ($section->section == $previousmarker) {
return;
}
// Mark the new one.
course_set_marker($course->id, $section->section);
$updates->add_section_put($section->id);
if ($previousmarker) {
$section = $modinfo->get_section_info($previousmarker);
$updates->add_section_put($section->id);
}
}
/**
* Remove highlight from a course sections.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids optional extra section ids to refresh
* @param int $targetsectionid not used
* @param int $targetcmid not used
*/
public function section_unhighlight(
stateupdates $updates,
stdClass $course,
array $ids = [],
?int $targetsectionid = null,
?int $targetcmid = null
): void {
global $DB;
$this->validate_sections($course, $ids, __FUNCTION__);
$coursecontext = context_course::instance($course->id);
require_capability('moodle/course:setcurrentsection', $coursecontext);
$affectedsections = [];
// Get the previous marked section and unmark it.
$modinfo = get_fast_modinfo($course);
$previousmarker = $DB->get_field("course", "marker", ['id' => $course->id]);
course_set_marker($course->id, 0);
$section = $modinfo->get_section_info($previousmarker, MUST_EXIST);
$updates->add_section_put($section->id);
foreach ($ids as $sectionid) {
$section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST);
if ($section->section != $previousmarker) {
$updates->add_section_put($section->id);
}
}
}
}

View File

@ -25,6 +25,7 @@
namespace format_topics\output\courseformat;
use core_courseformat\output\local\content as content_base;
use renderer_base;
/**
* Base class to render a course content.
@ -42,4 +43,17 @@ class content extends content_base {
*/
protected $hasaddsection = false;
/**
* Export this data so it can be used as the context for a mustache template (core/inplace_editable).
*
* @param renderer_base $output typically, the renderer that's calling this function
* @return stdClass data context for a mustache template
*/
public function export_for_template(renderer_base $output) {
global $PAGE;
$PAGE->requires->js_call_amd('format_topics/mutations', 'init');
$PAGE->requires->js_call_amd('format_topics/section', 'init');
return parent::export_for_template($output);
}
}

View File

@ -24,8 +24,8 @@
namespace format_topics\output\courseformat\content\section;
use context_course;
use core_courseformat\output\local\content\section\controlmenu as controlmenu_base;
use moodle_url;
/**
* Base class to render a course section menu.
@ -36,10 +36,10 @@ use core_courseformat\output\local\content\section\controlmenu as controlmenu_ba
*/
class controlmenu extends controlmenu_base {
/** @var course_format the course format class */
/** @var \core_courseformat\base the course format class */
protected $format;
/** @var section_info the course section class */
/** @var \section_info the course section class */
protected $section;
/**
@ -53,47 +53,11 @@ class controlmenu extends controlmenu_base {
$format = $this->format;
$section = $this->section;
$course = $format->get_course();
$sectionreturn = $format->get_section_number();
$coursecontext = context_course::instance($course->id);
if ($sectionreturn) {
$url = course_get_url($course, $section->section);
} else {
$url = course_get_url($course);
}
$url->param('sesskey', sesskey());
$coursecontext = $format->get_context();
$controls = [];
if ($section->section && has_capability('moodle/course:setcurrentsection', $coursecontext)) {
if ($course->marker == $section->section) { // Show the "light globe" on/off.
$url->param('marker', 0);
$highlightoff = get_string('highlightoff');
$controls['highlight'] = [
'url' => $url,
'icon' => 'i/marked',
'name' => $highlightoff,
'pixattr' => ['class' => ''],
'attr' => [
'class' => 'editing_highlight',
'data-action' => 'removemarker'
],
];
} else {
$url->param('marker', $section->section);
$highlight = get_string('highlight');
$controls['highlight'] = [
'url' => $url,
'icon' => 'i/marker',
'name' => $highlight,
'pixattr' => ['class' => ''],
'attr' => [
'class' => 'editing_highlight',
'data-action' => 'setmarker'
],
];
}
$controls['highlight'] = $this->get_highlight_control();
}
$parentcontrols = parent::section_control_items();
@ -116,4 +80,72 @@ class controlmenu extends controlmenu_base {
return array_merge($controls, $parentcontrols);
}
}
/**
* Return the course url.
*
* @return moodle_url
*/
protected function get_course_url(): moodle_url {
$format = $this->format;
$section = $this->section;
$course = $format->get_course();
$sectionreturn = $format->get_section_number();
if ($sectionreturn) {
$url = course_get_url($course, $section->section);
} else {
$url = course_get_url($course);
}
$url->param('sesskey', sesskey());
return $url;
}
/**
* Return the specific section highlight action.
*
* @return array the action element.
*/
protected function get_highlight_control(): array {
$format = $this->format;
$section = $this->section;
$course = $format->get_course();
$url = $this->get_course_url();
$highlightoff = get_string('highlightoff');
$highlighton = get_string('highlight');
if ($course->marker == $section->section) { // Show the "light globe" on/off.
$url->param('marker', 0);
$result = [
'url' => $url,
'icon' => 'i/marked',
'name' => $highlightoff,
'pixattr' => ['class' => ''],
'attr' => [
'class' => 'editing_highlight',
'data-action' => 'sectionUnhighlight',
'data-id' => $section->id,
'data-swapname' => $highlighton,
'data-swapicon' => 'i/marker',
],
];
} else {
$url->param('marker', $section->section);
$result = [
'url' => $url,
'icon' => 'i/marker',
'name' => $highlighton,
'pixattr' => ['class' => ''],
'attr' => [
'class' => 'editing_highlight',
'data-action' => 'sectionHighlight',
'data-id' => $section->id,
'data-swapname' => $highlightoff,
'data-swapicon' => 'i/marked',
],
];
}
return $result;
}
}

View File

@ -0,0 +1,49 @@
@format @format_topics
Feature: Sections can be highlighted
In order to mark sections
As a teacher
I need to highlight and unhighlight sections
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format | coursedisplay | numsections |
| Course 1 | C1 | topics | 0 | 5 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Test assignment name | Test assignment description | C1 | assign1 | 0 |
| book | Test book name | Test book description | C1 | book1 | 1 |
| chat | Test chat name | Test chat description | C1 | chat1 | 4 |
| choice | Test choice name | Test choice description | C1 | choice1 | 5 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
@javascript
Scenario: Highlight a section
When I open section "2" edit menu
And I click on "Highlight" "link" in the "Topic 2" "section"
Then I should see "Highlighted" in the "Topic 2" "section"
@javascript
Scenario: Highlight a section when another section is already highlighted
Given I open section "3" edit menu
And I click on "Highlight" "link" in the "Topic 3" "section"
And I should see "Highlighted" in the "Topic 3" "section"
When I open section "2" edit menu
And I click on "Highlight" "link" in the "Topic 2" "section"
Then I should see "Highlighted" in the "Topic 2" "section"
And I should not see "Highlighted" in the "Topic 3" "section"
@javascript
Scenario: Unhighlight a section
Given I open section "3" edit menu
And I click on "Highlight" "link" in the "Topic 3" "section"
And I should see "Highlighted" in the "Topic 3" "section"
When I open section "3" edit menu
And I click on "Remove highlight" "link" in the "Topic 3" "section"
Then I should not see "Highlighted" in the "Topic 3" "section"

View File

@ -0,0 +1,222 @@
<?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/>.
namespace format_topics\courseformat;
use core_courseformat\stateupdates;
use moodle_exception;
use stdClass;
/**
* Topics course format related unit tests.
*
* @package format_topics
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class stateactions_test extends \advanced_testcase {
/**
* Enrol a user into a course and login as this user.
*
* @param stdClass $course the course object
* @param string $rolename the rolename
*/
private function enrol_user(stdClass $course, string $rolename): void {
// Create and enrol user using given role.
if ($rolename == 'admin') {
$this->setAdminUser();
} else {
$user = $this->getDataGenerator()->create_user();
if ($rolename != 'unenroled') {
$this->getDataGenerator()->enrol_user($user->id, $course->id, $rolename);
}
$this->setUser($user);
}
}
/**
* Tests for section_highlight method.
*
* @dataProvider basic_role_provider
* @covers ::section_highlight
* @param string $rolename The role of the user that will execute the method.
* @param bool $expectedexception If this call will raise an exception.
*/
public function test_section_highlight(string $rolename, bool $expectedexception = false): void {
global $DB;
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$course = $generator->create_course(
['numsections' => 4, 'format' => 'topics'],
['createsections' => true]
);
$this->enrol_user($course, $rolename);
$sectionrecords = $DB->get_records('course_sections', ['course' => $course->id], 'section');
$sectionids = [];
foreach ($sectionrecords as $section) {
$sectionids[] = $section->id;
}
// Initialise stateupdates.
$courseformat = course_get_format($course->id);
$updates = new stateupdates($courseformat);
// All state actions accepts batch editing (an array of sections in this case). However,
// only one course section can be marked as highlighted. This means that if we send more
// than one section id only the first one will be highlighted and the rest will be ignored.
$methodparam = [
$sectionids[1],
$sectionids[2],
$sectionids[3],
];
// Actions have an array of ids as param but only the first one will be highlighted.
$highlightid = reset($methodparam);
$highlight = $sectionrecords[$highlightid];
if ($expectedexception) {
$this->expectException(moodle_exception::class);
}
// Execute given method.
$actions = new stateactions();
$actions->section_highlight(
$updates,
$course,
$methodparam
);
// Check state returned after executing given action.
$updatelist = $updates->jsonSerialize();
$this->assertCount(1, $updatelist);
$update = reset($updatelist);
$this->assertEquals('section', $update->name);
$this->assertEquals('put', $update->action);
$this->assertEquals($highlightid, $update->fields->id);
$this->assertEquals(1, $update->fields->current);
// Check DB sections.
$this->assertEquals($highlight->section, $DB->get_field("course", "marker", ['id' => $course->id]));
}
/**
* Tests for section_unhighlight method.
*
* @dataProvider basic_role_provider
* @covers ::section_unhighlight
* @param string $rolename The role of the user that will execute the method.
* @param bool $expectedexception If this call will raise an exception.
*/
public function test_section_unhighlight(string $rolename, bool $expectedexception = false): void {
global $DB;
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$course = $generator->create_course(
['numsections' => 4, 'format' => 'topics'],
['createsections' => true]
);
// Highlight section 1.
course_set_marker($course->id, 1);
$this->enrol_user($course, $rolename);
$sectionrecords = $DB->get_records('course_sections', ['course' => $course->id], 'section');
$sectionids = [];
foreach ($sectionrecords as $section) {
$sectionids[] = $section->id;
}
// Initialise stateupdates.
$courseformat = course_get_format($course->id);
$updates = new stateupdates($courseformat);
// The section_unhighlight accepts extra sections to refresh the state data.
$methodparam = [
$sectionids[3],
$sectionids[4],
];
if ($expectedexception) {
$this->expectException(moodle_exception::class);
}
// Execute given method.
$actions = new stateactions();
$actions->section_unhighlight(
$updates,
$course,
$methodparam
);
// The Unhilight mutation always return the previous highlighted
// section (1) and all the extra sections passed (3, and 4) to ensure
// all of them are updated.
$returnedsectionnumbers = [1, 3, 4];
// Check state returned after executing given action.
$updatelist = $updates->jsonSerialize();
$this->assertCount(3, $updatelist);
foreach ($updatelist as $update) {
$this->assertEquals('section', $update->name);
$this->assertEquals('put', $update->action);
$this->assertContains($update->fields->number, $returnedsectionnumbers);
$this->assertEquals(0, $update->fields->current);
}
// Check DB sections.
$this->assertEquals(0, $DB->get_field("course", "marker", ['id' => $course->id]));
}
/**
* Data provider for basic role tests.
*
* @return array the testing scenarios
*/
public function basic_role_provider(): array {
return [
'admin' => [
'role' => 'admin',
'expectedexception' => false,
],
'editingteacher' => [
'role' => 'editingteacher',
'expectedexception' => false,
],
'teacher' => [
'role' => 'teacher',
'expectedexception' => true,
],
'student' => [
'role' => 'student',
'expectedexception' => true,
],
'guest' => [
'role' => 'guest',
'expectedexception' => true,
],
'unenroled' => [
'role' => 'unenroled',
'expectedexception' => true,
],
];
}
}

View File

@ -2,6 +2,9 @@ This files describes API changes for course formats
Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
=== 4.2 ===
* New core_courseformat\base::get_context() to get the course context directly from the format instance.
=== 4.1 ===
* New \core_courseformat\stateupdates methods add_section_remove() and add_cm_remove() have been added to replace
the deprecated methods add_section_delete() and add_cm_delete().