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

This commit is contained in:
Jun Pataleta
2022-09-16 10:49:23 +08:00
21 changed files with 721 additions and 27 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -58,7 +58,10 @@ define(
// Eventually, core_courseformat/local/content/actions will handle all actions for
// component compatible formats and the default actions.js won't be necessary anymore.
// Meanwhile, we filter the migrated actions.
const componentActions = ['moveSection', 'moveCm', 'addSection', 'deleteSection'];
const componentActions = [
'moveSection', 'moveCm', 'addSection', 'deleteSection', 'sectionHide', 'sectionShow',
'cmHide', 'cmShow', 'cmStealth',
];
// The course reactive instance.
const courseeditor = editor.getCurrentCourseEditor();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,4 +1,4 @@
define("core_courseformat/local/content/section",["exports","core_courseformat/local/content/section/header","core_courseformat/local/courseeditor/dndsection"],(function(_exports,_header,_dndsection){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
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.
*
@@ -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);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"]'},this.classes={LOCKED:"editinprogress",HASDESCRIPTION:"description"},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;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;const sectioninfo=this.getElement(this.selectors.SECTIONINFO);sectioninfo&&sectioninfo.classList.toggle(this.classes.HASDESCRIPTION,element.hasrestrictions)}}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"},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){this.getElement("".concat(this.selectors.SECTIONBADGES," [data-type='iscurrent']")).classList.toggle(this.classes.HIDE,!section.current);this.getElement("".concat(this.selectors.SECTIONBADGES," [data-type='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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -217,6 +217,7 @@ export default class Component extends BaseComponent {
return [
// State changes that require to reload some course modules.
{watch: `cm.visible:updated`, handler: this._reloadCm},
{watch: `cm.stealth:updated`, handler: this._reloadCm},
// Update section number and title.
{watch: `section.number:updated`, handler: this._refreshSectionNumber},
// Collapse and expand sections.
+53 -1
View File
@@ -41,6 +41,17 @@ import jQuery from 'jquery';
// Load global strings.
prefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']);
// Mutations are dispatched by the course content actions.
// Formats can use this module addActions static method to add custom actions.
// Direct mutations can be simple strings (mutation) name or functions.
const directMutations = {
sectionHide: 'sectionHide',
sectionShow: 'sectionShow',
cmHide: 'cmHide',
cmShow: 'cmShow',
cmStealth: 'cmStealth',
};
export default class extends BaseComponent {
/**
@@ -68,6 +79,20 @@ export default class extends BaseComponent {
};
}
/**
* Add extra actions to the module.
*
* @param {array} actions array of methods to execute
*/
static addActions(actions) {
for (const [action, mutationReference] of Object.entries(actions)) {
if (typeof mutationReference !== 'function' && typeof mutationReference !== 'string') {
throw new Error(`${action} action must be a mutation name or a function`);
}
directMutations[action] = mutationReference;
}
}
/**
* Initial state ready method.
*
@@ -114,10 +139,22 @@ export default class extends BaseComponent {
}
// Invoke proper method.
const methodName = this._actionMethodName(target.dataset.action);
const actionName = target.dataset.action;
const methodName = this._actionMethodName(actionName);
if (this[methodName] !== undefined) {
this[methodName](target, event);
return;
}
// Check direct mutations or mutations handlers.
if (directMutations[actionName] !== undefined) {
if (typeof directMutations[actionName] === 'function') {
directMutations[actionName](target, event);
return;
}
this._requestMutationAction(target, event, directMutations[actionName]);
return;
}
}
@@ -356,6 +393,21 @@ export default class extends BaseComponent {
}
}
/**
* Basic mutation action helper.
*
* @param {Element} target the dispatch action element
* @param {Event} event the triggered event
* @param {string} mutationName the mutation name
*/
async _requestMutationAction(target, event, mutationName) {
if (!target.dataset.id) {
return;
}
event.preventDefault();
this.reactive.dispatch(mutationName, [target.dataset.id]);
}
/**
* Disable all add sections actions.
*
+66 -1
View File
@@ -24,6 +24,7 @@
import Header from 'core_courseformat/local/content/section/header';
import DndSection from 'core_courseformat/local/courseeditor/dndsection';
import Templates from 'core/templates';
export default class extends DndSection {
@@ -38,11 +39,18 @@ export default class extends DndSection {
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`,
};
// Most classes will be loaded later by DndCmItem.
this.classes = {
LOCKED: 'editinprogress',
HASDESCRIPTION: 'description',
HIDE: 'd-none',
HIDDEN: 'hidden',
};
// We need our id to watch specific events.
@@ -112,7 +120,7 @@ export default class extends DndSection {
}
/**
* Update a course index section using the state information.
* Update a content section using the state information.
*
* @param {object} param
* @param {Object} param.element details the update details.
@@ -121,11 +129,68 @@ export default class extends DndSection {
// Update classes.
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.locked = element.locked;
// The description box classes depends on the section state.
const sectioninfo = this.getElement(this.selectors.SECTIONINFO);
if (sectioninfo) {
sectioninfo.classList.toggle(this.classes.HASDESCRIPTION, element.hasrestrictions);
}
// Update section badges and menus.
this._updateBadges(element);
this._updateActionsMenu(element);
}
/**
* Update a section badges using the state information.
*
* @param {object} section the section state.
*/
_updateBadges(section) {
const current = this.getElement(`${this.selectors.SECTIONBADGES} [data-type='iscurrent']`);
current.classList.toggle(this.classes.HIDE, !section.current);
const hiddenFromStudents = this.getElement(`${this.selectors.SECTIONBADGES} [data-type='hiddenfromstudents']`);
hiddenFromStudents.classList.toggle(this.classes.HIDE, section.visible);
}
/**
* Update a section action menus.
*
* @param {object} section the section state.
*/
async _updateActionsMenu(section) {
let selector;
let newAction;
if (section.visible) {
selector = this.selectors.SHOWSECTION;
newAction = 'sectionHide';
} else {
selector = this.selectors.HIDESECTION;
newAction = 'sectionShow';
}
// Find the affected action.
const affectedAction = this.getElement(selector);
if (!affectedAction) {
return;
}
// Change action.
affectedAction.dataset.action = newAction;
// Change text.
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;
}
// Change icon.
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, '');
}
}
}
}
@@ -56,6 +56,49 @@ export default class {
return JSON.parse(ajaxresult);
}
/**
* Execute a basic section state action.
* @param {StateManager} stateManager the current state manager
* @param {string} action the action name
* @param {array} sectionIds the section ids
* @param {number} targetSectionId optional target section id (for moving actions)
* @param {number} targetCmId optional target cm id (for moving actions)
*/
async _sectionBasicAction(stateManager, action, sectionIds, targetSectionId, targetCmId) {
const course = stateManager.get('course');
this.sectionLock(stateManager, sectionIds, true);
const updates = await this._callEditWebservice(
action,
course.id,
sectionIds,
targetSectionId,
targetCmId
);
stateManager.processUpdates(updates);
this.sectionLock(stateManager, sectionIds, false);
}
/**
* Execute a basic course module state action.
* @param {StateManager} stateManager the current state manager
* @param {string} action the action name
* @param {array} cmIds the cm ids
* @param {number} targetSectionId optional target section id (for moving actions)
* @param {number} targetCmId optional target cm id (for moving actions)
*/
async _cmBasicAction(stateManager, action, cmIds, targetSectionId, targetCmId) {
const course = stateManager.get('course');
this.cmLock(stateManager, cmIds, true);
const updates = await this._callEditWebservice(
action,
course.id,
cmIds,
targetSectionId,
targetCmId
);
stateManager.processUpdates(updates);
this.cmLock(stateManager, cmIds, false);
}
/**
* Mutation module initialize.
@@ -87,6 +130,51 @@ export default class {
return fields;
}
/**
* Hides sections.
* @param {StateManager} stateManager the current state manager
* @param {array} sectionIds the list of section ids
*/
async sectionHide(stateManager, sectionIds) {
await this._sectionBasicAction(stateManager, 'section_hide', sectionIds);
}
/**
* Show sections.
* @param {StateManager} stateManager the current state manager
* @param {array} sectionIds the list of section ids
*/
async sectionShow(stateManager, sectionIds) {
await this._sectionBasicAction(stateManager, 'section_show', sectionIds);
}
/**
* Show cms.
* @param {StateManager} stateManager the current state manager
* @param {array} cmIds the list of cm ids
*/
async cmShow(stateManager, cmIds) {
await this._cmBasicAction(stateManager, 'cm_show', cmIds);
}
/**
* Hide cms.
* @param {StateManager} stateManager the current state manager
* @param {array} cmIds the list of cm ids
*/
async cmHide(stateManager, cmIds) {
await this._cmBasicAction(stateManager, 'cm_hide', cmIds);
}
/**
* Stealth cms.
* @param {StateManager} stateManager the current state manager
* @param {array} cmIds the list of cm ids
*/
async cmStealth(stateManager, cmIds) {
await this._cmBasicAction(stateManager, 'cm_stealth', cmIds);
}
/**
* Move course modules to specific course location.
*
@@ -159,8 +159,9 @@ class controlmenu implements named_templatable, renderable {
$url = clone($baseurl);
if (!$isstealth) {
if (has_capability('moodle/course:sectionvisibility', $coursecontext, $user)) {
$strhidefromothers = get_string('hidefromothers', 'format_' . $course->format);
$strshowfromothers = get_string('showfromothers', 'format_' . $course->format);
if ($section->visible) { // Show the hide/show eye.
$strhidefromothers = get_string('hidefromothers', 'format_'.$course->format);
$url->param('hide', $section->section);
$controls['visiblity'] = [
'url' => $url,
@@ -170,11 +171,13 @@ class controlmenu implements named_templatable, renderable {
'attr' => [
'class' => 'icon editing_showhide',
'data-sectionreturn' => $sectionreturn,
'data-action' => 'hide',
'data-action' => ($usecomponents) ? 'sectionHide' : 'hide',
'data-id' => $section->id,
'data-swapname' => $strshowfromothers,
'data-swapicon' => 'i/show',
],
];
} else {
$strshowfromothers = get_string('showfromothers', 'format_'.$course->format);
$url->param('show', $section->section);
$controls['visiblity'] = [
'url' => $url,
@@ -184,7 +187,10 @@ class controlmenu implements named_templatable, renderable {
'attr' => [
'class' => 'icon editing_showhide',
'data-sectionreturn' => $sectionreturn,
'data-action' => 'show',
'data-action' => ($usecomponents) ? 'sectionShow' : 'show',
'data-id' => $section->id,
'data-swapname' => $strhidefromothers,
'data-swapicon' => 'i/hide',
],
];
}
@@ -83,6 +83,7 @@ class cm implements renderable {
'anchor' => "module-{$cm->id}",
'name' => external_format_string($cm->name, $cm->context, true),
'visible' => !empty($cm->visible),
'stealth' => $cm->is_stealth(),
'sectionid' => $section->id,
'sectionnumber' => $section->section,
'uservisible' => $cm->uservisible,
+166 -2
View File
@@ -16,8 +16,8 @@
namespace core_courseformat;
use core_courseformat\base as course_format;
use core_courseformat\stateupdates;
use core\event\course_module_updated;
use cm_info;
use section_info;
use stdClass;
@@ -25,7 +25,6 @@ use course_modinfo;
use moodle_exception;
use context_module;
use context_course;
use cache;
/**
* Contains the core course state actions.
@@ -258,6 +257,171 @@ class stateactions {
$this->course_state($updates, $course);
}
/**
* Hide course sections.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids section ids
* @param int $targetsectionid not used
* @param int $targetcmid not used
*/
public function section_hide(
stateupdates $updates,
stdClass $course,
array $ids = [],
?int $targetsectionid = null,
?int $targetcmid = null
): void {
$this->set_section_visibility($updates, $course, $ids, 0);
}
/**
* Show course sections.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids section ids
* @param int $targetsectionid not used
* @param int $targetcmid not used
*/
public function section_show(
stateupdates $updates,
stdClass $course,
array $ids = [],
?int $targetsectionid = null,
?int $targetcmid = null
): void {
$this->set_section_visibility($updates, $course, $ids, 1);
}
/**
* Show course sections.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids section ids
* @param int $visible the new visible value
*/
protected function set_section_visibility (
stateupdates $updates,
stdClass $course,
array $ids,
int $visible
) {
$this->validate_sections($course, $ids, __FUNCTION__);
$coursecontext = context_course::instance($course->id);
require_all_capabilities(['moodle/course:update', 'moodle/course:sectionvisibility'], $coursecontext);
$modinfo = get_fast_modinfo($course);
foreach ($ids as $sectionid) {
$section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST);
course_update_section($course, $section, ['visible' => $visible]);
}
$this->section_state($updates, $course, $ids);
}
/**
* Show course cms.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids cm ids
* @param int $targetsectionid not used
* @param int $targetcmid not used
*/
public function cm_show(
stateupdates $updates,
stdClass $course,
array $ids = [],
?int $targetsectionid = null,
?int $targetcmid = null
): void {
$this->set_cm_visibility($updates, $course, $ids, 1, 1);
}
/**
* Hide course cms.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids cm ids
* @param int $targetsectionid not used
* @param int $targetcmid not used
*/
public function cm_hide(
stateupdates $updates,
stdClass $course,
array $ids = [],
?int $targetsectionid = null,
?int $targetcmid = null
): void {
$this->set_cm_visibility($updates, $course, $ids, 0, 1);
}
/**
* Stealth course cms.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids cm ids
* @param int $targetsectionid not used
* @param int $targetcmid not used
*/
public function cm_stealth(
stateupdates $updates,
stdClass $course,
array $ids = [],
?int $targetsectionid = null,
?int $targetcmid = null
): void {
$this->set_cm_visibility($updates, $course, $ids, 1, 0);
}
/**
* Internal method to define the cm visibility.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids cm ids
* @param int $visible the new visible value
* @param int $coursevisible the new course visible value
*/
protected function set_cm_visibility(
stateupdates $updates,
stdClass $course,
array $ids,
int $visible,
int $coursevisible
): void {
global $CFG;
$this->validate_cms($course, $ids, __FUNCTION__);
// Check capabilities on every activity context.
foreach ($ids as $cmid) {
$modcontext = context_module::instance($cmid);
require_all_capabilities(['moodle/course:manageactivities', 'moodle/course:activityvisibility'], $modcontext);
}
$format = course_get_format($course->id);
$modinfo = get_fast_modinfo($course);
$cms = $this->get_cm_info($modinfo, $ids);
foreach ($cms as $cm) {
// Check stealth availability.
if (!$coursevisible) {
$section = $cm->get_section_info();
$allowstealth = !empty($CFG->allowstealth) && $format->allow_stealth_module_visibility($cm, $section);
$coursevisible = ($allowstealth) ? 0 : 1;
}
set_coursemodule_visible($cm->id, $visible, $coursevisible);
course_module_updated::create_from_cm($cm, $modcontext)->trigger();
$updates->add_cm_put($cm->id);
}
}
/**
* Extract several cm_info from the course_modinfo.
*
+292
View File
@@ -30,6 +30,14 @@ use stdClass;
*/
class stateactions_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/lib/externallib.php');
}
/**
* Helper method to create an activity into a section and add it to the $sections and $activities arrays.
*
@@ -578,4 +586,288 @@ class stateactions_test extends \advanced_testcase {
],
];
}
/**
* Internal method for testing a specific state action.
*
* @param string $method the method to test
* @param string $role the user role
* @param string[] $idrefs the sections or cms id references to be used as method params
* @param bool $expectedexception whether the call should throw an exception
* @param int $expectedtotal the expected total number of state puts
* @param string|null $coursefield the course field to check
* @param int|string|null $coursevalue the section field value
* @param string|null $sectionfield the section field to check
* @param int|string|null $sectionvalue the section field value
* @param string|null $cmfield the cm field to check
* @param int|string|null $cmvalue the cm field value
* @return array the state update summary
*/
protected function basic_state_text(
string $method = 'section_hide',
string $role = 'editingteacher',
array $idrefs = [],
bool $expectedexception = false,
int $expectedtotal = 0,
?string $coursefield = null,
$coursevalue = 0,
?string $sectionfield = null,
$sectionvalue = 0,
?string $cmfield = null,
$cmvalue = 0
): array {
$this->resetAfterTest();
// Create a course with 3 sections, 1 of them hidden.
$course = $this->create_course('topics', 3, [2]);
$references = $this->course_references($course);
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
$this->setUser($user);
// Add some activities to the course. One visible and one hidden in both sections 1 and 2.
$references["cm0"] = $this->create_activity($course->id, 'assign', 1, true);
$references["cm1"] = $this->create_activity($course->id, 'book', 1, false);
$references["cm2"] = $this->create_activity($course->id, 'glossary', 2, true);
$references["cm3"] = $this->create_activity($course->id, 'page', 2, false);
if ($expectedexception) {
$this->expectException(moodle_exception::class);
}
// Initialise stateupdates.
$courseformat = course_get_format($course->id);
$updates = new stateupdates($courseformat);
// Execute the method.
$actions = new stateactions();
$actions->$method(
$updates,
$course,
$this->translate_references($references, $idrefs),
);
// Format results in a way we can compare easily.
$results = $this->summarize_updates($updates);
// Most state actions does not use create or remove actions because they are designed
// to refresh parts of the state.
$this->assertEquals(0, $results['create']['count']);
$this->assertEquals(0, $results['remove']['count']);
// Validate we have all the expected entries.
$this->assertEquals($expectedtotal, $results['put']['count']);
// Validate course, section and cm.
if (!empty($coursefield)) {
foreach ($results['put']['course'] as $courseid) {
$this->assertEquals($coursevalue, $results['put']['course'][$courseid][$coursefield]);
}
}
if (!empty($sectionfield)) {
foreach ($results['put']['section'] as $section) {
$this->assertEquals($sectionvalue, $section->$sectionfield);
}
}
if (!empty($cmfield)) {
foreach ($results['put']['cm'] as $cm) {
$this->assertEquals($cmvalue, $cm->$cmfield);
}
}
return $results;
}
/**
* Test for section_hide
*
* @covers ::section_hide
* @dataProvider basic_role_provider
* @param string $role the user role
* @param bool $expectedexception if it will expect an exception.
*/
public function test_section_hide(
string $role = 'editingteacher',
bool $expectedexception = false
): void {
$this->basic_state_text(
'section_hide',
$role,
['section1', 'section2', 'section3'],
$expectedexception,
7,
null,
null,
'visible',
0,
null,
null
);
}
/**
* Test for section_hide
*
* @covers ::section_show
* @dataProvider basic_role_provider
* @param string $role the user role
* @param bool $expectedexception if it will expect an exception.
*/
public function test_section_show(
string $role = 'editingteacher',
bool $expectedexception = false
): void {
$this->basic_state_text(
'section_show',
$role,
['section1', 'section2', 'section3'],
$expectedexception,
7,
null,
null,
'visible',
1,
null,
null
);
}
/**
* Test for cm_show
*
* @covers ::cm_show
* @dataProvider basic_role_provider
* @param string $role the user role
* @param bool $expectedexception if it will expect an exception.
*/
public function test_cm_show(
string $role = 'editingteacher',
bool $expectedexception = false
): void {
$this->basic_state_text(
'cm_show',
$role,
['cm0', 'cm1', 'cm2', 'cm3'],
$expectedexception,
4,
null,
null,
null,
null,
'visible',
1
);
}
/**
* Test for cm_hide
*
* @covers ::cm_hide
* @dataProvider basic_role_provider
* @param string $role the user role
* @param bool $expectedexception if it will expect an exception.
*/
public function test_cm_hide(
string $role = 'editingteacher',
bool $expectedexception = false
): void {
$this->basic_state_text(
'cm_hide',
$role,
['cm0', 'cm1', 'cm2', 'cm3'],
$expectedexception,
4,
null,
null,
null,
null,
'visible',
0
);
}
/**
* Test for cm_stealth
*
* @covers ::cm_stealth
* @dataProvider basic_role_provider
* @param string $role the user role
* @param bool $expectedexception if it will expect an exception.
*/
public function test_cm_stealth(
string $role = 'editingteacher',
bool $expectedexception = false
): void {
set_config('allowstealth', 1);
$this->basic_state_text(
'cm_stealth',
$role,
['cm0', 'cm1', 'cm2', 'cm3'],
$expectedexception,
4,
null,
null,
null,
null,
'stealth',
1
);
// Disable stealth.
set_config('allowstealth', 0);
// When stealth are disabled the validation is a but more complex because they depends
// also on the section visibility (legacy stealth).
$this->basic_state_text(
'cm_stealth',
$role,
['cm0', 'cm1'],
$expectedexception,
2,
null,
null,
null,
null,
'stealth',
0
);
$this->basic_state_text(
'cm_stealth',
$role,
['cm2', 'cm3'],
$expectedexception,
2,
null,
null,
null,
null,
'stealth',
1
);
}
/**
* Data provider for basic role tests.
*
* @return array the testing scenarios
*/
public function basic_role_provider() {
return [
'editingteacher' => [
'role' => 'editingteacher',
'expectedexception' => false,
],
'teacher' => [
'role' => 'teacher',
'expectedexception' => true,
],
'student' => [
'role' => 'student',
'expectedexception' => true,
],
'guest' => [
'role' => 'guest',
'expectedexception' => true,
],
];
}
}
+28 -6
View File
@@ -1642,6 +1642,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
$coursecontext = context_course::instance($mod->course);
$modcontext = context_module::instance($mod->id);
$courseformat = course_get_format($mod->get_course());
$usecomponents = $courseformat->supports_components();
$editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign');
$dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport');
@@ -1680,7 +1681,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
}
// Move (only for component compatible formats).
if ($courseformat->supports_components()) {
if ($usecomponents) {
$actions['move'] = new action_menu_link_secondary(
new moodle_url($baseurl, [
'sesskey' => sesskey(),
@@ -1754,7 +1755,11 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
new moodle_url($baseurl, array('hide' => $mod->id)),
new pix_icon('t/hide', '', 'moodle', array('class' => 'iconsmall')),
$str->modhide,
array('class' => 'editing_hide', 'data-action' => 'hide')
[
'class' => 'editing_hide',
'data-action' => ($usecomponents) ? 'cmHide' : 'hide',
'data-id' => $mod->id,
]
);
} else if (!$displayedoncoursepage && $sectionvisible) {
// Offer to "show" only if the section is visible.
@@ -1762,7 +1767,11 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
new moodle_url($baseurl, array('show' => $mod->id)),
new pix_icon('t/show', '', 'moodle', array('class' => 'iconsmall')),
$str->modshow,
array('class' => 'editing_show', 'data-action' => 'show')
[
'class' => 'editing_show',
'data-action' => ($usecomponents) ? 'cmShow' : 'show',
'data-id' => $mod->id,
]
);
}
@@ -1772,18 +1781,31 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
new moodle_url($baseurl, array('hide' => $mod->id)),
new pix_icon('t/unblock', '', 'moodle', array('class' => 'iconsmall')),
$str->makeunavailable,
array('class' => 'editing_makeunavailable', 'data-action' => 'hide', 'data-sectionreturn' => $sr)
[
'class' => 'editing_makeunavailable',
'data-action' => ($usecomponents) ? 'cmHide' : 'hide',
'data-sectionreturn' => $sr,
'data-id' => $mod->id,
]
);
} else if ($unavailable && (!$sectionvisible || $allowstealth) && $mod->has_view()) {
// Allow to make visually hidden module available in gradebook and other reports by making it a "stealth" module.
// When the section is hidden it is an equivalent of "showing" the module.
// Activities without the link (i.e. labels) can not be made available but hidden on course page.
$action = $sectionvisible ? 'stealth' : 'show';
if ($usecomponents) {
$action = 'cm' . ucfirst($action);
}
$actions[$action] = new action_menu_link_secondary(
new moodle_url($baseurl, array($action => $mod->id)),
new moodle_url($baseurl, array('stealth' => $mod->id)),
new pix_icon('t/block', '', 'moodle', array('class' => 'iconsmall')),
$str->makeavailable,
array('class' => 'editing_makeavailable', 'data-action' => $action, 'data-sectionreturn' => $sr)
[
'class' => 'editing_makeavailable',
'data-action' => $action,
'data-sectionreturn' => $sr,
'data-id' => $mod->id,
]
);
}
}
+1 -1
View File
@@ -85,7 +85,7 @@ Feature: Course activity controls works as expected
| weeks | 1 | "1 January - 7 January" | should not | should not | "8 January - 14 January" |
| weeks | 1 | "General" | should | should not | "8 January - 14 January" |
Scenario Outline: General activities course controls using topics and weeks formats, and paged mode and not paged mode works as expected
Scenario Outline: General activities course controls using topics and weeks formats, and paged mode and not paged mode works as expected without javascript
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |